menu arrow_back 湛蓝安全空间 |狂野湛蓝,暴躁每天 chevron_right ... chevron_right 007-papers chevron_right 0385-CVE-2015-7547简单分析与调试.md
  • home 首页
  • brightness_4 暗黑模式
  • cloud
    xLIYhHS7e34ez7Ma
    cloud
    湛蓝安全
    code
    Github
    0385-CVE-2015-7547简单分析与调试.md
    13.45 KB / 2021-07-17 00:01:34
        # CVE-2015-7547简单分析与调试
    
    0x00 漏洞信息
    =========
    
    * * *
    
    最近glibc有一个栈溢出的漏洞具体情况,漏洞的具体信息可以参考下面链接。
    
    [CVE-2015-7547: glibc getaddrinfo stack-based buffer overflow](https://googleonlinesecurity.blogspot.com/2016/02/cve-2015-7547-glibc-getaddrinfo-stack.html)
    
    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。![ldd ./client](http://drops.javaweb.org/uploads/images/1e10a62a13a4724be919284579c67c4f5eb2ee17.jpg)
    
    这个时候就可以用GDB调试glibc中的函数了。
    
    1.3 配置本地dns服务器
    --------------
    
    运行poc的python服务器。修改`/etc/resolv.conf`。将域名服务器改为127.0.0.1就好了。不过这样一来这台机器访问网络就会出问题了。
    
    ```
    nameserver 127.0.0.1
    
    ```
    
    0x02 漏洞分析
    =========
    
    * * *
    
    2.1 运行POC
    ---------
    
    使用gdb启动客户端直接运行,出现崩溃堆栈。
    
    ![crash](http://drops.javaweb.org/uploads/images/d0b4794a3167529a3c5b8c4eb758ac13c4a8ea88.jpg)
    
    2.2 寻找溢出函数
    ----------
    
    可以看到栈都被覆盖为0x42424242,根据[google提供的分析](https://googleonlinesecurity.blogspot.com/2016/02/cve-2015-7547-glibc-getaddrinfo-stack.html),出问题的是send_dg和send_vc函数。分别在send_vc和send_dg上下断点,重新运行程序,会发现先调用send_dg函数再调用send_vc函数。
    
    ![寻找溢出函数](http://drops.javaweb.org/uploads/images/dce45d0f06797660929e43fc4b2e7e1dec8532e3.jpg)
    
    可以看出是在send_vc的时候发生了栈溢出。
    
    因为根据[google提供的分析](https://googleonlinesecurity.blogspot.com/2016/02/cve-2015-7547-glibc-getaddrinfo-stack.html)可以知道是在读取socket的时候发生的溢出,可以通过结合源码调试来分析。剔除不需要看的代码,核心代码如下,总共干了四件事。
    
    ```
    [1]选择适当的缓存
    [2]读取dns包的长度
    [3]读取dsn包
    [4]判断是否需要读取第二个数据包。
    
    ```
    
      
    
    ```
    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
    
    ```
    
    ![寻找溢出函数](http://drops.javaweb.org/uploads/images/f7f6c2d31156230bc7672826debebca510671c11.jpg)
    
    通过调用栈可以得知,read发生了两次[4],而且第一次是正确的,在第二次read之后发生了溢出。通过[1]可以得知,在两次调用read的时候cp指向的内存不同。
    
    第一次调用`read`函数时,缓冲区为**anscp**指向的内存。
    
    第二次调用`read`函数时,缓冲区为**ansp**指向的内存。**这里暂时不用考虑二级指针的问题。**
    
    可以断定,ansp指针索引的地址出现了问题。ansp是调用时从参数传入的。所以需要通过分析send_vc的调用函数。
    
    2.3 内存分配错误
    ----------
    
    send_vc的调用函数如下:
    
    ```
    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`对参数进行修改及新内存的申请。
    
    ```
    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;
                }
            }
    
    ```
    
    ![寻找溢出函数](http://drops.javaweb.org/uploads/images/057a4ccae657626885fac0302ab1c8c9fb55bf52.jpg)
    
    通过调试可以看出,ansp仍然指向大小为2048的缓冲区,而anscp指向了大小为65536的缓冲区。之后这两个指针又被传递给了send_vc。
    
    2.4 溢出原因
    --------
    
    所以溢出的原因是,`*anssizp`因为在之前的`send_dg`中被赋值为65536,`send_vc`中第二次调用`read`函数时,认为ansp指向的缓冲区的大小为`*anssizp`即65536,而实际上ansp指向了一块只有2048大小的缓冲区。所以在从socket读取大于2048个字节之后产生了栈溢出。
    
    ![寻找溢出函数](http://drops.javaweb.org/uploads/images/f6a7147d95f5d26b603acbf5681f7c92fe22604f.jpg)
    
    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
    
    links
    file_download