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 */
/* check/set/clear inuse bits in known places */
_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中的前一个块和后一个块,与合并块时的隐式链表中的前一个块和后一个块不同
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 */
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 | High |----------------| |
但是,由于当前glibc的加固检测机制,会检查显式链表中前一个块的fd与后一个块的bk是否都指向当前需要unlink的块。这样攻击者就无法替换chunk1(或chunk0)的fd与bk。相关代码如下:
1 | if (__builtin_expect (FD->bk != P || BK->fd != P, 0)) \ |
针对这种情况,需要在内存中找到一个指向需要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 | #!/usr/bin/env python |