首页 / 资讯中心 / 文章详情

堆溢出原理与实战:从堆布局到控制流劫持的完整指南

堆溢出原理与实战:从堆布局到控制流劫持的完整指南 ★ FEATURED ARTICLE
1. 堆溢出到底是个什么东西如果你已经入门了栈溢出大概率会对堆溢出又好奇又头疼。栈溢出靠几行精心构造的shellcode就能打通链路流程相对固定工具链也成熟。可一旦转到堆这块情况就完全不一样了——没有固定的溢出即劫持路径环境不同、堆结构不同、分配器不同利用手法完全可能是另一套打法。这也是很多初学者在栈溢出走得挺顺一接触堆溢出就懵圈的根本原因。先给个大致定义。堆溢出指的是程序在堆上分配缓冲区后写入数据时越过了缓冲区的边界污染了堆内存中相邻的其他数据。这里的关键在于这种越界不是简单的程序崩了而是能让你去破坏堆分配器用来管理内存的核心元数据——只要这些元数据结构被你控制住就能进一步获得任意地址写、任意地址读甚至直接改写函数指针、GOT表项把执行流劫持到你自己手里。那么问题来了为什么堆溢出比栈溢出难这么多因为栈上数据是线性的、分布紧凑溢出后很容易直接打return address。而堆是动态分配、动态释放的复杂结构你溢出的那块只是整片堆里的一个小碎片它周围还住着其他数据块、管理头、空闲链表指针等等。你不能直接跨过这些结构让程序往你希望的地方跳你需要非常清楚堆管理器会把内存排成什么样子才能精准地把脏数据写进该写的地方。从工程实践的角度说堆溢出通常不是我直接编写攻击代码这么简单更多的是一种理解程序内存布局、理解分配器行为、利用特定接口构造数据的能力。换句话说想真正学会堆溢出你首先要学会的是读内存、看堆布局、理解分配器的每一步动作。这篇文章我不会泛泛而谈堆溢出的所有利用手法而是聚焦原理层——让你搞清楚堆到底是怎么组织的、分配和释放时发生了什么、为什么溢出能造成劫持以及我在实际调试过程中踩过的一系列坑。如果你属于那种原理知一点、实操全靠猜的群体这篇文章应该正合你的胃口。2. 堆的核心机制先弄明白这两件事2.1 堆不是你以为的那个堆很多人听C语言动态内存分配会以为堆就是一块自由的空地里面想放什么就放什么。这话对应用程序来说是成立的但堆管理器为了实现这块自由空地给自己加了很多规则。主流Linux下运行的堆管理器是ptmalloc2它基于经典的dlmalloc做了多线程优化。这玩意儿维护的核心数据结构有两大类一类是分配出去的chunk也就是你用的那块内存另一类是回收后未分配的空闲chunk。这两者在内存里长着不同的脸。先说occupied chunk也就是已经被用户持有的内存块。它的布局大致是这样的当前chunk的大小size字段前一个chunk是否被使用的标志位prev_inuse位用户数据区域fd和bk字段对用户不可见如果真的存在下一个chunk下一个chunk的头部听着有点绕对不对我直接画一下。以64位系统为例一个典型的allocated chunk长这样------------------- | prev_size | 8字节只有当前一个chunk是空闲时才有效 ------------------- | size | 8字节包含当前chunk大小及标志位 ------------------- | user_datafd | 用户可读写区域要多少给多少 -------------------注意这不是全部。真正的chunk为了内存对齐实际大小会向上取整到16的倍数。也就是说你请求malloc(1)实际分配出来的chunk size可能是0x2032字节前16字节被header占据后16字节给你用——当然你可以用header之后的全部字节。这里有个新手极容易忽略的点ptmalloc不会为每个chunk都记录前一个chunk的prev_size只有在前一个chunk处于free状态时它才会使用prev_size字段。如果前一个chunk正在被使用那prev_size就作为普通用户数据的一部分可以写入任何内容。2.2 释放一个chunk时堆管理器在做什么接着往下说。当我们把指针传给free()堆管理器做的第一件事就是检查这个指针指的位置附近是否有合法的chunk header。它通过size字段的低位判断前一个chunk是否空闲如果空闲就把两个相邻的空闲chunk合并成一个更大的空闲块这个过程叫coalescing合并。之后这个合并出来的大块会被放进对应的bin箱子里管理。bin是啥你可以理解为空闲chunk的分类链表。ptmalloc管理着多个bin类型主要包括fastbin尺寸小于等于0x80的小块释放后不会立即合并存取都很快unsorted bin所有中等大小的释放块都会先进这个篮子等待后续切割或重新分配small bin尺寸固定的64个链表按大小精确匹配large bin尺寸范围广包含63个链表用于更大的内存块这里有一个很经典的陷阱fastbin中的chunk不会设置prev_inuse标志位它们永远是看起来被使用的状态。这导致当你尝试把fastbin chunk跟相邻chunk合并时堆管理器经常会拒绝——因为它误以为前面的chunk还在使用。那这和溢出有什么关系呢有太有关系了。你溢出时写入脏数据脏数据可能恰好落在另一个chunk的header位置。如果你把这个header的size字段改大、修改prev字段、伪造标志位堆管理器在进行垃圾回收或合并时就会把你的垃圾数据当作管理信息来解读从而做出完全错误的内存管理决策。这就是堆溢出利用的根基——你不需要在溢出后写什么魔法值你只要想办法让堆管理器相信某个地址附近存在它想要的数据然后诱导它把这块假chunk当作真实分配结果返回给你。一旦你拿到的指针不是程序原本期望的位置而是你伪造的假地址后面的事情就水到渠成了。2.3 堆溢出的前提条件在很多实际漏洞里堆溢出并不是逻辑上天然存在而是一个常规的边界检查缺失。最常见的情况void parse_data(char *input, size_t len) { char buf[64]; memcpy(buf, input, len); // len 可能大于 64 }这里如果len受外部控制且没有校验就能造成堆缓冲区溢出。但问题在于——buf不是分配在栈上而是分配在堆上那你溢出后能打到什么完全取决于这段内存紧挨着谁。如果buf分配后紧接着就是另一个堆块那么写入超过64字节的数据就会污染下一个chunk的header。当然现实中的堆溢出往往没有这么耿直很多是发生在循环写、字符串操作、格式化输出的边界误判上。不管源头是什么它们的共同点是程序在某个点连续写入超过缓冲区长度的数据且没有检查容忍边界。这也是为什么做堆溢出分析时第一步永远是搞清楚这个溢出能打多远——不是看代码直觉而是实际用内存调试器去看堆底部的排列方式确认哪些数据段可以被越界写入。3. 堆溢出的利用思路从破坏元数据到劫持控制流3.1 直接击中下一个chunk的header堆溢出最常见的首发目标就是紧邻缓冲区之后的下一个chunk header。如果你能在溢出过程中改写这个chunk header里的size字段和prev_inuse标志位堆管理器在后续分配或释放时就会基于你构造的假数据行动。具体做法有很多常见的一种流程是申请两个相邻堆块A和BA可溢出B是要被释放的目标溢出A的数据修改B的header——把B的size改大比如把0x20改成0x80释放B堆管理器把这个扩大的大小当成真实大小把B以及B之后原本属于别的chunk的内存统一合并成一个大块下一轮malloc请求返回这块扩大后的区域你能控制的内存范围远大于预期这种改size引发错位管理的思路是很多早期Linux堆利用的核心。一旦你能骗过分配器让一个chunk覆盖到其他chunk甚至GOT全局偏移表、栈上返回地址等敏感位置剩下的就是布局数据的问题。这里有个实测经验你修改size后一定要保证所有flag位符合堆管理器的预期。尤其是prev_inuse位如果前一个chunk明明空闲却又被你设成inuse合并逻辑可能直接崩溃或走错分支。很多初学者在这上面栽跟头改size改得爽却没注意用调试器核对内存布局结果一释放直接Segmentation Fault。3.2 unlink攻击经典中的经典如果把堆溢出利用按历史经典程度排序unlink绝对名列前茅。它利用的是双向链表在删除节点时的写操作——当一个空闲chunk被从small bin或unsorted bin中取出时堆管理器会对这个chunk进行unlink操作伪代码大概是这样if (P-fd-bk P P-bk-fd P) { P-fd-bk P-bk; P-bk-fd P-fd; }也就是说假设P是一个正在被释放或取出的chunk堆管理器相信P-fd指向的那个内存地址处有一个chunk而这个chunk的bk字段应该指向P。同理P-bk指向的内存地址处应该有一个chunk那个chunk的fd应该指向P。只有这个校验通过后P-fd和P-bk之间才会进行互写。那怎么利用呢如果你能控制P-fd的值为目标地址-0x18P-bk的值为写入地址-0x10且这两个地址对应的空间都满足上述校验条件那么unlink发生时堆管理器就会执行目标地址-0x18-bk 写入地址-0x10和写入地址-0x10-fd 目标地址-0x18这两个写操作。虽然绝对值取决于环境但本质就是你拥有了一个向任意地址写任意数据的原语。unlink利用的关键难点在于校验要求P-fd-bk P 和 P-bk-fd P这两条校验很难绕过。后期很多环境都加上了更严格的检查但理解它依然必要因为很多现代手法其实是unlink思路的变体——比如通过伪造chunk结构让堆管理器相信你构造的地址是合法的链表节点。在实际学习中我推荐你至少在调试器里亲手做一次unlink攻击不用急着打远程环境先把伪造chunk 触发unlink 观察两次写操作这套流程跑通你才算真正理解了堆攻击的本质。3.3 Fastbin dup最容易上手的第一步如果unlink让你头大那先别急着啃换个思路试试fastbin dup。fastbin是ptmalloc中管理小尺寸空闲块的容器它的特点是LIFO后进先出——释放的chunk会先进链表头部下次分配时优先从头部取。利用思路极其简单分配三个大小相同的chunk A、B、C释放A释放B再释放Adouble free此时fastbin链表的顺序是A - B - A连续两次malloc请求拿到的都是A第三次malloc拿到的是B关键点在于第三步你把一个chunk释放了两次导致fastbin链表里出现环形引用。这样攻击者就能反复拿到同一块内存并在里面伪造数据。如果后续程序在某处把这块重复返回的内存当作不同的变量来处理攻击机会就来了。Fastbin dup之所以适合新手入门是因为它的原理足够简单只需要理解链表头插、LIFO、重复释放导致重复分配这三个概念不需要去触碰复杂的内存布局计算。但它的局限性也很明显fastbin只覆盖小块内存且要求释放过程不发生合并一旦chunk周围环境不满足条件攻击就会失败。我第一次做fastbin dup的时候卡在了一个让人哭笑不得的环节上——我用的是glibc比较新的版本里面给double free加了一个key字段校验导致同样的操作在旧教程里没问题在我环境里直接报错。这提醒我学堆溢出一定要看版本、看环境、看配置不能把教程里的流程当教条。3.4 从溢出到shell的完整路径上面讲了具体攻击原语现在串一下完整APP利用路径。在真实CTF题目或真实漏洞利用里堆溢出往往不是单独使用的而是作为起点、配合其他能力逐步搭建成体系。典型的利用链可以概括为触发堆溢出污染相邻chunk的header利用被污染的header实现任意地址分配即malloc返回一个你指定的地址称为arbitrary allocation利用任意地址分配改写某个关键指针——比如__free_hook、__malloc_hook或者更常用的GOT表项等待程序调用被劫持的函数执行system(/bin/sh)或执行一段ROP链这里最核心的一环是把堆溢出转成任意地址写。如果你能稳定地让分配器返回你想要的内存地址后续的劫持几乎是势如破竹的。而一旦你做不到任意地址分配那就退而求其次利用溢出一点点改数据做更精细的记忆体布局操作。值得注意的细节是新版本的glibc对__malloc_hook、__free_hook这类钩子函数机制做了各种调整部分环境已经禁用了这些函数指针的赋值。在我实操时我一般优先检查目标程序的导出符号如果环境里还有__free_hook那利用路径会简单得多如果没戏就要考虑通过攻击GOT表或者触发malloc_consolidate实现更复杂的利用。总之溢出→任意写→劫持这个思路是通用的具体落点则要看环境。4. 从堆布局到实战利用手把手走一遍完整流程4.1 环境准备老规矩做堆溢出分析你需要一套趁手的工具链。我自己的标配是Ubuntu 18.04或更早版本glibc 2.27及以下便于利用经典的fastbin机制gdb 调试器pwndbg 或 gef 插件必须没有人会手搓堆内存的十六进制视图一个提供堆内存可视化功能的脚本这里我推荐pwndbg自带的heap命令视野极其友好注意一点不同glibc版本对堆布局和校验逻辑影响巨大。比如glibc 2.23和2.27中malloc的校验机制就完全不同fastbin dup在2.23环境是畅通无阻的但2.27加入了key字段校验后double free就得再多加几步绕过手段。我强烈建议初学阶段直接锁定一个教学友好的glibc版本不要一上来就挑战新版本。4.2 搭建可溢出的实验程序为了讲实操我准备了一个极简的堆溢出演示代码。它模拟了实际环境中常见的用户可控长度写入固定缓冲区场景#include stdio.h #include stdlib.h #include string.h int main() { char *a malloc(0x20); char *b malloc(0x20); char *c malloc(0x20); printf(a: %p\nb: %p\nc: %p\n, a, b, c); // 模拟堆溢出向a写入超长数据覆盖b的header memset(a, A, 0x40); gets(a); free(b); free(c); return 0; }这段代码的逻辑比较直白分配三个0x20的chunk然后向a写入远超0x20大小的数据。如果没有检查a的数据会一直往高地址方向蔓延一路覆盖到b的chunk header甚至可能污染b之后的数据。4.3 观察堆布局找到注入点好我们现在进入实操。先编译程序然后在gdb里跑起来用pwndbg的heap命令观察堆布局。gdb ./heap_overflow pwndbg break main pwndbg run pwndbg heap你会看到类似这样的输出Allocated chunk | 0x603000 | prev_size: 0, size: 0x31 Allocated chunk | 0x603030 | prev_size: 0, size: 0x31 Allocated chunk | 0x603060 | prev_size: 0, size: 0x31三块0x31大小的chunk分别位于0x603000、0x603030、0x603060每个chunk的头部是0x10字节prev_sizesize用户数据区从0x603020开始。所以当我们调用gets(a)并输入大量字符时数据会从0x603020开始写逐步越过0x603030b的header一直到0x603060c的header甚至越到更远处。这里有一个非常重要的细节即使你只是多输入了8字节也可能把b的header的size字段的某些位改掉。在64位系统里size字段的最低那个位是prev_inuse标志。如果你改掉了它free(b)时堆管理器的合并逻辑会受到干扰直接引发不可预测的行为。那怎么利用这个溢出点呢最经典的做法是溢出a区域把b的prev_inuse位置为0即让堆管理器认为b前面的chunk也是空闲的同时把b的size字段改大比如从0x31改成0x61释放b触发堆管理器把b以及b之后的大块内存合并成一个空洞后续malloc会返回这块被扩大的区域但如果目的是攻击unsorted bin你还需要伪造prev_size字段——因为这个字段会被堆管理器当作前一个chunk的大小来使用在合并操作中被加到新的偏移量里。这也是堆溢出最考验细致程度的部分你不仅要改当前chunk的header还要伪造虚假的前一个chunk来欺骗堆管理器。4.4 实测改装b的size字段观察free行为让我实际操作一下感受一下。// 向a写入 0x20 字节的 A再把 b 的 size 改为 0x61 // 在 gdb 中这样操作 // set {long} (a0x20) 0x61执行后a的空间被填满b的header的size被改成0x61原本是0x31。然后我们调用free(b)。此时堆管理器会查看b的size0x61发现它和b前的chunk也就是a之间的距离是0x20字节。它认为在这个位置存在一个chunk header于是它读取前一个chunk的prev_inuse位。如果这个位是0那么它就会执行合并把b和前一个chunk其实是我们伪造出来的那个位置拼成一个0x90这样的大块。在我调试时执行这一步直接崩了。原因是我的b前面并没有一个合法的空闲chunk它的prev_size字段实际上是a的数据区内容堆管理器读取到的是一个不可预测的值。这就告诉我——实际的利用需要对内存进行极其精细的化妆不是简单改个size就能成功的。4.5 完整利用的简化步骤参考给想看完整利用的读者一个思路导图具体手法不同环境有区别但骨架如下找出溢出点确定可控长度比如能写0x50字节设计目标堆块判断它会在溢出后被谁释放、在什么时候释放构造payloadpadding 伪造的header区域 目标函数地址触发释放观察堆管理器的处理过程验证能否通过堆溢出拿到任意地址写能力将任意地址写转化为代码执行改GOT、改hook、写ROP链这一路上每一步都需要用gdb验证内存状态不要想着一次到位。我见过太多人在崩溃后直接无脑盲调里卡住最有效的方法反而是每次操作后都看一眼堆的实际布局用x/20gx 地址这种命令检查内存变化慢慢逼近目标状态。5. 实际调试中避不开的那些坑5.1 编译器优化影响堆布局防不胜防做漏洞分析时我们经常拿本地编译的程序来测试。但你有没有发现加了-O2优化后堆布局和-O0完全不同原因很简单优化可能改变局部变量的生命周期变量可能在函数结束前就被释放也可能从栈上被搬进寄存器导致you原本预期的相邻堆块根本不存在。所以我的个人经验是如果你是为了研究原理那就用-O0 -g编译这样便于调试如果你是为了模拟真实场景那最好用和目标程序相同的编译选项和glibc版本不要贪图方便直接拿本地默认环境跑。另外ASLR地址空间布局随机化默认开启的机器每次运行时堆的基址都会变化。这不会影响堆块之间的相对偏移但会影响你需要硬编码的绝对地址。在实际利用时最好通过信息泄露的方式动态计算地址而不是写死。5.2 64位和32位的差异绝不是改个地址长度那么简单很多堆溢出教程还在用32位系统讲原理如果你直接照搬到64位绝对会踩坑。最大的差异在于chunk size的最小单位不同64位下chunk大小按16字节对齐32位是8字节指针大小从4字节变成8字节导致fd和bk字段的偏移变化地址空间范围不同64位下高地址往往包含不可读的canonical地址写数据时容易触发无效地址参数传递方式不同64位用寄存器传参对利用链的要求更高以fastbin dup为例32位下你只需要伪造一个4字节的fd指针但64位下你必须保证这个地址上是合法的堆管理数据结构否则malloc会直接拒绝分配。这意味着在64位下做堆利用你往往要构造一个假的chunk header让堆管理器误认为这个地址处是一个合法的空闲chunk。5.3 不要迷信unlink一定可行我在一些入门博客上看到很多人把unlink攻击当作堆溢出的万能钥匙。但实际环境中新版glibc对unlink的校验已经做了加强尤其是P-fd-bk P P-bk-fd P这条校验在没有先构造好特定内存布局时几乎无法通过。如果你还是想用unlink至少需要满足两个条件一是目标堆块在small bin或unsorted bin中且它的fd和bk字段是你能控制的二是你必须在目标地址处预先准备两个看起来像chunk的数据结构让校验通过。这两个条件在真实程序中往往很难同时满足。所以我在实际项目里更倾向于用如下替代方案利用double free制造fastbin循环实现任意地址分配利用large bin中的fd_nextsize和bk_nextsize字段实现无校验的写原语利用tcache新版本glibc的线程局部缓存机制做更直接的攻击这些手法虽然对初学者来说略显复杂但实际利用的成功率比unlink高得多。5.4 tcache时代的堆攻击思路要换代如果你用的是glibc 2.26以上的环境现在绝大多数发行版都是那堆管理的样子又变了。tcachethread local cache引入后每个线程都会有一个小型的缓存池里面存着最多7个已释放的chunk。它的特点是单链表结构、无双重链表校验导致攻击方式大幅简化。关键点在于tcache对double free的检测非常薄弱。如果你释放同一个chunk两次只要第二次释放前把chunk的fd字段改成0或改成干扰值就能绕过大多数检测。然后你就能做tcache poisoning直接改写tcache链表的fd指针让下一次malloc返回任意地址。我给一个简单的tcache poisoning思路申请两个chunk A和B释放A释放B释放Adouble free修改A-fd 目标地址比如__free_hook连续malloc两次第一次返回原来的A第二次返回目标地址在目标地址写入你的数据比如system函数的地址流程和fastbin dup类似但tcache的实现更简单、限制更少所以在现代环境里几乎是最常用的堆攻击手段。我个人建议如果你是新手可以从tcache poisoning入手它比unlink和fastbin dup都容易理解而且应用场景广。只要你理解了链表头插、取出分配这两个基本动作就能推演出一整套tcache攻击变化。5.5 学堆溢出不能只靠读得亲手堆一次这是我最想强调的一点。堆溢出的原理并不复杂但你若只在脑子里推演我给chunk A写入超长数据覆盖了chunk B的header然后……那你永远学不会。原因很直白堆管理器有太多隐式行为——比如校验失败后的fallback路径、不同的malloc调用导致的切割行为、合并时对边界条件的处理——这些不通过实战观察根本无法真正内化。我学堆溢出时的做法是给自己定了一个三步拆解的练习方法构造最小实验写一段20行以内的代码制造一个可控的堆溢出不追求利用只追求溢出后程序还能稳定运行观察溢出点周围的内存变化。目标劫持在上一步基础上尝试让某个无关变量被溢出数据改变从而实现简单的逻辑跳转。完整利用尝试在开启ASLR、PIE、Full RELRO的环境下完成一个完整的shell获取流程。每一步都记录下你观察到的内存状态、堆管理器的反应、以及你微调了哪几个字节导致行为变化。这套方法看上去笨但它的好处是让你把堆管理器的行为当成一种可以被预测和操纵的现象而不是黑盒魔法。6. 调试工具与问题排查速查6.1 常用的gdb调试命令如果你要在堆溢出上继续深入下面的命令必须用得行云流水。我把它们整理成了一张速查表。命令作用使用频率heap查看当前堆所有chunk的地址、大小、状态极高heap bins查看fastbin、small bin、unsorted bin等链表内容极高x/20gx 地址以16字节为单位的十六进制查看指定地址内存极高set {long}地址 值修改指定地址的内存值高find 起始地址 结束地址 值在内存中搜索指定值中vmmap查看进程内存映射定位堆段高break *地址对指定地址下断点方便观察函数调用点中另外你在调试时如果想让程序停在某个glibc内部函数里可以break free或break malloc这样能更直观地看到堆管理器何时介入。6.2 常见问题及排查思路跑堆溢出实验时你大概率会遇到下面这些状况。我直接给排查方向。现象可能原因排查方式崩溃在free()内部目标chunk的header被破坏堆管理器校验失败用heap查看附近chunk布局检查size和prev_inuse位是否合理崩溃在malloc()内部释放的chunk出现在非法链表或链表指针被污染用heap bins查看bin链表检查fd、bk指针是否指向合法地址直接Segmentation Fault写入地址超出可写内存区域用vmmap查看内存权限检查你是否写到不可写页攻击后没有任何效果溢出数据没有到达目标位置用x/20gx观察溢出缓冲区周围内存计算写入长度和偏移量所有地址都不稳定ASLR开启地址随机化尝试关闭ASLRsetarch -R或先练习固定地址环境输出乱码或崩溃在printf缓冲区数据不是合法的函数指针用info func确认目标函数地址检查偏移是否正确这些坑我都踩过尤其是攻击后没有效果这种问题——很多时候不是你的payload写错了而是溢出长度根本没算对差了那么8字节攻击落点完全偏移。所以我在调试时总会反复核对我要从哪个字节开始篡改目标chunk的哪个字段而不是一头扎进payload里。6.3 一个福利向提示常用防护机制速查最后提一下常见的防护机制因为堆溢出利用是否可以落地极大程度上受这些机制影响。防护机制含义对策NX栈不可执行用ROP或直接调用现成函数ASLR地址随机化靠信息泄露获取地址PIE程序基址随机化泄露程序基址或利用相对偏移Full RELROGOT表不可写改__free_hook、__malloc_hook等Partial RELROGOT表部分可写可尝试改写GOT表项Stack Canary栈保护堆溢出时通常不触发但要注意write顺序堆溢出研究到后半段你会越来越感受到这些防护机制带来的压迫感。每次研究新目标第一步永远是checksec看防护设置主流的调试工具都内置了这条命令五秒钟就能看清底牌。7. 写在最后的几点经验堆溢出学习的确是个陡峭的坡。它不像栈溢出那样学完原理就能写出exp而是需要你同时具备几种能力对内存布局的直觉、对堆管理器行为的掌握、对调试工具的熟练运用、以及对程序运行流程的全局理解。我个人的体会是最有价值的学习路径不是照着某篇writeup一步步抄而是拿一个你完全不了解的堆溢出漏洞从定位溢出点开始自己一步步推导出利用流程。过程中你会犯很多错但这些错误恰恰是理解堆管理器行为的入口。如果你现在正卡在某个堆溢出的细节上试着手动用gdb把堆的完整布局打印出来然后逐个字段去对照源码里堆管理器的处理逻辑。你可能会发现原来那个让你百思不得其解的崩溃只是在某个标志位上差了一位而已。另外如果你想在这个方向上继续深入之后可以去看看堆风水Heap Feng Shui、House of系列如House of Orange、House of Einherjar相关的内容。这些高阶技巧本质上仍然是在做同一件事用堆管理器的规则塑造出攻击者想要的内存状态。原理框架不变变的只是更精细的布局手法和更多样的触发路径。最后再分享一个实用小技巧。调试堆溢出时我习惯在每个关键操作前后各截一张内存快照分配前看一眼分配后看一眼释放前再看一眼。这样能直观暴露出堆管理器对每个操作产生的内存变化找错点的时候省下大量时间。别怕麻烦这些快照就是你的现场笔录等你能对着快照流利说出这一步应该是unsorted bin这里走了切割分支的时候堆溢出的底子就算真正打牢了。
阅读完成 · 觉得有帮助?
咨询建站