junh00🌈

ARM环境下的漏洞利用

前言

本文是边翻译边跟做实验完成的,主要是ARM环境下的漏洞利用的基础,太基础!很适合入门。大牛请绕行!

原文是在iPhone4的终端上完成所有操作的,我觉得麻烦就改成在电脑上操作了,截图已全部换成自己的。

原文作者Twitter:@bellis

Part 1: 准备工作

这篇教程讲的是ARM平台上漏洞利用的基础。主要针对一些对漏洞利用和黑客行为感兴趣的初学者,所以在阅读本文之前不需要有太多相关的知识。但是你应该明白一些基础的内容:CPU是怎么工作的,二进制,十六进制,什么是汇编代码以及C语言编程。

为了跟随这篇教程,你需要一台32位的ARM设备。由于我的主要工作集中在iPhone和iOS上,所以我在这个教程中使用的是一台iPhone4设备。这台设备有一个Apple A4(S5L8930X)芯片——一个ARMv7(32位)的处理器。

如果你有一台64位(ARM64)的设备,你仍然可以尝试跟着一起做,但是可能会出现一些复杂情况,会碰到些困难。

在开始之前你需要安装一些工具,这些工具能让你在设备上编译和调试C代码。添加http://cydia.radare.org到你的Cydia源,然后安装radare2-arm32。这样你的设备上就安装了radare调试器,它对于分析代码,和反编译程序了解程序工作是非常有用的。

同样还需要添加源http://coolstar.org/publicrepo,安装LLVM+Clang。这个程序能够使你无需电脑的帮助,在设备上直接编译C程序。你同样还需要一个SDK(软件开发工具包)。我当前在设备上使用的是iPhoneOS8.1sdk。你可以在网上下载一个或者通过Xcode移动到你的设备上。

还有两样必要的东西:iFileTerminal(我使用的是MTerminal)。我假设大多数人已经安装了这两个应用。

Part 2: 创建一个有漏洞的程序

开始这个实验前,我们需要一个有漏洞的程序让我们能够操作和利用,这被称为漏洞利用(exploitation)。漏洞利用经常用于控制或者劫持进程的执行流程,让它做些它本不会做的事。在iOS越狱过程中,内核的漏洞利用用于重新安装iOS的文件系统比如R/W,允许在设备上执行未签名的二进制文件等等。这个教程中的漏洞利用是非常基础的,比真实世界中所碰到的情况(比如iOS越狱等等)简单的多。但是我主要就是讲解漏洞利用的基础,因此如果你感兴趣的话,这应该是你开始的好地方。

下面就是我们将要进行漏洞利用的程序源码:

#include <stdio.h>
#include <stdlib.h>

void secret() {
	printf("You shouldn't be here ;P\n");
	system("ls -la /");
}

int main() {
	char buff[12];
	gets(buff);
	
	printf("You entered %s\n", buff);
	
	return 0;
}

通过iFile(或者Terminal)创建一个hello.c文件,将上面的代码复制进去。 但愿你有些C语言基础。如果没有,我会简要的解释下这个程序做了什么。 正如你所见,这个程序只有两个函数,main()secret()。程序中secret()函数是一个实际不会被调用到(不被使用)的函数。我们在之后会回来讲到这个函数。

这个程序中只有main()函数是一个会被执行。如你所见,函数内声明了一个名为buff的12个字符缓冲区。这本质上就是一个由字符组成的数组,或者说是一个包含12个字符的字符串。

下一行使用了libc库中的gets()函数,用于捕获用户的输入并存到12个字符的buffer中。

函数剩余部分就是把用户所提供的输入打印并返回一个0。

要编译和测试这个程序我们需要用到clang。在设备上的MTerminal中,进入到你存放hello.c的目录

cd /path/to/directory

然后输入clang hello.c -isysroot /path/to/sdk -fno-pie -fno-stack-protector -mno-thumb -o hello 这个命令会把你的源代码编译成一个可执行文件。参数-fno-pie -fno-stack-protector用于关闭一些会使本教程面对很多挑战的安全特性。-mno-thumb确保这个程序将会在ARM模式下执行,而不是THUMB模式。你可以输入ls来罗列当前目录下的所有文件,就可以看到编译后的hello可执行文件了。

Part 3: 调试和漏洞利用

CD到你存放执行文件的目录后在终端输入./hello就可以执行程序了。 你会发现在程序还没开始执行之前,有句提示语警告我们gets()函数是有缺陷或者说是安全隐患的函数。这是一个非常危险的函数,因为它没有做输入的校验,所以它会获取你所输入的任何大小的数据,并尝试放到12个字符的buffer里。很明显,如果我们输入50个字符,不可能塞进12个字符的缓冲里。剩余的字符最终会被写入栈里(进程内存的一块区域),但是很可能会覆盖掉栈上保存的其他信息,比如返回地址和其他的一些重要的值。这种类型的安全隐患被称为stack buffer overflow

在上面的截屏中你可以看到,我输入了”YYYYYY”到程序中(小于12个字符,所以表现良好)然后程序正常输出。现在我输入超过12字符看会发生什么。

我们发现情况有些不一样。程序输出了Segmentation fault: 11。这意味着程序崩溃了(可能是因为我们输入了太多的字符)。要操作这个崩溃并为这个简单的程序写一个漏洞利用,我们还需要从Cydia找到CrashReporter来进行崩溃的分析。

CrashReporter将会提供你很多信息。我们只需要注意寄存器这块。 这里显示了每个寄存器在崩溃前所保存的值。ARM上有15个寄存器(实际上有更多,但我们只需要考虑这15个)。R0-R12是通用寄存器,R13-R15是特殊寄存器。R13保存的是栈指针,R14是链接寄存器,R15是程序计数器(PC)。PC保存的是CPU要执行的,下一条指令的内存地址。一般情况下,这个值会增加,这样CPU就会按照顺序执行指令。 IMG

在上面的崩溃日志中,你可以看到PC在崩溃时所记录的值0x41414140。0x41是字母A的16进制值,意味着PC寄存器的值是AAAA。因为0x41414140是是一个无效的内存地址,当程序尝试跳转到这个地址并尝试执行的时候,因为找不到这个地址中的指令,所以就崩溃了。在攻击者眼里,这种崩溃是一种非常棒的标志,崩溃日志的分析结果更是让他们兴奋。因为这证明了我们有PC寄存器完全的控制权。这是由于main()函数的返回地址是被存储在栈上的某个地方,当我们写入太多的字符(在这个实验中就是一串”A”),超过了12个字符穿冲去的边界,并且覆盖了栈上的重要信息。因为我们从崩溃报告中看到,我们覆盖了main()函数的返回地址。这意味着我们能塞任何我们想要存到PC寄存器的值,并且程序会愉悦的执行它。

如果我们放置一个有效的地址来替换0x41414140,程序将会跳到这个地址并继续执行。这就导致我们劫持了程序执行流,可以做任何我们想做的事情。

之前我们提到,程序中有一个secret()的函数,包含了一些应该永远不会被执行的代码。使用这个缓冲溢出的缺陷,我们应该能够重定向执行流程,使它执行到这个函数。在做这些之前,我们需要反汇编程序,来找到这个函数的地址。

回到MTerminal,输入”r2 hello”来使用radare打开”hello”,然后输入”aa”让它自动分析程序。输入”afl”来列出这个程序里的所有函数。

在列出来的函数中,看到有secret函数,左边是它的起始地址。这个0x0000bee4地址就是我们想要程序跳转到的地址,也就是用于覆盖掉栈上的返回地址的地址。0x0000bee4是一块包含了secret函数第一条指令地址的内存,所以跳转到这个地址就能够有效的调用它。

如果你想要分析secret()函数的更多细节,你可以通过输入”s sym._secret”进行反汇编,然后输入”pdf”来打印函数的反汇编结果。

上面的截屏显示了secret()函数的汇编代码。在这个简易的漏洞利用教程中,你不需要明白ARM汇编,但是如果你以后想要玩转涉及ROP的更高级的东西的话,这就非常值得学习一下了。

另外,所有我们所需要知道的这个函数的第一条指令的地址也在输出的汇编代码中 - 0x0000bee4。当这个值被弹回PC,程序就会跳转到secret函数。

为了达到这目的,我们需要知道字符串”A”覆盖到的究竟是哪块地址。我们可以简单的通过一个格式化的输入来找到这块地址。如果我们在程序中输入”AAAABBBBCCCCDDDDEEEEFFFFGGGGHHHHIIIIJJJJ”,当我们检查崩溃日至时,我们就会看到PC寄存器中包含的究竟是哪4个ASCII字符,因为这些字符的值都是不同的。

在使用这个字符串使程序崩溃后,你可以看到PC中包含的是值0x46464644,也就是FFFF的16进制。这以为着,”FFFF”的位置就是我们需要替换成secret()函数的位置。

实际构建漏洞利用的字符串时,我们需要直接使用16进制来表示,而不只是ASCII字符。我们可以使用printf并输出”灌入hello程序。这是通过输入”printf”,然后输入字符串并添加双引号,然后再使用” “字符,后面再跟程序名字,这样就能把printf的输出作为这个程序的输入来执行。

在使用printf输入十六进制值的时候,你可以键入”\x”后面跟十六进制值。我们就使用这种方式来输入secret()函数地址。然而,我们必须键入反向的地址,因为ARMv7处理器是小端的(Little Endian)。这表示它都是反过来读取字节的。所以地址0x0000bee4在输入的时候变成0xe4be0000

好了,我们最终用来漏洞利用的字符串应该是:”AAAABBBBCCCCDDDDEEEE\xe4\xbe\x00\x00”

在上面的截屏中你可以发现,我们能够使用printf来提供我们用于漏洞利用的字符串。gets()函数尝试将所有的字符写入12个字符的buffer,但是并没有那么多空间。剩余的字符都被写在了栈上,覆盖了已经被存储的函数返回地址。十六进制值0x0000bee4恰巧被写在了返回地址的地方,导致程序在函数返回时跳转到这个地址。

你可以看到程序不再是由于Segmentation fault或是其他的原因崩溃,这次程序输出了”You shouldn’t be here ;P”,然后罗列了设备上/目录下的文件。我们可以在源代码中发现这正是执行了secret()函数的结果。漏洞利用成功!

Part 4: 结论

如果你是新手的话,希望这篇教程对你有用,同样希望能使你明白软件漏洞是如何利用的。secret()函数在这篇教程中作为攻击者渴望执行的目标代码。在真实情况下,这个函数能够生成一个root shell,并能使你完全访问设备。另外,它也可能是程序中的,能给你额外功能,并且你正常不应该能访问的函数。很明显,在真实情况下它不会是这么简单的返回一个单一的函数。面向返回编程(ROP)是一种把许多不同函数串连起来以控制程序的方法,我计划在之后的教程中会讲到这些。