资讯详情

栈溢出入门实战:从jarvisoj_level2学会ret2text与返回地址覆盖

📅 2026/9/9 15:13:33 | 华诺云谱 👁 阅读
栈溢出入门实战:从jarvisoj_level2学会ret2text与返回地址覆盖
得看到这个标题老pwn手应该都懂——又一道经典的栈溢出入门题。BUUCTF上的jarvisoj_level2属于那种你刷完一遍之后会把整个ret2text和ret2shellcode的底层逻辑都彻底理顺的题目。如果你刚开始接触PWN第一次打开这个题目一脸懵甚至不知道从哪里下手那这篇文章就是给你准备的。我会把这题的完整解题思路、每一步的原理、踩过的坑全部拆开揉碎保证你看完能自己完整打一遍而不是单纯抄个exp就完事。先交代一下这道题的底细。jarvisoj_level2是jarvis OJ上的一个经典PWN题后来被BUUCTF收录到平台里作为新手进阶或者新手村毕业的必刷题之一。题型属于典型的32位栈溢出而且是比较“温柔”的那一种——程序本身就存在一个后门函数或者存在system函数以及/bin/sh字符串你不需要构造特别复杂的ROP链只需要把返回地址覆盖掉让它跳到你想要的地方执行就行。但正是因为“温柔”它能帮你把栈溢出、返回地址、函数调用约定这几个最基础又最重要的概念彻底打通。对新手来说这题最大的价值在于它逼着你学会看汇编、会用IDA、会算偏移、会写exp、会调试。这些技能不是看几篇博客就能会的必须亲手跑一遍。我当年第一次做这题的时候光一个偏移量就算错了好几次打了半天打不通后来才发现是我把缓冲区大小数错了白白浪费了好几个小时。这篇文章我会把这题从头到尾的完整流程都写出来包括那些我在实操过程中踩过的坑和怎么避免踩坑希望能帮你少走点弯路。1. 题目概览与考点分析1.1 核心考点一次完整的“栈溢出到控制流劫持”实战先别急着开终端我们先把这题的“题眼”搞清楚。PWN题目不管包装得多花哨最终目标基本都是拿到flag而拿flag最常见的方式就是获得shell。Level2这道题看似简单但它的考点其实覆盖了PWN入门的几个最关键环节第一栈溢出漏洞的识别。你要能在IDA或者Ghidra的反汇编界面里一眼看出哪里存在危险的函数调用——这里最经典的就是gets、strcpy、read这类不检查边界或你手动指定大长度的函数。Level2里用的是gets它的特性就是毫不设防读多少写多少写到栈上直到遇到换行符为止。第二控制流劫持。这是PWN永恒的核心主题。栈上溢出之后你覆盖的是栈上返回地址的位置那么当函数执行ret指令时CPU就会跳到你这个攻击者指定的地址去执行。怎么精准地覆盖到那个位置就需要你精确地计算栈布局——也就是“偏移量”。第三32位程序的函数调用约定。在32位ELF里函数参数是通过栈来传递的。这意味着如果你想调用system(/bin/sh)你需要在调用前把/bin/sh这个字符串的地址压入栈上作为参数。这一步如果不理解你即使拿到了system地址也不知道怎么构造payload。第四信息收集和工具链。做PWN的第一步永远是checksec、file、run然后才是静态分析。这一步决定了你后面用什么策略是ret2text、ret2libc、还是shellcode。说白了Level2就是一次“全要素演练”——从信息收集、漏洞分析到exp编写、远程打通一套流程走下来你才算真正迈过了PWN的门槛。很多新人卡在“不知道从哪下手”本质上是脑子里没有一个标准化的解题管线而这题恰好能把管线完整跑一遍。1.2 环境准备你需要一台能跑pwn的Linux虚拟机在正式动手之前我们先把环境弄好。做PWN题我强烈建议你在Linux下操作最好是Ubuntu 18.04或20.04的虚拟机。为什么因为在Linux下程序的行为更接近CTF比赛的出题环境而且pwntools这个核心工具链在Linux下支持得最好。你如果非要用Windows加WSL也不是不行但很多调试工具比如gdb的peda插件、pwndbg在WSL下体验多少有点问题出了问题排查起来很麻烦不建议新手折腾。具体需要装的东西有这么几个# 1. 安装python3和pip一般系统自带 sudo apt update sudo apt install python3 python3-pip # 2. 安装pwntools pip3 install pwntools # 3. 安装gdb以及pwndbg插件gdb-multiarch可以顺手装上 sudo apt install gdb git clone https://github.com/pwndbg/pwndbg cd pwndbg ./setup.sh # 4. 安装checksecpwntools自带但独立的checksec更好用 sudo apt install checksec这里我多说一句pwndbg插件对新手来说真的能救命。它会在gdb启动时自动显示当前栈、寄存器、反汇编、栈上的字符串你调试的时候一眼就能看到自己构造的payload在栈上的布局省去了大量手动输入命令的麻烦。我之前用原版gdb调栈溢出每次都要手动x/20wx $esp去看栈内容效率极低后来装上pwndbg之后效率提升不是一点半点。另外你需要把题目文件从BUUCTF下载下来。文件一般是一个压缩包解压后里面是一个ELF可执行文件和一个libc文件有可能没有libcLevel2这题通常不需要libc。拿到文件后先给它加执行权限chmod x level2然后把它丢到一个干净的目录里比如~/pwn/level2/后面所有的分析、exp编写都在这个目录下进行。2. 信息收集拿到文件后的第一波“体检”2.1 checksec先看看程序穿了什么“盔甲”拿到一个ELF文件第一件事不是运行而是对它进行“体检”。体检的工具主要是两个file和checksec。我们先来看看它们输出的信息说明了什么。$ file level2 level2: ELF 32-bit LSB executable, Intel 80386, version 1 (SYSV), dynamically linked, interpreter /lib/ld-linux.so.2, for GNU/Linux 2.6.32, BuildID[sha1]..., not stripped32-bit告诉我们这是32位程序意味着参数传递走的是栈而不是寄存器函数调用约定是cdecl。dynamically linked说明它依赖系统动态链接库not stripped是个好消息——符号表还在反汇编的时候函数名都直接可见不用靠猜。接下来是checksec这是PWN入门必须刻进DNA的一句命令$ checksec level2 Arch: i386-32-little RELRO: Partial RELRO Stack: No canary found NX: NX enabled PIE: No PIE (0x8048000)这里重点看几个字段。No canary found说明栈上没有金丝雀保护这意味着我们可以直接覆盖返回地址不用额外考虑绕过canary的问题。NX enabled栈不可执行说明我们不能直接在栈上写shellcode然后跳过去执行——但这没关系因为程序里已经有可用的后门了我们走ret2text路线就行。Partial RELRO和No PIE意味着GOT表可写且程序地址固定这在后面如果做GOT劫持或ret2libc时会有用但Level2里我们甚至用不太上。看到这组保护状态基本可以确定这题的利用路线就是栈溢出 覆盖返回地址 跳转到程序中已有的危险函数。不需要绕过任何额外保护整体属于“裸奔”状态非常适合入门。2.2 运行程序感受一下程序的正常逻辑在静态分析之前先运行一下程序看看它的正常行为是什么样的$ ./level2 Input: AAAA $程序会打印一个Input:提示然后通过gets读入我们输入的内容读取完成后程序退出了。注意这里有一个细节当我们输入AAAA后shell提示符$出现了说明程序没有额外做什么只是简单读入然后返回。这个运行结果透露了两个信息。第一程序中确实存在一个输入点而且很可能是gets这是栈溢出的经典入口。第二程序没有额外逻辑说明题目的核心就是在输入这里做文章。你可以尝试输入一段超长字符串试试什么反应$ python3 -c print(A * 200) | ./level2 Input: Segmentation fault回车之后程序直接段错误了。这说明我们成功地把栈上的返回地址覆盖成了一串0x41414141程序执行完返回时跳到了一个非法地址崩溃了。恭喜你漏洞已经确认存在接下来就是精细地控制这个崩溃点。2.3 判断利用策略为什么这题不需要libc可能有读者会问checksec显示NX开启了栈不可执行那我们是不是得去libc里找system和/bin/sh这就是Level2和纯ret2libc题目的区别点。我们可以先通过IDA反汇编看一眼如果程序自己已经包含了system函数以及/bin/sh字符串那就没必要从libc里找直接构建一个简单的调用链就行了。判断方法也很简单用readelf -s看符号表或者用IDA直接看函数列表。正常情况下这种题是给出了一个可以直接利用的后门点的。这也是接下来静态分析的核心任务。3. 逆向分析用IDA定位漏洞和关键资产3.1 主函数与漏洞点gets函数的威力用IDA 32位打开level2程序很小函数列表一目了然。主函数逻辑也很简单反编译后大概是这样的int __cdecl main(int argc, const char **argv, const char **envp) { vulnerable_function(); return 0; }再看vulnerable_functionint vulnerable_function() { char buf[136]; gets(buf); return 0; }buf长度只有136字节而gets会一直读到换行符为止完全不检查写入的字节数。这就意味着我们可以向栈上写入远超136字节的数据把返回地址覆盖掉。这个vulnerable_function的命名也在明示这题就是以漏洞函数为诱饵的栈溢出题。这里要注意一个细节栈上的布局是“局部变量向下增长返回地址在更高地址”。具体来说在vulnerable_function的栈帧中buf从栈底低地址开始往上依次是其他局部变量、保存的EBP、返回地址。所以我们输入的前136字节会填满buf紧接着的4字节会覆盖保存的EBP再接下来的4字节就是返回地址。我们只需要构造好偏移让返回地址恰好落在我们想跳转的位置就行。3.2 找到system和/bin/sh这题的关键底牌继续用IDA查看你会发现程序里除了main和vulnerable_function之外还有一个函数或者一段隐藏代码。这道题目比较经典的处理方式是程序里存在一个名为system的导入函数同时在.rodata段或者.data段存在/bin/sh字符串。你要做的就是在IDA里找到这两个东西的地址。找到后利用思路就非常清晰了通过栈溢出覆盖vulnerable_function的返回地址为system函数的地址。在返回地址之后构造一个合法的返回地址可以是任意值但为了栈平衡通常填一个无害地址。在返回地址之后填入/bin/sh字符串的地址。为什么是这个布局因为32位程序调用system时参数是通过栈传递的。当CPU执行ret指令时它会从栈顶弹出返回地址到EIP同时ESP指向栈顶的下一个位置。此时栈顶存放的正好是我们构造的“返回地址”字段再下一个就是第一个参数。按照cdecl调用约定system会把这个参数当作const char*来解析也就是我们希望它执行的命令字符串。所以整个payload就是payload bA * 偏移 p32(system_addr) p32(exit_addr或任意) p32(binsh_addr)需要注意的是这个布局里的exit_addr并不是必须的但如果你不填system执行完ret时会继续往下取栈上数据作为返回地址如果取到一个非法地址程序就会崩溃。虽然shell已经拿到了但有时候会导致远程连接异常所以稳妥起见我们会填一个exit的地址或者直接填一个main的地址让它重新跑效果会更好。3.3 偏移量计算1364别数错了偏移量的计算是栈溢出最基础也最容易出错的一步。在IDA里vulnerable_function的反汇编会告诉你buf在栈上的位置。通常看到的是这样的代码var_8C -0x8C ; buf从这里开始其中0x8C就是buf地址相对于EBP的偏移。因为这是相对于EBP的偏移所以从buf到返回地址的距离是buf到EBP的距离 EBP自身的长度也就是0x8C 4。这个0x8C换算成十进制是140再加4等于144字节。所以你的payload前144个字节是填充物第145到148字节也就是第145个字节开始是返回地址。很多新手在这里会犯一个错误直接在IDA里看到0x8C就当成偏移写进exp结果打不通。记住0x8C是buf相对于EBP的偏移你要再往上走4字节才是返回地址。我当年第一次做的时候就是这个数错了怎么都拿不到shell后来用gdb一看栈上的布局才恍然大悟。你如果不想手算也可以在gdb里用cyclic生成一个模式串来测试# 生成200字节的模式串 cyclic 200 # 在gdb里运行程序输入模式串崩溃后看EIP的值 cyclic -l 0x41414141pwntools的cyclic功能可以帮你自动算出偏移量但这个题固定是144你只要记牢就行。4. Exploit编写与调试本地打通是第一步4.1 一个可以直接打通的本地exp现在进入实操环节。本地打通这个exp是整个入门过程中最让人兴奋的一步也是最容易出问题的一步。我先贴一个完整的exp然后逐行解释它的含义。from pwn import * # 根据本机情况设置 context.arch i386 context.log_level debug # 连接本地进程 p process(./level2) # 如果打远程把下面这行取消注释替换成实际地址和端口 # p remote(node4.buuoj.cn, 30000) # 关键地址从IDA里查到的 system_addr 0x08048420 binsh_addr 0x0804A024 # 构造payload payload bA * (0x8C 4) # 覆盖到返回地址前的填充 payload p32(system_addr) # 覆盖返回地址为system payload p32(0xdeadbeef) # system执行完的返回地址随便填 payload p32(binsh_addr) # system的参数/bin/sh # 发送payload p.sendline(payload) # 进入交互模式可以看到shell p.interactive()解释几个关键点bA * (0x8C 4)填充的是buf到返回地址之间的所有空间包括保存的EBP。这里把EBP覆盖成AAAA是没问题的因为函数vulnerable_function返回到main之后实际上不会再用到这个EBP而且我们直接控制它跳转到system。p32(system_addr)把system函数地址按小端序压入栈中覆盖返回地址。p32(0xdeadbeef)是system执行完ret之后要返回的地址。这里填一个无意义的地址其实程序大概率不会再正常返回但为了栈布局正确必须先占个位置。p32(binsh_addr)是system的第一个参数在32位cdecl约定下这个位置在栈顶。运行脚本看到交互式shell出来了输入ls、cat flag就能验证是否成功。本地打通了意味着你的漏洞利用逻辑是正确的继续去打远程就行。4.2 从本地到远程同一套exp直接打远程BUUCTF平台上的题目通常在题目页面会给出一个远程地址和端口比如node4.buuoj.cn:XXXXX。你只需要把exp里的process(./level2)换成remote(node4.buuoj.cn, XXXXX)然后重新运行即可。这里有个非常常见的坑本地打通过远程打不通。原因有哪些呢第一远程环境的libc版本和本地不一样但Level2用的是程序内部的system和binsh一般不依赖libc版本第二远程地址和端口填错了或者平台临时换了端口第三网络延时导致sendline之后没有及时进入交互模式程序已经崩溃了。排查顺序一般是先确认远程地址端口无误再确认payload没有因为发送方式不同而出错最后用nc手动连一下远程看看程序是否正常启动。我第一次打远程的时候脚本一直报连接被重置后来发现是题目平台当天有段时间在维护换了个时间再打就通了。所以如果你确信exp没问题多试几次或者换个时间段再试也是解题的一部分经验。4.3 调试技巧用gdb看栈上的payload长什么样如果本地打不通最有效的调试方式是用gdb配合pwndbg来看程序崩溃时的栈和寄存器。# 先运行gdb gdb ./level2 # 在gets后下断点 b *0x0804849B # 运行输入一个模式串 run断下来之后用stack 20或者x/20wx $esp查看栈内容。你会看到你输入的字符串按16进制排列从这些内容里可以直接数出偏移量找到哪里开始是0x41414141A的十六进制表示再找到哪里有0x08048420system地址两者之间的位置关系一目了然。pwndbg的好处是它会自动高亮栈上的数据你甚至可以直观地看到返回地址被覆盖的位置。这比单纯对着IDA数偏移要直观得多也是我强烈建议新手装pwndbg的原因。5. 常见问题与排查技巧实录5.1 现场速查表问题现象可能原因解决办法本地直接Segmentation fault没有拿到shell偏移量算错system或binsh地址不对重新确认偏移量检查IDA里的地址是否正确出现/bin/sh: 0: Cant open错误远程shell被断开或payload发送不完整检查sendline是否带了换行检查网络连接是否稳定远程连接被重置远程环境崩溃平台端口问题用nc手动连一次确认服务正常换时间段再试exp运行后卡住没有任何输出程序等待输入但payload没有触发检查是否使用了sendline而不是send确认payload已发送输入超长字符串后程序正常退出没有崩溃0x8C4偏移数错可能覆盖的位置不是返回地址用cyclic模式串精确定位偏移拿到shell但无法执行cat flag远程shell的当前目录不对先执行ls查看目录内容找到flag文件名后再cat这张表是我根据自己的做题经验和帮别人排查问题时整理的常见故障如果你遇到不是这几种的情况也别慌用调试器一步步看总是能找到问题的。5.2 做题经验Level2教会我的三件事做Level2这道题我的心得可以浓缩成三句话送给所有刚入坑PWN的新手第一永远先看保护再定策略。checksec输出的几个字段决定了你后面所有的路线。没有canary、没有PIE、NX开启这就是一道标准ret2text题。如果你连保护都没看就想着堆漏洞或者格式化字符串方向就错了。第二偏移量是栈溢出的命根子。很多人把注意力放在system和binsh的地址上反而忽略了偏移量结果怎么都打不通。记住偏移量的一切以实测为准IDA算出来的只是一个参考最好用gdb验证一次。做题多了以后你会发现有很多题目在IDA里的反汇编信息和实际栈布局存在细微差异这时候就用调试器去验证别死磕静态分析。第三本地打通只是第一步远程才是最终目标。有些人在本地拿到shell就兴奋得不行觉得题目做完了。实际上远程环境可能和本地有细微差别比如/bin/sh的路径、环境变量、libc版本等都会影响exp的稳定性。Level2因为用了程序内部的system和binsh环境差异影响不大但如果你养成了“本地打完还要打远程”的习惯后面做ret2libc题目时会省很多事情。5.3 延伸拓展从Level2到更复杂的栈利用Level2做完之后你的下一步应该是什么我建议按这个顺序去刷题巩固jarvisoj_level4同样是栈溢出但这次程序里没有现成的system和binsh需要从libc里泄露地址这是ret2libc的入门。BUUCTF ciscn_2019_es_2栈迁移的经典题锻炼你对栈布局的控制能力。BUUCTF pwn1_sctf_2016利用字符串长度转换绕过输入限制属于格式化/栈溢出的变种。ret2libc系列学习如何通过GOT表泄露libc基址这个技能在未来几乎每道题都用得上。推荐的学习路径是先彻底吃透Level2然后自己动手把“从程序内部找system到从libc找system”这个跨度打通你的PWN水平就有了质的飞跃。Level2只是一个起点但它能把栈溢出利用的底层逻辑讲得明明白白。当你亲手在终端里敲下cat flag并看到flag输出的时候那种感觉和看任何writeup都不一样。我的建议是看完这篇分析后一定要自己动手操作一遍哪怕只是重新把IDA的每一步走一遍、把exp的每一行敲一遍都比只是“看过”有用得多。最后分享一个小技巧做PWN题时文件名尽量改成有辨识度的名字比如level2_jarvis同时在目录里放一个readme.txt记录这道题的利用思路和踩过的坑。等到你刷了几十道题之后再回头看这些记录那就是你自己独一无二的“PWN字典”。后面的路还很长慢慢来比较快。
📝

华诺云谱内容团队

资深建站顾问 · 行业研究员

10年+企业数字化服务经验,专注智能建站、SEO优化与品牌营销,持续输出建站技巧、行业洞察与营销干货,已帮助5000+企业实现数字化增长。

你可能需要的服务

订阅华诺云谱资讯周报

每周一封,精选建站技巧、SEO与营销干货,直达邮箱。已有 8,000+ 企业主订阅,助你少走弯路。