转载

CVE-2015-7547简单分析与调试

0x00 漏洞信息

最近glibc有一个栈溢出的漏洞具体情况,漏洞的具体信息可以参考下面链接。

CVE-2015-7547: glibc getaddrinfo stack-based buffer overflow

poc在github上:https://github.com/fjserna/CVE-2015-7547

0x01 环境准备

操作系统:ubuntu15.04 glibc版本:glibc-2.2.0 

1.1 glibc源码编译

在ubuntu系统下,只需要执行源码和调试符的命令之后就可以使用gdb对glibc的跟踪调试,安装指令如下:

sudo apt-get install libc6-dbg sudo apt-get source libc6-dev 

但是因为系统自带的glibc是 发行版 的,所以在编译的是时候选用了优化参数 -O2 ,所以在调试的过程中会出现 变量被优化无法读取 以及代码运行的时候与 源码的行数 对不上的情况。

所以需要自己编译一个可调式并且没有过度优化的glibc来进行调试。

首先,从glibc的官网下载glibc的源码。我选择了2.20的版本。编译安装glibc的方法很容易可以在网上找到。需要注意的是在进行configure时需要设置一些特殊的参数。如果需要调试宏可以添加 -gdwarf-2,glibc无法使用-O0编译,不过-O1也够用了。

/opt/glibc220/configure --prefix=/usr/local/glibc220/ --enable-debug CFLAGS="-g -O1" CPPFLAGS = "-g -O1" 

configure 执行完成之后只需要简单执行编译与安装就好了。

sudo make sudo make install 

1.2 使用调试版本glibc编译POC

在glibc编译安装成功后,系统默认的glibc还是原来的那个。所以需要选择指定的glibc来编译POC代码。

gcc -o client CVE-2015-7547-client.c -Wl,-rpath /usr/local/glibc220 

通过ldd指令可以看到,确实使用了刚编的glibc。 CVE-2015-7547简单分析与调试

这个时候就可以用GDB调试glibc中的函数了。

1.3 配置本地dns服务器

运行poc的python服务器。修改 /etc/resolv.conf 。将域名服务器改为127.0.0.1就好了。不过这样一来这台机器访问网络就会出问题了。

nameserver 127.0.0.1 

0x02 漏洞分析

2.1 运行POC

使用gdb启动客户端直接运行,出现崩溃堆栈。

CVE-2015-7547简单分析与调试

2.2 寻找溢出函数

可以看到栈都被覆盖为0x42424242,根据 google提供的分析 ,出问题的是send_dg和send_vc函数。分别在send_vc和send_dg上下断点,重新运行程序,会发现先调用send_dg函数再调用send_vc函数。

CVE-2015-7547简单分析与调试

可以看出是在send_vc的时候发生了栈溢出。

因为根据 google提供的分析 可以知道是在读取socket的时候发生的溢出,可以通过结合源码调试来分析。剔除不需要看的代码,核心代码如下,总共干了四件事。

[1]选择适当的缓存 [2]读取dns包的长度 [3]读取dsn包 [4]判断是否需要读取第二个数据包。 
#!c static int send_vc(res_state statp,     const u_char *buf, int buflen, const u_char *buf2, int buflen2,     u_char **ansp, int *anssizp,     int *terrno, int ns, u_char **anscp, u_char **ansp2, int *anssizp2,     int *resplen2, int *ansp2_malloced) {     const HEADER *hp = (HEADER *) buf;     const HEADER *hp2 = (HEADER *) buf2;     u_char *ans = *ansp;     int orig_anssizp = *anssizp;      [...]                               //这段干的事情可以无视。                              read_len:     //----------------[2]-------------start----------------     cp = (u_char *)&rlen16;     len = sizeof(rlen16);     while ((n = TEMP_FAILURE_RETRY (read(statp->_vcsock, cp,                            (int)len))) > 0) {         cp += n;         if ((len -= n) <= 0)             break;     }     if (n <= 0) {         [...]   //出错处理无视。     }     int rlen = ntohs (rlen16);      //----------------[2]-------------end----------------      //----------------[1]-------------start----------------     int *thisanssizp;     u_char **thisansp;     int *thisresplenp;     if ((recvresp1 | recvresp2) == 0 || buf2 == NULL) { //第一次从read_len开始读取网络包进入这个分支。         thisanssizp = anssizp;                          //第一次调用read时可用内存65536         thisansp = anscp ?: ansp;                       //第一次调用read时使用的缓存anscp           assert (anscp != NULL || ansp2 == NULL);         thisresplenp = &resplen;     } else {         if (*anssizp != MAXPACKET) {              [...]                                       //重现流程中不会进入这块。         } else {             /* The first reply did not fit into the                user-provided buffer.  Maybe the second                answer will.  */             *anssizp2 = orig_anssizp;                   //第二次调用时可用内存长度65536             *ansp2 = *ansp;                             //第二次调用read时使用的缓存ansp         }          thisanssizp = anssizp2;         thisansp = ansp2;         thisresplenp = resplen2;     }     //----------------[1]-------------end----------------       anhp = (HEADER *) *thisansp;          *thisresplenp = rlen;     if (rlen > *thisanssizp) {          [...]       //重现流程中不会进入这块。     } else         len = rlen;      if (__glibc_unlikely (len < HFIXEDSZ))       {         [...]       //重现流程中不会进入这块。     }      cp = *thisansp; //*ansp;     //---------------[2]--------------------start-----------------     while (len != 0 && (n = read(statp->_vcsock, (char *)cp, (int)len)) > 0){ //溢出点。         cp += n;         len -= n;     }     //---------------[2]--------------------start-----------------       if (__glibc_unlikely (n <= 0))       {         [...]       //重现流程中不会进入这块。     }     if (__glibc_unlikely (truncating))       {         [...]       //重现流程中不会进入这块。     }     /*      * If the calling application has bailed out of      * a previous call and failed to arrange to have      * the circuit closed or the server has got      * itself confused, then drop the packet and      * wait for the correct one.      */      //---------------[4]--------------------start-----------------     if ((recvresp1 || hp->id != anhp->id)                   //不进。         && (recvresp2 || hp2->id != anhp->id)) {         [...]       //重现流程中不会进入这块。         goto read_len;     }      /* Mark which reply we received.  */     if (recvresp1 == 0 && hp->id == anhp->id)               //第一次运行recvresp1=1 recvresp2=0       recvresp1 = 1;     else       recvresp2 = 1;     /* Repeat waiting if we have a second answer to arrive.  */     if ((recvresp1 & recvresp2) == 0)                       // 调用goto,回到前面。         goto read_len;     //---------------[4]--------------------end-----------------     /*      * All is well, or the error is fatal.  Signal that the      * next nameserver ought not be tried.      */     return resplen; } 

根据源码分析,从socket读取网络包数据的时候是溢出的地方,所以在这里下断点。

gdb> b res_send.c:853 

CVE-2015-7547简单分析与调试

通过调用栈可以得知,read发生了两次[4],而且第一次是正确的,在第二次read之后发生了溢出。通过[1]可以得知,在两次调用read的时候cp指向的内存不同。

第一次调用 read 函数时,缓冲区为 anscp 指向的内存。

第二次调用 read 函数时,缓冲区为 ansp 指向的内存。 这里暂时不用考虑二级指针的问题。

可以断定,ansp指针索引的地址出现了问题。ansp是调用时从参数传入的。所以需要通过分析send_vc的调用函数。

2.3 内存分配错误

send_vc的调用函数如下:

#!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, int *ansp2_malloced) {   [...]   if (__glibc_unlikely (v_circuit))       {             /* Use VC; at most one attempt per server. */             try = statp->retry;             n = send_vc(statp, buf, buflen, buf2, buflen2,  //statp状态,buff,bufflen第一组发送数据,buff,2bufflen2第二组发送数据。                     &ans, &anssiz, &terrno,                 //u_char **ansp, int *anssizp,int *terrno,                     ns, ansp, ansp2, nansp2, resplen2,      //int ns, u_char **anscp, u_char **ansp2, int *anssizp2,int *resplen2,                                   ansp2_malloced);                        //int *ansp2_malloced             if (n < 0)                 return (-1);             if (n == 0 && (buf2 == NULL || *resplen2 == 0))                 goto next_ns;         } else {             /* Use datagrams. */                            //经过send_dg函数调用,ansp指向65536buff,ans指向2048buff。             n = send_dg(statp, buf, buflen, buf2, buflen2,                     &ans, &anssiz, &terrno,                     ns, &v_circuit, &gotsomewhere, ansp,                     ansp2, nansp2, resplen2, ansp2_malloced);             if (n < 0)                 return (-1);             if (n == 0 && (buf2 == NULL || *resplen2 == 0))                 goto next_ns;             if (v_circuit)               // XXX Check whether both requests failed or     Z               // XXX whether one has been answered successfully                 goto same_ns;         }   [...] } 

因为在调用 send_vc 之前程序先调用了 send_dg ,且两个函数参数基本相同,通过阅读源码会发现, send_dg 对参数进行修改及新内存的申请。

#!c static int send_dg(res_state statp,     const u_char *buf, int buflen, const u_char *buf2, int buflen2,     u_char **ansp, int *anssizp,     int *terrno, int ns, int *v_circuit, int *gotsomewhere, u_char **anscp,     u_char **ansp2, int *anssizp2, int *resplen2, int *ansp2_malloced) {     //ans指向大小为2048的缓冲器     //ansp指向ans     //anscp指向ans     const HEADER *hp = (HEADER *) buf;     const HEADER *hp2 = (HEADER *) buf2;     u_char *ans = *ansp;     int orig_anssizp = *anssizp;     struct timespec now, timeout, finish;     struct pollfd pfd[1];     int ptimeout;     struct sockaddr_in6 from;     int resplen = 0;     int n;      [...]   else if (pfd[0].revents & POLLIN) {         int *thisanssizp;         u_char **thisansp;         int *thisresplenp;          if ((recvresp1 | recvresp2) == 0 || buf2 == NULL) { //send_dg第一次进入这个分支。             thisanssizp = anssizp;             thisansp = anscp ?: ansp;                       //thisansp被赋值为anscp             assert (anscp != NULL || ansp2 == NULL);             thisresplenp = &resplen;         } else {             [...]                                           //第一次调用不会进入。         }          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                     ) {             u_char *newp = malloc (MAXPACKET);             if (newp != NULL) {                 *anssizp = MAXPACKET;                       //anssizp谁为65536                 *thisansp = ans = newp;                     //anscp指向65536的buffer,但是ansp指向仍然指向原来的2048的buffer                 if (thisansp == ansp2)                   *ansp2_malloced = 1;             }         } 

CVE-2015-7547简单分析与调试

通过调试可以看出,ansp仍然指向大小为2048的缓冲区,而anscp指向了大小为65536的缓冲区。之后这两个指针又被传递给了send_vc。

2.4 溢出原因

所以溢出的原因是, *anssizp 因为在之前的 send_dg 中被赋值为65536, send_vc 中第二次调用 read 函数时,认为ansp指向的缓冲区的大小为 *anssizp 即65536,而实际上ansp指向了一块只有2048大小的缓冲区。所以在从socket读取大于2048个字节之后产生了栈溢出。

CVE-2015-7547简单分析与调试

0x03 参考&感谢

感谢分享:)

  1. CVE-2015-7547 --- glibc getaddrinfo() stack-based buffer overflow

    https://sourceware.org/ml/libc-alpha/2016-02/msg00416.html

  2. Linux glibc再曝漏洞:可导致Linux软件劫持

    http://www.freebuf.com/news/96244.html

  3. CVE-2015-7547: glibc getaddrinfo stack-based buffer overflow

    https://googleonlinesecurity.blogspot.com/2016/02/cve-2015-7547-glibc-getaddrinfo-stack.html

  4. glibc编译debug版本

    http://blog.csdn.net/jichl/article/details/7951996

  5. glibc的编译和调试

    http://blog.chinaunix.net/uid-20786208-id-4980168.html

原文  http://drops.wooyun.org/papers/13040
正文到此结束
Loading...