CVE-2015-7547簡單分析與除錯

wyzsk發表於2020-08-19
作者: mrh · 2016/02/23 10:42

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。

這個時候就可以用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

2.2 尋找溢位函式

可以看到棧都被覆蓋為0x42424242,根據google提供的分析,出問題的是send_dg和send_vc函式。分別在send_vc和send_dg上下斷點,重新執行程式,會發現先呼叫send_dg函式再呼叫send_vc函式。

尋找溢位函式

可以看出是在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

尋找溢位函式

透過呼叫棧可以得知,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;
            }
        }

尋找溢位函式

透過除錯可以看出,ansp仍然指向大小為2048的緩衝區,而anscp指向了大小為65536的緩衝區。之後這兩個指標又被傳遞給了send_vc。

2.4 溢位原因

所以溢位的原因是,*anssizp因為在之前的send_dg中被賦值為65536,send_vc中第二次呼叫read函式時,認為ansp指向的緩衝區的大小為*anssizp即65536,而實際上ansp指向了一塊只有2048大小的緩衝區。所以在從socket讀取大於2048個位元組之後產生了棧溢位。

尋找溢位函式

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

本文章來源於烏雲知識庫,此映象為了方便大家學習研究,文章版權歸烏雲知識庫!

相關文章