nc warmup.chal.example 1337— "I heard ASLR makes overflows impossible. Prove me wrong." One statically-stripped binary, no source.
Every pwn starts with the same question, so I answer it first: what's turned on?
- arch
- amd64-64-little
- libc
- 2.35 (Ubuntu glibc 2.35-0ubuntu3.4)
- category
- pwn · 200 pts
- NXon
- PIEon
- RELROpartial
- canaryoff
- ASLRon
checksec sets the route before I touch a disassembler. NX kills stack
shellcode, so the answer is ROP or ret2libc. Canary off means a straight
overflow reaches the return address — no leak needed there. But PIE + ASLR
mean I don't know where anything is, so stage one is a leak and stage two is the
shell.
the bug#
One function, and it's exactly as advertised:
read takes 0x200 bytes into a 64-byte buffer. The frame underneath it is the
whole challenge:

buf starts at rbp-0x40, so it's 64 bytes to the saved rbp, plus 8 for the
register itself — the saved return address sits at offset . Past
that, I'm writing the control flow.
- offset
- the distance from the start of a controllable buffer to the saved return
address. Get it wrong by 8 and you land in the saved
rbp, notrip.
stage one — leak libc#
NX and PIE are on, but the binary still imports puts, and puts prints a
string at a pointer I choose. So I call puts(puts@got) to print libc's runtime
address of puts, then return into vuln for a second go with known addresses.
|
The leak is the runtime puts; libc's base is that minus the symbol's file
offset:
PIE on the main binary would block the first ROP chain too — but this binary
leaks an elf.plt/elf.got pair that's only correct because the loader didn't
relocate a no-PIE .plt. Read the checksec line again: PIE: on is the
binary, but the gadgets here come from libc once it's based. Mind which object
each address belongs to.1
stage two — ret2libc#
Same overflow, now with a libc base. system("/bin/sh"), with the one alignment
footgun that eats more first-bloods than any mitigation:
movaps alignment
glibc's system runs movaps on a 16-byte-aligned stack. If rsp is off by
8 when you land, it SIGSEGVs inside libc and you'll swear the offset is
wrong. Burn one extra ret gadget to re-align before the call.
On the wire the second payload is just the chain past the padding — the return slot is the only part that matters, so it's the one I highlight when I dump it:
00000000 b0 13 40 00 00 00 00 00 37 12 41 00 00 00 00 00 |..@.....7.A.....| 00000010 2f 62 69 6e 2f 73 68 00 c0 45 a1 f7 7f 00 00 00 |/bin/sh..E......|
the shell, and the flag
The whole challenge is two sends: leak, then shell. ASLR didn't make the overflow impossible — it just made it two-stage.
-
47 bits of ASLR entropy on x86-64 sounds like a lot until you remember you don't brute it — you print one pointer and subtract. Entropy only helps the defender when there's no leak primitive at all. ↩