堆任意分配与IO流攻击:realloc和unsorted bin的完整利用链

发布时间:2026/10/17 15:02:09
堆任意分配与IO流攻击:realloc和unsorted bin的完整利用链 打CTF或者做二进制研究的朋友对“堆任意分配”这个词应该都不陌生。但真正让我把这一整套流程跑通还是最近复盘某场比赛的堆题时题目只给了UAF和realloc能力却要求先泄露libc再拿shell。我当时的思路是先搞定任意分配再打IO流泄露信息中间还顺手用了一下realloc特性和unsorted bin的expand逻辑。今天就把这条利用链完整地拆开讲从原语建立到信息泄露的每一步包括为什么这么做、怎么做、以及我踩过的坑。这篇笔记适合已经了解堆基础结构、想进阶利用手法的读者也适合在做防御分析时想搞清楚对手攻击路径的朋友。1. 利用链全景四个环节各司其职1.1 为什么绕一大圈去攻击IO流先说一个很直接的问题堆利用的目标通常是getshell但为什么第一步往往不是控制rip而是泄露信息原因在于现代glibc的保护机制。题目给的libc基址往往是未知的PIE下程序基址也可能不固定。你伪造一个vtable指针、改一个__free_hook都需要先知道这些符号的真实地址。没有信息泄露后续所有劫持都无从下手。所以“泄露信息”不是可有可无的步骤而是整条利用链的第一优先级。那信息泄露的方式有很多打unsorted bin的fd/bk泄露main_arena地址、打stdout的write_base泄露libc段内容甚至利用格式化字符串直接读栈。但为什么专门攻击IO流因为很多题目给的原语很有限。可能你只有一次UAF、一次edit或者只能malloc固定大小。直接读unsorted bin地址需要你有打印功能但对方可能就是断掉了常规的堆信息输出。这时候攻击标准库的IO结构体让程序自己帮我们把内存“打印”出来就成了一个强制性更高、容错性更好的路径。这也带出了标题里的核心链条拿到堆上的任意分配原语以后把目标指向_IO_2_1_stdout_伪造一个FILE结构体然后触发一次put或printf让libc的输出机制替我们完成信息泄露。整个过程需要解决三个问题分配原语怎么来、FILE结构体怎么伪造、伪造之后怎么触发输出且不崩溃。1.2 四个原语在利用链中的分工把标题里的四个关键词拆开看堆任意分配这是利用链的“脚”。让malloc返回一个我们能控制的地址相当于拿到了一个写任意地址的窗口。没有这一步后面所有操作都够不到IO结构体。攻击IO流这是利用链的“手”。通过构造位于libc数据段的fake_IO_FILE篡改输出边界让标准库输出函数泄露我们指定的内存区域。realloc特性这是利用链的“杠杆”。realloc在特定条件下可以原地扩大或缩小chunk借助它我们可以改变一个指针所指向chunk的真实size制造出堆重叠为后续对unsorted bin的操作创造条件。unsorted bin expand这是利用链的“放大器”。通过unsorted bin分配时对chunk的分割逻辑结合伪造的size分配出一个超出原有范围的chunk进一步扩大可控区域。这四个原语不一定是独立使用的。实际利用中realloc特性往往是制造堆重叠的手段而堆重叠又能让你改写相邻chunk的size从而触发unsorted bin expandexpand之后又能腾出一块更大的可控区域去精确覆盖到FILE结构体。整个过程像搭积木一样一环套一环。2. 第一环堆任意分配原语的建立2.1 任意分配的本质与常见入口所谓“任意分配”本质上是让glibc的malloc在返回指针时指向了我们想要的内存地址。这中间没有什么魔法核心在于操纵bin链表中的指针。最常见的入口是tcache poisoning。glibc 2.26以后引入了tcache它本质上是每个size一个的单向链表结构很简单一个chunk的fd字段直接存下一个chunk的地址。malloc在tcache_get时直接从head取出第一个entry然后head entry-next。如果攻击者能改写这个next指针malloc下次request对应size时就会把next指针指向的地址当作chunk返回给他。其他常见入口还包括fastbin的double-free链、unsorted bin attack配合伪造bk、large bin attack的bk_nextsize覆盖等等。但tcache因为结构简单、利用门槛低已经成为“任意分配”最优先考虑的手段。需要特别强调的是tcache poisoning并不是万能的它有一些前置条件你首先要有一个UAF或者edit原语能在chunk被free进tcache之后、再次malloc之前改写它的fd字段。同时目标地址必须对齐到16字节malloc返回的chunk指针本身是对齐的。大多数情况下这就够了。2.2 基于UAF的tcache poisoning实操以最常见的组合“UAF edit”为例整个过程分三步第一步准备好一个被free进tcache的chunk。假设我们有一个p malloc(0x40)然后free(p)此时tcache[0x50]的头部指向pp-fd原本为NULL。第二步利用UAF或edit功能把p的fd字段改写为目标地址target。注意如果题目给的是write接口写入位置通常是chunk的data区域起始即chunk指针本身也是fd所在位置。写入内容用p64(target_addr)即可。第三步调用两次malloc(0x40)。第一次malloc返回p同时tcache的头部变成我们伪造的next指针第二次malloc时glibc直接从tcache的head取出target_addr把它当作一个合法chunk返回。# 伪代码示例 free(p) edit(p, p64(target)) # 改写 tcahce 的 next a malloc(0x40) # 返回 p b malloc(0x40) # 返回 target这里有一个初学者容易踩的坑target的取值和第二个malloc的request size必须匹配。因为tcache是按size分桶的target_addr必须是“看起来像一个合法chunk起始”的地址。如果我们想分配到_IO_2_1_stdout_的起始地址那么第二次malloc(0x40)时候target应设为stdout_addr但如果我们要分配到某个结构体中间比如从_IO_write_base开始写target就要减去对应偏移。我在做某道题时就遇到过因为target没有对齐导致malloc直接abort的情况。glibc在tcache_get时会检查aligned_OK(e)不对齐会直接触发malloc_printerr。所以16字节对齐这条红线千万别碰。2.3 目标地址的选择与对齐策略任意识别到“任意地址”后第一件事是想清楚要写哪里。如果你只是想泄露信息目标通常是三类_IO_2_1_stdout_、_IO_2_1_stderr_、_IO_list_all。stdout用得最多因为程序基本都会用puts或printf往外打数据改stdout的字段最直接。这里有一个很实用的对齐技巧如果目标地址是stdout但我们只想改_IO_write_base这一个字段偏移0x20那我们可以把第二个malloc的返回地址设成stdout_addr直接覆盖整个_IO_FILE或者设成stdout_addr 0x20这样malloc返回的指针正好落在write_base处后续只需写入从write_base开始的几个字段即可。不过要注意如果把返回指针定在结构体中间那第一个malloc返回的那个chunk和这个“中间地址”之间会产生一个奇怪的堆对象可能会被后续的free或realloc误操作。建议尽量用完整的fake FILE布局覆盖把前面几个字段也一并写掉避免遗留脏数据。在实际调试中我会习惯先把stdout的偏移打印出来通过泄露libc基址后加偏移再用pwnlib.gdb在malloc调用处下断点观察返回的指针和预期值是否一致。这一点不赘述太多后面调试部分会细说。3. 第二环攻击IO流泄露信息3.1 _IO_FILE关键字段速查伪造IO流之前先把_IO_FILE结构体里跟输出相关的关键字段弄清楚。结构体定义在libio.h中简化后长这样偏移字段作用0x00_flags文件状态标志决定IO模式0x08_IO_read_ptr读缓冲区的当前位置0x10_IO_read_end读缓冲区的末尾0x18_IO_read_base读缓冲区的起始0x20_IO_write_base写缓冲区的起始0x28_IO_write_ptr写缓冲区的当前位置0x30_IO_write_end写缓冲区的末尾0x38_IO_buf_base缓冲区基地址0x40_IO_buf_end缓冲区末尾在C语言里printf最终会调用_IO_puts或_IO_new_file_xsputn其内部逻辑是如果_IO_write_ptr _IO_write_end就把数据从write_ptr开始写入直到write_end。也就是说输出的“边界”由write_base到write_ptr决定。我们攻击stdout就是篡改这组指针。经典思路是让_IO_write_base指向我们要泄露的内存起始地址比如某个libc内地址让_IO_write_ptr指向更高的地址这样libc会把从base到ptr这一大段内容原样输出。3.2 构造伪造stdout泄露指定内存我以修改_IO_2_1_stdout_为例给出一个可以直接用的fake布局# 假设已经拿到任意写把以下数据写到 stdout 起始处 fake_stdout b fake_stdout p64(0xfbad1800) # _flags MAGIC CURRENTLY_PUTTING IS_APPENDING fake_stdout p64(0) * 3 # _IO_read_ptr/end/base 清零 fake_stdout p64(leak_from) # _IO_write_base从哪开始泄露 fake_stdout p64(leak_to) # _IO_write_ptr泄露到哪结束 fake_stdout p64(leak_to) # _IO_write_end 与 ptr 保持一致 fake_stdout p64(0) * 4 # buf_base/buf_end 等后续字段置零_flags值0xfbad1800是我最常用的一个组合。它包含_IO_MAGIC(0xfbad0000)、_IO_CURRENTLY_PUTTING(0x0800)、_IO_IS_APPENDING(0x1000)。这个组合能确保_IO_puts走我们期望的输出分支。在部分libc版本中0xfbad2887也可以但0xfbad1800在2.23到2.31的兼容性都不错。设好这些字段后调用一次puts(x)你会在输出里看到一段从leak_from到leak_to的内存内容。通常我们把leak_from设为某个libc地址附近比如__environ或GOT表就能一次性把libc基址、程序基址甚至栈地址都泄露出来。这里有一个非常容易翻车的地方_IO_write_ptr和_IO_write_base的差值不能太大。glibc内部在输出时可能会对长度做一些限制或者因为缓冲区边界问题触发_IO_flush_all等额外操作导致崩溃。我实际测试下来把差值控制在0x200以内比较稳够泄露你需要的地址段即可别贪多。另一个坑是_flags如果设置得不对puts可能直接走_IO_OVERFLOW分支然后调用vtable里的函数而不是我们预期的xsputn输出路径。表现为程序卡住或直接abort。这时候优先检查flags位确认_IO_CURRENTLY_PUTTING和_IO_IS_APPENDING这两个位是否置上。3.3 vtable校验与版本差异从glibc 2.24开始FILE结构体上方的vtable指针在_IO_FILE结构体末尾偏移0xd8附近不能随意指向任意地址了。IO_validate_vtable会检查vtable指针是否落在libc的__libc_IO_vtables段内不在就调_IO_vtable_check然后abort。这就意味着在老版本里直接把vtable劫持到我们控制的堆地址的做法行不通了。现在的思路是把vtable指向libc内部的合法vtable比如_IO_str_jumps、_IO_wfile_jumps等配合伪造的_IO_strfile结构体让_IO_str_overflow用我们控制的函数指针完成RCE如果只是为了泄露信息很多时候不需要劫持vtable只需要改对write系列字段vtable保持原样即可。所以在我这篇笔记里泄露信息这一步vtable并不是优先级最高的事。只要你的任意写能力能精确覆盖到stdout的0x20偏移处把write_base和write_ptr这两个字段改好输出机制就会按你的剧本走。真正需要动vtable是后面getshell阶段的事那个是另一个话题了。不过有一类情况例外如果你分配的地址不在_IO_FILE开头而是直接落在vtable区域附近比如想改_IO_wfile_jumps那就要仔细核对版本对应的vtable偏移。每个libc版本的_IO_2_1_stdout_偏移、_IO_str_jumps偏移都是不同的调试时需要用libc-database或pwninit的符号表对一下别凭经验硬套老偏移。3.4 泄露之后的收尾处理伪造stdout泄露完信息并不意味着攻击结束。程序后面可能还要继续用stdout输出正常内容甚至还要多次输入输出。如果我们的fake FILE没有恢复下一次puts会继续从我们设置的write_base输出一大段垃圾严重影响流程稳定性。所以泄露完信息之后我一般会把stdout恢复成原样。实际操作中泄露libc地址以后利用任意写能力把原先保存好的原始stdout前0x40字节重新写回去再触发一次puts输出就恢复正常了。这里有一个技巧在篡改stdout之前先通过正常的puts输出把原始_IO_2_1_stdout_的内容dump下来。怎么得方法很多最直接的是利用堆上已泄露的地址算到stdout偏移后直接读libc的虚拟内存镜像。libc的_IO_2_1_stdout_初始内容在内存中是确定且可预测的还可以直接用libc.dump_io()或从本地的libc.so文件里提取出来。拿原始数据跟我们的fake布局拼接只改需要改的字段其他保持原样这样更稳定。在我的exp里通常用一个字典保存关键偏移然后在泄露完成后把write_base和write_ptr两个字段改成正常值其余字段不动这样能省一次完整的重写。4. 第三环realloc特性——堆布局的搅局者4.1 realloc三种行为模式的判定realloc在许多题目里是默认提供的功能看起来平平无奇但它的内部逻辑非常值得玩味。glibc的realloc(ptr, size)在不同条件下会走完全不同的路径ptr NULL等价于malloc(size)。size 0等价于free(ptr)然后返回NULL。这个特性叫REALLOC_ZERO_BYTES_FREES在很多版本中默认开启。ptr ! NULL size ! 0这个是重头戏会进入_int_realloc根据新旧大小和相邻chunk的状态决定是原地扩展、原地收缩、还是分配新块后拷贝。这三种行为里前两种可以当成现成的free和malloc来用。第三种才是我们真正需要研究透的。这里有个很关键的判断逻辑_int_realloc在nb oldsize新size超过旧size时会检查相邻的下一个chunknextchunk。如果nextchunk是空闲的且oldsize next_size nb它就尝试把nextchunk从bin链中摘下来跟oldp合并实现原地扩展如果nextchunk是整块top chunk也能直接扩容。如果都不满足才走“malloc新块 memcpy free旧块”的兜底路径。兜底路径也有文章可做它实质上对旧块做了一次free。如果题目逻辑里没有别的free机会你可以用realloc的兜底路径作为“合法free”来触发unsorted bin或tcache的释放这个思路在构造攻击链时非常有用。4.2 原地扩展与收缩的边界条件来看_int_realloc中“原地扩展”的检查逻辑我用伪代码还原一下if (nb oldsize) { // 原地收缩把旧chunk分成两块 // 前面一块大小为 nb后面剩余部分作为新chunk返回给系统 // 注意剩余部分如果进入top就不会产生可用的bin chunk // 如果被free进bin则产生一个新的可控chunk } if (nb oldsize) { nextchunk chunk_at_offset(oldp, oldsize); if (nextchunk top) { // 如果 topchunk 足够大直接扩展 oldp // top 指针向上移动oldsize 变大 } else { nextsize chunksize(nextchunk); if (nextsize oldsize nb !prev_inuse(nextchunk)) { // 这里会 unlink(nextchunk) // unlink 成功后 oldp 扩展覆盖到 nextchunk 的区域 } } }也就是说“原地扩展”的触发条件本质是下一个物理相邻的chunk是空闲的两件套加在一起足够覆盖新的size。而unlink操作会触碰nextchunk的fd和bk指针。如果能控制这两个指针就能在unlink的时候完成一次任意地址写入这就是realloc参与攻击的一个方式。另一个容易被利用的点是“原地收缩”。realloc(ptr, smaller_size)时oldp会被分割前面一部分作为返回值后面一部分成为一个新的chunk。如果这个新chunk紧接着某个目标结构比如另一个chunk的fd字段那它天然就提供了一个可控的相邻chunk后面配合溢出或改size就能实现更复杂的布局。4.3 用realloc制造堆重叠我在实际利用中用realloc制造堆重叠的典型场景是这样的首先malloc两个相邻chunk A和B大小都为0x100。free(B)B进入unsorted bin。此时B的fd和bk都指向main_arena附近。接着用realloc(A, 0x300)。由于B刚好是空闲的且A和B的总大小满足0x300的需求_int_realloc会把B从unsorted bin中unlink然后A扩展为0x300完全覆盖B原来的区域。现在A的data区域中包含B原本的fd和bk以及B被free后的整个chunk内容。你不仅拿到了一个更大的chunk还把这个大chunk的边界扩展到了原来B再往后的区域。如果再配合一次malloc(0x200)返回的指针会落在A0x200的内部但逻辑上它属于A这个大chunk这就形成了重叠。在这个重叠状态下你能做两件事第一直接读写A的data去修改“内部chunk”的数据比如改它的size字段伪造出一个更大的chunk第二通过A的扩展边界覆盖到原本在B后面的一个chunk C改写C的头字段。C如果是free状态修改它的size就能触发后面要讲的unsorted bin expand。这里有个关键细节当realloc把B unlink时会执行经典的unlink校验fd-bk P bk-fd P。B的fd和bk在unsorted bin中都是正常的指向main_arena附近所以只要你不提前改坏它们这个unlink能顺利通过。如果你已经用其他方式改过B的fd或bk那就必须先修复它们否则realloc会直接abort。5. 第四环unsorted bin expand的原理与配合5.1 unsorted bin的分配与分割逻辑unsorted bin是glibc中一个特殊的bin它存储的chunk不按size分类而是FIFO双向链表。当一个chunk被free到unsorted bin后下一次malloc时会优先在unsorted bin里找。_int_malloc在遍历unsorted bin时会有几个分支如果unsorted chunk的size正好等于request size直接取出。如果unsorted chunk的size大于request size则把它分割成两块一块给request剩余部分remainder继续留在unsorted bin。如果unsorted chunk的size小于request size则看下一块。那个“分割剩余部分”的过程就是unsorted bin最核心的逻辑也常被称为last remainder机制。攻击的要点在于分割的依据是chunk头部记录的size而不是真实分配时的大小。如果我们能伪造一个unsorted chunk的size字段为很大的值那么malloc一个小请求时系统就会基于这个伪造的size执行分割返回一个覆盖范围远超实际的chunk。这就是unsorted bin expand的本质不是真的把chunk扩大而是让分配算法相信一个chunk“足够大”从而给你切出一块超出常规范围的区域。5.2 伪造size触发expand的实操假设现在的状态是我们已经通过realloc制造了堆重叠可以改写某个已free chunk的header。具体做法如下。让一个chunk X处于unsorted bin中它的物理地址附近目标结构是T。我们先泄露出X的真实地址通过之前的信息泄露然后利用任意写能力把X的size字段改成一个更大的值使得X的边界扩展到T附近或者至少扩展到能覆盖到我们要控制的相邻chunk。伪代码习惯如下# offset 是 X 与目标区域之间的字节偏移 fake_size real_size offset edit(chunk_X_addr, p64(fake_size))改完size后调用malloc(real_size)时注意请求大小要小于fake_size。_int_malloc检查到X的size为fake_size会执行分割切出request大小的一个chunkremainder被重新放回unsorted bin。关键点在于这一次分割的边界是以fake_size计算的。因此切出来的chunk虽然名义上是request大小但它实际占用的物理范围可以跨越到原X之后很大一片区域只要fake_size涵盖得到。一旦malloc返回这个chunk你就获得了从X开始的、大小远超常规的可写区域目标结构T就在里面。这也就是标题里“unsorted bin expand”最直白的一种实现。它在逻辑上等价于用一个不可信的size头让unsorted bin的分配器做了超出常规尺度的切分从而把一个“饱满”的可控区域交到你手里。5.3 expand与realloc的组合拳expand和realloc在完整利用链里通常是连续动作顺序一般是第一步用realloc制造初始重叠。这一步的核心是让相邻chunk能被合法unlink从而让A覆盖到B和C。第二步用得到的重叠区域改写C的size字段伪造一个更大的size。C必须处于free状态且位于unsorted bin。如果不是需要想办法先把它free或者把C的prev_inuse位处理好。第三步malloc一个适当的size触发unsorted bin对C的分割expand返回一个覆盖范围远大于请求大小的chunk。第四步在这个大chunk里定位到IO结构体比如stdout写入fake FILE布局触发输出完成信息泄露。实际操作中第二、三步之间还有一个隐含约束伪造size时必须保证chunk不会在free检查或unsorted bin遍历时被判定为非法。比如chunk的size不能小于MINSIZEsize的低三位要正确处理PREV_INUSE、IS_MMAPPED、NON_MAIN_ARENA标志。通常低三位设为0或者只保留PREV_INUSE即可。另外remainder判定的“last remainder reuse”机制也值得关注。如果连续多次malloc的大小都从同一个large chunk切分glibc会不断更新剩余部分的位置。这个特性在堆风水中有妙用可以制造出“指针在堆上漂移”的效果。但恰恰因为它会更新remainder的fd/bk如果你在过程中篡改了这些指针很容易触发malloc_printerr。我在一次调试中就是因为在remainder的fd上动了手脚导致下一次malloc时unsorted bin corrupted直接崩了。这一点务必注意。6. 完整利用链串联与调试实录6.1 完整流程的执行序列把前面各环节拼起来一条完整利用链大致长这样利用UAF泄露堆地址。从fastbin或tcache chunk的fd中读出堆指针算清楚堆的基址。利用tcache poisoning实现第一次任意分配。目标设在我们能写的位置通常是堆上的某个chunk头部先把这个chunk的size伪造好为realloc重叠做准备。调用realloc触发合并。用realloc把chunk A从0x100扩到0x300让空闲的B被unlink实现堆重叠。在重叠区域写C的size头伪造fake_size让unsorted bin在下次malloc时执行expand分割返回一个覆盖到目标区域附近的chunk。在这个大chunk的可控范围内直接覆写_IO_2_1_stdout_。写入fake FILE布局flags设为0xfbad1800write_base指向我们要泄露的内存起点write_ptr指向终点。调用puts触发输出从输出中解析libc基址、堆基址、栈地址。泄露完成后将stdout恢复为正常字段继续后续的攻击。这个序列里第1步和第2步其实是很多题目共有的起手式。第3步到第5步是亮点也是最容易卡壳的地方。6.2 调试关键点断点该下在哪调试这类利用链最忌讳的是在肉眼层面猜测堆的状态。我的习惯是在__libc_realloc和_int_malloc处分别下断点观察几个关键寄存器进入realloc时rdi是oldprsi是new size。确认oldp是我们预期的chunk。在_int_realloc的unlink分支处断点检查nextchunk的fd和bk值是否正常。unlink校验失败会直接走到abort所以这一步能提前暴露错误。在_int_malloc的unsorted bin遍历循环中观察当前遍历的chunk的size值。如果你伪造的fake_size没有生效这里能看到真实值直接定位到问题。在_IO_puts入口断点查看_IO_2_1_stdout_的write_base和write_ptr是否已经被正确改写。确认之后再决定是继续触发输出还是回寄存器去修字段。如果你用的是pwntools可以这样快速在gdb中定位gdb.attach(p, set pagination off break *__libc_realloc commands silent printf realloc: old%p, size%#lx\\n, $rdi, $rsi continue end )一个很重要的经验是不要把所有断点都一口气enable那样调试信息太密集反而看不清楚。我通常先观察堆状态用vis看堆布局再针对当前环节强制跳转一次命中后再继续。6.3 常见问题排查速查表现象可能原因排查方向malloc返回后程序abort报malloc(): unaligned tcache chunk目标地址未按16字节对齐检查tcache的next地址退位或前移到对齐位置realloc时报corrupted double-linked listunlink校验失败说明nextchunk的fd/bk被改写重新泄露并修复nextchunk的fd/bkputs崩溃或卡在_IO_OVERFLOW分支fake FILE的flags设置不正确检查flags是否包含_IO_CURRENTLY_PUTTING和_IO_IS_APPENDINGputs正常执行但输出为空write_base和write_ptr指向了不可读的输出区域确认leak_from指向的内存可读且ptr - base差值合适第二次malloc触发unsorted bin corruptedfake_size覆盖了unsorted bin中remainder的fd/bk停止在remainder上动手脚或者分配前重新初始化它的fd/bk泄露内容包含大量\x00但找不到地址目标地址距离write_base太远中间被空字节截断把write_base精度调细分段泄露排查时往往还有一个现象是“第一次运行正常、第二次必崩”。这大多是因为你第一次伪造的fake FILE没有复原或者tcache的链表被改了之后没有修复完整的next链。我的建议是每次malloc之后都用heap和bins命令检查链表状态确认下一次分配不会踩到残留的旧指针。除速查表之外还有一个核心调试技巧gdb中直接调用分配函数来测试一个步骤。比如在任意分配写完了fake FILE后用gdb的call (int)puts(x)看输出。如果这一步在gdb里能正常泄露而在实际运行中崩溃多是因为gdb环境下的libc版本或者环境变量与我们exp中计算偏移的基础不一致优先检查ldd和版本符号。让这条链成为你自己的生产力我第一次把这条利用链完整跑通花了整整一个晚上大部分时间都耗在那些“看似合理但就是不对”的细节上。比如realloc到底有没有触发unlink——这个判断决定了堆布局是否按预期变化再比如伪造unsorted bin chunk的size时低三位的标志位处理一个PREV_INUSE没置对就能让系统以为上一块还在用合并逻辑整个错乱。但恰恰是这些细节让我对glibc分配器的运行逻辑有了更立体的理解。以前“malloc就是给块内存”的简单认知被彻底替换成了“malloc是一套基于链表和标志位的状态机”。当你把tcache、realloc、unsorted bin这三个齿轮咬合在一起你会发现你能做的远不止泄露信息后续的任意写、劫持控制流都只是这套机制的延伸。如果你也在做类似的利用链分析我的建议是不要贪图一上来就跑完整exp。先把每一步拆开单独验证原语是否成立tcache能不能poison成功realloc能不能原地扩展unsorted bin能不能按伪造的size切分一步一步在gdb里看到状态变化再串联整体成功率会高很多。最后一个小技巧每次大改动前先把libc版本、偏移值、保护状态这些基础信息打印出来核对一遍。我踩过无数次“版本偏移记错导致未定义行为”的坑现在每次都老老实实把符号表拿出来对一遍再动手。这不会影响你的攻击效率反而能省掉搜索报错的数个小时。堆利用是一条越走越深的路线但只要你把每一步的原理吃透它的套路其实比想象中要清晰得多。

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询