尧图建网站 尧图建网站 YAOTU WEB BUILD 免费咨询
ARTICLE DETAIL

资讯详情

深耕网站建设与建站编程的一线实战洞察。

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

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位这直接影响栈帧结构和利用方式。objdump或IDA Pro/Ghidra静态反汇编分析找到漏洞点和目标函数。gdb配合pwndbg/peda/gef动态调试精准计算偏移观察栈布局。python3与pwntools编写利用脚本的神器。首先用file命令看一下程序基本信息。通常CTFHub的题目会给一个可执行文件比如ret2text。执行file ret2text输出可能会显示“ELF 32-bit LSB executable”或者“ELF 64-bit LSB executable”。32位和64位的利用在细节上有所不同主要体现在参数传递寄存器 vs 栈和地址长度上我们先以更常见的32位为例。接着用checksec检查保护checksec --fileret2text你可能会看到类似下面的输出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()或者直接就是一个用了不安全函数gets或scanf的循环。双击进入这个危险函数漏洞一目了然。例如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]返回地址在[ebp0x4]。那么从s的起始位置到返回地址的偏移就是0x70 (s到ebp的距离) 0x4 (ebp本身的4字节) 0x74即十进制116字节。这是32位下的情况。64位下rbp是8字节返回地址也是8字节计算方式类似但要注意对齐。方法二动态调试验证推荐静态计算可能因为编译器优化、对齐等因素有细微出入动态调试更可靠。用gdb打开程序gdb ./ret2text在危险函数如gets调用之后、函数返回ret之前的位置下断点。运行程序发送一串有规律的非重复字符作为输入。我最喜欢用pwntools的cyclic工具生成。 在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中按下ShiftF12打开字符串窗口搜索“/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(oslinux, archi386, log_leveldebug) # 设置上下文32位 # context(oslinux, archamd64, log_leveldebug) # 如果是64位 p process(./ret2text) # 本地运行 # p remote(challenge.ctfhub.com, 10000) # 远程连接 offset 116 # 计算出的偏移量 backdoor_addr 0x8048586 # 后门函数地址 payload bA * offset p32(backdoor_addr) # 32位用p32打包地址 # payload bA * offset p64(backdoor_addr) # 64位用p64 p.sendlineafter(bsomething:, 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 bA*offset p64(pop_rdi_ret) p64(bin_sh_addr) p64(ret_gadget) p64(system_plt)。你需要用ROPgadget或ropper工具在二进制文件中搜索ret。2. 输入处理与截断如果程序使用gets那万事大吉它读到换行符\n0x0a停止。但如果程序使用scanf(“%s”)它会在空格或换行处停止。而read函数则严格读取指定字节数。如果你的payload里不小心包含了0x0a或0x20空格可能会被提前截断。确保你发送的地址字节里不包含这些敏感字节。如果不幸包含了可以考虑调整溢出点或者寻找另一个不包含坏字节的等价地址比如后门函数内部偏移几个字节。3. 地址中的空字节Null Byte在32位系统中地址如0x08048586最高位是0x08不是零。但如果地址是0x8040000打包成p32后是\x00\x00\x04\x80小端序开头就是空字节。strcpy、gets等函数遇到空字节会认为字符串结束导致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 --fileret2text 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);。栈结构buf在ebp-0x70返回地址在ebp4。偏移 0x70 4 0x74 116。 在字符串窗口找到/bin/sh交叉引用找到函数shell地址0x8048586。步骤3动态验证偏移编写一个简单的测试脚本test_offset.pyfrom pwn import * context(log_leveldebug) p process(./ret2text) payload cyclic(200) p.sendlineafter(binput:, 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(oslinux, archi386) # p process(./ret2text) p remote(xxx.xxx.xxx.xxx, 10000) # 替换为实际题目地址 offset 116 shell_addr 0x8048586 payload bA * offset p32(shell_addr) p.sendlineafter(binput:, payload) p.interactive()步骤5运行与获取flag运行脚本如果一切正常你会看到一个$或者#提示符输入cat flag或ls; cat flag等命令即可拿到flag。7. 常见问题排查与解决思路即使按照步骤来也可能会遇到问题。这里记录几个我踩过的坑和解决方法问题1Segmentation fault (core dumped)但偏移计算应该没错。可能原因1栈平衡破坏。后门函数开头有push ebp; mov ebp, esp结尾有leave; ret。leave指令相当于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(binput:, payload)中的binput:必须与程序打印的提示符完全一致包括空格和换行。最好用p.recvuntil(binput: )来接收然后用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()或sleep8. 从ret2text到更广阔的漏洞利用掌握了基础的ret2text你其实已经拿到了PWN世界的入场券。它教会了你几个最核心的概念控制流劫持通过溢出覆盖返回地址可以指挥CPU去执行任何你想要的代码在内存可执行的前提下。地址计算精确计算偏移是成功利用的前提。工具链使用checksec、IDA、gdb、pwntools这一套组合拳。在此基础上你可以自然过渡到其他更复杂的技巧Ret2shellcode如果程序关闭了NX栈可执行你可以把一段机器码shellcode放在栈上然后让返回地址跳转到栈上执行它。Ret2libc当程序没有现成后门但可以泄露libc函数地址时通过计算偏移调用libc中的system函数。ROPReturn-Oriented Programming当NX开启且没有直接可用的后门时通过串联程序本身代码段中的一个个以ret结尾的小片段gadget像搭积木一样完成复杂的操作如给函数传参、调用系统调用等。CTFHub技能树里ret2text之后的题目比如ret2shellcode、ret2libc、rop都是沿着这条路逐步深入的。理解每一步的原理比单纯抄写exp重要得多。我个人的习惯是每做一道题不仅要写出能打通的exp还要画一画栈布局的变化图理清楚每一步执行后esp、ebp、eip寄存器以及栈上数据是怎么变化的。这个过程很慢但积累下来你对程序运行的理解会深刻很多。最后一个小技巧分享在编写pwntools脚本时善用context.log_level ‘debug’。它会打印出所有发送和接收的数据对于调试交互过程非常有用。当然正式脚本记得关掉不然输出太乱。另外对于需要多次尝试的题目可以把偏移计算、地址查找等步骤也写成脚本的一部分实现半自动化提高效率。
返回列表