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站点中下载,然后编译安装:

1
2
3
4
5
6
tar zxvf glibc-2.19.tar.gz
cd /glibc-2.19
mkdir build && cd build
../configure --prefix=/usr/local/glibc219/ --enable-debug CFLAGS="-g -O1" CPPFLAGS="-g -O1"
make
make install

在google给出的poc中,一份python用来监控53端口模拟DNS服务器以产生构造的DNS响应包,另一份client简单调用getaddrinfo函数触发漏洞。

我们需要将google给出的poc中的client使用存在漏洞的glibc进行编译,需要参数rpath和dynamic-linker来指定glibc与对应的ld-linux:

1
2
-Wl,-rpath=/path/to/new/glibc/lib
-Wl,-dynamic-linker=/path/to/newglibc/ld-linux.so.2

然后修改本地dns服务器为127.0.0.1,对应的配置文件为/etc/resolv.conf。之后分别启动python文件和client,就可以看到崩溃:

1
2
root@Tencent1:~/CVE-2015-7547$ ./client
Segmentation fault

可以用gdb确认一下崩溃位置:

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
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
root@Tencent1:~/CVE-2015-7547$ gdb client
GNU gdb (Debian 7.11.1-2) 7.11.1
Copyright (C) 2016 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law. Type "show copying"
and "show warranty" for details.
This GDB was configured as "x86_64-linux-gnu".
Type "show configuration" for configuration details.
For bug reporting instructions, please see:
<http://www.gnu.org/software/gdb/bugs/>.
Find the GDB manual and other documentation resources online at:
<http://www.gnu.org/software/gdb/documentation/>.
For help, type "help".
Type "apropos word" to search for commands related to "word"...
Reading symbols from client...(no debugging symbols found)...done.
gdb-peda$ r
Starting program: /root/CVE-2015-7547/client
Program received signal SIGSEGV, Segmentation fault.
[----------------------------------registers-----------------------------------]
RAX: 0x7fffffffe2a8 ('B' <repeats 150 times>, "lrouters")
RBX: 0x4242424242424242 ('BBBBBBBB')
RCX: 0xffffffffffffffff
RDX: 0x4242424242424242 ('BBBBBBBB')
RSI: 0x0
RDI: 0xffffffff
RBP: 0x7fffffffd0b0 --> 0x7ffff7ddc080 --> 0x200000001
RSP: 0x7fffffffcde0 --> 0x1000001045e
RIP: 0x7ffff741986c (<__GI___libc_res_nquery+1084>: movzx eax,BYTE PTR [rbx+0x3])
R8 : 0x24 ('$')
R9 : 0x7fffffffcc68 --> 0x7fffffffda40 --> 0x424242424242de8c
R10: 0x7fffffffc610 --> 0x0
R11: 0x206
R12: 0xbcc
R13: 0x7fffffffe2b0 ('B' <repeats 142 times>, "lrouters")
R14: 0x24 ('$')
R15: 0x7ffff7ddc080 --> 0x200000001
EFLAGS: 0x10206 (carry PARITY adjust zero sign trap INTERRUPT direction overflow)
[-------------------------------------code-------------------------------------]
0x7ffff7419859 <__GI___libc_res_nquery+1065>: lea rsi,[rip+0x8b29] # 0x7ffff7422389
0x7ffff7419860 <__GI___libc_res_nquery+1072>: lea rdi,[rip+0x8b81] # 0x7ffff74223e8
0x7ffff7419867 <__GI___libc_res_nquery+1079>: call 0x7ffff7415820 <__assert_fail@plt>
=> 0x7ffff741986c <__GI___libc_res_nquery+1084>: movzx eax,BYTE PTR [rbx+0x3]
0x7ffff7419870 <__GI___libc_res_nquery+1088>: and eax,0xf
0x7ffff7419873 <__GI___libc_res_nquery+1091>: jne 0x7ffff7419a5f <__GI___libc_res_nquery+1583>
0x7ffff7419879 <__GI___libc_res_nquery+1097>: movzx ecx,WORD PTR [rbx+0x6]
0x7ffff741987d <__GI___libc_res_nquery+1101>: ror cx,0x8
[------------------------------------stack-------------------------------------]
0000| 0x7fffffffcde0 --> 0x1000001045e
0008| 0x7fffffffcde8 --> 0x6f6f660300000000
0016| 0x7fffffffcdf0 --> 0x6f6f670672616203
0024| 0x7fffffffcdf8 --> 0x6d6f6303656c67
0032| 0x7fffffffce00 --> 0x1de8c01000100
0040| 0x7fffffffce08 --> 0x100
0048| 0x7fffffffce10 --> 0x726162036f6f6603
0056| 0x7fffffffce18 --> 0x3656c676f6f6706
[------------------------------------------------------------------------------]
Legend: code, data, rodata, value
Stopped reason: SIGSEGV
__GI___libc_res_nquery (statp=statp@entry=0x7ffff7ddc080 <_res@GLIBC_2.2.5>, name=0x400717 "foo.bar.google.com", class=class@entry=0x1,
type=type@entry=0xf371, answer=answer@entry=0x7fffffffda40 "\214\336", 'B' <repeats 198 times>..., anslen=anslen@entry=0x800,
answerp=0x7fffffffe2b0, answerp2=0x7fffffffe2a8, nanswerp2=0x7fffffffe2a4, resplen2=0x7fffffffe2a0) at res_query.c:262
262 if ((hp->rcode != NOERROR || ntohs(hp->ancount) == 0)
gdb-peda$ bt
#0 __GI___libc_res_nquery (statp=statp@entry=0x7ffff7ddc080 <_res@GLIBC_2.2.5>, name=0x400717 "foo.bar.google.com", class=class@entry=0x1,
type=type@entry=0xf371, answer=answer@entry=0x7fffffffda40 "\214\336", 'B' <repeats 198 times>..., anslen=anslen@entry=0x800,
answerp=0x7fffffffe2b0, answerp2=0x7fffffffe2a8, nanswerp2=0x7fffffffe2a4, resplen2=0x7fffffffe2a0) at res_query.c:262
#1 0x00007ffff7419c47 in __libc_res_nquerydomain (statp=statp@entry=0x7ffff7ddc080 <_res@GLIBC_2.2.5>, name=<optimized out>,
name@entry=0x400717 "foo.bar.google.com", domain=<optimized out>, domain@entry=0x0, class=class@entry=0x1, type=type@entry=0xf371,
answer=answer@entry=0x7fffffffda40 "\214\336", 'B' <repeats 198 times>..., anslen=0x800, answerp=0x7fffffffe2b0, answerp2=0x7fffffffe2a8,
nanswerp2=0x7fffffffe2a4, resplen2=0x7fffffffe2a0) at res_query.c:582
#2 0x00007ffff7419fbe in __GI___libc_res_nsearch (statp=0x7ffff7ddc080 <_res@GLIBC_2.2.5>, name=name@entry=0x400717 "foo.bar.google.com",
class=class@entry=0x1, type=type@entry=0xf371, answer=answer@entry=0x7fffffffda40 "\214\336", 'B' <repeats 198 times>...,
anslen=anslen@entry=0x800, answerp=0x7fffffffe2b0, answerp2=0x7fffffffe2a8, nanswerp2=0x7fffffffe2a4, resplen2=0x7fffffffe2a0)
at res_query.c:378
#3 0x00007ffff762b866 in _nss_dns_gethostbyname4_r (name=0x400717 "foo.bar.google.com", pat=0x4242424242424242,
buffer=0x4242424242424242 <error: Cannot access memory at address 0x4242424242424242>, buflen=0x4242424242424242, errnop=0x7fffffffe8ac, herrnop=0x7fffffffe880, ttlp=0x4242424242424242) at nss_dns/dns-host.c:314
#4 0x4242424242424242 in ?? ()
#5 0x4242424242424242 in ?? ()
#6 0x4242424242424242 in ?? ()
#7 0x4242424242424242 in ?? ()
#8 0x4242424242424242 in ?? ()
#9 0x4242424242424242 in ?? ()
#10 0x4242424242424242 in ?? ()
#11 0x4242424242424242 in ?? ()
#12 0x726c424242424242 in ?? ()
#13 0x000073726574756f in ?? ()
#14 0x0000000000000000 in ?? ()
gdb-peda$

具体分析

从gdb看到的调用栈可以看出,崩溃的调用栈是这样的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
getaddrinfo() /sysdeps/posix/getaddrinfo.c
|
gaih_inet() /sysdeps/posix/getaddrinfo.c
|
gethostbyname4_r() /resolv/nss_dns/dns-host.c
|
__libc_res_nsearch() /resolv/res_query.c
|
__libc_res_nquerydomain() /resolv/res_query.c
|
__libc_res_nquery() /resolv/res_query.c
|
__libc_res_nsend() /resolv/res_send.c
|
send_dg() /resolv/res_send.c
(send_vc() /resolv/res_send.c)

其中,我们给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)。

cve_2015_7547_0.png

首先,gethostbynamr4_r()首先在栈上用alloca分配了2048 bytes的空间,同时也定义了ans2p、nans2p、resplen2,这几个变量是用于标识第二个数据包(因为我们需要发送IPV4和IPV6两个DNS请求,因此是有两个数据包,如果使用TCP的DNS请求的响应也分为两次回应的话)的在buffer中的位置、buffer大小以及对应response的大小。相关代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
301 union
302 {
303 querybuf *buf;
304 u_char *ptr;
305 } host_buffer;
306 querybuf *orig_host_buffer;
307 host_buffer.buf = orig_host_buffer = (querybuf *) alloca (2048);
308 u_char *ans2p = NULL;
309 int nans2p = 0;
310 int resplen2 = 0;
311
312 int olderr = errno;
313 enum nss_status status;
314 int n = __libc_res_nsearch (&_res, name, C_IN, T_UNSPEC,
315 host_buffer.buf->buf, 2048, &host_buffer.ptr,
316 &ans2p, &nans2p, &resplen2);

之后一路调用到__libc_res_nsend,这个函数用于调用(或者是反复调用)send_dg(处理UDP数据包的发送与接收)与send_vc(处理TCP数据包的发送与接收)来完成DNS查询,也就是说send_dgsend_vc只负责单次的DNS请求(但是这个单次不一定就是只有一个数据包,根据需要IPV4还是IPV6还是都需要来决定 ,如果都需要就会在一次调用中完成IPV4与IPV6数据的请求与接收),如果出了什么问题比如timeout或者truncated都会直接返回由__libc_res_nsend来决定是否继续发送及使用什么方式发送。

现在我们可以来看send_dg函数中产生漏洞的地方,send_dg函数主要是完成了数据包的发送与接收,漏洞出在接收数据包的代码中,我们可以跳过无关的发送代码。首先是定义了一些局部变量:

1
2
3
1191 int *thisanssizp;
1192 u_char **thisansp;
1193 int *thisresplenp;

这三个变量用于标识用于接收当前数据包(IPV4的或IPV6)的buffer信息,分别是buffer大小、buffer指针、response长度。我们当前的情况是需要接收两个数据包,IPV4的查询响应与IPV6的查询响应,当接收第一个包的时候,if条件满足(buf2指向之前要发送的第二个数据包buffer,这里用来判断是否只需要接收一个包即可),进入这一段:

1
2
3
4
5
6
7
1194
1195 if ((recvresp1 | recvresp2) == 0 || buf2 == NULL) {
1196 thisanssizp = anssizp;
1197 thisansp = anscp ?: ansp;
1198 assert (anscp != NULL || ansp2 == NULL);
1199 thisresplenp = &resplen;
1200 } else {

这一段代码是没有问题的,直接使用了传进来的stack中的2048bytes的buffer去接收数据。但是如果这个2048 bytes的buffer不够,会进入下一段代码使用malloc分配64k的空间进行接收:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
1228 if (*thisanssizp < MAXPACKET
1229 /* Yes, we test ANSCP here. If we have two buffers
1230 both will be allocatable. */
1231 && anscp
1232 #ifdef FIONREAD
1233 && (ioctl (pfd[0].fd, FIONREAD, thisresplenp) < 0
1234 || *thisanssizp < *thisresplenp)
1235 #endif
1236 ) {
1237 u_char *newp = malloc (MAXPACKET);
1238 if (newp != NULL) {
1239 *anssizp = MAXPACKET;
1240 *thisansp = ans = newp;
1241 }
1242 }

其实单单看这一段代码是没有问题的,从对这个漏洞的patch也可以看出,只不过是换了一个host_buffer的指针去修改host_bufer指向newp,本质上在这里其实并没有什么改变:

1
2
3
4
- *anssizp = MAXPACKET;
- *thisansp = ans = newp;
+ *thisanssizp = MAXPACKET;
+ *thisansp = newp;

其实问题是这样产生的,从上面给出的变量图可以看到,这里是想要host_buffer指向新的buffer以及修改anssiz来反映这个buffer的大小变化,然后让__libc_res_nsend中的ans依然指向2048bytes因为如果第二个数据包比较小还依然能存在栈中,这个意图没有问题,实际上问题出在下面的关于准备第二个数据包的buffer相关变量中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
1200 } else {
1201 if (*anssizp != MAXPACKET) {
1202 /* No buffer allocated for the first
1203 reply. We can try to use the rest
1204 of the user-provided buffer. */
1205 #ifdef _STRING_ARCH_unaligned
1206 *anssizp2 = orig_anssizp - resplen;
1207 *ansp2 = *ansp + resplen;
1208 #else
... [...]
1214 #endif
1215 } else {
1216 /* The first reply did not fit into the
1217 user-provided buffer. Maybe the second
1218 answer will. */
1219 *anssizp2 = orig_anssizp;
1220 *ansp2 = *ansp;
1221 }
1222
1223 thisanssizp = anssizp2;
1224 thisansp = ansp2;
1225 thisresplenp = resplen2;
1226 }

问题是出在关于orig_anssizp变量的处理上,这个变量是在send_dg一开始这样赋值的:

1
1000 int orig_anssizp = *anssizp;

结合第二个数据包的buffer准备就可以看到问题了,本来用来标识64k的heap中的buffer大小的变量却被用来标志2048bytes的栈buffer的大小,然后传递给recvfrom来接受数据:

1
2
3
1246 *thisresplenp = recvfrom(pfd[0].fd, (char*)*thisansp,
1247 *thisanssizp, 0,
1248 (struct sockaddr *)&from, &fromlen);

这个时候一旦接个大的数据包就有问题了,stack-overflow就产生了。从patch中也可以证明:

1
- int orig_anssizp = *anssizp;

以及

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
@@ -1154,50 +1123,48 @@ send_dg(res_state statp,
assert (anscp != NULL || ansp2 == NULL);
thisresplenp = &resplen;
} else {
- if (*anssizp != MAXPACKET) {
- /* No buffer allocated for the first
- reply. We can try to use the rest
- of the user-provided buffer. */
-#if _STRING_ARCH_unaligned
- *anssizp2 = orig_anssizp - resplen;
- *ansp2 = *ansp + resplen;
-#else
- int aligned_resplen
- = ((resplen + __alignof__ (HEADER) - 1)
- & ~(__alignof__ (HEADER) - 1));
- *anssizp2 = orig_anssizp - aligned_resplen;
- *ansp2 = *ansp + aligned_resplen;
-#endif
- } else {
- /* The first reply did not fit into the
- user-provided buffer. Maybe the second
- answer will. */
- *anssizp2 = orig_anssizp;
- *ansp2 = *ansp;
- }
-
thisanssizp = anssizp2;
thisansp = ansp2;
thisresplenp = resplen2;
}

可以看出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(),所以这里是个问题,但也不是不可能的,在特定场景下或者搭配其他漏洞应该是可以过去的,这里还需要进一步折腾。