
1. 从一道CTF题看堆利用的三种经典路径最近在复盘一些经典的CTF堆利用题目BUUCTF平台上的“网鼎杯 2018 第三场”的wdb_2018_3rd_pesp这道题可以说是一个绝佳的堆漏洞利用“演武场”。这道题本身是一个标准的菜单堆题漏洞点也很直接但它的价值在于它几乎完美地串联起了三种在CTF和实际漏洞利用中极为关键的堆利用技术利用__realloc_hook劫持控制流、利用unlink攻击进行任意地址写以及通过堆布局向BSS段写入数据。很多初学者在接触堆利用时往往觉得这些技术点孤立且复杂而这道题恰恰提供了一个将它们融会贯通的视角。今天我就结合这道题的具体场景把这三种方法的原理、操作细节和背后的“为什么”彻底拆解清楚希望能帮你建立起一个更立体的堆利用知识框架。这道题的程序逻辑并不复杂提供了常见的malloc、free、edit和show功能。漏洞存在于edit函数中存在一个典型的堆溢出漏洞允许我们向一个堆块写入超出其本身大小的数据。这个漏洞就是我们发起所有攻击的起点。我们的最终目标通常是执行system(“/bin/sh”)来获取shell。为了实现这个目标我们需要找到一个方法将system函数的地址写入到某个能够被程序执行的地方比如__malloc_hook或__free_hook。这道题的巧妙之处在于它限制了我们malloc的大小使得直接通过fastbin attack攻击__malloc_hook变得困难从而迫使我们思考更多样化的利用路径。下面我们就逐一深入这三种方法。2. 方法一迂回攻击——利用__realloc_hook作为跳板当我们无法直接向__malloc_hook写入system地址时__realloc_hook就成了一个非常理想的跳板。这里首先要理解__realloc_hook和__malloc_hook的关系。在glibc的内存分配器中malloc函数在入口处会检查__malloc_hook是否为空如果不为空则直接跳转到__malloc_hook指向的函数去执行。而__realloc_hook是realloc函数内部的一个钩子。关键在于realloc函数在开始执行时会先检查__realloc_hook如果它不为空同样会跳转执行。更重要的是realloc函数内部在特定路径下会调用malloc。这就为我们提供了一个链式调用的机会。注意在较新版本的glibc2.34之后中这些_hook变量已被移除这种利用方式随之失效。但在2.23-2.32等经典版本中这仍是核心技巧。攻击链路的构建逻辑如下目标我们最终希望执行malloc时能触发system(“/bin/sh”)。直接障碍无法直接向__malloc_hook写入system地址例如由于size限制无法构造合适的fake chunk。迂回策略我们将__realloc_hook覆盖为system函数的地址。同时将__malloc_hook覆盖为realloc函数内部某个特定偏移的地址。触发流程当我们调用malloc(size)时程序首先跳转到__malloc_hook即realloc某偏移。从这个偏移开始执行realloc代码realloc一开始就检查__realloc_hook发现其指向system于是跳转去执行system。此时realloc函数的第一个参数通常存储在rdi寄存器会作为system的参数。如果我们能控制这个参数为/bin/sh的地址就能成功getshell。那么realloc的第一个参数是什么是realloc原本要处理的原内存地址。但当我们是从malloc“拐”进来的并没有原指针。实际上此时rdi寄存器中的值很大程度上取决于我们调用malloc时传入的size参数以及__malloc_hook被覆盖为realloc的具体偏移。经过调试可以发现存在某个偏移使得此时rdi寄存器恰好指向我们即将通过malloc申请到的chunk的用户数据区。因此我们只需要提前在这个chunk的用户数据区布置好字符串/bin/sh再触发这个精心构造的malloc调用就能让system(“/bin/sh”)成功执行。在这道题中的实操要点首先利用堆溢出通过fastbin attack或unsorted bin attack等手法实现向__malloc_hook和__realloc_hook所在的内存区域写地址的能力。由于题目限制直接攻击__malloc_hook可能困难但攻击它附近的__realloc_hook区域可能size条件更宽松。计算正确的realloc偏移。这个偏移不是固定的需要根据本地的glibc版本通过调试确定。通常我们会尝试realloc函数开头往后的一些偏移观察哪个偏移能让rdi指向可控的内存区域。将/bin/sh字符串写入一个即将被申请的chunk中。覆盖__malloc_hook为reallocoffset覆盖__realloc_hook为system地址。调用malloc申请那个存有/bin/sh的chunk触发攻击链。这种方法的核心思想是“借道而行”利用内存分配器内部函数的调用关系将一个难以直接利用的钩子__malloc_hook转换成一个容易利用的钩子__realloc_hook体现了在限制条件下灵活寻找攻击面的思路。3. 方法二釜底抽薪——利用unlink实现任意地址写如果说_hook攻击是“劫持流程”那么unlink攻击就是“篡改数据”。它的威力在于一旦成功可以让我们向任意地址写入一个任意值通常是一个堆地址这为后续的利用打开了无限可能。unlink是glibc中用于从双向链表如smallbin或largebin中摘除一个空闲chunk的操作。攻击的关键在于伪造一个空闲chunk并诱使程序对这个fake chunk执行unlink操作。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 (“check_action”); else { FD-bk BK; BK-fd FD; ... } }这段代码看起来只是简单的链表节点删除操作P-fd-bk P-bk;和P-bk-fd P-fd;。但正是这两个赋值语句如果P、fd、bk都是我们可控的就能实现任意地址写。攻击条件与伪造chunk的构造要成功利用unlink我们需要伪造一个free状态的chunkP并让它看起来处于一个双向链表中。这意味着我们需要控制P的fd和bk指针。同时为了通过unlink时的安全检查FD-bk P BK-fd P我们需要让FD-bk和BK-fd都指向P。这听起来像是一个“先有鸡还是先有蛋”的问题但解决方案很巧妙我们让FD和BK指向P附近的内存地址。假设我们在一个可控的堆块记为chunk0里伪造了一个free的chunk结构PP的fd指针指向(目标写地址 - 0x18)bk指针指向(目标写地址 - 0x10)。同时我们确保目标写地址处的内容即FD-bk和BK-fd恰好是P的地址。这通常通过堆布局让另一个堆块chunk1的prev_size和size域被我们控制从而在free(chunk1)时glibc会认为前面的chunk0即我们的fake chunkP是空闲的并尝试将其从所谓的“链表”中unlink出来从而触发我们的恶意逻辑。在这道题中的具体步骤布局堆内存申请两个相邻的chunkchunk0和chunk1。chunk0的大小要足以让我们在里面伪造一个完整的chunk结构含prev_size,size,fd,bk等。伪造freechunk在chunk0的数据区构造一个fake chunk。设置其size域并将PREV_INUSE位设为0表示前一个chunk是空闲的这里就是它自己这是一种欺骗。最关键的是设置fd和bk使其满足unlink检查条件并指向我们想要写入的目标地址例如global_max_fast一个全局变量或BSS段上的一个函数指针。触发unlink通过堆溢出修改chunk1的prev_size为chunk0中fake chunk的大小并将chunk1的size域的PREV_INUSE位清零。然后调用free(chunk1)。free操作会检查前一个chunk是否空闲根据chunk1的PREV_INUSE位发现是“空闲”后会尝试将前一个chunk即我们伪造的chunkP与chunk1合并。合并前需要将fake chunkP从它所在的“链表”中unlink出来从而执行我们预设的写操作。利用写入结果unlink操作最终会执行FD-bk BK即*(目标写地址) BK。BK是我们伪造的bk指针通常指向一个堆地址。这意味着我们成功向目标写地址写入了一个堆地址。这个堆地址可以用来做什么我们可以进一步利用这个写入的地址通过编辑chunk0的内容因为写入的地址可能指向chunk0附近来实现对目标写地址处的值进行二次修改例如将其改为system地址。unlink攻击是一种相对古典但非常强大的技术它不依赖于任何特定的钩子函数而是直接利用堆管理器的内部操作逻辑来实现内存篡改。理解unlink是深入理解glibc堆管理机制的重要一环。4. 方法三直击要害——向BSS段写入可控数据BSS段通常存储着程序的全局变量和静态变量。如果程序在BSS段上存储了函数指针比如printf的got表项虽然不在BSS段但一些程序自定义的函数指针或atexit链表可能在BSS段那么向BSS段写入system地址就是一种直接的攻击方式。即使没有现成的函数指针向BSS段写入可控数据也能为后续利用创造条件比如在BSS段上构造一个fakeFILE结构体进行FSOP攻击。那么如何从堆区“穿越”到BSS段进行写操作呢这通常需要一种能够实现任意地址写的原语。而这道题展示的正是如何利用堆漏洞培育出这样一个原语。常见的培育路径制造一个“野指针”首先我们需要一个指向我们想要修改的BSS段地址的指针。这个指针本身可能存在于堆上、栈上或者BSS段自身。通过unlink攻击如上所述我们可以向BSS段的某个地址比如一个全局指针变量global_ptr写入一个堆地址。现在global_ptr就指向了堆上的某个位置例如chunk0的fd指针所在处。通过编辑“野指针”指向的内容实现任意写由于global_ptr现在指向堆上的可控数据区程序如果提供了基于global_ptr的编辑功能或者我们能通过其他漏洞触发编辑我们就可以修改global_ptr所指向的内存内容。但我们的目标是修改BSS段的其他地方比如一个函数指针func_ptr。将“任意写”能力进行转移关键在于我们可以在堆上伪造一个数据结构。例如我们在chunk0中布置一个fake_chunk并将其fd指针设置为func_ptr - 0x18为后续可能的unlink或类似操作做准备。然后我们通过编辑global_ptr指向的内存即chunk0的开头将global_ptr本身的值覆盖为fake_chunk的地址。现在global_ptr就指向了我们伪造的fake_chunk。触发最终写入程序再次使用global_ptr进行写操作时例如调用一个以global_ptr为参数的edit函数写入的数据就会落到fake_chunk的fd指针等位置上。如果我们能进一步触发某个操作比如再次unlink或者程序逻辑会解引用fake_chunk-fd并写入就有可能将system地址写入func_ptr。在这道题中的串联应用在这道wdb_2018_3rd_pesp中BSS段上可能就存在一些有用的全局变量。解题思路往往是复合式的首先利用unlink攻击向BSS段上的一个全局指针比如ptr_array[0]写入一个堆地址。这建立了堆到BSS段的初步联系。然后利用程序的edit功能通过这个已被篡改的ptr_array[0]去修改堆上的数据在堆上精心布置一个fake fastbin chunk其fd指针指向__malloc_hook附近的一个合法可分配内存地址。接着通过malloc申请到这个fake chunk从而获得一个指向__malloc_hook附近区域的指针。最后再利用edit功能通过这个指针直接覆写__malloc_hook为one_gadget或system地址。这种方法体现了堆利用中“步步为营”的思想。从一个简单的堆溢出开始通过一系列精巧的内存布局和操作链将漏洞的破坏力逐步传导至关键的内存位置。向BSS段写入只是中间环节目的是为了搭建一个跳板最终目标仍然是劫持控制流。5. 三种方法的对比与实战选择方法核心原理优势劣势/条件适用场景__realloc_hook跳板利用realloc内部调用malloc及钩子检查机制形成调用链劫持控制流。1. 不依赖unlink等复杂构造。2. 只需覆盖两个钩子触发简单。3. 是绕过__malloc_hook直接攻击限制的有效方法。1. 依赖特定glibc版本2.34。2. 需要精确计算realloc偏移可能因环境而异。3. 需要能在__malloc_hook附近进行写操作。存在堆溢出且能攻击_hook附近内存但直接写__malloc_hook受阻时。unlink攻击伪造空闲chunk利用unlink宏的安全检查漏洞实现任意地址写。1. 实现真正的任意地址写能力强大。2. 不依赖特定函数指针或钩子通用性较强。3. 是理解glibc堆管理机制的必修课。1. 构造复杂需精心布局满足FD-bkP BK-fdP。2. 通常需要堆溢出能修改下一个chunk的头部信息。3. 写入的值通常是一个堆地址需二次利用。存在堆溢出且能修改相邻chunk的size/prev_size程序存在可被篡改的全局指针。写入BSS段通过堆漏洞培育出的任意写能力修改BSS段上的关键数据如指针、函数地址。1. BSS段地址通常固定且已知目标明确。2. 可为后续更复杂的利用如FSOP奠定基础。3. 常作为利用链中的关键中间步骤。1. 通常不是最终利用需要结合其他技术如unlink获得初始写能力。2. 需要BSS段上存在有价值的目标。程序BSS段存在全局指针或函数指针且能通过堆漏洞获得一次写任意地址的机会。在实际解题或漏洞利用中选择哪种方法并非一成不变而是需要根据题目给出的具体条件进行判断检查保护机制首先看RELRO保护。如果是Partial RELROGOT表可写那么直接通过堆漏洞写GOT表可能是最直接的。如果是Full RELRO则需转向_hook或FSOP。评估漏洞能力你的溢出能写多远能修改哪些元数据这决定了你能发起哪种攻击的初始条件。观察程序特性BSS段有没有大的数组程序是否使用malloc_trim或exit这些都可能提示不同的利用路径。灵活组合很多时候单一方法不足以完成利用。例如先用unlink在BSS段创造一个可控指针再用这个指针去修改_hook这就是两种方法的组合。wdb_2018_3rd_pesp这道题之所以经典就是因为它给出的条件如malloc大小限制迫使你不得不去思考和尝试这些组合技。它像是一个微型的“漏洞利用实验室”让你在安全的环境中实践这些高级技术。6. 从理论到实践解题过程中的关键调试技巧理解了原理实战中依然会踩坑。调试是堆利用不可或缺的一环。以下是一些在实践这三种方法时至关重要的调试心得1. 精确计算偏移与地址realloc偏移不要死记硬背偏移量。在你的调试环境中gdb-peda/pwndbg通过p __realloc_hook和disas realloc找到__realloc_hook被调用的指令地址。__malloc_hook需要填入的地址通常是这条指令之前、能正确设置rdi的某个地址。多次尝试观察调用malloc时rdi寄存器的值是否指向你布置的/bin/sh字符串。unlink检查在伪造fd/bk时牢记公式FD 目标地址 - 0x18,BK 目标地址 - 0x10。并在执行free之前用调试器查看FD-bk和BK-fd的值是否确实等于fake chunk的地址P。这是unlink成功的关键。BSS段地址使用readelf -S ./pwn | grep .bss或objdump -t ./pwn | grep “\.bss”来获取准确的BSS段起始地址。结合反汇编objdump -d或gdb中的info variables来定位具体的全局变量地址。2. 堆布局可视化单纯看内存十六进制是痛苦的。善用pwndbg的堆命令heap查看所有堆块的基本信息。bins查看各个bin的状态这是判断unsorted bin attack或fastbin attack是否成功的关键。vis_heap_chunk图形化查看堆块布局对于检查伪造的chunk结构、prev_size和size域是否正确设置非常直观。 在布局unlink时我习惯在关键操作如修改chunk1的prev_size、执行free前后都执行vis_heap_chunk确保内存状态符合预期。3. 利用脚本的模块化与断点调试不要试图一口气写出完整的exp。应该模块化编写和测试。def create_overflow_setup(): # 1. 布置初始堆块 ... def trigger_unlink(): # 2. 触发unlink并验证是否向目标地址写入了堆地址 ... def achieve_arbitrary_write(): # 3. 利用unlink的结果实现真正的任意写 ...每完成一个函数就在gdb中运行到相应位置检查内存是否达到预期状态。在关键函数如free、malloc和unlink宏内部设置断点单步跟踪程序流和内存变化。例如在free函数入口和unlink宏处设断可以清晰看到整个合并与解链过程。4. 应对环境差异CTF题目提供的libc可能与你本地环境不同。务必使用题目提供的libc文件。在gdb中使用set debug-file-directory和file命令加载带符号的libc。计算偏移时使用pwntools的LibcSearcher或手动计算system_addr libc_base libc.symbols[‘system’]。one_gadget工具可以帮助你快速找到执行execve(“/bin/sh”, NULL, NULL)的gadget地址有时比system更稳定。5. 一个常见的“坑”size与prev_size的混淆在构造fake chunk或修改下一个chunk头部时最容易出错的就是size域的计算。记住size是整个chunk的大小包括头部prev_size和size本身和用户数据区并且必须是2*SIZE_SZ的整数倍64位下通常是0x10的倍数。prev_size只有在前一个chunk空闲时才有意义且必须等于前一个chunk的size。在unlink攻击中我们通过溢出修改chunk1的prev_size为fake chunk的大小并将chunk1的size的PREV_INUSE位清零以此欺骗glibc认为chunk0fake chunk是空闲的。如果这两个值对不上free时会直接抛出“corrupted size vs. prev_size”错误。7. 举一反三技术变种与防御演进通过解剖这道题我们掌握了三种经典技术。但漏洞利用技术是“道高一尺魔高一丈”的博弈。了解这些技术的变种和相应的防御措施能让我们理解更深刻。技术变种house of系列这些都是基于特定堆场景的高级利用技术。例如house of spirit是在栈或BSS段伪造fastbin chunkhouse of force是通过溢出修改top chunk的size实现近乎任意地址的mallochouse of orange则利用_IO_FILE结构进行攻击。它们可以看作是本文所述基础技术的组合与延伸。tcache攻击在引入tcacheglibc 2.26后出现了新的攻击面如tcache poisoning直接修改tcache链上的next指针和tcache stashing unlink attack结合smallbin和tcache。其思路与fastbin attack类似但约束更少利用更容易。FSOPFile Stream Oriented Programming当_hook不可用时攻击_IO_FILE结构如stdout、stderr成为主流。其核心也是通过堆漏洞在内存中伪造一个_IO_FILE结构并利用vtable劫持控制流。这通常需要结合向BSS段或堆写的能力来修改_IO_list_all等全局指针。防御措施与绕过unlink检查强化现代glibc对unlink的检查更加严格如FD-bk P BK-fd P但通过精心构造fd/bk依然可以满足只是条件更苛刻。tcache双指针保护某些版本的tcache对next指针有简单的异或加密增加了利用难度但并非不可破。safe-linkingglibc 2.32引入的tcache和fastbin单链表指针加密机制显著增加了利用难度需要结合信息泄露才能破解。移除_hookglibc 2.34及以上版本移除了__malloc_hook、__free_hook等封堵了这条直接路径迫使攻击者转向FSOP或其他更复杂的方法。作为攻击者我们需要持续关注这些变化理解新机制的弱点作为防御者或安全研究者理解这些攻击原理是设计更安全的内存管理器和编写更健壮代码的基础。这道wdb_2018_3rd_pesp题就像一本旧但并未过时的教科书它所传授的“任意地址写”的核心思想在今天的漏洞利用中依然闪烁着光芒。