【CVE分析】CVE-2017-14492 dnsmasq堆缓冲区溢出漏洞
dnsmasq 堆缓冲区溢出漏洞
从 exploit-db 上搜到了这个漏洞,最近看了 dnsmasq 所以来分析一下。
漏洞详情
- https://www.linuxidc.com/Linux/2017-10/147397.htm
- https://security.googleblog.com/2017/10/behind-masq-yet-more-dns-and-dhcp.html
Dnsmasq 是轻型 DNS 转发器和 DHCP 服务器。Google 安全团队对 dnsmasq 进行测试后,共发现了 7 个漏洞,披露如下:
CVE | 影响 | Vector | 备注 | PoC |
---|---|---|---|---|
CVE-2017-14491 | RCE | DNS | 基于堆的溢出(2字节)。2.76之前,此提交溢出不受限制。 | PoC、说明和ASAN报告 |
CVE-2017-14492 | RCE | DHCP | 基于堆的溢出。 | PoC、说明和ASAN报告 |
CVE-2017-14493 | RCE | DHCP | 基于堆栈的溢出。 | PoC、说明和ASAN报告 |
CVE-2017-14494 | 信息泄露 | DHCP | 可以帮助绕过 ASLR。 | PoC和说明 |
CVE-2017-14495 | OOM/DoS | DNS | 这里缺少 free()。 | PoC和说明 |
CVE-2017-14496 | DoS | DNS | 这里没有边界检查。整数下流导致一个巨大的memcpy。 | PoC、说明和ASAN报告 |
CVE-2017-13704 | DoS | DNS | 与CVE-2017-13704的漏洞产生碰撞 |
这里主要对 CVE-2017-14492 进行分析。本质上看,CVE-2017-14492 利用了 dnsmasq 在处理 NDP 邻居发现协议的数据包时,option 字段可写入的数据长度大于了 dnsmasq 内部定义的缓冲区大小,从而导致了缓冲区溢出。由于写入的字符被转义,目前来看难以实现远程代码执行。
漏洞复现
环境搭建
Google 团队将 poc 等相关内容放在了 github 上:https://github.com/google/security-research-pocs/tree/master/vulnerabilities/dnsmasq 。接下来按照 poc 的内容来复现该环境。
我在 ubuntu 16.04 下复现该环境。
由于已经给出了复现环境使用的 dockerfile,我一开始使用这个 dockerfile 做复现,但是中间遇到了很多问题:
FROM debian
版本:Dockerfile 里没有指明版本,我在运行 apt-get 的时候参数--force-yes
不支持;安装相关的包时也出现了版本不支持的问题。(最主要的是,我没怎么用过 debian,不知道应该选择 debian 的哪个版本来进行安装)RUN git clone git://thekelleys.org.uk/dnsmasq.git
dnsmasq 版本:并未指明 dnsmasq 的版本,目前下载将编译最新版,当然没有漏洞了。。- 安装了 ASAN:可能用于内存分析,但是我并不想用这个软件。
基于这些原因,我决定直接在 ubuntu 16.04 下手动编译安装 dnsmasq 2.76 版本。当然还是使用 Clion 调试。我喜欢图形化界面。
1 | wget https://thekelleys.org.uk/dnsmasq/dnsmasq-2.76.tar.gz |
用 clion 打开这个目录。(新版本的 clion 支持了 Makefile 管理工程,就不需要额外的操作了)
修改 Makefile 中的 PREFIX 路径:
1 | # old: PREFIX = /usr/local |
修改 Makefile 中的 CFLAGS 参数,添加调试信息:
1 | # old: CFLAGS = -Wall -W -O2 |
然后直接 make
即可在 src 目录下看到可执行文件 dnsmasq。
漏洞测试
查看 dnsmasq 的帮助文档:
1 | ubuntu@ubuntu:~/Desktop/cve-2017-14492-dnsmasq/dnsmasq-2.76/src$ ./dnsmasq --help | grep "config" |
可以用 -C
来指定参数的位置。
编写 dnsmasq.conf 如下:
1 | interface=ens33 |
编写 resolv.dnsmasq.conf 如下:
1 | nameserver 127.0.0.1 |
将两个配置文件都放在 /tmp 目录下。然后启动 dnsmasq:
1 | sudo ./dnsmasq -k -C /tmp/dnsmasq.conf --no-daemon --dhcp-range=fd00::2,fd00::ff --enable-ra |
在另一个 terminal 运行 poc:
1 | sudo python 14492.py ::1 |
此时,dnsmasq 触发崩溃:
漏洞分析
接下来分析程序是如何崩溃的。
分析之前的准备
先看看 poc 都做了什么:
1 | from struct import pack |
脚本构造了一个数据包,发送给 dnsmasq,然后 dnsmasq 就崩溃了。构造的数据包内容是 NDP 报文(邻居发现协议,neighbor discovery protocol)。
参考链接:
- 下一代IP协议—IPv6邻居发现协议(NDP) (baidu.com)
- 为什么在IPv6中ARP被NDP取代? (qastack.cn)
- 11.NDP协议分析与实践_百里奔跑-CSDN博客_ndp协议
NDP 协议与 ARP 协议
邻居发现协议用于替代 IPv4 的 ARP 协议,并且添加了新的功能。ARP 存在一些问题:
- ARP 欺骗
- MAC 地址泛洪:arp 协议保存物理地址到 ip 地址的映射,但是存满之后,不知道如何处理的数据包会广播到所有地址;
- MAC 复制:在 MAC 复制攻击中,交换机会误以为两个端口具有相同的 MAC 地址。
NDP 协议是基于 ICMPv6 报文实现的。所以 NDP 是三层协议(网络层)。下面介绍 NDP 协议的相关功能。
NDP 协议的地址解析功能
对应 arp 协议的功能。主机A 要和主机 B 通信,需要知道主机B的二层地址。
地址解析使用两种报文,邻居请求 NS(neighbor solicitation),邻居通告 NA(neighbor advertisement)。从下图可以看到,本质上使用广播的方式来确认对方的 MAC 地址。
NDP 报文格式
- 类型 : 消息类型, RS(路由请求) 固定为 133,RA(路由通告)固定为 134
- 代码 : 发送者固定为 0,接收者忽略
- 校验和 : 用于校验 ICMPv6 和部分 IPv6 首部完整性
- 选项 : 源链路层地址选项,发送者的链路层地址,如果知道的话
参考一个正常的 NDP 请求:
可以看到,Option 字段 Type = 1 时表示 Source link-layer address,长度为 8 字节(mac 地址是 6 字节,Length 是 8 字节对齐,1 表示 8 字节)。所以下面构造的数据包中,link-layer addr 的长度设置为了 255 字节。
1 | # ND_ROUTER SOLICIT = 133 |
构建调试环境
参考文章 CSDN: Clion编译调试Makefile项目,在 Custom Build Target 中添加一项:(例如 build_target ):
build 和 clean 的语句留空即可。保存并编译工程后,在 run 处就能看到对应的目标了:
然后配置调试项:
选择 all 而不是 dnsmasq(如果选 dnsmasq 能用的话应该也可以)
在 Executable 选项选择 src 目录下编译得到的可执行文件 dnsmasq
Program arguments 填写前面给出的参数:
1
-k -C /tmp/dnsmasq.conf --no-daemon --dhcp-range=fd00::2,fd00::ff --enable-ra
勾选“使用 root 权限运行”,否则会报权限错误
分析漏洞
以调试模式启动 dnsmasq。然后运行 poc,Clion 检测到程序崩溃。切换到 gdb 命令行窗口输入 gcore /tmp/core-dnsmasq
将当前崩溃信息导出 coredump。使用 gdb -c /tmp/core-dnsmasq
查看 coredump 内容:
可以很明显看到,rax 的值是非法地址,引用地址时出错。Clion 给出的函数调用栈为:
在 rdav.c 中的 icmp6_packet()
函数中,在使用 bridge 指针时,bridge 指针的值是非法地址(213 行),如下:
1 | for (bridge = daemon->bridges; bridge; bridge = bridge->next) |
因此单步调试 icmp6_packet()
函数:
188 行及后续内容如下,可以看出是在检查 NDP 报文的对应内容。
1 | if (packet[1] != 0) // 判断是否是一个NDP请求,RS的该标志位都是0 |
packet 对应的就是 ndp 报文:
1 | (gdb) x/20xg packet |
继续向后看:
1 | else if (packet[0] == ND_ROUTER_SOLICIT) |
packet[8]
对应的是 poc 设置的 ICMP6_OPT_SOURCE_MAC
标志位, packet[9]
对应 poc 设置的 size 位,对应的大小是 255*8 字节。
print_mac()
函数的功能,是将 namebuff 中保存的 mac 地址转换为可读格式:
1 | // in print_mac() util.c, line 567 |
然而,在 option.c 文件的 4495 行,定义了 daemon->namebuff
缓冲区,可以知道 namebuff 的长度是 1025 字节。
1 | // #define MAXDNAME 1025 |
显然,构造的数据包 255*8 字节的数据保存到 namebuff 之后,造成了缓冲区溢出。
本来觉得能够溢出,就能看看能不能写写指针,但是写入的数据都被 %.2x
转义了。感觉利用无望啊。