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.
- 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:
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
Aas typeB(a badstatic_cast, a union mismatch, a deserialization bug).B's virtual call reads avptrslot that, inA, 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):
|
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):
segfault{this_is_in_rdi}
the defences, and why this is still everywhere#
- CFI (Control-Flow Integrity). Clang's
-fsanitize=cfi-vcallverifies, 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.