尧图网络 高端网站定制 · 原创设计
免费咨询热线
400-888-6620
免费获取方案
从栈溢出到堆利用:二进制安全学习路线与PWN实战调试笔记
1. 从栈到堆为什么我坚持把这条学习路线走完刚接触二进制安全那会儿我和很多人一样觉得栈溢出就是全部。一个gets、一个strcpy配上pattern_create和pattern_offset算出偏移量控制EIP跳转到shellcode或者system(/bin/sh)一套流程下来行云流水感觉自己已经摸到了门槛。但真正到了实际场景里你会发现栈溢出只是冰山浮在水面上的那一角。现代程序里栈上的保护机制越来越密——Canary、NX、ASLR、PIE一层层叠上来光靠栈溢出能打通的场景越来越少。而堆这个看起来“没那么直观”的区域反而成了很多真实漏洞的温床。我整理这条学习路线不是为了写一份教科书式的笔记而是想把自己从栈溢出一路踩到堆利用的过程中那些真正卡住我的点、那些让我反复调试到凌晨的细节完整地摊开来讲。栈溢出是理解控制流劫持的起点堆利用则是理解内存管理漏洞的进阶。两者之间不是割裂的栈上的很多思路——比如覆盖返回地址、构造ROP链——在堆里会以另一种形式重现只不过操作的对象从栈帧变成了chunk从返回地址变成了fd/bk指针或者tcache链表。这篇文章适合谁如果你已经能独立完成一个基础的栈溢出题目知道什么是偏移量、什么是ROPgadget但一看到堆相关的题目就不知道从哪下手那这篇整理就是为你准备的。我会从栈溢出的核心机制重新梳理一遍然后自然过渡到堆的结构、分配器行为、常见利用手法每个环节都配上我实际调试过的例子。关键词里的“栈溢出”“堆利用”“PWN”“二进制安全”“漏洞利用”会贯穿全文但我不打算堆砌术语而是尽量用调试器里的真实反馈来说话。有一点需要提前说明这篇文章里的所有例子都是我自己构造的模拟场景不涉及任何真实系统或真实漏洞。我用的环境是Linux下的64位程序工具链是gdb配合pwndbg插件编译器是gcc调试过程中会用到pwntools写exp。如果你用的是其他环境思路是一样的只是具体地址和偏移量需要自己重新算。2. 栈溢出的本质不只是覆盖返回地址那么简单2.1 栈帧布局与覆盖点的精确定位很多人学栈溢出第一个卡住的地方不是原理而是怎么精确算出偏移量。我见过不少新手拿着一个pattern生成的字符串丢进去程序崩溃了然后对着EIP的值一脸茫然。其实定位偏移量的方法很直接用cyclic生成一段有规律的字符串长度要足够覆盖整个栈帧然后看崩溃时EIP或RIP里装的是哪四个或八个字节再用cyclic -l反查位置。但这里有个细节容易被忽略32位和64位的栈帧结构不一样。32位程序里函数参数是直接压在栈上的返回地址在EBP上方64位程序里前六个参数走寄存器返回地址在RBP上方而且栈对齐要求是16字节。这意味着你在64位程序里算偏移量时不能简单照搬32位的经验。我自己的习惯是先在gdb里disassemble目标函数看清楚sub rsp, 0xXX这条指令0xXX就是局部变量占用的栈空间大小再加上保存的RBP8字节就是到返回地址的偏移。举个例子一个64位程序里vuln函数的反汇编是这样的push rbp mov rbp, rsp sub rsp, 0x40 lea rax, [rbp-0x40] mov rsi, rax ...那么从rbp-0x40这个缓冲区起始位置到返回地址的偏移就是0x40 8 0x48也就是72字节。这个计算过程看起来简单但实际调试时编译器可能会插入额外的栈空间用于对齐所以最稳妥的办法还是用cyclic实测一遍和理论值对照。2.2 从ret2text到ret2libc保护机制逼出来的进化最早的栈溢出利用就是ret2text——程序里自带一个system(/bin/sh)或者类似的函数你只要把返回地址覆盖成那个函数的地址就行。这种场景在CTF入门题里很常见但现实中几乎不可能这么幸运。于是有了ret2shellcode把shellcode写到栈上然后跳过去执行。但NX保护一开栈不可执行这条路就断了。接下来就是ret2libc。思路是不直接执行shellcode而是调用libc里现成的函数比如system参数是/bin/sh字符串的地址。但这里有个问题system的地址在libc里而libc的加载基址受ASLR影响每次运行都不一样。所以你需要先泄露一个已经执行过的函数的实际地址比如puts的GOT表项然后根据libc版本算出system和/bin/sh的偏移最后再构造第二次溢出跳过去。这个过程中泄露地址和再次溢出往往需要在同一个程序运行周期内完成因为ASLR每次运行都会重新随机化。所以通常的做法是第一次溢出控制程序返回到putsplt参数设为putsgot把puts的真实地址打印出来然后让程序返回到main函数重新开始第二次溢出时用算好的system地址和/bin/sh地址构造ROP链。我踩过的一个坑是64位程序的参数传递需要用到pop rdi; ret这样的gadget而32位程序是直接把参数压在栈上。如果你在64位程序里按32位的方式写exp参数根本传不进去。找gadget可以用ROPgadget或者ropper命令很简单ROPgadget --binary ./vuln --only pop|ret输出里找pop rdi; ret记下地址这就是你构造ROP链的关键零件。2.3 栈溢出调试中的三个高频翻车点第一个翻车点是栈对齐。64位程序里system函数内部会用到movaps指令这条指令要求栈地址是16字节对齐的。如果你构造的ROP链在调用system时栈没有对齐程序会在movaps那里崩掉报SIGSEGV。解决办法是在ROP链里加一个retgadget相当于多弹一次栈调整对齐。这个坑我卡了整整一个晚上后来在gdb里单步跟到movaps才恍然大悟。第二个翻车点是libc版本不匹配。你本地调试用的libc和远程环境用的libc可能不是同一个版本算出来的偏移量自然对不上。解决办法是用libc-database或者libcsearcher根据泄露的地址低12位去查或者直接用题目提供的libc文件。我现在的习惯是拿到题目先看有没有附libc有的话就用patchelf把程序的libc换掉保证本地和远程一致。第三个翻车点是输入函数的行为差异。gets会一直读到换行符read需要指定长度scanf遇到空格会截断。如果你用read读入payload但payload里包含了\x0a换行符read是不会截断的但如果你用getspayload里的\x0a就会提前结束输入。这个细节在构造包含地址的payload时特别重要因为地址里很可能出现\x0a。3. 堆的脾气从chunk结构到分配器的真实行为3.1 堆chunk的物理布局与size字段的玄机栈是自动管理的函数进来分配出去回收规规矩矩。堆不一样堆是程序员手动要、手动还的分配器glibc里是ptmalloc在中间做调度。你每次malloc分配器给你一块内存这块内存的头部藏着两个关键字段prev_size和size。size字段的低三位有特殊含义最低位是PREV_INUSE表示前一个chunk是否在使用倒数第二位是IS_MMAPPED表示这个chunk是不是通过mmap分配的倒数第三位是NON_MAIN_ARENA表示这个chunk属不属于主分配区。这三个位里PREV_INUSE是最常被利用的。当你free一个chunk时如果它前面的chunk是空闲的分配器会把它们合并。合并的时候会用当前chunk的prev_size字段去定位前一个chunk的起始位置。如果你能控制prev_size就能让分配器合并到一个你伪造的地址上这就是unlink和overlapping chunk的基础。我刚开始学堆的时候最困惑的是chunk的大小是怎么算的。比如你malloc(0x18)实际得到的chunk大小是0x20因为size字段本身占8字节而且chunk大小要按16字节对齐。所以malloc的参数和实际chunk大小之间有一个换算关系malloc请求大小实际chunk大小64位用户可用区域0x00 - 0x180x200x180x19 - 0x280x300x280x29 - 0x380x400x380x39 - 0x480x500x48这个表我建议你记在心里因为后面算偏移、构造overlap的时候全靠这个换算。3.2 tcache最快但也最容易被玩坏的缓存glibc 2.26之后引入了tcache每个线程默认有64个tcache bin每个bin最多存7个chunk大小从0x20到0x410。tcache的设计初衷是加速小内存的分配和释放但它有个致命的特点几乎没有安全检查。在早期的glibc版本里tcache的fd指针直接指向下一个空闲chunk你只要覆盖这个fd下一次malloc就会返回你指定的地址。tcache的利用手法里最经典的是tcache poisoning。假设你有一个UAFUse After Free漏洞能往已经free的chunk里写数据那你就可以覆盖它的fd指针把它改成你想分配的目标地址。然后连续malloc两次同样大小的chunk第一次返回原来的chunk第二次就返回你指定的地址。这个手法在glibc 2.27到2.29上非常稳定2.31之后加了key字段做校验但依然有绕过方法。我实测下来tcache的调试有个小技巧在gdb里用p *(struct tcache_perthread_struct*)tcache可以看每个bin里存了几个chunk用p tcache-entries[i]可以看第i个bin的链表头。pwndbg插件里直接有tcachebins命令输出更直观。当你看到某个bin的计数是7的时候说明这个大小的tcache已经满了再free就会进fastbin或者unsorted bin。3.3 fastbin与unsorted bin不同大小走不同路径fastbin管理的是0x20到0x80大小的chunk64位下默认它的特点是先进后出而且free的时候不会清除PREV_INUSE位也不会合并相邻chunk。fastbin的fd指针指向下一个空闲chunk但只用了4字节32位或8字节64位的低位高位是0。这个特性导致了一个经典的利用手法fastbin attack通过覆盖fd指针的低位让分配器返回一个几乎任意地址。unsorted bin是另一个重要的区域。当你free一个大于fastbin范围的chunk或者tcache满了之后free的chunk会先进入unsorted bin。unsorted bin是个双向链表fd和bk指针分别指向前后chunk。当你再次malloc时分配器会先遍历unsorted bin找到合适大小的chunk就返回。如果找不到就把这些chunk按大小归入small bin或large bin。unsorted bin的利用价值在于它的fd和bk指针在free之后会指向main_arena中的某个地址。如果你能泄露这个地址就能算出libc的基址。这就是unsorted bin leak是堆题里泄露libc最常用的手段之一。具体来说当你free一个chunk进入unsorted bin它的fd和bk都会被写成main_arena0x60左右的地址。如果你能读这个chunk的内容就能拿到这个地址减去已知偏移就是libc基址。4. 堆利用的实战手法从UAF到House of系列4.1 UAF堆漏洞的万恶之源UAF全称Use After Free意思是释放之后继续使用。程序free了一个指针但没有把指针置空后续又通过这个指针读写数据。这个漏洞本身不是内存破坏但它是一系列堆利用手法的入口。有了UAF你可以往已经释放的chunk里写数据而这块chunk可能已经被分配器放进了tcache或fastbin你的写入会直接修改它的fd指针。我构造过一个最简单的UAF例子程序有两个功能add和freeadd的时候malloc一个chunk并读入内容free的时候释放但不置空。我先add一个0x20的chunk然后free它再用add一个同样大小的chunk这时候分配器会把刚才释放的chunk重新分配给我。如果我在free之后、再次add之前通过某种方式比如另一个指针往那个chunk里写数据就能控制新chunk的内容。实际调试时我会在gdb里用heap命令看chunk的布局用bins命令看各个bin的状态。UAF的利用往往需要配合tcache poisoning或者fastbin attack单独一个UAF很难直接拿到shell但它提供了写fd指针的能力。4.2 tcache poisoning的完整利用链假设我们有一个UAF能往free后的chunk里写8个字节。目标是在__malloc_hook或__free_hook附近分配一个chunk然后写入system的地址最后触发free或malloc拿到shell。步骤是这样的分配两个0x20的chunk记为A和B。释放A再释放B。此时tcache的0x20 bin里B在链表头B-fd A。通过UAF往B里写数据覆盖B-fd把它改成__free_hook的地址。连续malloc两次0x20的chunk。第一次返回B第二次返回__free_hook。往第二次分配的chunk里写入system的地址。释放一个内容为/bin/sh的chunk触发__free_hook拿到shell。这个链条里最关键的是第3步的地址计算。__free_hook的地址在libc里你需要先泄露libc基址。泄露的方法可以用前面说的unsorted bin leak也可以用tcache里残留的指针。我通常会在free一个chunk之前先往里面写一个已知的libc地址比如通过格式化字符串泄露然后free之后读出来。4.3 unsorted bin attack与House of Orangeunsorted bin attack的目标是往任意地址写一个main_arena相关的值。它的原理是当你free一个chunk进入unsorted bin然后再次malloc一个更大的chunk时分配器会把unsorted bin里的chunk取出来同时把它的bk指针写到unsorted_chunks(av)-bk-fd的位置。如果你能控制bk指针就能往任意地址写一个堆地址。这个手法单独用很难直接拿shell但配合其他技术就很强。比如House of Orange它的核心是在没有free功能的情况下通过malloc一个比当前top chunk还大的chunk触发sysmalloc把旧的top chunk放进unsorted bin然后利用unsorted bin attack修改_IO_list_all最终劫持_IO_FILE结构体拿到shell。House of Orange的利用链比较长我建议先理解_IO_FILE的结构和虚表劫持的原理再回头看这个手法。实际调试时_IO_list_all的地址和system的地址都需要精确计算一个字节的偏差就会导致崩溃。4.4 堆利用中的信息泄露没有leak就没有一切堆题和栈题最大的区别在于栈题往往可以直接跳转到某个函数堆题几乎必须依赖信息泄露。因为堆的地址、libc的地址都是随机的你不知道这些地址就没法构造有效的利用链。泄露的手段主要有几种unsorted bin leakfree一个chunk进unsorted bin读它的fd/bk得到main_arena相关地址算出libc基址。tcache leak在某些glibc版本里tcache的fd指针会指向堆地址可以用来泄露堆基址。格式化字符串如果程序有格式化字符串漏洞可以直接泄露栈上或任意地址的内容。stdout leak通过修改_IO_2_1_stdout_结构体把libc地址打印出来。我自己的习惯是拿到堆题先找有没有show功能有的话优先考虑unsorted bin leak。如果没有show那就得找其他泄露途径比如UAF配合printf或者利用malloc返回的指针做文章。5. 调试堆的笨办法与巧办法5.1 用gdb pwndbg看清每一个chunk堆调试最痛苦的地方在于你没法像栈那样直接x/20gx $rsp就看清楚布局。堆的chunk散落在内存各处靠肉眼找效率极低。我刚开始学的时候用x/100gx一块一块翻经常翻到眼花。后来用了pwndbg效率提升了一个量级。pwndbg的heap命令会列出所有chunk的地址、大小、状态是否在使用bins命令会显示tcache、fastbin、unsorted bin、small bin、large bin里各有哪些chunk。vis_heap_chunks命令更是直接把堆内存可视化出来每个chunk的头部和用户区域都标得清清楚楚。我常用的几个命令heap # 查看所有chunk bins # 查看所有bin的状态 tcachebins # 只看tcache vis_heap_chunks # 可视化堆布局 p *(struct malloc_chunk*)0x555555757000 # 查看指定地址的chunk结构还有一个技巧是在malloc和free处下断点每次调用都打印参数和返回值。这样你能清楚地看到每次分配和释放对堆状态的影响。比如b malloc commands silent printf malloc(%d) , $rdi finish printf %p\n, $rax continue end这个断点脚本会打印每次malloc的大小和返回地址对于跟踪chunk的分配顺序非常有用。5.2 写exp时的常见错误与排查思路写堆利用的exp最常遇到的错误是地址算错。比如__free_hook的偏移不同libc版本可能差几十个字节。我的做法是先用readelf -s libc.so.6 | grep __free_hook确认偏移然后在gdb里p __free_hook验证。如果本地和远程libc不一致那就用patchelf换掉或者用libcsearcher查。第二个常见错误是chunk大小算错。前面说过malloc(0x18)实际得到0x20的chunk但如果你malloc(0x20)实际得到的是0x30的chunk。这个换算关系在构造tcache链表时特别重要因为不同大小的chunk进不同的bin搞错了就串不起来。第三个错误是忽略了tcache的计数。tcache每个bin最多7个chunk第8个free会进fastbin或unsorted bin。如果你以为所有free的chunk都在tcache里构造利用链时就会发现链表对不上。我通常会在exp里加一句print(tcache_count)或者直接在gdb里看tcachebins的输出。5.3 从本地打通到远程打通环境差异的坑本地打通了远程打不通这是堆题最让人抓狂的情况。原因通常有三个第一libc版本不同。远程的libc可能比你本地的旧或新__free_hook、system、/bin/sh的偏移都不一样。解决办法是尽量用题目提供的libc或者用libc-database根据泄露的地址反查。第二ASLR的随机性。有时候本地打通是因为运气好地址恰好落在可用的范围里。远程多跑几次就崩了。解决办法是确保你的利用链不依赖任何随机地址所有地址都从泄露的值算出来。第三网络延迟和输入输出缓冲。远程交互时recvuntil的时机很重要。如果程序先输出提示再读输入你就要先recv到提示再send。我习惯在exp里加context.log_level debug把所有的收发都打印出来方便定位卡在哪一步。6. 从栈到堆的学习路径建议如果你现在还在栈溢出的阶段我的建议是先把栈溢出打扎实。不要急着跳到堆因为栈溢出里的ROP链构造、GOT/PLT机制、libc基址计算这些在堆利用里都会反复用到。栈溢出是理解控制流劫持的基础堆利用是理解内存管理漏洞的进阶两者是递进关系不是替代关系。具体来说我建议按这个顺序走栈溢出基础ret2text、ret2shellcode、ret2libc理解偏移量计算、gadget查找、ROP链构造。栈溢出进阶绕过Canary、绕过NX、绕过ASLR理解__libc_csu_init、ret2dlresolve。堆基础理解chunk结构、tcache、fastbin、unsorted bin能在gdb里看懂堆布局。堆利用入门UAF、tcache poisoning、fastbin attack能独立写出利用exp。堆利用进阶unsorted bin attack、House of Orange、_IO_FILE劫持理解更复杂的利用链。每个阶段都要动手调试不要只看文章。我见过很多人把文章看了一遍觉得自己懂了一上手就卡住。堆利用尤其如此gdb里的实际内存布局和文章里的示意图往往有出入只有自己调过一遍才能记住那些细节。最后分享一个我自己的习惯每做完一道题我都会把exp和调试笔记整理成一个markdown文件记录下偏移量、关键地址、踩过的坑。下次遇到类似的题目直接翻笔记效率高很多。堆利用的手法虽然多但核心思路就那么几个——控制fd指针、伪造chunk大小、泄露关键地址。把这些核心思路吃透剩下的就是组合和变通。
RELATED

相关推荐

RepVGG结构重参数化:图像分类模型训练推理解耦与加速实战

RepVGG结构重参数化:图像分类模型训练推理解耦与加速实战

简介:压缩包内含2000个文件,共986.61MB,其中以2435个png图片为数据主体,配合12个Python源码、2个pth权重文件、2个json配置及1个txt说明,构成一套完整的RepVgg图像分类实战项目。项目围绕“VGG式”结构展开&#xff0c…

📅 2026/10/12 20:35:43
Python轻量级裁判文书量刑回归建模实战

Python轻量级裁判文书量刑回归建模实战

简介:本资源是一套面向法律科技从业者、司法AI研究者及Python机器学习学习者的裁判文书量刑预测实践项目,聚焦于利用机器学习提升量刑建议的客观性与效率。压缩包共131个文件,含77个Python脚本(覆盖数据清洗、特征工程、模型训练与…

📅 2026/10/12 20:35:43
基于VGG-16的图像检索系统:从特征提取到相似度排序的完整落地路径

基于VGG-16的图像检索系统:从特征提取到相似度排序的完整落地路径

简介:这份资源面向人工智能与信息检索方向的学习者,提供一套基于VGG-16的图像检索系统完整项目实践。项目以深度学习特征替代传统颜色、形状、纹理等手工特征,采用Keras预训练模型对图像库逐张抽取特征并存入h5文件,检索阶段以夹角…

📅 2026/10/12 20:35:43
MORE NEWS

更多资讯

📰

学术投稿信与回应信写作指南:从编辑视角到高效返修策略

我先说一个当年踩过的坑。有一次我写完一篇稿件,投稿信就写了两句话:一句说稿件已附上,另一句说有新结果请考虑,然后直接提交。结果当天就被编辑退回,理由是投稿信中没有说明本文的创新点,也没解释为什么适…

📰

校园共享厨房创业计划书:财务模型、运营安全与避坑指南

简介:这份创业计划书面向参加“互联网”大学生创新创业大赛的高校团队,聚焦校园共享厨房这一创意赛道,帮助解决宿舍条件受限、学生难以自由烹饪与开展餐饮实践的问题。文档围绕项目背景、项目概述、项目意义、主要业务与盈利模式、成员组成、…

📰

LFW人脸识别数据集实战:从lfw_home.zip到验证评估全流程

简介:这份资源是经典人脸识别数据集 LFW 的本地化整理包,面向人脸验证与识别方向的研究者、算法工程师及深度学习入门学习者,用于训练和评估模型判断两张人脸是否属于同一人的能力。压缩包共 9 个文件,约 241.84MB,以 …

📰

NSGA-II多目标优化MATLAB代码实现:非支配排序与拥挤距离详解

简介:这份资源是NSGA-II多目标优化算法的MATLAB实现代码,面向正在学习智能优化算法、准备课程设计或科研实验的高校学生与研究人员。代码围绕非支配排序、拥挤距离计算、精英策略与基因操作等核心模块展开,并针对二维测试函数ZDT1-6与三维测试…

📰

用Python实现注册登录:文本文件存储与密码哈希实战

最近帮一个刚学编程的朋友看作业,需求就一句话:"实现注册、登录功能。用户名、密码存储在 user_g.txt 文件中。"看完我笑了,越是这种看着简单的小需求,做起来越容易翻车。文件存用户信息,听起来谁都会&#…

📰

网申简历附件太大,先换模板还是压缩 PDF?

网申简历附件太大,先换模板还是压缩 PDF? 先看“大”从哪里来,再决定怎么改。模板里塞了整页照片、复杂背景,和一份扫描成图片的简历,处理方法不一样。别一上来就把 PDF 压到字也看不清;能减少装饰图片时&a…

TODAY

今日更新

THIS WEEK

本周精选

THIS MONTH

本月热门

读完文章,想聊聊您的网站?

告诉我们您的行业与需求,资深顾问一对一梳理方案与报价,全程免费。

📞 💬