安全路透社
当前位置:安全路透社 > 安全客 > 正文

【漏洞分析】CVE-2017-14491 dnsmasq 堆溢出分析

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

Larryxi@360GearTeam

0x00 问题背景


谷歌安全团队对dnsmasq进行了测试并发现了多个漏洞,其中的CVE-2017-14491是一个堆溢出漏洞,存在RCE的风险。不过其在相关博客中只给出了PoC脚本测试步骤和相关的报错asan,需要我们自己分析过程中的调用流程,进一步有可能开发出RCE的exp脚本。

攻击流程可分为三步:

1.攻击者伪造成为dnsmasq的上游DNS服务器,即运行PoC脚本。

2.攻击者在客户端向dnsmasq发送PTR请求,dnsmasq不存在相应PTR记录便向上游DNS查询,然后获得PTR的查询结果缓存并应答客户端。

3.攻击者再次在客户端向dnsmasq发送PTR请求,dnsmasq便解析展开之前的PTR记录,由于数据包的构建都在堆上,而且上游的恶意的PTR相应记录的大小超过了堆上分配的内存空间,最后造成了堆溢出。

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

攻击面初步猜想就是攻击者当控制上游DNS服务器后,通过配置特定的PTR响应和客户端的反向查询,即可实现对dnsmasq主机的远程代码执行。


0x01 调试环境


操作系统:Ubuntu 14.04 x86_64

软件版本:dnsmasq v2.75

PoC脚本:https://github.com/google/security-research-pocs 

Debugger:peda-gdb

另外需要说明的是:

1.关于存在漏洞的软件版本,根据谷歌的博客公告,dnsmaqs全部版本都存在此CVE堆溢出漏洞,并且早于2.76和用于此commit的版本堆溢出都没有限制,否则只能溢出两个字节。

2.软件可直接下载tar压缩包clone下来checkout相应版本,并在Makefile中加入-g选项编译安装,方便后续调试。

3.

sudo gdb dnsmasq

启动dnsmasq后使用

set args -p 53535 --no-daemon --log-queries -S 127.0.0.2 --no-hosts --no-resolv

设置启动参数进行调试。

4.dns请求默认是有重传机制,而且在调试过程中会中断程序,重传会导致程序重复执行某些代码影响调试,所以在dig时可指定不进行重传:

 

0x02 流程追踪


数据包的堆分配

谷歌给出的asan是基于2.78test2版本的dnsmasq,其中堆的分配是在dnsmasq.c中的safe_malloc函数:

调试的2.75版本也同样存在safe_malloc,只不过其内部使用的是malloc分配堆:

其中的daemon是全局可以访问的结构体,而daemon->packet主要是用于存储数据包内容的内存空间,通过safe_malloc会为其在堆上分配空间,为后续的数据包构建做准备。在dnsmasq.c:96处下断点,可以看到daemon->packet_buff_sz为0x1000大小:

malloc之后分配的堆空间起始地址为0x648f00:

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

接下来运行PoC看看程序崩溃时的环境:

其中有以下三点要注意:

1.PoC在执行第一次PTR查询时,程序没有产生崩溃,而是在在第二次崩溃。

2.在bt的输出中,#1的answer_request函数的参数中header为0x648f00是最开始为数据包分配的堆地址,同时limit为0x64900,两者相差正好0x1000,可能是限制堆溢出的操作。

3.那么问题来了:a.为什么第一次不会崩溃;b.为什么看似有限制但还是堆溢出了。

第一次查询

通过下断点得知第一次PTR反向查询过程中,首先也会调用dnsmasq.c:1004处的check_dns_listeners函数,然后将listener传入dnsmasq.c:1515处的reccive_query函数,在其定义处,可以看到局部变量header指针指向的就是构建数据包的那块堆的起始地址:

在后续的操作中,首先会在forward.c:1178处会接收udp请求,将请求数据包的内容存储在堆中:

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

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

继续跟进在forward.c:1398~1415行中是先本地查询,如果没有结果向上游DNS服务器查询:

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

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

所以在第一次查询中会进入dnamasq.c:1409的forward_query函数,在其内部对sendto函数或发送完数据包的522行处下断点,即可看到其在堆上构建的向上游服务器查询的数据包:

同样的思路,在dnsmasq发送完向上游DNS的PTR请求后,肯定要接收响应数据,所以对recvfrom函数下断点,即可知道其在dnsmasq.c:1510中会调用reply_query函数,在其内部首先会接收上游服务器的响应,数据包的存储也还是用的daemon->packet,但是在这里也使用recvfrom函数的参数来确定了接收数据包的长度和堆分配的长度一致,所以在存储时没有产生溢出: 

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

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

紧接着reply_query函数会对数据包头部进行整理,然后把得到的响应数据包通过send_from函数传给客户端,而且值得注意的是原始PoC中构造的DNS响应数据包的大小本身也是没有超过daemon->packet_buff_sz,即分配的堆空间大小:

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

第二次查询

第二次查询的前半部分和第一次查询类似,也是由check_dns_listeners进入receive_query函数,在dnsmasq接到客户端的第二次PTR请求后,还是会进过先调用answer_request函数然后经过forward_query函数对客户端响应。实际上和开头提到的一样,查询在进入answer_request函数就崩溃了,崩溃附近的源代码如下:

http://p5.qhimg.com/t01eb58a02d141260a0.png

当产生崩溃时,查看bt full得知anscount的值为0x51,即循环了81次后,再次调用cache_find_by_addr造成非法的内存引用产生崩溃。这里的源代码逻辑就是通过循环,调用cache_find_by_addr将cache保存至crecp指针中,并通过cache_get_name获取ptr记录的name,再调用add_resource_record添加记录,经过81此后在add_resource_record中产生堆溢出。

记录里的record cache应该是通过第一次查询的结果向内存中保存了相关的数据结构,观察cache.c文件后对cache_insert函数下断点,可得知第一次查询后的函数堆栈:

对应的源代码则是在第一次查询中,在把响应发送给客户端之前,调用process_reply函数,再其内部调用extract_addresses函数,通过遍历循环响应中的ancount,把记录中的name等信息cache_insert至crec结构体构成的双向链表中:

第一个cache_insert函数执行后,可知其crec地址为0x64a2d0,并且后续的crec结构体中都把PoC中向前引用的name给完全解析扩展开,这样就一下增大了响应数据包的大小,造成后续的堆溢出: 

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

溢出原因


具体跟进add_resource_record函数,可以定位到rfc1035.c:1440行的do_rfc1035_name函数,该函数类似于一个copy操作,就是把解析的域名放入响应数据包的RDATA字段,由于解析域名后的数据包就扩展的很大,超出了分配的堆空间,所以造成了溢出:

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

再次下断点b rfc1035.c:1855 if anscount == 0x51 ,查看在即将产生崩溃时的上下文环境。由于PoC的数据包在扩展解析后直接溢出到了接近0x64a300的位置,而在cache_find_by_addr函数的内部会访问到第一个crec结构体的地址0x64a2d0,由于该地址被Z字符溢出,所以最终造成了非法地址的引用:

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

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

但有趣的是,在add_resource_record函数的末尾是有对溢出的检查:

不过溢出的与否只是影响了返回值,而且在返回后影响的只是anscount变量是否加1,并未其他的安全处理,所以这里的安全检查就形同虚设了。

分析补充

经过胡牛的提示,其实上图中对于溢出的检测也是有点作用的,关键点是1476行的*pp = p 会把写入数据包的指针向后移动,以便后续的answer在数据包的写入,但是一旦发生溢出这个指针就不会移动,我们就只能从同一个位置开始反复地进行越界写,所以越界写的机会只有一次。

在2.76版本中,daemon->packet_buff_sz为5131字节,limit最大为4096字节,当我们溢出后继续写入一个answer,4096+12(answer头部大小)+1024(最大域名长度)-5131+1(最后的补0字节)=2,所以谷歌官方所说的越界2个字节是这么来的。


0x03 补丁分析


为了省事,这里就以最新版本的dnsmasq补丁看一下修复的原理,首先是定义了CHECK_LIMIT函数,如果指针和要写入的size超过了限制就直接跳转:

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

跳转之后直接返回,也就不能执行写入操作了:


0x04 总结


1.开始时使用PoC测试2.78test2版本没有产生崩溃,相关原因还有待测试探究。

2.调试的过程主要是关注函数的调用栈,在程序中下好断点,同时结合源码分析程序代码的逻辑,积极思考探讨找到问题所在。

3.该漏洞还需要根据堆溢出的环境来构建RCE的exp,但堆上临近的区域都是大型的结构体无法找相关函数指针覆盖,可能的思路是如果再次反向查询一个ptr,使服务器再次记录就有可能引起cache_unlink操作,感兴趣的同学可以探究一下。

4.关于此漏洞的防御,可以直接使用yum或者apt-get进行安全更新,也可以去官网下载最新版本的dnsmasq构建安装。


0x05 相关参考


https://security.googleblog.com/2017/10/behind-masq-yet-more-dns-and-dhcp.html 

http://thekelleys.org.uk/gitweb/?p=dnsmasq.git;a=summary 

https://yi-love.github.io/blog/node.js/javascript/dns/2016/11/11/dns-request.html 

https://tools.ietf.org/html/rfc1035 

http://bobao.360.cn/learning/detail/4515.html 


本文地址:http://bobao.360.cn/learning/detail/4598.html

未经允许不得转载:安全路透社 » 【漏洞分析】CVE-2017-14491 dnsmasq 堆溢出分析

赞 (0)
分享到:更多 ()

评论 0

评论前必须登录!

登陆 注册