Pwn rop
攻击原理
ROP 技术概述
ROP(Return-Oriented Programming,面向返回的编程) 是一种高级的代码复用攻击技术,由 ret2libc 技术发展而来。当程序开启了 NX(栈不可执行)保护时,无法在栈上执行 shellcode,ROP 技术通过利用程序中已有的代码片段(gadget)来构造攻击链。
ROP 的核心思想
ROP 的核心思想是利用程序中的现有代码片段,通过精心构造的返回地址链,让程序按照攻击者的意图执行一系列操作,最终实现攻击目标。
Gadget 的概念
Gadget 是 ROP 的基本单元,是一段以 ret 指令结尾的短代码序列。每个 gadget 完成一个简单的操作,多个 gadget 组合起来可以完成复杂的功能。
常见的 gadget 类型:
-
寄存器操作:
pop rax; ret、pop rdi; ret、mov rax, rdi; ret -
内存操作:
mov [rax], rdi; ret、pop rax; pop rdi; mov [rax], rdi; ret -
算术运算:
add rax, rdi; ret、sub rax, rdi; ret -
系统调用:
syscall; ret、int 0x80; ret
ROP 链的构造
ROP 链是一系列 gadget 地址的序列,每个地址指向一个 gadget。执行流程:
1 | 函数返回 → ret (pop rip) → 弹出第一个 gadget 地址 |
64位 vs 32位 ROP
64位系统(x86-64)
-
参数传递:前6个参数通过寄存器传递(rdi, rsi, rdx, rcx, r8, r9)
-
常用 gadget:
pop rdi; ret、pop rsi; ret、pop rdx; ret -
栈对齐:函数调用时要求栈16字节对齐
示例:调用 system("/bin/sh")
1 | payload = b'A' * offset |
32位系统(x86)
-
参数传递:所有参数通过栈传递
-
调用约定:参数从右到左压栈,调用后需要清理栈
-
常用 gadget:
pop eax; ret、pop ebx; ret等
示例:调用 system("/bin/sh")
1 | payload = b'A' * offset |
ROP 攻击的优势
-
绕过 NX 保护:不需要执行栈上的代码,只跳转到已有的代码段
-
图灵完备:理论上可以构造任意复杂的攻击链
-
难以检测:只使用程序自身的代码,不引入新的代码
ROP 的局限性
-
需要大量 gadget:复杂攻击需要很多 gadget,可能难以找到
-
地址泄露:如果开启 PIE,需要泄露地址
-
栈空间:需要足够的栈空间来放置 ROP 链
-
ASLR:如果开启 ASLR,需要泄露基地址
常见 ROP 攻击场景
-
ret2libc:调用 libc 中的函数(如 system)
-
ret2syscall:通过 syscall gadget 直接调用系统调用
-
mprotect + shellcode:修改内存权限后执行 shellcode
-
SROP(Sigreturn ROP):利用 sigreturn 系统调用恢复所有寄存器
例题
思路总结
ROP 攻击的核心是构造 gadget 链,通过程序中的现有代码片段实现攻击目标。构造 ROP 链需要理解调用约定、参数传递方式和栈清理机制。
攻击步骤
-
分析程序保护机制
1
checksec ./binary
- 确认开启了 NX 保护(需要使用 ROP)
- 检查 PIE 和 ASLR(可能需要泄露地址)
- 确认没有 canary 或可以泄露 canary
-
查找可用的 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']) -
确定目标函数和参数
- 确定要调用的函数(如 system、execve)
- 确定函数需要的参数
- 确定参数的获取方式(泄露、计算、构造)
-
构造 ROP 链
- 根据调用约定设置参数
- 调用目标函数
- 处理返回地址(如果需要继续执行)
-
发送 payload 并执行
64位 ROP 链构造
基本结构
1 | # 64位:参数通过寄存器传递 |
调用 system(“/bin/sh”)
1 | # 查找必要的 gadget |
调用 execve(“/bin/sh”, NULL, NULL)
1 | # 需要设置三个参数 |
32位 ROP 链构造
基本结构
1 | # 32位:参数通过栈传递 |
调用 system(“/bin/sh”)
1 | payload = b'A' * offset |
栈参数清理 Gadget
在 32 位系统中,某些函数调用约定(如 __cdecl)要求调用者清理栈参数。如果调用的函数需要清理栈,需要使用栈清理 gadget。
问题场景:
-
调用
printf、system等__cdecl约定的函数 -
这些函数不会自动清理栈上的参数
-
需要在函数返回后清理栈
解决方案:使用 add esp, n; ret 或 pop; pop; ...; ret gadget
1 | # 方法1:使用 add esp, n; ret |
查找栈清理 Gadget:
1 | # 查找 add esp, n; ret |
实际示例:调用 printf 后继续执行
1 | # 假设需要调用 printf("%s", str),然后继续执行 |
关键技巧
-
Gadget 查找策略
- 优先查找常用的 gadget(pop rdi, pop rsi, pop rdx)
- 如果找不到直接 gadget,可以组合多个简单 gadget
- 使用
retgadget 进行栈对齐(64位)
-
参数设置技巧
- 64位:按顺序设置 rdi, rsi, rdx, rcx, r8, r9
- 32位:从右到左压栈,注意栈清理
- 如果参数是 NULL,可以使用
pop rax; ret+xor rax, rax; ret
-
链式调用
1
2
3# 调用多个函数
payload += p64(pop_rdi) + p64(arg1) + p64(func1)
payload += p64(pop_rdi) + p64(arg2) + p64(func2) -
处理返回值
- 如果函数有返回值,可以使用
movgadget 保存 - 通常最后一步调用 system/execve,不需要处理返回值
- 如果函数有返回值,可以使用
-
栈对齐(64位)
- 在关键函数调用前添加
retgadget 确保栈对齐 - 计算栈指针位置,确保是16的倍数
- 在关键函数调用前添加
常见问题
-
找不到合适的 gadget:尝试组合多个简单 gadget,或使用其他函数
-
栈空间不足:使用 stack pivot 技术迁移栈
-
地址泄露:如果开启 PIE/ASLR,需要先泄露地址
-
32位栈清理:注意调用约定,使用正确的栈清理 gadget
-
参数构造:某些参数(如结构体)需要特殊构造
进阶技巧
-
SROP(Sigreturn ROP):利用 sigreturn 系统调用一次性设置所有寄存器
-
JOP(Jump-Oriented Programming):使用间接跳转而非返回
-
BROP(Blind ROP):在不知道程序内容的情况下进行 ROP 攻击
-
GOT 表劫持:通过 ROP 修改 GOT 表,改变函数行为
