ctf-wiki 堆利用基础:深入剖析 ptmalloc2 的 unlink 宏与 malloc_printerr 错误处理机制
文档网络安全教程【免费下载链接】ctf-wikiCome and join us, we need you!项目地址https://gitcode.com/gh_mirrors/ct/ctf-wiki点击查看免费下载本文以 ctf-wiki 仓库中《ptmalloc2 实现 - 基礎操作》文档为核心结合 glibc malloc 的源码级实现free/malloc中的调用链与malloc_chunk数据结构系统讲解双向链表摘除操作unlink的使用场景、宏实现、安全校验及其在地址泄漏中的妙用并完整解析错误检测入口malloc_printerr的触发与终止流程。读完本文你将掌握 unlink 的原理与绕过思路能够理解堆溢出漏洞中常见的 corrupted double-linked list 报错根源并为后续阅读 ctf-wiki 中的 unlink 利用实战 打下坚实基础。1. unlink 是什么在 ptmalloc2 的堆管理中所有空闲 chunk会被挂载到对应的双向链表bin中。unlink的作用就是把一个已经存在于双向链表中的空闲 chunk 从链表中摘除。由于堆的分配与释放过程会频繁地插入、摘除 chunkglibc 将这一高频操作实现为一个宏unlink(AV, P, BK, FD)。1.1 使用场景根据仓库文档的归纳unlink 主要出现在以下四类路径中触发函数具体场景malloc从恰好大小合适的 large bin 中获取 chunk注意fastbin 与 small bin 的取出不使用 unlink这正是漏洞常出现在这两个 bin 的原因依次遍历处理 unsorted bin 时也不使用 unlink从比请求 chunk 所在 bin 更大的 bin 中取 chunk 时使用free后向合并合并物理相邻的低地址空闲 chunk前向合并合并物理相邻的高地址空闲 chunktop chunk 除外malloc_consolidate后向合并、前向合并除 top chunk 外将 fastbin 中的 chunk 与相邻空闲 chunk 合并realloc前向扩展合并物理相邻的高地址空闲 chunktop chunk 除外从仓库的 free 实现文档 可以看到_int_free中正是通过unlink(av, p, bck, fwd)完成后向合并通过unlink(av, nextchunk, bck, fwd)完成前向合并在 malloc 实现文档 的 large bin 分配路径中取出合适 chunk 前同样会执行unlink(av, victim, bck, fwd)。也就是说凡是涉及“空闲链表摘除”的操作都逃不开 unlink。2. unlink 宏的源码级拆解glibc 中 unlink 的实现如下即 基礎操作文档 中的原始宏/* Take a chunk off a bin list */ // unlink p #define unlink(AV, P, BK, FD) { \ // 由于 P 已经在双向链表中所以有两个地方记录其大小所以检查一下其大小是否一致。 if (__builtin_expect (chunksize(P) ! prev_size (next_chunk(P)), 0)) \ malloc_printerr (corrupted size vs. prev_size); \ FD P-fd; \ BK P-bk; \ // 防止攻击者简单篡改空闲的 chunk 的 fd 与 bk 来实现任意写的效果。 if (__builtin_expect (FD-bk ! P || BK-fd ! P, 0)) \ malloc_printerr (check_action, corrupted double-linked list, P, AV); \ else { \ FD-bk BK; \ BK-fd FD; \ // 下面主要考虑 P 对应的 nextsize 双向链表的修改 if (!in_smallbin_range (chunksize_nomask (P)) \ // 如果P-fd_nextsize为 NULL表明 P 未插入到 nextsize 链表中。 // 那么其实也就没有必要对 nextsize 字段进行修改了。 // 这里没有去判断 bk_nextsize 字段可能会出问题。 __builtin_expect (P-fd_nextsize ! NULL, 0)) { \ // 类似于小的 chunk 的检查思路 if (__builtin_expect (P-fd_nextsize-bk_nextsize ! P, 0) \ || __builtin_expect (P-bk_nextsize-fd_nextsize ! P, 0)) \ malloc_printerr (check_action, \ corrupted double-linked list (not small), \ P, AV); \ // 这里说明 P 已经在 nextsize 链表中了。 // 如果 FD 没有在 nextsize 链表中 if (FD-fd_nextsize NULL) { \ // 如果 nextsize 串起来的双链表只有 P 本身那就直接拿走 P // 令 FD 为 nextsize 串起来的 if (P-fd_nextsize P) \ FD-fd_nextsize FD-bk_nextsize FD; \ else { \ // 否则我们需要将 FD 插入到 nextsize 形成的双链表中 FD-fd_nextsize P-fd_nextsize; \ FD-bk_nextsize P-bk_nextsize; \ P-fd_nextsize-bk_nextsize FD; \ P-bk_nextsize-fd_nextsize FD; \ } \ } else { \ // 如果在的话直接拿走即可 P-fd_nextsize-bk_nextsize P-bk_nextsize; \ P-bk_nextsize-fd_nextsize P-fd_nextsize; \ } \ } \ } \ }2.1 宏的语义拆解逐段理解这个宏大小一致性检查chunksize(P)取的是 P 自身size字段记录的掩去标志位后的大小prev_size(next_chunk(P))是 P 的物理相邻后一块 chunk 中记录的“前一块大小”。由于空闲 chunk 会有两个地方记录自身大小自身size与后一 chunk 的prev_size这里检查二者是否一致防止伪造 size。取出前后指针FD P-fd、BK P-bk即 P 在链表中的后继与前驱。双向链表完整性检查FD-bk ! P || BK-fd ! P。正常情况下P 的后继的bk必然指向 PP 的前驱的fd也必然指向 P。这一检查的目的正是防止攻击者直接篡改空闲 chunk 的 fd/bk 实现任意写——如果攻击者把 fd 改成target-12、bk 改成expect那么FD-bk BK等价于*(target-1212) expect直接构成任意地址写所以 glibc 用双向一致性校验加以拦截。摘除主链FD-bk BK; BK-fd FD;完成双向链表的标准摘除。nextsize 链处理仅当 P 属于large bin 范围!in_smallbin_range且P-fd_nextsize ! NULL时才需要处理。large bin 中的 chunk 除了挂在fd/bk主链上还会按大小挂一条fd_nextsize/bk_nextsize的“跳表”链fd_nextsize指向下一个不同大小的空闲块bk_nextsize指向上一个不同大小的空闲块用于加速按大小查找。摘除 P 时需要同步维护这条链先检查P-fd_nextsize-bk_nextsize P与P-bk_nextsize-fd_nextsize P若 FD 不在 nextsize 链中FD-fd_nextsize NULL若 nextsize 链只有 P 一个节点P-fd_nextsize P则让 FD 自成环否则把 FD 插入到 nextsize 链中若 FD 已在 nextsize 链中则直接摘除 P 即可。源码结构提示宏中只判断了fd_nextsize是否为 NULL 来决定是否进入 nextsize 分支而没有判断bk_nextsize这是 glibc 源码中一个值得留意的细节在构造 large bin 场景的堆布局时可能被利用。2.2 small bin 场景的直观理解原文档以small bin 的 unlink为例给出示意图large bin 的 unlink 与之类似只是多了 nextsize 链的处理可以看到P 最后的 fd 和 bk 指针本身并不会发生变化但由于前驱后继的指针被改写遍历整个双向链表时已经无法再遍历到 P。这一特性在堆利用中非常有用——可以利用 unlink 后的残留指针来泄漏地址泄漏 libc 地址P 位于双向链表头部bk泄漏P 位于双向链表尾部fd泄漏双向链表只包含一个空闲 chunk 时P 位于链表中fd和bk均可以泄漏。泄漏堆地址双向链表包含多个空闲 chunk 时P 位于双向链表头部fd泄漏P 位于双向链表中fd和bk均可以泄漏P 位于双向链表尾部bk泄漏。注意这里的“头部”指 bin 的fd指向的 chunk即双向链表中最新加入的 chunk“尾部”指 bin 的bk指向的 chunk即双向链表中最先加入的 chunk。2.3 检查代码小结无论是fd/bk还是fd_nextsize/bk_nextsizeunlink 都会对指针的一致性进行校验对应代码如下// fd bk if (__builtin_expect (FD-bk ! P || BK-fd ! P, 0)) \ malloc_printerr (check_action, corrupted double-linked list, P, AV); \ // next_size related if (__builtin_expect (P-fd_nextsize-bk_nextsize ! P, 0) \ || __builtin_expect (P-bk_nextsize-fd_nextsize ! P, 0)) \ malloc_printerr (check_action, \ corrupted double-linked list (not small), \ P, AV);表面上看这些检查都很正常P 的 forward chunk后继的bk自然是 Pbackward chunk前驱的fd也自然是 P。但如果没有这些检查攻击者只需修改 P 的fd与bk就能轻松实现任意地址写。这正是 unlink 利用 章节中“古老的 unlink”与“当前带检查的 unlink”两种利用手法的分水岭——后者通过伪造fakeFD 12与fakeBK 8指向同一处可控内存来绕过校验。注意堆的第一个 chunk 所记录的prev_inuse位默认为 1。这一约定在 heap-structure 文档 中有详细说明size字段低三位分别是NON_MAIN_ARENA(0x4)、IS_MAPPED(0x2)、PREV_INUSE(0x1)堆中第一个被分配的内存块的 P 位被置为 1以阻止向前访问非法内存当 P 位为 0 时才能通过prev_size字段回溯上一个 chunk 的大小与地址进而进行空闲 chunk 的合并。3. malloc_printerr错误检测与终止在 glibc malloc 检测到堆元数据被破坏如 unlink 校验失败时会调用malloc_printerr函数。在 glibc 2.23 版本中其实现如下static void malloc_printerr(const char *str) { __libc_message(do_abort, %s\n, str); __builtin_unreachable(); }该函数的核心是调用__libc_message并传入do_abort动作标志最终触发abort()终止进程if ((action do_abort)) { if ((action do_backtrace)) BEFORE_ABORT(do_abort, written, fd); /* Kill the application. */ abort(); }在abort()函数内部glibc 2.23 版本会首先执行fflush(NULL)刷新所有输出流/* Flush all streams. We cannot close them now because the user might have registered a handler for SIGABRT. */ if (stage 1) { stage; fflush (NULL); }从实践角度理解malloc_printerr它不返回__builtin_unreachable()告知编译器后续代码不可达程序必然终止触发的典型报错字符串与 unlink 直接相关的有corrupted size vs. prev_size大小一致性检查失败、corrupted double-linked listfd/bk 双向链表校验失败、corrupted double-linked list (not small)large bin 的 nextsize 链校验失败。当你在调试堆漏洞时看到这些错误就说明 unlink 宏中的某一道安全防线被触碰了此时应回头检查伪造的 chunk 头size、prev_size与fd/bk以及 large bin 场景的fd_nextsize/bk_nextsize是否满足宏中的约束。4. 与相邻模块的关联unlink 并不是孤立的原语它与 ptmalloc2 的其它实现细节紧密相关这里给出仓库内的延伸阅读路径堆数据结构malloc_chunkunlink 操作的字段fd、bk、fd_nextsize、bk_nextsize只有在 chunk 空闲时才有效而prev_size的复用机制解释了为什么 unlink 需要同时检查两处记录的大小。free 实现_int_free中“后向合并 / 前向合并”两处 unlink 的调用上下文以及“只有非 fastbin 的 chunk 才会触发 unlink”的结论fastbin 中的 chunk 的 inuse 位始终为 1不与相邻空闲 chunk 合并。malloc 实现large bin 取出 chunk 时的 unlink 调用以及 unsorted bin 遍历FIFO不使用 unlink 的细节。unlink 利用实战从“古老的 unlink”无检查可直接改写 GOT到“当前 safe unlink”通过伪造fakeFD 12与fakeBK 8指向同一指针使*P P - 8、*P P - 12将指向 chunk 的指针改写为ptr - 0x18再到 2014 HITCON stkof、2016 ZCTF note2 等真实题目的完整利用链。5. 小结unlink 的本质从双向空闲链表中摘除一个节点宏内实现涉及fd/bk主链与 large bin 的fd_nextsize/bk_nextsize跳表链两套数据结构。安全校验大小一致性检查 双向链表完整性检查 large bin nextsize 链检查任何一项失败都会进入malloc_printerr并最终abort()。利用价值尽管 unlink 不再能直接实现任意写但“摘除后 P 的指针残留”可被用于泄漏 libc/堆地址配合精心构造的 fake chunk 与指针布局仍可完成指针改写如ptr → ptr - 0x18这是众多堆利用技巧house-of-* 系列、unsorted bin attack 等的基础。建议读者在掌握本文的宏语义后继续阅读 unlink 利用实战 中的完整 exp亲手验证“伪造 fakeFD/fakeBK 绕过检查”与“通过 unlink 将指针改为ptr - 0x18”这两个核心手法。赞分享文档网络安全教程【免费下载链接】ctf-wikiCome and join us, we need you!项目地址https://gitcode.com/gh_mirrors/ct/ctf-wiki点击查看免费下载相关推荐CTF-Wiki 堆利用基础深入剖析 glibc ptmalloc2 的 perturb_byte 内存填充机制CTF Wiki 堆利用基础深入剖析 glibc ptmalloc2 的 perturb_byte 内存填充机制 perturb_byte 是 glibc p文档网络安全教程ctf-wiki ptmalloc2 堆检查机制全解析malloc/free/unlink 的安全校验与报错信息对照ctf wiki ptmalloc2 堆检查机制全解析malloc/free/unlink 的安全校验与报错信息对照 导读 本文以 ctf wiki 仓库中文档网络安全教程CTF-Wiki 堆利用系列ptmalloc2 Unlink 攻击原理与实战HITCON/ZCTF 经典题目全解析CTF Wiki 堆利用系列ptmalloc2 Unlink 攻击原理与实战HITCON/ZCTF 经典题目全解析 本文以 CTF Wiki 仓库中 un文档网络安全教程上一篇5个高效技巧进阶用户如何优化LaTeX-Workshop配置下一篇MASTG 移动应用安全测试框架定制开发文档开发过程文档创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考