Return-oriented programming: building a ROP chain
Once the stack is non-executable, the classic "jump to your shellcode" trick stops working. Return-oriented programming is the answer: instead of injecting code, you reuse code that is already in the binary, stitching together short fragments that each end in a return. This walkthrough builds a chain from first principles up to a call to execve.
Why ROP exists
Modern binaries ship with the no-execute (NX) bit set on the stack, so even if you overflow a buffer and land your bytes there, the CPU refuses to run them. ROP sidesteps the rule entirely. It never executes attacker-supplied code. It executes the program's own bytes, in an order the author never intended, by controlling the one thing the overflow already gives you: the return address, and everything above it on the stack.
A ROP chain is a program written in a language whose only instruction is "the address of some code that ends in ret." the mental model that makes gadgets click
Gadgets: the vocabulary
A gadget is a short sequence of instructions ending in ret. When the CPU returns, it pops the next address off the stack and jumps there, so a stack full of gadget addresses runs them back to back. Each ret advances to the next entry you placed. You find gadgets by scanning the binary:
# list useful register-loading gadgets $ ROPgadget --binary ./vuln | grep ': pop rdi' 0x0000000000401283 : pop rdi ; ret $ ROPgadget --binary ./vuln | grep ': pop rsi' 0x0000000000401281 : pop rsi ; pop r15 ; ret
On x86-64 the System V calling convention passes the first arguments in rdi, rsi, rdx. So a pop rdi ; ret gadget lets you load the first argument from the stack, then continue the chain. That single primitive is most of what you need to call a function with controlled arguments.
The plan: call execve("/bin/sh")
The goal is a shell. The cleanest route, when the binary or libc gives you the pieces, is to set up a syscall to execve with rdi pointing at the string "/bin/sh", the right syscall number in rax, and the argument and environment pointers null. The chain is: control the saved return address, then lay out gadget addresses and the values they pop.
# pwntools: build the chain after the overflow padding from pwn import * elf = ELF("./vuln") rop = ROP(elf) binsh = next(elf.search(b"/bin/sh\x00")) # or place it yourself rop.rax = 59 # execve syscall number rop.rdi = binsh # filename rop.rsi = 0 # argv rop.rdx = 0 # envp rop.raw(rop.find_gadget(["syscall"]).address) payload = b"A" * offset + rop.chain() p = process("./vuln") p.sendline(payload) p.interactive()
The offset is the number of bytes from the start of your input to the saved return address, which you find with a cyclic pattern and a crash. Everything after it is your chain, popped one ret at a time.
When the binary is small: ret2libc and leaks
Small challenge binaries rarely contain every gadget you want. The usual move is to call into libc, where the gadgets and useful functions live. But position-independent libc is randomised, so you first need a leak: use a chain that calls puts on a known GOT entry to print a libc address, compute the base, then send a second chain that calls system("/bin/sh") at the now-known address.
- Stage one leaks a libc pointer and returns to
mainso you can send again. - Stage two uses the resolved base to build the final call.
- Keep the two stages in one script so the leaked base never goes stale between connections.
The transferable habit
ROP is disciplined bookkeeping. Find the offset, enumerate your gadgets, and treat the stack as a sequence of small, verifiable steps. Test each stage in gdb and confirm a register holds what you expect before adding the next gadget. The same patience that recovers a leaked libc base is the patience that carves an implant out of a memory image, and that recognises an exploitable difference the way a blind SQL injection exposes one bit at a time.
Sources
- Shacham, H. "The Geometry of Innocent Flesh on the Bone: Return-into-libc without Function Calls" (ACM CCS 2007). Read the paper
- pwntools. "ROP" (documentation). Read the docs
- MITRE. "CWE-787: Out-of-bounds Write." Read the CWE entry