新文章
PWN 专题
堆管理器 和 堆漏洞
堆内存
堆内存是一种允许在程序运行过程中动态分配和使用的内存区域,相比于栈内存,堆没有固定的生命周期和固定的内存区域,程序可以动态地申请和释放不同大小的内存,被分配后,如果没有进行明确的释放操作,该堆的内存区域都是一直有效的。
+------------------------+ 高地址
| 命令行参数 | (argc, argv[])
+------------------------+
| 环境变量 | (environ)
+------------------------+
| 栈 (Stack) |
| - 函数参数 |
| - 局部变量 |
| - 返回地址 |
| - 保存的寄存器 |
| (向下增长) |
+------------------------+
| 空闲区 (可选) |
+------------------------+
| 堆 (Heap) |
| - malloc分配的内存 |
| (向上增长) |
+------------------------+
| BSS段 (.bss) |
| - 未初始化的全局/静态变量
+------------------------+
| 数据段 (.data) |
| - 已初始化的全局/静态变量
+------------------------+
| 代码段 (.text) |
| - 程序的机器指令 |
+------------------------+ 低地址
对于不同的应用来说,由于内存的需求各不相同等特性,因此目前堆的实现有很多种,具体如下
dlmalloc – General purpose allocator
ptmalloc2 – glibc
jemalloc – FreeBSD and Firefox
tcmalloc – Google
libumem – Solaris
这里我们主要以 glibc 中 ptmalloc2 堆的实现为主进行介绍。
堆结构
Ptmalloc2 分配的最基础的内存结构为 chunk,其定义如下
struct malloc_chunk {
INTERNAL_SIZE_T prev_size; /* Size of previous chunk (if free). */
INTERNAL_SIZE_T size; /* Size in bytes, including overhead. */
struct malloc_chunk* fd; /* double links -- used only if 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 free. */
struct malloc_chunk* bk_nextsize;
};
下面我们来看 chunk 结构体,各个字段的具体的解释如下:
1. prev_size
-
含义:
- 如果物理相邻的前一个 chunk(即低地址方向,地址差值为前一个 chunk 大小)是空闲的,则此字段记录前一个 chunk 的总大小(包括 chunk 头部)。
- 如果前一个 chunk 已被分配,该字段可以用于存储用户数据。
-
用途:
- 当前一个 chunk 空闲时,用来辅助合并空闲块。
2. size
-
含义:
- 当前 chunk 的总大小(包含头部),64位系统中,chunk 最小为 32 字节,且 必须是 8 的整数倍。
-
低 3 位标志位(从高到低):
标志位 含义 NON_MAIN_ARENA是否属于主线程(1:不属于;0:属于主线程) IS_MAPPED是否通过 mmap方式分配(1:是;0:否)PREV_INUSE前一个 chunk 是否正在使用(1:已使用;0:空闲) -
注意事项:
- 第一个分配的内存块通常会将
PREV_INUSE位置 1,以避免访问非法内存。 - 如果
PREV_INUSE = 0,可以通过prev_size字段确定前一个 chunk 的地址及大小,便于空闲块合并。
- 第一个分配的内存块通常会将
3. fd 和 bk
-
含义(仅空闲 chunk 时使用):
fd:指向下一个(非物理相邻的)空闲 chunk。bk:指向上一个(非物理相邻的)空闲 chunk。
-
说明:
- 空闲 chunk 被链接到对应的空闲管理链表中。
- 当 chunk 处于分配状态时,从
fd开始的区域为用户数据区。
4. fd_nextsize 和 bk_nextsize
-
适用范围:仅适用于空闲的large chunk(较大的空闲块)。
-
含义:
fd_nextsize:指向前一个大小不同的空闲块(不包含 bin 的头指针)。bk_nextsize:指向后一个大小不同的空闲块(不包含 bin 的头指针)。
-
目的:
- 支持按照 chunk 大小由大到小的顺序组织 large chunk,减少遍历次数,提高分配效率。
+------------+
| prev_size | // 仅当前一个 chunk 空闲时有效
+------------+
| size | // 含标志位
+------------+
| fd | // 空闲时: 指向下一个空闲 chunk
+------------+
| bk | // 空闲时: 指向上一个空闲 chunk
+------------+
| fd_nextsize| // 仅 large chunk 使用
+------------+
| bk_nextsize| // 仅 large chunk 使用
+------------+
| User Data | // 分配状态: 用户数据开始
| |
+------------+
chunk 在管理器中分为 3 中形式,分别为 allocated chunk free chunk top chunk。
当用户申请一块内存后,就返回一个 allocated chunk。
当内存被释放后,就得到了一个 free chunk。
top chunk是分给堆区域的一大块内存,如果没有可用的 free chunk,就会从 top chunk 中分出一块 allocated chunk。
Bin 结构分类
在 ptmalloc2 中,根据 chunk 大小的不同,空闲的 chunk 被组织在不同的 Bin 结构中,分别为 Fast Bin、Small Bin、Large Bin 和 Unsorted Bin。
1. Fast Bin
- 范围:chunk 大小为 32~128(0x20~0x80)字节。
- 特点:
- 如果 chunk 在释放时发现其大小符合要求,则将该 chunk 放入 Fast Bin。
- 被放入 Fast Bin 后,不修改前一个 chunk 的
PREV_INUSE标志位。 - Fast Bin 以单链表形式组织,不同大小的 Fast Bin 存储在对应大小的单链表结构中。
- Fast Bin 的取出机制为 LIFO(后进先出)。
- 注意:
- 新释放的 chunk 指向链表头,形成 Fast Bin 链。
2. Small Bin
- 范围:chunk 大小为 32~1024(0x20~0x400)字节。
- 特点:
- 每个 Small Bin 作为双向链表,不同大小的 chunk 存放在对应的链表中。
- Small Bin 的存储和连接方式是 FIFO(先进先出)。
- 补充:
- 由于是双链表结构,相比 Fast Bin,Small Bin 的速度稍慢。
3. Large Bin
- 范围:chunk 大小 > 1024(0x400)字节。
- 特点:
- Large Bin 结构最复杂,速度也最慢。
- 使用
fd和bk指针串联,同一大小的 Large Bin 使用普通链表连接。 - 不同大小的 Large Bin 通过
fd_nextsize和bk_nextsize按大小顺序链接。
4. Unsorted Bin
- 作用:
- 相当于 ptmalloc2 堆管理器的垃圾桶。
- 特点:
-
当一个较大的 chunk 被分割成两半后,如果剩下的部分大于 MINSIZE,就会被放到 unsorted bin 中。
-
释放一个不属于 fast bin 的 chunk,并且该 chunk 不和 top chunk 紧邻时,该 chunk 会被首先放到 unsorted bin 中。
-
堆管理器首先在 Unsorted Bin 查找是否有合适 chunk。
-
如果 Fast Bin 和 Small Bin 中都没有合适的 chunk,才在 Unsorted Bin 中查找。
-
如果找到符合申请大小的 chunk,则直接取出或拆分使用。
-
5. Tcache Bin
-
作用:
- Tcache(Thread Cache)是 glibc 2.26 后引入的一种堆优化机制。
-
特点:
- 每个线程都有一个独立的 Tcache 结构(线程私有,不同线程互不影响)。
- 小块(一般小于 0x410 字节)的空闲 chunk 可以存入 Tcache。
- Tcache Bin 采用 单链表组织,同一大小的 chunk 放入对应大小的 Tcache Bin。
- 取出方式为 LIFO(后进先出),即最近释放的 chunk 最先被再次使用。
-
限制:
- 每个大小类别的 Tcache Bin 有一个最大容量(一般默认是 7 个 chunk)。
- 超过容量后,新的释放 chunk 不再放入 Tcache,而是走正常的 Fast Bin/Small Bin 等流程。
-
优点:
- 由于线程本地存储,无需加锁,大大提高了 malloc/free 的性能。
- 减少了线程间同步带来的开销。
-
与其他 Bin 的关系:
- 如果 Tcache 中有符合条件的 chunk,malloc 时优先从 Tcache 取出。
- 如果 Tcache 中没有合适的 chunk,再从主堆管理器(Fast Bin/Small Bin 等)分配。
堆漏洞
未初始化读
被释放的堆块被重新分配时,如果没有清空内存,就可能泄露之前存储的内容
堆溢出
分为堆块内溢出和堆块外溢出
-
堆块内溢出可以修改堆块内的其他内容,比如字符串指针或函数指针,从而实现读取任意地址或执行任意函数。
-
堆块外溢出可以操纵 bin 中 free chunk 的指针,使其指向错误的区域,从而实现任意地址读写
释放后使用
堆块被释放后,仍然保留和使用指向堆块的指针,就能够直接向被释放的堆块写入内容。
之后操作同堆溢出。