news 2026/9/9 15:13:31

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

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
栈溢出入门实战:从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的反汇编界面里,一眼看出哪里存在危险的函数调用——这里最经典的就是getsstrcpyread这类不检查边界或你手动指定大长度的函数。Level2里用的是gets,它的特性就是毫不设防,读多少写多少,写到栈上,直到遇到换行符为止。

第二,控制流劫持。这是PWN永恒的核心主题。栈上溢出之后,你覆盖的是栈上返回地址的位置,那么当函数执行ret指令时,CPU就会跳到你这个攻击者指定的地址去执行。怎么精准地覆盖到那个位置,就需要你精确地计算栈布局——也就是“偏移量”。

第三,32位程序的函数调用约定。在32位ELF里,函数参数是通过栈来传递的。这意味着,如果你想调用system("/bin/sh"),你需要在调用前把/bin/sh这个字符串的地址压入栈上作为参数。这一步如果不理解,你即使拿到了system地址也不知道怎么构造payload。

第四,信息收集和工具链。做PWN的第一步永远是checksecfilerun,然后才是静态分析。这一步决定了你后面用什么策略,是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. 安装checksec(pwntools自带,但独立的checksec更好用) sudo apt install checksec

这里我多说一句,pwndbg插件对新手来说真的能救命。它会在gdb启动时自动显示当前栈、寄存器、反汇编、栈上的字符串,你调试的时候一眼就能看到自己构造的payload在栈上的布局,省去了大量手动输入命令的麻烦。我之前用原版gdb调栈溢出,每次都要手动x/20wx $esp去看栈内容,效率极低,后来装上pwndbg之后,效率提升不是一点半点。

另外,你需要把题目文件从BUUCTF下载下来。文件一般是一个压缩包,解压后里面是一个ELF可执行文件和一个libc文件(有可能没有libc,Level2这题通常不需要libc)。拿到文件后,先给它加执行权限:

chmod +x level2

然后把它丢到一个干净的目录里,比如~/pwn/level2/,后面所有的分析、exp编写都在这个目录下进行。

2. 信息收集:拿到文件后的第一波“体检”

2.1 checksec:先看看程序穿了什么“盔甲”

拿到一个ELF文件,第一件事不是运行,而是对它进行“体检”。体检的工具主要是两个,filechecksec。我们先来看看它们输出的信息说明了什么。

$ 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 stripped

32-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 RELRONo 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_function

int 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查看,你会发现程序里除了mainvulnerable_function之外,还有一个函数或者一段隐藏代码。这道题目比较经典的处理方式是:程序里存在一个名为system的导入函数,同时在.rodata段或者.data段存在/bin/sh字符串。

你要做的就是在IDA里找到这两个东西的地址。找到后,利用思路就非常清晰了:

  1. 通过栈溢出覆盖vulnerable_function的返回地址为system函数的地址。
  2. 在返回地址之后,构造一个合法的返回地址(可以是任意值,但为了栈平衡通常填一个无害地址)。
  3. 在返回地址之后,填入/bin/sh字符串的地址。

为什么是这个布局?因为32位程序调用system时,参数是通过栈传递的。当CPU执行ret指令时,它会从栈顶弹出返回地址到EIP,同时ESP指向栈顶的下一个位置。此时栈顶存放的正好是我们构造的“返回地址”字段,再下一个就是第一个参数。按照cdecl调用约定,system会把这个参数当作const char*来解析,也就是我们希望它执行的命令字符串。所以整个payload就是:

payload = b'A' * 偏移 + p32(system_addr) + p32(exit_addr或任意) + p32(binsh_addr)

需要注意的是,这个布局里的exit_addr并不是必须的,但如果你不填,system执行完ret时会继续往下取栈上数据作为返回地址,如果取到一个非法地址,程序就会崩溃。虽然shell已经拿到了,但有时候会导致远程连接异常,所以稳妥起见,我们会填一个exit的地址或者直接填一个main的地址让它重新跑,效果会更好。

3.3 偏移量计算:136+4,别数错了

偏移量的计算是栈溢出最基础也最容易出错的一步。在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 0x41414141

pwntools的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 = b'A' * (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()

解释几个关键点:

  • b'A' * (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出来了,输入lscat flag就能验证是否成功。本地打通了,意味着你的漏洞利用逻辑是正确的,继续去打远程就行。

4.2 从本地到远程:同一套exp直接打远程

BUUCTF平台上的题目通常在题目页面会给出一个远程地址和端口,比如node4.buuoj.cn:XXXXX。你只需要把exp里的process('./level2')换成remote('node4.buuoj.cn', XXXXX),然后重新运行即可。

这里有个非常常见的坑:本地打通过,远程打不通。原因有哪些呢?第一,远程环境的libc版本和本地不一样,但Level2用的是程序内部的systembinsh,一般不依赖libc版本;第二,远程地址和端口填错了,或者平台临时换了端口;第三,网络延时导致sendline之后没有及时进入交互模式,程序已经崩溃了。排查顺序一般是:先确认远程地址端口无误,再确认payload没有因为发送方式不同而出错,最后用nc手动连一下远程看看程序是否正常启动。

我第一次打远程的时候,脚本一直报连接被重置,后来发现是题目平台当天有段时间在维护,换了个时间再打就通了。所以如果你确信exp没问题,多试几次,或者换个时间段再试,也是解题的一部分经验。

4.3 调试技巧:用gdb看栈上的payload长什么样

如果本地打不通,最有效的调试方式是用gdb配合pwndbg来看程序崩溃时的栈和寄存器。

# 先运行gdb gdb ./level2 # 在gets后下断点 b *0x0804849B # 运行,输入一个模式串 run

断下来之后,用stack 20或者x/20wx $esp查看栈内容。你会看到你输入的字符串按16进制排列,从这些内容里可以直接数出偏移量:找到哪里开始是0x41414141(A的十六进制表示),再找到哪里有0x08048420(system地址),两者之间的位置关系一目了然。

pwndbg的好处是它会自动高亮栈上的数据,你甚至可以直观地看到返回地址被覆盖的位置。这比单纯对着IDA数偏移要直观得多,也是我强烈建议新手装pwndbg的原因。

5. 常见问题与排查技巧实录

5.1 现场速查表

问题现象可能原因解决办法
本地直接Segmentation fault,没有拿到shell偏移量算错;system或binsh地址不对重新确认偏移量,检查IDA里的地址是否正确
出现/bin/sh: 0: Can't open错误远程shell被断开或payload发送不完整检查sendline是否带了换行,检查网络连接是否稳定
远程连接被重置远程环境崩溃;平台端口问题用nc手动连一次确认服务正常;换时间段再试
exp运行后卡住没有任何输出程序等待输入但payload没有触发检查是否使用了sendline而不是send,确认payload已发送
输入超长字符串后程序正常退出,没有崩溃0x8C+4偏移数错,可能覆盖的位置不是返回地址用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字典”。后面的路还很长,慢慢来,比较快。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/9 15:13:14

连续-离散耦合模拟SHPB岩石动态破裂:FLAC-PFC建模与参数标定全流程

做岩石动力学试验的人应该都有这种感觉:真实试件里的裂纹是怎么起裂、怎么扩展、又是在哪个应力水平下突然碎成几块的,光靠实验曲线只能猜个大概。尤其SHPB这类冲击试验,加载时间只有几十到几百微秒,高速相机能拍到宏观破碎过程&a…

作者头像 李华
网站建设 2026/9/9 15:13:03

SQL Server附加失败?MDF还在数据就还在:三大修复路线实战

接手任何一个“数据库迁移后附加失败”的问题,很多人的第一反应都是先慌一会儿:服务器换了、报错看不懂、数据库挂在“正在恢复”或者直接变成 SUSPECT,现场客户盯着你,网上的教程搜来搜去还都是“删掉 LDF 重建日志”这种一句话带…

作者头像 李华
网站建设 2026/9/9 15:12:29

手写线程安全消息队列:条件变量与生产者消费者模型实战

1. 从"裸奔"的共享变量到消息队列:先看一个真实案例我接手过一个采集程序,最初版本用的是最直白的做法:一个生产者线程不断往全局链表里塞数据,消费者线程定时醒来遍历链表,为了防冲突给链表挂了一把大锁。前…

作者头像 李华
网站建设 2026/9/9 15:09:47

后摩揽月V2.1.0开发套件:从边缘计算到模型量化部署的实战解析

后摩揽月的V2.1.0版本,我在拿到手的第一周就把手头一个人脸检测工程完整跑通了。这个版本最明显的感觉是,开发体验从“能用”向“好用”跨了一大步。这篇就详细讲讲这次版本更新的核心内容、实际开发中的操作细节,以及我踩过的几个坑&#xf…

作者头像 李华
网站建设 2026/9/9 15:09:27

老板让全面拥抱AI,结果AI助理把公司的工资表暴露了......

前天老板开完会,突然宣布: 公司要全面拥抱AI。 准备搭一个内部AI机器人,把公司资料都传进去,以员工为核心做AI化改造。 说白了就是: 以后谁需要什么资料,不用到处找人,直接问AI。 人事大姐执…

作者头像 李华
网站建设 2026/9/9 15:07:48

SQL注释完全指南:语法、兼容性与最佳实践

这次我们直接来聊 SQL 注释。 很多开发同学写 SQL 的时候,注释基本不写,或者只在复制查询时顺手加两行 -- 。真到接手别人留下的存储过程、批量脚本和报表 SQL 时,才意识到注释不是“锦上添花”,而是维护成本的一部分。Neso Ac…

作者头像 李华