algoatson.github.io ~/posts
segfault@algoatson.github.io
back to index

BROP — return-oriented programming without the binary

2026-05-144 min readpwn rop

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.

targetremote forking server with a stack buffer overflow
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:

guesses256×(8 canary+8 saved RIP)

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_init pop sequencepop rbx; pop rbp; pop r12..r15; ret. You find it by signature: lay out [candidate][stop]x6[trap]. If candidate pops 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 +0x9 gives you a pop rdi; ret, at the right offset a pop rsi; ret.
probing for the pop-6 gadgettxt
1
2
3
4
stack:  [ CANDIDATE ][ STOP ][ STOP ][ STOP ][ STOP ][ STOP ][ STOP ][ TRAP ]
          if CANDIDATE == pop rbx;pop rbp;pop r12;pop r13;pop r14;pop r15;ret
          -> it consumes the 6 STOPs, rets into a STOP -> CONNECTION SURVIVES
          else -> falls into TRAP -> crash

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.

brop.py (sketch)python
1
2
3
4
5
6
7
8
canary, ret = stack_read(oracle)          # 1: byte-by-byte, defeats canary+ASLR
stop          = find_stop_gadget(oracle)  # 2: a "stays up" address
brop          = find_brop_gadget(oracle)  # 2: the __libc_csu_init pop-6
pop_rdi       = brop + 0x9                 #    overlapping decode -> pop rdi; ret
write_plt     = find_plt_write(oracle)     # 3: locate write@plt by probing
dump = rop(pop_rdi, sock, pop_rsi, text_base, pop_rdx, 0x1000, write_plt)
binary = recv_until_len(0x1000)            # the .text, leaked to ourselves
# -> from here it's ordinary ROP with a known binary: system / execve
from a blind overflow to a shell
txt
1
2
3
4
5
[*] canary  = 0x8f3a...00   (1792 probes)
[*] text    @ 0x4006xx      (saved RIP read)
[*] BROP gadget @ 0x40075a; write@plt @ 0x400560
[*] dumped 0x1000 bytes -> found a real pop rdi + system
segfault{i_never_had_the_binary}

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.