原址:https://www.sdk.cn/news/2189
Glibc中处理DNS查询的代码中存在栈溢出漏洞,远端攻击者可以通过DNS服务回应特定构造的DNS响应数据包,导致glibc相关的应用程序crash或者利用栈溢出运行任意代码。应用程序调用使用getaddrinfo 函数将会收到该漏洞的影响。该漏洞编号CVE-2015-7547。
◆◆◆
CentOS6 所有版本
CentOS7 所有版本
SUSE Linux Enterprise Server 11 SP3
SUSE Linux Enterprise Server 12
Ubuntu Server 14.04.1 LTS 32位
Ubuntu Server 14.04.1 LTS 64位
Ubuntu Server 12.04 LTS 64位
Debian 8.2 32位
Debian 8.2 64位
Debian 7.8 32位
Debian 7.8 64位
Debian 7.4 64位
CoreOS 717.3.0 64位
◆◆◆
修复方法请点击文末的“阅读原文”获取。
PS: 目前腾讯云提供的基础操作系统镜像 CentOS系列、Ubuntu 系列、Debian 系列都已修复该问题,新创建的云服务器不存在该漏洞。腾讯云提供的更新源已经提供了修改该漏洞的glibc版本,对于已经运行的云服务器可以通过手动更新glibc修复。
◆◆◆
要搞清楚漏洞的产生,我们需要从代码层面分析一下这个漏洞。
首先从getaddrinfo函数开始,getaddrinfo函数在resolv/nss_dns/dns-host.c中,调用了alloca 在栈上分配了2048个字节。
host_buffer.buf = orig_host_buffer = (querybuf *) alloca (2048);
int n = __libc_res_nsearch (&_res, name, C_IN, T_UNSPEC,
host_buffer.buf->buf, 2048, &host_buffer.ptr,
&ans2p, &nans2p, &resplen2);
alloca 函数在栈分配2048个字节,用于存放DNS服务器的响应数据。函数还传递了ans2p,nans2p,resplen2三个参数,存放DNS服务器的响应数据。ans2p用于返回存放第二份响应数据包缓冲区地址,nans2p返回存放第二份响应数据包的扩充缓冲区的大小,resplen2返回第二份响应数据包数据包的大小。函数的返回值是第一份响应数据包的大小。
getaddrinfo函数如果传递的是AF_UNSPEC参数的话,会同时进行IPv4,IPv6的查询,分配组建IPv4和IPv6的数据包发送和接受。
__libc_res_nsearch继续调用__libc_res_nquery函数。__libc_res_nquery在调用__libc_res_nsend函数。__libc_res_nsend用来发送和接受DNS相关的数据包。
__libc_res_nsend(statp, query1, nquery1, query2, nquery2, answer,
anslen, answerp, answerp2, nanswerp2, resplen2)
__libc_res_nsend中会调用send_vc和send_dg函数和DNS服务器交互,TCP场景下调用send_vc,UDP的话调用send_dg。本文以send_dg为例进行分析。
send_dg(statp, buf, buflen, buf2, buflen2,
&ans, &anssiz, &terrno,
ns, &v_circuit, &gotsomewhere, ansp,
ansp2, nansp2, resplen2)
send_dg首先调用__sendmmsg发送buf, buflen, buf2, buflen2中的数据包到DNS服务器。buf, buflen, buf2, buflen2是对应的查询消息。再调用recvfrom接受从DNS的响应包,而问题就出现在这段代码。
首先分析send_dg函数中recvfrom的使用,使用thisansp变量标识接受数据缓冲区的地址,使用thisanssizp变量标识接受数据缓冲区的大小。
*thisresplenp = recvfrom(pfd[0].fd, (char*)*thisansp,
*thisanssizp, 0,
(struct sockaddr *)&from, &fromlen);
继续分析send_dg对thisansp,thisanssizp变量的处理逻辑。
第一次收到数据包,使用之前在栈上分配的2048个字节,代码处理如下:
if ((recvresp1 | recvresp2) == 0 || buf2 == NULL) {
thisanssizp = anssizp;
thisansp = anscp ?: ansp;
assert (anscp != NULL || ansp2 == NULL);
thisresplenp = &resplen;
}
当第二次收到数据包时,处理如下:
if (*anssizp != MAXPACKET) {
/* 判断第一个缓冲区长度anssizp 是否是65536,如果不是,表示2048个缓冲区在接受第一个数据包的时候足够,因此第二个缓冲区继续使用2048个缓冲区的剩余部分*/
*anssizp2 = orig_anssizp - resplen;
*ansp2 = *ansp + resplen;
} else {
/* anssizp 等于65536,说明之前的接受数据包大于2048个字节,栈中分配的空间不足以存放,但是第二次查询的数据有可能小于2048,因此尝试使用栈中的内存保存响应包*/
*anssizp2 = orig_anssizp;
*ansp2 = *ansp;
}
/*修改thisanssizp thisansp thisresplenp,分别表示调用recvfrom函数接受缓冲区的大小,接受缓冲区地址,接受到的数据包长度*/
thisanssizp = anssizp2;
thisansp = ansp2;
thisresplenp = resplen2;
if (*thisanssizp < MAXPACKET
/* 判断接受的数据包,thisanssizp 是否足够存放,如果不够的话,调用malloc 从堆上分配65536个字节,用来存放接受到的数据包*/
&& anscp
&& (ioctl (pfd[0].fd, FIONREAD, thisresplenp) < 0
|| *thisanssizp < *thisresplenp)) {
u_char *newp = malloc (MAXPACKET);
if (newp != NULL) {
*anssizp = MAXPACKET; /*修改anssizp 变量表示已经从堆上分配了内存*/
*thisansp = ans = newp; /*修改thisansp 变量,本地recvfrom使用新分配的内存进行存放*/
}
}
而问题就出现在这段代码上,存在两个问题:
1.在使用新分配的内存是,修改了thisansp变量,但是没有修改thisanssizp 变量为新分配的malloc内存的大小。
2.更新了anssizp 标识第一个缓存区的大小,但是没有更新ansp变量,ansp还是指向之前在栈上分配的2048个字节。
考虑如下场景:
* 第一次的DNS响应数据包是2048,正好使用完了在栈上分配的2048个字节.。
* 接受第二次DNS响应数据包,由于之前第一个数据包已经使用完2048个字节,所以代码会走到malloc流程从堆上分配内存,但是由于前面提到的bug,thisanssizp 没有被更新,而thisanssizp 在这种场景下为0,会导致recvfrom返回失败,导致send_dg直接退出。这个时候,ansp指向栈上的2048个内存区,但是anssizp被修改为65536。
* send_dg再次被调用,这个时候接受第三个DNS响应数据包,ansp指向栈上的2048个内存区,anssizp被修改为65536. 这个时候如果接受超过2048个数据包,会导致栈溢出。因此攻击者可以构造一个65536的数据包,前面2048个字节是正规的DNS数据,后面63487 个字节利用栈溢出执行自己的代码。
◆◆◆
结论
触发该漏洞的触发条件:
* 程序调用getaddrinfo函数,传递AF_UNSPEC参数进行DNS查询。
* DNS服务器第一次响应2048字节的数据包。
* DNS服务器响应第二个数据包,触发send_dg出错退出.
* DNS服务器响应第三个数据包,该数据包可以被攻击者利用,前2048包含有效的DNS响应数据包,后63487个字节可以作为栈溢出漏洞利用,执行攻击者自己的代码。