堆溢出
在C/C++中,可以通过malloc等方法分配heap的内存空间。分配空间本身没有风险,风险来自于释放该空间。在此针对libc,介绍heap的漏洞和利用。
在介绍如何利用漏洞前,首先简单了解一下heap的数据结构。
内存布局
整个heap段与stack相反,是从低地址向高地址增长的。

Chunk
一个在heap空间中分配的单位叫做chunk。他的头部追加了元数据。特别注意malloc返回的指针是指向数据区域,而非元数据头部。后续新申请的chunk在空间上往高地址紧密相邻。
Chunk的大小需要满足”字节对齐“的原则,在32位系统上为8字节对齐,在64位系统上16字节对齐。也就是说,分配的大小必须是这个”对齐字节数“的整数倍。
当申请n个字节空间时,实际上分配的空间为(n + “对齐字节数“ + padding)。其中第一个”对齐字节数“长度的空间存储元数据。最后如果未对齐,要加padding。
举个例子,当在64位系统上malloc(0x200),分配的空间大小就为0x210。
同一个数据结构适配2种状态
libc的chunk对应的数据结构源代码如下:
struct malloc_chunk {
INTERNAL_SIZE_T mchunk_prev_size; /* Size of previous chunk, if it is free. **/
INTERNAL_SIZE_T mchunk_size;* /* Size in bytes, including overhead. **/
struct malloc_chunk* fd; /* double links -- used only if this chunk is free. **/
struct malloc_chunk* bk;
/* Only used for large blocks: pointer to next larger size. **/
struct malloc_chunk* fd_nextsize; /* double links -- used only if this chunk is free. **/
struct malloc_chunk* bk_nextsize;
};
typedef struct malloc_chunk* mchunkptr;
chunk有2种状态:allocated和freed。从注释可知,如果一个chunk正在使用,未释放,则只有mchunk_size是被用到的。而fd_nextsize和bk_nextsize是仅在当前chunk属于large block才有意义。
allocated chunk的实际内存布局如下:
chunk-> +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Size of previous chunk, if unallocated (P clear) |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Size of chunk, in bytes |A|M|P|
mem-> +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| User data starts here... .
. .
. (malloc_usable_size() bytes) .
. |
nextchunk-> +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| (size of chunk, but used for application data) |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Size of next chunk, in bytes |A|0|1|
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
freed chunk的实际内存布局如下:
chunk-> +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Size of previous chunk, if unallocated (P clear) |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
`head:' | Size of chunk, in bytes |A|0|P|
mem-> +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Forward pointer to next chunk in list |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Back pointer to previous chunk in list |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Unused space (may be 0 bytes long) .
. .
. |
nextchunk-> +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
`foot:' | Size of chunk, in bytes |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Size of next chunk, in bytes |A|0|0|
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
在64位系统上,上述每行占8个字节(为“对齐字节数“的一半)。不论chunk的状态如何,都必须存储的是mchunk_size。这个size包括了整个元数据和数据区的大小之和。由于内存空间的相邻特性,这个size起到了在heap中遍历chunk的作用。
由于前面所述的对齐问题,size的最后3位必定是0。为了性能考虑,减少冗余空间,这3位被用作了chunk的状态标记位。我们需要关注最后一个标记位P的含义:它表示上一个chunk是否正在使用。可见allocated chunk的下一个chunk的P位是1,而freed chunk的下一个chunk的P位是0。
图中还有一点令人费解的是一个chunk的最后一行和下一个chunk的第一行重叠了。这也是为了节约内存的设计。由于Size of previous chunk仅在前一个chunk释放时(当前的P标志位=0)才有意义,反之如果它没有被释放,则该内存区域不会被当前chunk使用。
类似的,chunk释放后,原本的数据区的前两行被写入了Forward pointer to next chunk in list和Back pointer to previous chunk in list。被释放的chunk之间是通过双向链表关联起来的。
Heap的分配策略
分配heap的策略可简化为以下3步:
- 如果之前有被释放的chunk,且其大小能符合分配要求,heap manager就会重用这块chunk。
- 如果在顶层(最高位处的chunk,称为top chunk,一般size特别大)还有可用的空间,heap manager就会利用之分配新的空间。
- 若再没有空间,就会拓展heap顶层的内存极限。

关于第1点的重用,实现方法为:当chunk被释放后,就会被置入各种bin(类似一个注册表),每个bin(除了tcache和fastbin)都是一个双向链表。这个双向链表的实现就是依赖于上述的Forward pointer to next chunk in list和Back pointer to previous chunk in list字段。
此处可能存在一个漏洞,即当再申请的大小可以被之前释放的大小容纳时,Chunk的重用是可预测的。按照后进先出的原则(最后被Free的chunk会被下一次申请相同空间的malloc分配到)。
Fastbin attack就是基于这个漏洞。当某个chunk被释放后,FD POINTER会被用于记录在Fastbin中单向链表的下一个chunk。假如能篡改这个FD POINTER指向一个伪造的Chunk,就有机会在之后通过malloc得到那个伪造的Chunk,从而实现对这个伪造chunk区域的数据控制。

gdb相关命令
在gdb中,使用heap bins命令可以查看各种bin的实时状态
gef➤ heap bins
[+] No Tcache in this version of libc
────────────────────── Fastbins for arena 0x7ffff7dd1b20 ──────────────────────
Fastbins[idx=0, size=0x10] ← Chunk(addr=0x602010, size=0x20, flags=PREV_INUSE)
Fastbins[idx=1, size=0x20] 0x00
Fastbins[idx=2, size=0x30] 0x00
Fastbins[idx=3, size=0x40] 0x00
Fastbins[idx=4, size=0x50] 0x00
Fastbins[idx=5, size=0x60] 0x00
Fastbins[idx=6, size=0x70] 0x00
───────────────────── Unsorted Bin for arena 'main_arena' ─────────────────────
[+] Found 0 chunks in unsorted bin.
────────────────────── Small Bins for arena 'main_arena' ──────────────────────
[+] Found 0 chunks in 0 small non-empty bins.
────────────────────── Large Bins for arena 'main_arena' ──────────────────────
[+] Found 0 chunks in 0 large non-empty bins.
使用heap chuncks命令可以查看chunk的相关数据
gef➤ heap chunks
Chunk(addr=0xb7dfd008, size=0x108, flags=PREV_INUSE)
[0xb7dfd008 08 50 53 b7 10 d1 df b7 b0 54 e0 b7 00 00 00 00 .PS......T......]
Chunk(addr=0xb7dfd110, size=0x83a0, flags=PREV_INUSE)
[0xb7dfd110 66 64 74 61 73 6b 00 00 00 00 00 00 00 00 00 00 fdtask..........]
Chunk(addr=0xb7e054b0, size=0x18b58, flags=PREV_INUSE) ← top chunk
Heap的漏洞利用思路
Chunk的free和malloc机制比较复杂,然而一旦其工作机制能被预测,通常可以实现以下几点:
- 扰乱从被已经free的chunk中重新malloc的机制,使得取回一个伪造的chunk,这个受我们控制的伪造chunk可能都不在heap段,从而实现任意地址写。
- 已经free的chunk会记录一些元数据,这些元数据可能会导致动态地址的泄露(InfoLeak)。
- 其中的更多挑战是,为了防止上述漏洞的利用,heap的机制会做额外的元数据验证,攻击者需要对源码特别熟悉,设法绕过那些验证。