Every other part of this series assumed you had the binary. BROP (Bittau et al., "Hacking Blind") is what you do when you don't — a remote target, a stack overflow, and nothing else. Two properties make it possible: the server forks a child per connection (so addresses and the canary are identical every time, and a child crash doesn't kill the parent), and a crash is observable (the connection drops). That's an oracle, and an oracle is enough.
- have
- no binary, no libc, ASLR + canary on
- oracle
- connection CRASHES vs STAYS UP after my overwrite
the oracle: crash or no crash#
Because the parent re-forks an identical child each time, you can overwrite the stack with a guess, observe whether that child crashes, and try again — the addresses never reshuffle. One bit per attempt, but a reliable bit, and that's the whole foundation.
step 1 — read the stack a byte at a time#
Overwrite a single byte of the saved canary with a guess and let the function return. Wrong guess → the canary check fails → crash. Right guess → it passes → no crash. The low byte is the null terminator, so:
recovers the canary and the saved return address — and that return address is a code pointer, so ASLR is now defeated for the text segment. Same byte-by-byte brute force as the canary on a fork server, extended to read whatever sits on the stack.
step 2 — find gadgets by probing#
Now you can redirect execution to any address; you just don't know what's there. Probe by setting the return address to a candidate and reading the oracle:
- A stop gadget is any address that keeps the connection alive when returned
to (it blocks, loops, or re-enters
accept). It's your "no crash" marker — the known-good you build every other test against. - The BROP gadget is the
__libc_csu_initpop sequence —pop rbx; pop rbp; pop r12..r15; ret. You find it by signature: lay out[candidate][stop]x6[trap]. Ifcandidatepops six values (landing on the stop gadgets) and rets cleanly, the connection survives; if it pops a different number, it hits the trap and crashes. That signature pinpoints the gadget — and entering it at+0x9gives you apop rdi; ret, at the right offset apop rsi; ret.
step 3 — dump the binary over the socket#
With pop rdi/pop rsi/pop rdx in hand, find a write (scan the PLT — it has a
regular stride) and call write(socket_fd, code_addr, length) to stream the
binary's .text back over your own connection. Now you have the binary; the blind
phase is over.
|
from a blind overflow to a shell
the conditions, and the lesson#
- Forking is the enabler. A server that re-forks identical children (classic
nginx/openssh-style pre-fork) gives stable addresses and survivable crashes. A server that re-execves (fresh ASLR each time) breaks stack reading. - The crash signal is everything. Connection reset, timeout, error — any distinguishable "it died" turns into the oracle. Remove the signal and BROP stalls.
- It collapses the usual prerequisites. No binary, no libc, ASLR + canary on — and yet a single overflow plus a fork server yields the binary and then a shell.
BROP is the ROP toolkit's proof that "we don't have the binary" is not the
defense it sounds like. Give an attacker a stack overflow, a fork server, and a way
to tell a crash from a hang, and they will read your secrets off the stack and
politely ask your own write() to mail them the executable.