从栈溢出到Shell:深入剖析jarvisoj_level2的ret2libc实战

张开发
2026/4/10 10:19:00 15 分钟阅读

分享文章

从栈溢出到Shell:深入剖析jarvisoj_level2的ret2libc实战
1. 初识jarvisoj_level2一个典型的栈溢出漏洞第一次看到jarvisoj_level2这个题目时我注意到它是一个32位的ELF可执行文件。用file命令查看文件类型用checksec检查保护机制发现NX保护是开启的这意味着我们不能直接在栈上执行shellcode。这种保护机制在现代系统中很常见所以理解如何绕过它非常重要。这个程序的主要漏洞在vulnerable_function函数中。虽然它使用了相对安全的read函数而非危险的gets但读取长度设置得过大0x100字节而缓冲区只有0x88字节这就造成了典型的栈溢出漏洞。我画了个简单的栈结构图来帮助理解------------------ | 返回地址 | - 我们要覆盖的目标 ------------------ | 保存的ebp | ------------------ | 局部变量区 | | (0x88字节) | ------------------通过IDA反编译可以看到程序虽然调用了system函数但参数都是无害的echo命令。这意味着我们需要自己构造system(/bin/sh)的调用。这就是ret2libc技术大显身手的地方了。2. ret2libc攻击原理深度解析2.1 什么是ret2libc攻击ret2libc全称是return to libc是一种利用程序中已有的库函数来实现攻击的技术。当程序开启了NX保护无法执行栈上的shellcode时这种技术特别有用。它的核心思想是通过栈溢出控制程序流跳转到libc中的函数如system执行。我第一次尝试ret2libc时最大的困惑是如何确定函数的地址。后来明白在动态链接的程序中函数的实际地址会在运行时由动态链接器填充到.got.plt表中。但有趣的是我们不需要知道libc中system的绝对地址只需要知道它在.plt表中的跳转地址即可。2.2 构造payload的关键要素一个完整的ret2libc payload需要包含以下几个部分填充数据覆盖从缓冲区开始到返回地址之间的空间system函数地址覆盖原来的返回地址伪造的返回地址system执行完后跳转的位置参数地址/bin/sh字符串的地址在32位系统中函数参数是通过栈传递的。所以payload的结构应该是[填充数据][system地址][fake_ret][参数地址]这模拟了正常函数调用的栈布局。我刚开始经常搞错参数的顺序后来记住一个技巧参数在返回地址之后就像正常函数调用时参数是在call指令之后压栈的一样。3. 动态链接机制与地址解析3.1 PLT和GOT表的作用理解动态链接机制对ret2libc攻击至关重要。PLTProcedure Linkage Table是程序链接表GOTGlobal Offset Table是全局偏移表。它们协同工作来实现延迟绑定机制。我第一次分析时用objdump查看.plt节发现它包含了很多跳转指令。比如08048320 systemplt: 8048320: ff 25 00 a0 04 08 jmp *0x804a000 8048326: 68 00 00 00 00 push $0x0 804832b: e9 e0 ff ff ff jmp 8048310 _init0x2c这个跳转会转到GOT表中存储的地址。第一次调用函数时GOT表会指向PLT中的解析代码由动态链接器填充真实地址之后调用就直接跳转到libc中的函数了。3.2 地址泄露与ASLR绕过如果系统开启了ASLR地址空间布局随机化libc的基地址会随机变化这时就需要先泄露某个函数的实际地址然后计算libc基址。不过在这个题目中我们很幸运程序中已经有/bin/sh字符串地址0x0804A024system的PLT地址固定0x08048320没有开启PIE所以这些地址都是确定的这使得我们不需要复杂的地址泄露步骤大大简化了攻击过程。4. 完整攻击过程实战4.1 确定偏移量首先需要确定从缓冲区开始到返回地址的偏移量。我通常使用两种方法静态分析通过IDA查看栈布局动态调试用gdb和cyclic模式在这个题目中IDA显示buf位于ebp-0x88加上4字节的保存ebp总共需要0x884140字节的填充数据。4.2 构造payload根据前面的分析我们需要以下关键地址systemplt: 0x08048320/bin/sh字符串: 0x0804A024返回地址我选择main函数的地址0x08048480这样system执行完后程序能正常退出使用pwntools构造payload非常方便from pwn import * padding bA*140 system_addr p32(0x08048320) fake_ret p32(0x08048480) binsh_addr p32(0x0804A024) payload padding system_addr fake_ret binsh_addr4.3 本地测试与远程攻击本地测试时我习惯先检查payload是否正确p process(./level2) gdb.attach(p, b *vulnerable_function0x35\nc) p.sendline(payload) p.interactive()确认本地能获取shell后就可以攻击远程服务器了p remote(node5.buuoj.cn, 29654) p.sendline(payload) p.interactive()成功执行后就能拿到flag了。记得在真实CTF比赛中获取shell后要查看当前目录下的文件通常flag就在其中。5. 常见问题与调试技巧5.1 段错误排查刚开始尝试时我经常遇到段错误。通过gdb调试发现几个常见原因偏移量计算错误使用cyclic生成测试字符串然后在崩溃时查看覆盖的返回地址栈对齐问题某些系统调用要求栈16字节对齐可以在跳转地址前加个ret指令调整地址无效确保所有使用的地址都是可读的5.2 增强攻击可靠性在实际环境中网络延迟等因素可能导致攻击不稳定。我通常会在payload前加一段nop指令作为landing zone使用多个备用的返回地址添加错误处理比如捕获异常后重试5.3 进阶技巧ROP链构造当简单的ret2libc不可行时可以考虑构造ROP链。这需要找到多个gadget按顺序执行。常用的工具是ROPgadgetROPgadget --binary level2 --ropchain虽然这个题目不需要ROP链但掌握这项技术对更复杂的漏洞利用很有帮助。6. 防御措施与安全编程理解了攻击原理后更能体会到安全编程的重要性。作为开发者应该永远不使用不安全的函数如gets使用安全的替代函数并正确检查长度开启所有安全保护NX, ASLR, PIE等使用现代的防护机制如stack canary在CTF中我们学习攻击技术是为了更好地防御。每次完成一个题目后我都会思考如何修复这个漏洞这让我对系统安全有了更全面的认识。7. 扩展思考64位系统的差异虽然这个题目是32位的但64位系统的ret2libc有些重要区别参数通过寄存器传递rdi, rsi, rdx等需要找到pop rdi; ret这样的gadget来设置参数栈对齐要求可能不同我建议在学习完32位的基础后尝试分析64位的题目这能帮助你全面理解不同架构下的漏洞利用技术。

更多文章