CVE-2015-7547
CVE-2015-7547,由google安全团队披露的glibc中getaddrinfo()函数产生的stack overflow漏洞,影响glibc版本2.9-2.22,产生漏洞的原因在于getaddrinfo在进行DNS查询的过程中,调用到send_dg()或者send_vc()的时候,由于逻辑错误导致标识buffer大小的变量和buffer的实际大小出现了不统一,进一步导致stack-overflow的发生。
这里有google团队给出的POC,但是保留了EXP——“We will not release our exploit code, but a non-weaponized Proof of Concept has been made available simultaneously with this blog post”。经过分析来看RCE是可行的,但是还需要去绕过一些保护机制和check。
漏洞复现
这个用户态的漏洞还是比较好复现的。首先我们需要获取一份未patch的存在漏洞的glibc,可以从GNU站点中下载,然后编译安装:
|
|
在google给出的poc中,一份python用来监控53端口模拟DNS服务器以产生构造的DNS响应包,另一份client简单调用getaddrinfo函数触发漏洞。
我们需要将google给出的poc中的client使用存在漏洞的glibc进行编译,需要参数rpath和dynamic-linker来指定glibc与对应的ld-linux:
|
|
然后修改本地dns服务器为127.0.0.1,对应的配置文件为/etc/resolv.conf。之后分别启动python文件和client,就可以看到崩溃:
|
|
可以用gdb确认一下崩溃位置:
|
|
具体分析
从gdb看到的调用栈可以看出,崩溃的调用栈是这样的:
|
|
其中,我们给getaddrinfo
函数传入的hints参数中,其ai_family为AF_UNSPEC,这个参数使得getaddrinfo
在后来会调用gethostbyname4_r()
来进行IPV4与IPV6的DNS的并发查询。我们要关注的重点就是gethostbyname4_r()
、__libc_res_nsend()
、send_dg()
。漏洞发生在send_dg
函数中。
函数调用栈中相关的变量关系大概是这样,我觉得搞明白这些变量的意义对于理解这个洞比较关键,但这些变量命名还是挺乱的,我们之后会逐步理清。另外还有一点是关于DNS的基础也是理解的前题,就是DNS一般来说是使用UDP进行请求与响应的,但是如果数据内容大于512bytes(取决于配置),DNS服务器在返回数据包中将会置位truncated flag,然后client将会重新使用TCP来进行本次DNS请求(当然也可以配置只使用TCP)。
首先,gethostbynamr4_r()
首先在栈上用alloca分配了2048 bytes的空间,同时也定义了ans2p、nans2p、resplen2,这几个变量是用于标识第二个数据包(因为我们需要发送IPV4和IPV6两个DNS请求,因此是有两个数据包,如果使用TCP的DNS请求的响应也分为两次回应的话)的在buffer中的位置、buffer大小以及对应response的大小。相关代码:
|
|
之后一路调用到__libc_res_nsend
,这个函数用于调用(或者是反复调用)send_dg
(处理UDP数据包的发送与接收)与send_vc
(处理TCP数据包的发送与接收)来完成DNS查询,也就是说send_dg
和send_vc
只负责单次的DNS请求(但是这个单次不一定就是只有一个数据包,根据需要IPV4还是IPV6还是都需要来决定 ,如果都需要就会在一次调用中完成IPV4与IPV6数据的请求与接收),如果出了什么问题比如timeout或者truncated都会直接返回由__libc_res_nsend
来决定是否继续发送及使用什么方式发送。
现在我们可以来看send_dg
函数中产生漏洞的地方,send_dg
函数主要是完成了数据包的发送与接收,漏洞出在接收数据包的代码中,我们可以跳过无关的发送代码。首先是定义了一些局部变量:
|
|
这三个变量用于标识用于接收当前数据包(IPV4的或IPV6)的buffer信息,分别是buffer大小、buffer指针、response长度。我们当前的情况是需要接收两个数据包,IPV4的查询响应与IPV6的查询响应,当接收第一个包的时候,if条件满足(buf2指向之前要发送的第二个数据包buffer,这里用来判断是否只需要接收一个包即可),进入这一段:
|
|
这一段代码是没有问题的,直接使用了传进来的stack中的2048bytes的buffer去接收数据。但是如果这个2048 bytes的buffer不够,会进入下一段代码使用malloc分配64k的空间进行接收:
|
|
其实单单看这一段代码是没有问题的,从对这个漏洞的patch也可以看出,只不过是换了一个host_buffer的指针去修改host_bufer指向newp,本质上在这里其实并没有什么改变:
|
|
其实问题是这样产生的,从上面给出的变量图可以看到,这里是想要host_buffer指向新的buffer以及修改anssiz来反映这个buffer的大小变化,然后让__libc_res_nsend
中的ans依然指向2048bytes因为如果第二个数据包比较小还依然能存在栈中,这个意图没有问题,实际上问题出在下面的关于准备第二个数据包的buffer相关变量中:
|
|
问题是出在关于orig_anssizp变量的处理上,这个变量是在send_dg
一开始这样赋值的:
|
|
结合第二个数据包的buffer准备就可以看到问题了,本来用来标识64k的heap中的buffer大小的变量却被用来标志2048bytes的栈buffer的大小,然后传递给recvfrom
来接受数据:
|
|
这个时候一旦接个大的数据包就有问题了,stack-overflow就产生了。从patch中也可以证明:
|
|
以及
|
|
可以看出patch的主要思路就是直接删除了在接受第二个数据包的时候对于栈上的buffer的考虑。
send_vc
中接受数据包的buffer处理基本和send_dg
相同。所以我们也就可以理清POC中的利用思路,首先先是UDP请求IPV4和IPV6的DNS响应,然后服务器返回一个较大的响应包并设置truncated flag,这个时候send_dg中接受到这个包看到TC标志就立刻设置相关变量并返回使得__libc_res_nsend
使用send_vc
继续(所以我觉得POC中的等到接受到再一次的TCP请求再将之前的UDP包返回来我是没搞明白其意义,这个时候client应该不会再理会这第二个UDP响应了,我尝试修改poc试了一下也确实没有问题,难道poc中这么做只是为了避免client可能的等待?),然后由于orig_anssizp的关系,第一个数据包使用栈中的buffer接受了,但是第二个大数据包到来的时候因为orig_anssizp太大而计算size错误导致了溢出。
还有一些
这个漏洞并不止google给出的POC中的一种触发姿势,只要能够先让malloc发生,然后立刻返回再send一次让orig_anssizp被改为64k就可以。比如也可以使用timeout的方式来完成。
但是POC离对这个CVE的RCE还有些距离,虽然没有canary,但除了要过ASLR和NX保护以外,还要过一些check,在gethostbyname4_r
返回前,host_buffer和ans2p这两个变量会被检查是否为null然后交给free()
,所以这里是个问题,但也不是不可能的,在特定场景下或者搭配其他漏洞应该是可以过去的,这里还需要进一步折腾。