【技术分享】如何通过CVE-2015-7547(GLIBC getaddrinfo)漏洞绕过ASLR

阅读量148968

|

发布时间 : 2017-02-20 11:07:38

x
译文声明

本文是翻译文章,文章来源:paloaltonetworks.com

原文地址:http://researchcenter.paloaltonetworks.com/2016/05/how-cve-2015-7547-glibc-getaddrinfo-can-bypass-aslr/

译文仅供参考,具体内容表达以及含义原文为准。

https://p2.ssl.qhimg.com/t0115d49e7d1d7212e5.jpg

翻译:uglyB0y

预估稿费:200RMB

投稿方式:发送邮件至linwei#360.cn,或登陆网页版在线投稿

0x01 前言

2016年2月16日,Google披露了一个重要的缓冲区溢出漏洞,该漏洞在GLIBC库中的getaddrinfo函数中触发。同时他们还提供了一份PoC。基于此,在本文中,我们将展示如何通过CVE-2015-7547绕过ASLR。


0x02 漏洞描述

getaddrinfo()函数的作用是通过查询DNS服务将主机名和服务解析为addrinfo结构体。

http://p7.qhimg.com/t01cb91787a6e15178c.png

在getaddrinfo()函数实现中,使用了alloca()函数(在堆栈上分配缓冲区)对DNS进行响应。开始的时候,该函数首先分配一段栈空间用于DNS响应, 如果响应时间过长,它将重新分配一个堆缓冲区用于响应。但由于更新代码将缓冲区为新分配的堆缓冲区后,旧的堆栈缓冲区未及时释放,仍旧再使用。 这个悬空指针便造成了一个经典的缓冲区溢出。

ASLR? ASLR!

在上述情况下,可以通过这个漏洞覆盖getaddrinfo()函数的返回地址,但是我们应该将返回地址覆盖到哪里呢?在启用ASLR的系统中,模块地址是随机的。因此,攻击者不能将攻击流地址设置为预先设定的地址。

fork()

fork()是Linux中创建新进程的方法。一个典型的fork使用方法如下图所示:

http://p8.qhimg.com/t01ebdab8a844203a3a.png

fork出的子进程和父进程使用的相同的指令段,他们只有pid不相同,而pid是fork函数返回给子进程的。和windows下的代码复用的区别在于,这里意味着子进程与其父进程共享许多特性–具有相同的寄存器状态,堆栈和内存布局。


0x03 程序流程示例

考虑一个服务器应用程序,其运行模式如下:

http://p6.qhimg.com/t011269f93e18374e33.png

1.   客户端远程连接到应用程序。

2.   应用程序自己fork一个子进程用于响应客户端请求

3.   在处理客户端请求的过程中,子进程使用"getaddrinfo()"函数解析主机名。 同时,它向其DNS服务器发送DNS请求。

4.   DNS服务器对DNS请求做出一个合法响应。

5.   子进程启动与已被解析的主机的连接。

每次主进程进行响应处理时,它都会自己fork一个子进程。根据前面的描述,这意味着所有子进程将共享相同的内存布局–包括加载模块的地址。 这种场景对于许多服务(例如HTTP代理,电子邮件服务器或DNS服务器)是非常常见的。


0x04 攻击流程示例

在实施攻击的过程中,我们假设攻击者具有能够响应受害者的任意DNS请求的能力。实现这种情形完全可以通过ARP欺骗、DNS欺骗完成。攻击场景如下图:

http://p0.qhimg.com/t0159674e5e22d3281c.png

1. 一个攻击者构造一个请求发送给受害服务器

2. 为了响应攻击者的请求,受害服务器的守护进程fork出一个子进程

3. 子进程处理请求时,发起一个DNS请求

4. 攻击者回复一个恶意的DNS响应,该响应将子进程的返回地址覆盖,在这里,我们将其设置未0x12121212

5. 攻击者获得子进程用connect()函数发起的TCP回连

如果0x12121212确实是getaddrinfo()的正确返回地址,那么该进程将正常运行,并通过connect()发起tcp连接。

http://p9.qhimg.com/t01a8dc5880ba117582.png

如果不是这种情况,并且攻击者将返回地址写为其他任何地址,则应用程序将由于内存段错误或执行无效的指令而崩溃。

http://p1.qhimg.com/t016c8c6a634665c30b.png

这种方式可以作为判断一个地址是否为getaddrinfo()的返回地址的一种方法,原因在于如果地址正确,那么一个TCP连接将会成功建立。由于模块的基址在不同的子进程中是没有随机化(前面提到的公用内存布局),于是这个地址在所有的子进程中可以通用。一个攻击者可以使用这种方式去遍历每一个可能的地址,知道正确建立TCP连接而获得正确的地址。

然而,采用这种方式进行基址定位需要猜解2的64次方的数量的地址,这并没有太大的现实意义。

逐字节的逼近

不过,攻击者可以每次只覆盖一个字节。 例如,假设getaddrinfo的返回地址为0x00007fff01020304:

http://p6.qhimg.com/t014747040b7dc1ac0c.png

http://p9.qhimg.com/t019e5af13616cca243.png

我们首先只覆盖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. 堆栈任意指针泄露

在下面的代码块中我们遇到了第一崩溃点:

http://p2.qhimg.com/t0102e6b7aa43ae94c2.png

rbx寄存器首先被覆盖,然后执行"mov BYTE PTR [rbx],sil"指令,该指令可释放对rbx指向的地址的指针。 rbx原先是指向栈上的,也就是说,如果我们采用逐字节逼近的方法,枚举其值使得该程序在堆栈上泄漏一个地址。

下图(output of /proc/PID/maps)显示了堆栈的边界。 正如图中所示,它的初始大小总是大于0x1000字节。

http://p2.qhimg.com/t017713cca2ed5c5798.png

寄存器rbx指向的地址必须是可写的,否则会引发分段错误导致程序崩溃。然而它的缺陷在于,无论在哪个地址写入“sil”值,只要它是可写地址,程序流将正确地继续,这意味着对于rbx的低12位设置什么值根本无关紧要,因为由于堆栈的缘故它总是可读可写的。

所以我们只要在堆栈范围内泄露了任意一个指针。当我们必须精确定位时,堆栈变量指向的地址并不会对程序流产生什么影响。

2. 泄露栈基址

由于应用程序的流程总是相同的,所以栈的大小总是相同的。 这意味着我们可以依赖于栈基址到这些变量,结构体和缓冲区的偏移量进行定位。而且在这样的情况下,我们往往依赖于这样的常量偏移。所以首先应该得到栈基址。

由于我们已经得到一个在栈空间内的地址,所以泄露栈基址比枚举任意地址更简单。而且我们知道它拥有两个属性,一是栈基址与页面边界(0x1000)对齐,二是栈基址将是栈后面的第一个不可读的地址。

http://p6.qhimg.com/t01c2d9daff106fc655.png

让我们假设堆栈基址在0x00007fffed008000。我们利用已经泄漏的任意堆栈地址,并将其对齐到页边界得到一个新的对齐地址,例如0x00007fffed000140对齐到0x00007fffed000000。然后,我们枚举堆栈基址,从这个对齐的地址开始进行覆盖,并在每次尝试之后递增0x1000(页大小)。在我们发送请求之后,等待一段时间,并检查服务器是否尝试连接到我们解析的IP。如果是,这意味着我们还没有达到堆栈基地。如果发生超时,说明服务器发生崩溃,我们达到了目标,获得了堆栈基址。

3. 堆栈偏移

在从getaddrinfo返回之前,程序会执行以下检查:

http://p6.qhimg.com/t0154204a5efd60c338.png

注意以红色突出显示的块。如果我们到达它并且传递一个无效的堆指针作为参数,应用程序崩溃,因为它试图释放(free()函数)一个无效的堆块。如果要绕过这个free()函数,r14和rdi必须相等。 r14指向原来的__alloca()函数堆栈缓冲区。 由于堆栈基址先前泄漏,并且__alloca()缓冲区与堆栈基址的偏移量应该是常量,因此我们不应该遇到任何问题。 然而,我们发现偏移在每次运行时略有不同。 为什么?

这涉及到内核代码对ASLR的处理问题,如下linux内核代码所示:

/arch/x86/kernel/process.c

http://p4.qhimg.com/t01d92df8514db9812c.png

观察上述内核代码,可以看到,如果ASLR被启用,每次堆栈分配时SP(堆栈指针)将减少一个随机数。这意味着在每次不同的运行时,在rsp和堆栈之间将存在一个随机增量。

幸运的是,这个增量非常小。我们可以轻松地枚举这个随机偏移。

看看上面的IDA代码片段,我们可以发现,如果rdi等于r14,程序将不会运行到释放rdi的那个分支。 因此,我们可以使用我们之前得到的堆栈基址,结合预先计算(即,如果该值于栈基址对齐程序将返回0),然后尝试所有其他2^9种可能性,便可得到此增量。

4. 泄漏LIBC模块地址

这部分的实现是超级简单的,因为我们可以使用前面提到的技术(byte-by-byte approach 逐字节逼近)来枚举返回地址的每个字节从而得到它。

5. 代码执行

剩下要做的就是构造一个ROP链,这是非常容易和直接。我们知道system()函数在libc的基址偏移的某个确定的位置,所以我们只需设置它的参数,并使用ROP调用它。


0x07 结论

在这项研究中,我们使用了Linux创建进程时的特性来绕过ASLR。这种技术同时也可以用于其他内存损坏漏洞的利用。因此,用户应始终尝试通过及时部署软件修补程序和更新来保护服务器。以保证我们在潜在威胁行动者行动前领先一步。

本文翻译自paloaltonetworks.com 原文链接。如若转载请注明出处。
分享到:微信
+10赞
收藏
安全小子
分享到:微信

发表评论

内容需知
  • 投稿须知
  • 转载须知
  • 官网QQ群8:819797106
  • 官网QQ群3:830462644(已满)
  • 官网QQ群2:814450983(已满)
  • 官网QQ群1:702511263(已满)
合作单位
  • 安全客
  • 安全客
Copyright © 北京奇虎科技有限公司 360网络攻防实验室 安全客 All Rights Reserved 京ICP备08010314号-66