从零开始的 Pwn 之旅 - 堆溢出初探

堆介绍

堆是动态分配内存的区域,通常用于存储在运行时需要动态分配的对象。堆内存的分配和释放由程序员控制,常见的操作包括 mallocfree 等。

操作系统内存布局示意图

当前 Linux 标准发行版使用 ptmalloc2 作为堆的实现。ptmalloc2 是 glibc 的一部分,基于 Doug Lea 的 malloc 实现。

heap 相关概念

chunk 结构

在 ptmalloc2 中,堆内存被划分为多个块(chunk)。每个 chunk 都有一个头部和一个尾部,用于存储块的大小和状态信息。chunk 的结构如下:

1
2
3
4
5
6
7
8
9
+-------------------------------+
| prev_size | size              |
+-------------------------------+
| fd        | bk                |
+-------------------------------+
| fd_nextsize | bk_nextsize     |
+-------------------------------+
|           chunk data...       |
+-------------------------------+
  • prev_size:前一个 chunk 的大小(如果存在)
  • size:当前 chunk 的大小和状态信息(如是否被分配)
    • A / IS_MMAPPED(M):当前 chunk 是否通过 mmap 分配(A/M 在不同版本表现不同)
    • P / PREV_INUSE:前一块 chunk 是否被分配(即前一个 chunk 是否 inuse)
    • M / NON_MAIN_ARENA:当前 chunk 是否属于主 arena(multi-arena 环境下)
  • fd 和 bk:双向链表指针,用于管理空闲块的链表(如果 chunk 是空闲的)
    • fd 指向下一个空闲块
    • bk 指向上一个空闲块
  • fd_nextsize 和 bk_nextsize:用于管理按大小排序的空闲块链表(如果 chunk 是空闲的且 chunk 较大)
    • fd_nextsize 指向下一个空闲块
    • bk_nextsize 指向上一个空闲块

一个已经分配的 chunk 称前两个字段称为 chunk header,后面的部分称为 user data。每次 malloc 申请得到的内存指针,其实指向 user data 的起始处

当一个 chunk 处于使用状态时,它的下一个 chunk 的 prev_size 域无效,所以下一个 chunk 的该部分也可以被当前 chunk 使用。这就是 chunk 中的空间复用

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
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)                      .
next    .                                                               |
chunk-> +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
        |             (size of chunk, but used for application data)    |
        +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
        |             Size of next chunk, in bytes                |A|0|1|
        +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

动态调试

编写如下 c 语言代码

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
#include <malloc.h>

int main(int argc, char *argv[]) {
  char *p1 = malloc(0x18);
  for (int i = 0; i < 0x18; i++) {
    p1[i] = 'a';
  }
  malloc(0x10);
  free(p1);
  return 0;
}

free(p1) 下断点, 查看 heap 情况

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
pwndbg> heap
pwndbg will try to resolve the heap symbols via heuristic now since we cannot resolve the heap via the debug symbols.
This might not work in all cases. Use `help set resolve-heap-via-heuristic` for more details.

Allocated chunk | PREV_INUSE
Addr: 0x602000
Size: 0x20 (with flag bits: 0x21)

Allocated chunk | PREV_INUSE
Addr: 0x602020
Size: 0x20 (with flag bits: 0x21)

Top chunk | PREV_INUSE
Addr: 0x602040
Size: 0x20fc0 (with flag bits: 0x20fc1)

pwndbg> x/8gx 0x602000
0x602000: 0x0000000000000000 0x0000000000000021
0x602010: 0x6161616161616161 0x6161616161616161
0x602020: 0x6161616161616161 0x0000000000000021
0x602030: 0x0000000000000000 0x0000000000000000
pwndbg>

可以看到, 这里展示了在内存中一个没有被释放的 chunk 情况

  • 头部大小 0x10, 数据区域 0x10, 下一个 chunk 的 prev 字段被前一个 chunk 使用, userdata 大小为 0x10 + 0x8 = 0x18, 符合我们申请的内存
  • 0x21 是 size 大小, 这里 size 的最低位 PREV_INUSE 被置为 1, 表示前一个 chunk 已经被使用, 对于 0x602000 这个 chunk 来说, 0x602020 是前一个 chunk 的地址, size 的最低位为 1, 所以这里的 size 是 0x21

执行 free(p1) 后, chunk 变成了空闲状态

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
pwndbg> heap
Free chunk (fastbins) | PREV_INUSE
Addr: 0x602000
Size: 0x20 (with flag bits: 0x21)
fd: 0x00

Allocated chunk | PREV_INUSE
Addr: 0x602020
Size: 0x20 (with flag bits: 0x21)

Top chunk | PREV_INUSE
Addr: 0x602040
Size: 0x20fc0 (with flag bits: 0x20fc1)

pwndbg> x/8gx 0x602000
0x602000: 0x0000000000000000 0x0000000000000021
0x602010: 0x0000000000000000 0x6161616161616161
0x602020: 0x6161616161616161 0x0000000000000021
0x602030: 0x0000000000000000 0x0000000000000000
pwndbg> fastbins
fastbins
0x20: 0x602000 ◂— 0
pwndbg>

可以看到, data 区域还残留了部分的数据,但是 userdata 的前 8 个字节被清空了 其实这里的前 8 个字节就是 fd, 当前 chunk 被放到了 fastbins 中, fd 指向下一个空闲 chunk 的地址, bk 指向上一个空闲 chunk 的地址

bins 是 ptmalloc2 中的重要概念,这里简单理解就好,后文会详细介绍

  • TODO: largebins malloc 结构