堆溢出

在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步:

  1. 如果之前有被释放的chunk,且其大小能符合分配要求,heap manager就会重用这块chunk。
  2. 如果在顶层(最高位处的chunk,称为top chunk,一般size特别大)还有可用的空间,heap manager就会利用之分配新的空间。
  3. 若再没有空间,就会拓展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机制比较复杂,然而一旦其工作机制能被预测,通常可以实现以下几点:

  1. 扰乱从被已经free的chunk中重新malloc的机制,使得取回一个伪造的chunk,这个受我们控制的伪造chunk可能都不在heap段,从而实现任意地址写。
  2. 已经free的chunk会记录一些元数据,这些元数据可能会导致动态地址的泄露(InfoLeak)。
  3. 其中的更多挑战是,为了防止上述漏洞的利用,heap的机制会做额外的元数据验证,攻击者需要对源码特别熟悉,设法绕过那些验证。