CTF PWN堆利用实战:从UAF到House of Cat的完整利用链构建
1. 项目概述从理论到实战的堆利用进阶之路在CTF的PWN领域堆漏洞利用一直是区分“脚本小子”和“真·二进制选手”的一道分水岭。它不像栈溢出那样有相对固定的套路堆的利用更像是在一个动态、复杂的内存迷宫里寻找那条唯一的、通往任意代码执行的路径。很多初学者在掌握了基础的堆块结构、分配释放原理后面对一道堆题依然无从下手原因就在于缺乏一套从漏洞识别到利用链构建的完整思维框架。今天我们就以“House of Cat”和“UAF”这两个极具代表性的利用技术为核心结合CISCN这类高质量赛事的真题来一次深度的堆利用实战剖析。这篇文章的目标是让你不仅能看懂别人的WP更能自己分析、构造出利用链真正理解堆漏洞利用的精髓。为什么是“House of Cat”和“UAF”它们代表了两种截然不同的利用哲学。UAF是堆漏洞的“经典款”它直接、暴力考验的是对堆管理器行为如glibc ptmalloc的深刻理解和对内存布局的精准控制。而“House of Cat”则是近年来在glibc 2.34及以上版本中兴起的一种新型利用技术它更像是一种“巧劲”通过劫持程序流到精心构造的“假IO_FILE”结构体最终实现任意地址读写或代码执行绕过了新版本中许多传统的劫持hook如__free_hook被移除的限制。掌握这两者基本上就覆盖了当前CTF堆题的大部分核心考点。本文将以一个虚构但融合了CISCN历年堆题常见考点的综合例题为背景模拟真实的解题过程。我们会从程序功能分析、漏洞点定位开始逐步深入到利用链的构思、payload的构造以及最后的exp编写与调试。过程中我会穿插大量我在实际做题和教学中踩过的坑、总结的技巧这些是你在标准文档里看不到的“干货”。无论你是正在备战比赛的CTFer还是希望深入理解Linux堆机制的安全研究者相信这篇指南都能给你带来实实在在的帮助。2. 核心漏洞原理与利用链设计解析在动手写exp之前我们必须像建筑师看蓝图一样彻底理解我们要利用的“材料”漏洞和想要建造的“建筑”利用链。盲目地堆砌payload只会导致崩溃。2.1 UAF的本质与常见触发场景UAF即“释放后使用”。它的核心危害在于一块已经被释放free归还给堆管理器的内存其指针在程序中未被置空成为野指针后续程序依然通过这个指针进行读写操作。此时这块内存可能已经被重新分配用于存放其他数据从而造成数据混淆或者被攻击者控制用于实现恶意目的。在CTF堆题中UAF的触发场景通常隐藏在程序的逻辑里删除功能逻辑错误程序有一个“删除”某数据项的功能它调用了free但没有将指向该数据的全局指针或数组项置为NULL。之后程序的其他功能如“编辑”、“显示”依然通过该指针访问内存。双重释放对同一个指针连续调用两次free如果没有合适的检测会导致堆管理结构损坏是更为严重的漏洞。结构体设计缺陷例如一个结构体包含一个指针成员该指针指向另一块堆内存。当释放这个结构体时只释放了结构体本身没有释放其内部的指针成员所指向的内存而后续又通过其他方式操作了那块“孤儿内存”。利用UAF的关键在于控制“释放后”到“再次使用前”这段时间窗口内那块被释放内存的内容。在ptmalloc中小内存块被释放后会进入对应大小的fastbin或tcache链表中。如果我们能在这个阶段通过其他功能如“添加”申请到同样大小的内存并写入我们控制的数据那么当那个野指针再次被使用时它读写的就是我们精心布置的数据了。注意现代glibc对tcache和fastbin都有一些简单的检测比如tcache会检查被释放的块是否已经在链表中防止双重释放到tcache但这些检查并非绝对安全尤其是在与其他漏洞结合时。2.2 House of Cat新时代的IO流劫持艺术随着glibc 2.34移除了__malloc_hook,__free_hook,__realloc_hook等常用的gadget传统的通过覆盖这些hook指针来劫持程序流的方法失效了。攻击者需要寻找新的“跳板”。“House of Cat”技术应运而生它将目标转向了另一个强大的结构体_IO_FILE_plus通常我们称之为FILE结构体或IO_FILE。每个通过fopen、stdin、stdout、stderr等打开的流在内部都对应一个_IO_FILE_plus结构体。这个结构体非常复杂包含了一系列的函数指针表vtable用于执行读、写、关闭等操作。House of Cat的核心思想是能够通过堆溢出、UAF等漏洞伪造一个_IO_FILE_plus结构体。能够触发一次对IO流的操作如调用exit函数它会刷新所有流或调用puts其内部会检查stdout的状态并且让程序误以为我们伪造的结构体是一个合法的流。在伪造的结构体中控制关键的字段和vtable中的函数指针最终导向任意地址调用或读写。为什么叫“House of Cat”这个命名延续了堆利用技术“House of X”的系列传统。它利用的是_IO_obstack_jumps这个相对“冷门”的vtable。在glibc源码中有一个用于内部内存分配的结构叫obstack它有一组特定的IO操作函数。House of Cat通过伪造vtable指向_IO_obstack_jumps并精心设置结构体中的字段如_IO_write_base,_IO_write_ptr,_IO_write_end使得在调用_IO_obstack_xsputn函数时可以实现任意地址写。再结合其他技巧就能完成利用链。关键点House of Cat不直接获得代码执行而是先获得一个强大的任意地址写原语。我们可以用这个原语去修改got表、修改关键函数指针或者修改tcache/fastbin的fd指针为后续获得代码执行铺平道路。2.3 例题场景与漏洞点假设为了综合演示我们假设一个CISCN风格的题目它有以下功能add(size, data)申请指定大小的堆块并写入数据。大小限制在0x400以下。show(idx)打印索引idx处堆块的内容。edit(idx, size, data)修改索引idx处堆块的内容可以重新指定大小存在堆溢出漏洞。delete(idx)释放索引idx处的堆块但未将指针置空存在UAF漏洞。程序使用libc-2.35.so保护全开RELRO, NX, PIE, ASLR。我们发现的漏洞组合是漏洞AUAFdelete后未置空指针show和edit功能仍能使用。漏洞B堆溢出edit功能中的size参数用户可控且新的size可以大于原堆块大小导致向相邻堆块溢出。我们的利用链设计思路信息泄露利用UAF和堆布局泄露堆地址和libc基址。这是所有利用的基础。构造任意地址写利用堆溢出配合UAF篡改某个已释放堆块的fd指针实现tcache poisoning从而让malloc返回一个我们可控的地址例如__free_hook所在附近区域。但注意glibc 2.35没有__free_hook。所以我们的目标改为在堆上伪造一个_IO_FILE_plus结构体。触发House of Cat通过篡改stdout或stderr结构体本身的某些字段或者通过tcache poisoning让一次malloc返回一个指向_IO_FILE_plus结构体的指针并最终触发一次IO操作例如通过溢出篡改exit函数相关的结构或者利用程序本身会调用puts打印菜单的特性执行我们伪造的vtable函数获得任意地址写能力。完成利用利用任意地址写修改got表中某个函数的地址为system或者写入shellcode到可写可执行区域如果题目特殊设置最后触发该函数调用拿到shell。3. 环境准备与动态调试技巧工欲善其事必先利其器。堆利用调试比栈溢出更依赖动态分析。3.1 调试环境搭建与工具链Linux环境推荐使用Ubuntu 22.04或Kali Linux其默认libc版本较高与当前CTF趋势相符。可以使用Docker容器来隔离环境避免污染宿主机。# 拉取一个带有调试工具的镜像 docker run -it --name pwn_env -v $(pwd):/workspace ubuntu:22.04 /bin/bash # 进入容器后安装必要工具 apt update apt install -y gdb gdb-multiarch python3 python3-pip git make pip3 install pwntools核心工具pwntoolsExp编写的瑞士军刀。一定要熟练使用它的process,remote,sendline,recvuntil等函数以及ELF,libc等模块来解析地址。gdb pwndbg/gef动态调试的不二之选。pwndbg和gef是增强插件能直观显示堆块状态、内存布局、寄存器信息。我个人更习惯pwndbg它的heap命令系列非常强大。# 安装pwndbg git clone https://github.com/pwndbg/pwndbg cd pwndbg ./setup.shlibc-database或libc.rip用于根据泄露的地址查找libc版本。本地可以搭建libc-database或者直接使用在线网站。one_gadget用于在libc中寻找执行execve(“/bin/sh”, 0, 0)的gadget。虽然House of Cat不直接依赖它但在最终获取shell时可能用到。gem install one_gadget one_gadget /path/to/libc.so.63.2 动态调试中的关键断点与观察点调试堆题下对断点事半功倍。关键函数断点# 在gdb中 break malloc break free break realloc break _int_malloc # glibc内部实现有时需要深入到这里 break _int_free当程序断在malloc或free时使用pwndbg的heap命令查看堆状态变化。观察点Watchpoint用于追踪关键内存数据的变化比如某个堆块的fd指针。# 假设0x555555757260是一个tcache的fd指针 watch *0x555555757260当该地址的值被修改时gdb会自动暂停可以回溯是哪个函数、哪条指令修改了它。利用pwndbg分析堆heap显示所有堆块。bins显示所有binstcache, fastbin, unsorted bin, small bin, large bin的状态。这是最常用的命令。vis以图形化方式查看堆内存非常直观。tcache单独显示tcache的状态。parseheap尝试解析堆结构。实操心得在编写exp时我习惯在关键的堆操作如一次精心布局的add或delete前后通过pwntools的pause()函数让程序暂停然后切换到gdb attach上去执行bins等命令查看堆布局是否符合预期。这个“预期管理”是调试堆利用的核心。3.3 题目信息收集与逆向分析拿到题目二进制文件不要急着运行。checksec ./challenge查看保护机制。确保你注意到了FULL RELRO意味着GOT表不可写和PIE代码段地址随机化。file ./challenge和ldd ./challenge查看文件类型和链接的libc。有时题目会附带一个libc.so.x一定要用它而不是系统自带的。静态分析用IDA Pro或Ghidra打开快速理清程序逻辑。找到main函数和各个功能函数add,show,edit,delete。重点关注edit函数查看它对size的处理。是否存在类似read_input(ptr, new_size)的调用而new_size可以大于堆块原本的size这就是堆溢出。重点关注delete函数查看它free之后是否对全局指针数组做了array[idx]0的操作。如果没有就是UAF。查看程序初始化时是否调用了setbuf或setvbuf来关闭缓冲区。这会影响IO有时也与利用相关。确定漏洞函数结合动态调试在疑似漏洞点下断输入特定数据验证猜想。例如对于疑似溢出的edit可以申请两个相邻块A和B然后尝试用edit修改A写入超过A大小的数据观察B的内容是否被覆盖。4. 利用链实战构造从泄露到House of Cat现在我们进入最核心的实战部分。假设通过逆向我们确认了edit存在堆溢出delete存在UAF。4.1 阶段一泄露堆与libc基址地址随机化ASLR下我们需要先泄露地址。堆地址通常通过main_arena中的指针泄露libc地址则通过unsorted bin中的指针泄露。步骤1布局堆与制造重叠申请多个小堆块如size0x100填满tcache的某个链表例如tcache[0x110]。申请两个不相邻的、大小大于tcache max size默认0x410的堆块例如chunk_A (size0x500)和chunk_B (size0x500)。当它们被释放时会进入unsorted bin。利用UAF在释放chunk_A后不释放其指针而是用show功能打印它。此时chunk_A的fd和bk指针在unsorted bin中指向的是main_arena内部的地址。这个地址与libc基址有固定偏移。注意在glibc 2.35中unsorted bin中的fd/bk可能指向main_arena内部的某个结构计算偏移时需要根据libc版本确定。可以使用pwntools的libc.symbols[‘main_arena’]或libc.address 0x1f12c0示例偏移来计算。计算libc_base leak_addr - libc.sym[‘main_arena’] - 0x10注意偏移可能因版本而异需动态调试确认。步骤2泄露堆地址堆地址的泄露通常通过tcache或fastbin的fd指针。因为当堆块在bin中时fd指向下一个堆块在堆区内。申请两个相同大小的小块如0x100先后释放它们到tcache。此时第一个块的fd指向第二个块。利用UAFshow第一个块就能读到堆地址。结合这个地址和已知的堆块大小可以推算出堆的起始地址heap_base。实操心得泄露阶段最怕的就是程序崩溃。务必确保在释放堆块到unsorted bin之前对应的tcache链表是满的否则小块也会进入unsorted bin干扰泄露。另外计算偏移时最好写一个本地测试脚本多次运行对比确保稳定性。4.2 阶段二构造任意地址写原语Tcache Poisoning有了地址我们就可以尝试篡改堆指针了。目标是让下一次malloc返回到一个我们控制的地址。准备目标地址我们计划在堆上伪造一个_IO_FILE_plus结构体。需要先申请一块足够大的内存比如0x200作为伪造结构体的存储区记下它的地址fake_io_addr。实施投毒假设我们有一个大小为0x110的tcache链表。通过add申请两个0x110的块P1和P2。delete(P2);delete(P1)。此时tcache[0x120]注意size包含chunk header链表为head - P1 - P2 - NULL。利用UAF和edit功能修改P1的fd指针因为P1已被释放但其指针仍可写。我们将P1-fd从原来的P2修改为target_addr。这个target_addr是我们希望malloc返回的地址。但这里有个关键target_addr需要看起来像一个合法的堆块头size字段。通常我们会找一个已知的、可写的内存区域其size字段我们可以通过堆溢出等方式提前布置好。一个更常用的技巧是让target_addr指向一个我们已控制的堆块内部例如fake_io_addr - 0x10减去0x10是为了让返回的指针指向chunk data区域时刚好对准我们伪造的结构体开始。现在tcache链表变为head - P1 - target_addr - ???。连续两次add(0x100)第一次会返回P1第二次就会返回target_addr我们就获得了一个指向目标地址的指针。注意target_addr必须满足对齐要求并且其对应的“size”字段要能通过tcache的检查例如size要在tcache范围内且对应链表未满。有时需要提前在target_addr处布置好伪造的chunk header。4.3 阶段三伪造IO_FILE结构体与House of Cat触发这是最精妙的一步。我们需要在fake_io_addr处布置一个伪造的_IO_FILE_plus结构体。结构体伪造_IO_FILE_plus包含前面的_IO_FILE结构体和后面的vtable指针。我们需要设置大量字段这里列举最关键的几个_flags需要设置一个合适的值例如0x8000或者参考真实stdout的_flags。_IO_write_base,_IO_write_ptr,_IO_write_end这是实现任意地址写的关键。_IO_write_ptr需要大于_IO_write_base_IO_write_end需要指向写入的结束地址。当调用_IO_obstack_xsputn时它会从_IO_write_base向_IO_write_ptr写入数据。如果我们控制_IO_write_base为一个目标地址比如freegot.plt_IO_write_ptr为目标地址8那么就会向目标地址写入8个字节的数据数据来源是_IO_read_ptr等字段也需要控制。vtable指向伪造的vtable。通常我们不直接伪造整个vtable表而是让vtable指向一个已知的、合法的vtable地址然后通过偏移计算让其某个函数指针如__overflow指向我们希望的gadget。House of Cat的精髓在于利用_IO_obstack_jumps这个现成的vtable。我们可以让vtable libc_base _IO_obstack_jumps_offset。然后我们需要计算_IO_obstack_jumps中__overflow函数的偏移并确保我们伪造的IO_FILE结构体能够引导执行流走到那里。其他字段如_IO_read_ptr,_IO_read_end,_IO_buf_base等通常需要设置为0或特定的值以避免崩溃具体需要根据glibc源码和调试确定。触发路径如何让程序使用我们伪造的IO_FILE常见方法有劫持stdout或stderr如果我们能通过任意地址写修改stdout结构体本身的vtable指针或者修改其_flags等关键字段使其指向我们伪造的结构体那么当下一次调用puts或printf时就会触发。FSOPFile Stream Oriented Programming如果程序在退出时调用了exit或_exitexit函数会调用_IO_cleanup来刷新所有IO流。我们可以伪造一个_IO_list_all全局变量指向的链表在其中插入我们伪造的IO_FILE从而在退出时触发。在我们的例题中假设程序菜单每次都会用puts打印。如果我们通过tcache poisoning让某次malloc返回的指针恰好是stdout结构体附近的某个可控地址然后通过edit修改stdout的部分内容也能实现劫持。构造任意写当伪造的IO_FILE被_IO_obstack_xsputn处理时我们之前设置的_IO_write_base和_IO_write_ptr就起作用了。通过精心构造我们可以向任意地址比如freegot.plt写入任意数据比如system的地址。一个简化的payload构造示例伪代码# 假设我们通过tcache poisoning能向地址fake_io_addr写数据 fake_io p64(0x8000) # _flags fake_io p64(0) * 若干字段... fake_io p64(target_addr) # _IO_write_base 指向想写的地址如freegot fake_io p64(target_addr 8) # _IO_write_ptr fake_io p64(target_addr 8) # _IO_write_end fake_io p64(0) * ... # 填充其他字段 fake_io p64(libc_base libc.sym[_IO_obstack_jumps]) # vtable # 将fake_io写入 fake_io_addr edit(controlled_chunk_idx, len(fake_io), fake_io) # 然后触发对伪造流的操作例如修改stdout的vtable指向fake_io_addr0x100vtable位置 # 或者通过FSOP触发4.4 阶段四劫持控制流与获取Shell通过House of Cat获得任意地址写能力后剩下的路就宽了。目标选择在FULL RELRO下GOT表不可写。我们通常选择修改exit相关函数指针如果程序会调用exit可以修改其地址为one_gadget或system。修改__malloc_context或__printf_function_table等全局函数指针表这些位置在某些情况下可写且会被调用。栈劫持如果能通过任意写修改栈上的返回地址或函数指针需要同时泄露栈地址。最稳健的方法利用任意写在堆上布置shellcode然后修改某个函数指针如_IO_file_jumps中的某个函数指向堆上的shellcode。但这需要堆可执行NX未开启时或者结合ROP。本题假设我们选择修改printf的GOT表项如果程序是Partial RELRO为system地址。然后在下次调用printf时如果我们的参数可控比如菜单中有一个功能是printf(buf)我们就可以传入/bin/sh字符串实际上就会执行system(“/bin/sh”)。写入数据利用House of Cat的任意写原语将system的地址写入printfgot.plt。触发调用程序中会触发printf的功能传入/bin/sh字符串。5. 完整Exploit编写与调试实录将上述步骤转化为pwntools脚本是一个系统工程。5.1 Exp脚本框架与交互函数#!/usr/bin/env python3 from pwn import * context.arch ‘amd64‘ context.log_level ‘debug‘ # 调试时开启能看到详细的发送接收数据 elf ELF(‘./challenge‘) libc ELF(‘./libc.so.6‘) # 使用题目提供的libc # 如果远程用 remote(‘host‘, port) p process(‘./challenge‘) def add(size, data): p.sendlineafter(b‘ ‘, b‘1‘) p.sendlineafter(b‘size: ‘, str(size).encode()) p.sendafter(b‘data: ‘, data) def show(idx): p.sendlineafter(b‘ ‘, b‘2‘) p.sendlineafter(b‘idx: ‘, str(idx).encode()) # 返回泄露的数据需要解析 def edit(idx, size, data): p.sendlineafter(b‘ ‘, b‘3‘) p.sendlineafter(b‘idx: ‘, str(idx).encode()) p.sendlineafter(b‘size: ‘, str(size).encode()) p.sendafter(b‘data: ‘, data) def delete(idx): p.sendlineafter(b‘ ‘, b‘4‘) p.sendlineafter(b‘idx: ‘, str(idx).encode()) # 1. 泄露libc和堆地址 # ... 具体代码调用上述函数进行堆布局和泄露 libc_base leak_addr - libc.sym[‘main_arena‘] - 0x10 log.success(f“libc_base: {hex(libc_base)}“) libc.address libc_base # 设置libc基址方便后面用libc.sym[‘system‘] # 2. Tcache Poisoning # ... 具体代码实现篡改fd指针 target_addr fake_io_addr - 0x10 # 假设目标地址 edit(poison_idx, len(p64(target_addr)), p64(target_addr)) # 3. 伪造IO_FILE结构体 fake_io_struct build_fake_io(libc_base, target_got) # 自己实现这个构建函数 # 将伪造的结构体写入可控内存 edit(fake_chunk_idx, len(fake_io_struct), fake_io_struct) # 4. 触发House of Cat实现任意写 # 修改stdout或触发FSOP trigger_house_of_cat() # 5. 修改GOT或函数指针 # 假设通过任意写将system地址写入printf_got system_addr libc.sym[‘system‘] printf_got elf.got[‘printf‘] # 利用House of Cat的任意写原语完成写入具体调用取决于你的触发方式 arbitrary_write(printf_got, p64(system_addr)) # 6. 触发system(“/bin/sh“) p.sendlineafter(b‘ ‘, b‘5‘) # 假设5号功能是printf(buf) p.sendlineafter(b‘input: ‘, b‘/bin/sh\x00‘) p.interactive()5.2 动态调试让Exp跑起来写好的exp第一次运行十有八九会崩溃。调试是关键。分阶段调试不要一次性写完所有功能。先测试泄露部分确保能稳定打印出libc地址。可以在脚本中pause()然后用gdb attach上去验证。# 在泄露代码后 log.info(“Leak phase done, attach gdb now.“) pause() # 此时脚本暂停等待用户输入在另一个终端gdb -p pid然后查看泄露的地址是否正确。观察堆状态在tcache poisoning前后使用gdb的heap bins命令确认tcache链表是否按预期被修改。跟踪IO操作在触发House of Cat前可以在_IO_obstack_xsputn或_IO_file_overflow等函数上下断点单步跟踪看程序是否走进了我们预设的路径我们伪造的结构体字段是否被正确读取。处理崩溃如果程序崩溃在malloc或free很可能是堆结构被破坏如corrupted size vs. prev_size。回顾之前的操作检查是否有溢出写坏了下一个堆块的size字段或prev_size字段。如果崩溃在IO函数内部可能是伪造的IO_FILE结构体某些字段不符合glibc内部检查需要对照源码或通过调试调整。常见崩溃点与调整_IO_validate_vtable失败glibc会检查vtable是否在一个合法的vtable列表中。_IO_obstack_jumps是合法列表中的所以用它。如果崩溃在这里检查vtable指针是否正确。写入地址不可写确保_IO_write_base指向的地址具有写权限。字段不匹配导致整数溢出或空指针解引用需要仔细调试逐个字段检查。有时需要将某些字段设置为0或特定的魔法值。5.3 优化与稳定化一个能本地跑通的exp打到远程可能因为环境差异如libc版本细微差别、堆布局随机性而失败。堆布局稳定化在泄露和布局阶段多申请几个“填充”块来稳定堆的布局减少ASLR带来的堆地址随机性影响。偏移自适应不要硬编码偏移。使用pwntools的DynELF如果可用或者通过泄露多个地址来计算关键符号的准确地址。错误处理在exp中加入一些尝试和重试的逻辑比如如果第一次泄露失败尝试另一种布局。日志输出在exp的关键步骤打印丰富的日志信息便于远程调试时分析失败原因。6. 常见问题排查与高阶技巧即使理解了原理实战中依然会遇到各种“妖孽”问题。6.1 典型错误与解决方案速查表问题现象可能原因排查与解决思路malloc(): corrupted top size堆顶top chunk的size字段被意外修改。检查是否有堆溢出写到了top chunk。确保你的溢出操作没有越过最后一个分配的块。free(): invalid pointer尝试释放一个非堆内存地址或堆块头被破坏。检查释放的指针是否在预期的堆区域内。检查该堆块前后的chunk header是否完整。free(): double free detected in tcache 2对同一个堆块进行了两次free且被tcache检测到。检查UAF逻辑确保没有对已释放的指针再次调用delete。注意如果tcache链表已满双重释放可能不会立即触发此错误但会破坏堆结构。malloc(): invalid size (unsorted)申请的大小不符合要求或者unsorted bin中的块size字段异常。检查malloc的size参数。检查unsorted bin中块的size字段是否被溢出修改。泄露的地址看起来不对打印到了错误的数据或者堆布局不符合预期。使用gdb的x/gx命令查看泄露指针所在内存的真实内容。确认释放的块是否真的进入了unsorted bin用heap bins unsorted查看。House of Cat触发后无效果伪造的IO_FILE结构体未被使用或字段设置错误。在_IO_obstack_xsputn等函数设断点看是否执行到。单步跟踪检查_IO_write_base等关键寄存器的值是否为目标地址。对照glibc源码检查结构体字段。远程打不通本地可以远程libc版本不同、堆初始化状态不同、网络延迟导致交互时序问题。确认远程libc版本调整偏移。在脚本关键点后增加sleep(0.1)。尝试更稳定的堆布局手法如大量填充块。6.2 高阶技巧应对沙箱与保护现代CTF题常常附加seccomp沙箱限制系统调用。检测沙箱使用seccomp-tools分析二进制文件。seccomp-tools dump ./challenge查看允许哪些系统调用。如果禁止了execve那么system(“/bin/sh”)的路就走不通了。ORWOpen-Read-Write利用如果沙箱允许open、read、write那么利用目标就是读取flag文件。此时利用链的终点不再是获取shell而是通过任意地址写构造ROP链调用open(“./flag”, 0)。然后调用read(fd, buf, 0x100)将flag读入内存如bss段。最后调用write(1, buf, 0x100)输出到标准输出。这需要你能控制栈如通过__malloc_context劫持栈指针或者有足够的gadget进行链式调用。栈迁移如果无法直接执行复杂ROP但能控制堆内存和某个指针如_IO_FILE的_chain字段或某个函数指针可以尝试将栈迁移到可控的堆内存上然后在堆上布置ROP链。这需要找到类似leave; ret或xchg rsp, rax; ret这样的gadget。6.3 思维提升从解题到出题真正掌握一项技术最好的方法是尝试出题。当你自己设计一道堆漏洞题目时你会思考如何将漏洞隐藏得更深比如把UAF放在一个不常用的结构体释放路径里。如何增加利用难度比如引入随机大小分配、限制分配次数、使用自定义的堆分配器。如何设置多重保护结合seccomp、FULL RELRO、PIE。如何引导选手走向预期的解法比如提供一些看似无用的字符串或函数作为one_gadget或system的提示。这个过程会让你对漏洞原理和利用技巧的理解达到新的高度。堆漏洞利用的学习曲线陡峭但回报丰厚。它锻炼的不仅仅是漏洞利用技巧更是对计算机系统底层内存管理的深刻理解、严谨的逻辑思维和强大的动态调试能力。从UAF到House of Cat从泄露到最终getshell每一步都像是在完成一件精密的微雕作品。希望这篇指南能成为你手中的刻刀助你在CTF的PWN世界中雕刻出属于自己的精彩作品。记住多看源码多动手调试所有的答案都在代码和内存的细微变化之中。