堆的深渊:Glibc Ptmalloc内存管理机制与Use-After-Free(UAF)漏洞推演
堆的深渊:Glibc Ptmalloc内存管理机制与Use-After-Free(UAF)漏洞推演
在 Pwn 的世界里,栈溢出只是入门,真正的深渊在堆(Heap)。
栈是由操作系统自动分配和销毁的,结构简单严谨。而堆,是程序员通过 malloc() 动态申请、通过 free() 手动释放的内存区域。为了在“分配速度”和“内存碎片”之间寻找极致的平衡,Linux Glibc 的 Ptmalloc 内存管理器构建了一套极其复杂的双向链表与缓存机制。
正是这种复杂性,孕育了 Pwn 领域最顶级的艺术:堆利用(Heap Exploitation)。本文将带你潜入这片深渊,推演最臭名昭著的 UAF(Use-After-Free)漏洞。
1. 堆块(Chunk)的微观结构
当我们调用 malloc(0x20) 申请 32 字节内存时,Ptmalloc 实际上在底层分配了一个更大的堆块(Chunk)。
一个 Chunk 在内存中的微观切面如下:
- size 字段:记录当前 Chunk 的大小。它的最低 3 个比特位(A, M, P)是标志位。最关键的是 P (PREV_INUSE) 标志位,用来表示“物理相邻的前一个 Chunk 是否正在使用中”。
- 如果当前 Chunk 被
free了,它的 User Data 区域会被立刻改写,放入fd(Forward Pointer) 和bk(Backward Pointer) 指针,用来把它挂入“空闲链表”中。
2. 垃圾回收站:Bin 链表机制
被 free 释放的 Chunk 不会立刻还给操作系统,而是被 Ptmalloc 扔进了一个叫 Bin 的垃圾回收站。
当程序再次 malloc 时,Ptmalloc 会优先去 Bin 里找合适大小的废弃 Chunk 重新分配,以此提高速度。
Ptmalloc 维护了多种 Bin,最核心的有:
- Fastbin:单向链表,采用 LIFO(后进先出)机制。专门存放极小的 Chunk(通常 <= 0x80 字节)。速度极快,也是堆攻击的重灾区。
- Unsorted Bin:双向链表。刚被释放的、不属于 Fastbin 大小的 Chunk 会先被扔到这里。
- Small Bin / Large Bin:按大小严格分类的双向链表。
Fastbin 的 LIFO 特性推演:
- 程序
free(A):Fastbin 链表变为Head -> A。 - 程序
free(B):Fastbin 链表变为Head -> B -> A。 - 程序再次
malloc()相同大小:Ptmalloc 从头部摘取,返回B。链表变回Head -> A。
3. 深渊的裂痕:Use-After-Free (UAF)
Use-After-Free(释放后重用) 漏洞的本质是:程序员调用 free(ptr) 释放了内存,但没有将 ptr 指针置为 NULL。这个指针变成了“悬现指针(Dangling Pointer)”。
这就像你退掉了酒店的房卡,但私自配了一把钥匙。虽然房间已经被前台标记为“空闲”,但你依然可以偷偷溜进去修改里面的陈设。
3.1 UAF 导致信息泄露 (Leak Libc)
当一个较大的 Chunk(如 0x100)被释放放入 Unsorted Bin 时,它的 fd 和 bk 指针会被 Ptmalloc 自动填入 main_arena 的地址(位于 libc 的数据段)。
如果程序存在 UAF 漏洞,允许我们在 free 之后依然去读取这块内存,我们就能直接读出 main_arena 的地址,从而计算出 libc 的基址,彻底击碎 ASLR 保护!
3.2 UAF 导致 Fastbin Double Free
如果你连续释放同一个 Fastbin Chunk 两次(free(A); free(A)),新版 Glibc 会检查并报错。
但利用 UAF,你可以绕过检查:free(A); free(B); free(A)。
此时 Fastbin 的链表结构被彻底扭曲,变成了循环链表:Head -> A -> B -> A。
当你再次连续三次 malloc 时:
- 第一次拿到
A。 - 第二次拿到
B。 - 第三次,你又拿到了
A! 这就意味着,有两个不同的变量指针,指向了同一块物理内存。你修改变量 1,变量 2 的内容也会跟着改变,程序逻辑瞬间崩溃。
4. 终极攻击:Fastbin Attack (劫持 GOT 或 __malloc_hook)
结合 UAF 造成的 Double Free 或堆溢出,我们可以发起顶级的 Fastbin Attack。
攻击逻辑推演:
- 利用 UAF,我们在已经释放挂在 Fastbin 里的 Chunk
A中,修改它的fd指针。 - 正常情况下,
A的fd指针应该指向下一个空闲的 ChunkB。但我们通过 UAF,强行把A的fd指针覆盖为一个目标地址(比如__malloc_hook的地址,或者某个函数的 GOT 表地址)。 - 此时,Ptmalloc 的 Fastbin 链表被欺骗了,它变成了:
Head -> A -> 目标地址。 - 我们连续调用两次
malloc:- 第一次
malloc,系统把A分配给我们。Fastbin 链表变为Head -> 目标地址。 - 第二次
malloc,系统竟然把“目标地址”当成一块合法的堆内存,分配给了我们!
- 第一次
终局:
我们获得了一个指针,它指向了 __malloc_hook(这是一个在每次调用 malloc 前都会执行的钩子函数指针)。
我们往这个指针里写入 system 甚至 One Gadget(Libc 中可以直接弹 Shell 的神奇地址)。
接下来,程序只要再调用一次 malloc,就会立刻触发钩子,直接 GetShell!
5. 防御与现代 Glibc 的反击
由于堆利用过于猖獗,Glibc 的维护者在最近几年疯狂打补丁:
- Safe Linking (Glibc 2.32+):对 Fastbin 和 Tcache 的
fd指针进行异或加密(fd ^ (地址 >> 12))。如果攻击者不知道堆的基址,就无法伪造有效的fd指针,彻底封杀了基础的 Fastbin Attack。 - Tcache Count 校验:严格校验缓存链表中的 Chunk 数量,防止恶意构造循环链表。
- Unlink 完整性检查:在双向链表脱链时,严格检查
P->fd->bk == P且P->bk->fd == P。如果指针被篡改,立刻触发corrupted double-linked list崩溃。
堆的深渊是二进制攻防最激烈的前线。在这里,每一次 malloc 和 free 都是内存管理机制与黑客精妙计算的无声较量。