翻译:uglyB0y
预估稿费:200RMB
投稿方式:发送邮件至linwei#360.cn,或登陆网页版在线投稿
0x01 前言
2016年2月16日,Google披露了一个重要的缓冲区溢出漏洞,该漏洞在GLIBC库中的getaddrinfo函数中触发。同时他们还提供了一份PoC。基于此,在本文中,我们将展示如何通过CVE-2015-7547绕过ASLR。
0x02 漏洞描述
getaddrinfo()函数的作用是通过查询DNS服务将主机名和服务解析为addrinfo结构体。
在getaddrinfo()函数实现中,使用了alloca()函数(在堆栈上分配缓冲区)对DNS进行响应。开始的时候,该函数首先分配一段栈空间用于DNS响应, 如果响应时间过长,它将重新分配一个堆缓冲区用于响应。但由于更新代码将缓冲区为新分配的堆缓冲区后,旧的堆栈缓冲区未及时释放,仍旧再使用。 这个悬空指针便造成了一个经典的缓冲区溢出。
ASLR? ASLR!
在上述情况下,可以通过这个漏洞覆盖getaddrinfo()函数的返回地址,但是我们应该将返回地址覆盖到哪里呢?在启用ASLR的系统中,模块地址是随机的。因此,攻击者不能将攻击流地址设置为预先设定的地址。
fork()
fork()是Linux中创建新进程的方法。一个典型的fork使用方法如下图所示:
fork出的子进程和父进程使用的相同的指令段,他们只有pid不相同,而pid是fork函数返回给子进程的。和windows下的代码复用的区别在于,这里意味着子进程与其父进程共享许多特性–具有相同的寄存器状态,堆栈和内存布局。
0x03 程序流程示例
考虑一个服务器应用程序,其运行模式如下:
1. 客户端远程连接到应用程序。
2. 应用程序自己fork一个子进程用于响应客户端请求
3. 在处理客户端请求的过程中,子进程使用"getaddrinfo()"函数解析主机名。 同时,它向其DNS服务器发送DNS请求。
4. DNS服务器对DNS请求做出一个合法响应。
5. 子进程启动与已被解析的主机的连接。
每次主进程进行响应处理时,它都会自己fork一个子进程。根据前面的描述,这意味着所有子进程将共享相同的内存布局–包括加载模块的地址。 这种场景对于许多服务(例如HTTP代理,电子邮件服务器或DNS服务器)是非常常见的。
0x04 攻击流程示例
在实施攻击的过程中,我们假设攻击者具有能够响应受害者的任意DNS请求的能力。实现这种情形完全可以通过ARP欺骗、DNS欺骗完成。攻击场景如下图:
1. 一个攻击者构造一个请求发送给受害服务器
2. 为了响应攻击者的请求,受害服务器的守护进程fork出一个子进程
3. 子进程处理请求时,发起一个DNS请求
4. 攻击者回复一个恶意的DNS响应,该响应将子进程的返回地址覆盖,在这里,我们将其设置未0x12121212
5. 攻击者获得子进程用connect()函数发起的TCP回连
如果0x12121212确实是getaddrinfo()的正确返回地址,那么该进程将正常运行,并通过connect()发起tcp连接。
如果不是这种情况,并且攻击者将返回地址写为其他任何地址,则应用程序将由于内存段错误或执行无效的指令而崩溃。
这种方式可以作为判断一个地址是否为getaddrinfo()的返回地址的一种方法,原因在于如果地址正确,那么一个TCP连接将会成功建立。由于模块的基址在不同的子进程中是没有随机化(前面提到的公用内存布局),于是这个地址在所有的子进程中可以通用。一个攻击者可以使用这种方式去遍历每一个可能的地址,知道正确建立TCP连接而获得正确的地址。
然而,采用这种方式进行基址定位需要猜解2的64次方的数量的地址,这并没有太大的现实意义。
逐字节的逼近
不过,攻击者可以每次只覆盖一个字节。 例如,假设getaddrinfo的返回地址为0x00007fff01020304:
我们首先只覆盖getaddrinfo()函数的返回地址最低有效位(LSB)的一字节。这里用0x00进行覆盖。由于在假设中getaddrinfo()的返回地址为0x00007fff01020304,将最低位覆盖为0x00,那么这里返回地址就会变为0x00007fff01020300,由于该地址是非法地址,函数返回后程序就会崩溃。于是我们继续重复上述操作,并且每次重复是LSB只加1(即第一次0x00,第二次0x01,第三次0x02,…),当我们将LSB增加到0x04时,getaddrinfo函数的返回地址为正确的返回地址0x00007fff01020304,此时程序不会崩溃,建立tcp连接。于是最低位的值便确定了。
接下来,我们重复上述整个操作,通过覆盖返回地址的两个字节(0x04 0x00)来枚举下一个字节,我们将返回地址的第一个字节设置为刚刚猜解出来的正确字节(0x04),于是我们只需要采取相同办法猜解第二个字节即可。猜解成功的标志和第一个字节一样,建立正确的连接。
接下来是第三个字节,第四个字节。。。
通过这种逐字节逼近的方法,我们最多只需要进行8*2^8次(每个字节最多2^8次猜解,总共8字节)尝试便可以得到正确的返回地址,这种方式在几秒内便可得到结果。
0x05 查找可利用的应用程序
http://codesearch.debian.net是一个包含超过18,000 Debian包的索引的网站。我们通过该网站查找所有调用 fork()和getaddrinfo()函数的应用程序(这些程序都是有可能进行利用的),发现超过1300个潜在的可利用的应用程序。 然后,进一步的我们需要检查每个应用程序的源代码,检查其流程是否适合我们的需要。
0x06 Tinyproxy
Tinyproxy是Linux下一个小型的http代理软件,通过审计,发现该应用程序的执行流程符合上述分析的执行流程。当它在响应HTTP连接请求时,会fork出一个子进程,然后调用getaddrinfo()函数来检索所请求的网站的IP地址。 然后使用connect()函数连接该主机获取网站内容。
1. 堆栈任意指针泄露
在下面的代码块中我们遇到了第一崩溃点:
rbx寄存器首先被覆盖,然后执行"mov BYTE PTR [rbx],sil"指令,该指令可释放对rbx指向的地址的指针。 rbx原先是指向栈上的,也就是说,如果我们采用逐字节逼近的方法,枚举其值使得该程序在堆栈上泄漏一个地址。
下图(output of /proc/PID/maps)显示了堆栈的边界。 正如图中所示,它的初始大小总是大于0x1000字节。
寄存器rbx指向的地址必须是可写的,否则会引发分段错误导致程序崩溃。然而它的缺陷在于,无论在哪个地址写入“sil”值,只要它是可写地址,程序流将正确地继续,这意味着对于rbx的低12位设置什么值根本无关紧要,因为由于堆栈的缘故它总是可读可写的。
所以我们只要在堆栈范围内泄露了任意一个指针。当我们必须精确定位时,堆栈变量指向的地址并不会对程序流产生什么影响。
2. 泄露栈基址
由于应用程序的流程总是相同的,所以栈的大小总是相同的。 这意味着我们可以依赖于栈基址到这些变量,结构体和缓冲区的偏移量进行定位。而且在这样的情况下,我们往往依赖于这样的常量偏移。所以首先应该得到栈基址。
由于我们已经得到一个在栈空间内的地址,所以泄露栈基址比枚举任意地址更简单。而且我们知道它拥有两个属性,一是栈基址与页面边界(0x1000)对齐,二是栈基址将是栈后面的第一个不可读的地址。
让我们假设堆栈基址在0x00007fffed008000。我们利用已经泄漏的任意堆栈地址,并将其对齐到页边界得到一个新的对齐地址,例如0x00007fffed000140对齐到0x00007fffed000000。然后,我们枚举堆栈基址,从这个对齐的地址开始进行覆盖,并在每次尝试之后递增0x1000(页大小)。在我们发送请求之后,等待一段时间,并检查服务器是否尝试连接到我们解析的IP。如果是,这意味着我们还没有达到堆栈基地。如果发生超时,说明服务器发生崩溃,我们达到了目标,获得了堆栈基址。
3. 堆栈偏移
在从getaddrinfo返回之前,程序会执行以下检查:
注意以红色突出显示的块。如果我们到达它并且传递一个无效的堆指针作为参数,应用程序崩溃,因为它试图释放(free()函数)一个无效的堆块。如果要绕过这个free()函数,r14和rdi必须相等。 r14指向原来的__alloca()函数堆栈缓冲区。 由于堆栈基址先前泄漏,并且__alloca()缓冲区与堆栈基址的偏移量应该是常量,因此我们不应该遇到任何问题。 然而,我们发现偏移在每次运行时略有不同。 为什么?
这涉及到内核代码对ASLR的处理问题,如下linux内核代码所示:
/arch/x86/kernel/process.c
观察上述内核代码,可以看到,如果ASLR被启用,每次堆栈分配时SP(堆栈指针)将减少一个随机数。这意味着在每次不同的运行时,在rsp和堆栈之间将存在一个随机增量。
幸运的是,这个增量非常小。我们可以轻松地枚举这个随机偏移。
看看上面的IDA代码片段,我们可以发现,如果rdi等于r14,程序将不会运行到释放rdi的那个分支。 因此,我们可以使用我们之前得到的堆栈基址,结合预先计算(即,如果该值于栈基址对齐程序将返回0),然后尝试所有其他2^9种可能性,便可得到此增量。
4. 泄漏LIBC模块地址
这部分的实现是超级简单的,因为我们可以使用前面提到的技术(byte-by-byte approach 逐字节逼近)来枚举返回地址的每个字节从而得到它。
5. 代码执行
剩下要做的就是构造一个ROP链,这是非常容易和直接。我们知道system()函数在libc的基址偏移的某个确定的位置,所以我们只需设置它的参数,并使用ROP调用它。
0x07 结论
在这项研究中,我们使用了Linux创建进程时的特性来绕过ASLR。这种技术同时也可以用于其他内存损坏漏洞的利用。因此,用户应始终尝试通过及时部署软件修补程序和更新来保护服务器。以保证我们在潜在威胁行动者行动前领先一步。