HEVD type confusion — walkthrough
TL;DR: HEVD’s TypeConfusion IOCTL reads a polymorphic object whose
Typefield is not validated before the driver casts to a richer struct. Attacker setsTypeso the driver follows a code path expectingCallback-bearing struct when memory was sized only for the simpler struct → out-of-bounds read / dereference of attacker-controlled bytes as a function pointer. Companion to type-confusion-kernel and hevd-uaf-walkthrough.
The bug
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
typedef struct _USER_TYPE_CONFUSION_OBJECT {
ULONG ObjectID;
ULONG ObjectType;
} USER_TYPE_CONFUSION_OBJECT, *PUSER_TYPE_CONFUSION_OBJECT;
typedef struct _KERNEL_TYPE_CONFUSION_OBJECT {
ULONG ObjectID;
union {
ULONG ObjectType;
FunctionPointer Callback; // ← union: 8 bytes here vs 4 in user struct
};
} KERNEL_TYPE_CONFUSION_OBJECT, *PKERNEL_TYPE_CONFUSION_OBJECT;
NTSTATUS TriggerTypeConfusion(PUSER_TYPE_CONFUSION_OBJECT UserBuf) {
PKERNEL_TYPE_CONFUSION_OBJECT Kernel = ExAllocatePoolWithTag(NonPagedPool,
sizeof(KERNEL_TYPE_CONFUSION_OBJECT), 'kcaH');
Kernel->ObjectID = UserBuf->ObjectID;
Kernel->ObjectType = UserBuf->ObjectType; // ← copies 4 bytes into 8-byte union slot
TypeConfusionObjectInitializer(Kernel);
return STATUS_SUCCESS;
}
void TypeConfusionObjectInitializer(PKERNEL_TYPE_CONFUSION_OBJECT Kernel) {
Kernel->Callback(); // ← BUG: Callback contains 4 bytes attacker + 4 bytes stale stack
}
The user struct is 8 bytes (two ULONGs); the kernel struct is 16 bytes (ULONG + union of {ULONG, 8-byte fnptr}). The kernel copies the user’s ObjectType (4 bytes) into the union — but the high 4 bytes of Callback are whatever was on the kernel pool just before allocation.
When Callback() is invoked, RIP = (stale upper 32 bits << 32) | (user-controlled lower 32 bits).
Step 1 — control the upper 32 bits
If you can leak the contents of the upper bits or arrange for them to be predictable through prior pool allocations, you control the full 64-bit pointer.
Strategy: pool grooming. Spray objects whose layout has known upper-32-bit values at the same offset where the Callback field lands. After grooming, allocate the bug → adjacent slot has predictable upper bytes.
Common groom: spray named-pipe attribute buffers with known content; free every other; the bug allocation lands in a hole where the high bits are known.
Step 2 — userland trigger
1
2
3
4
5
6
struct USER_OBJ {
ULONG ObjectID;
ULONG ObjectType;
} obj = { 0x1234, (ULONG)((ULONG_PTR)shellcode & 0xFFFFFFFF) };
DeviceIoControl(h, IOCTL_TYPE_CONFUSION, &obj, sizeof(obj), NULL, 0, &ret, NULL);
If pool was groomed with high bits matching your target shellcode’s high bits, you get RIP → shellcode.
Step 3 — alternative — use a kernel address with known upper bits
The kernel base is randomised but within a known range. Upper 32 bits of kernel module addresses cluster.
If a useful gadget exists at 0xFFFFF80?_XXXXXXXX and you can groom the pool so the high bits of Callback come out as 0xFFFFF80?, you control RIP to that gadget.
0xFFFFF80? is the typical ntoskrnl base in modern Win10/11.
Step 4 — shellcode and return
Same as hevd-stack-overflow-walkthrough: token-stealing shellcode, swapgs; iretq restore.
What type confusion teaches
Real-world type confusion bugs (browser engines, OS kernels) are about the invariant the code expects vs. what attacker provides. The mitigation pattern: validate the dynamic type before each cast.
In Chakra / V8, Type Inference and Hidden Classes are a huge attack surface — attacker confuses the JIT compiler into emitting code that expects type A while the live value is type B. See browser-exploitation-primer.
In Windows kernel, IOCTL handlers that take polymorphic union types are frequent sources. The defence: explicit Type field check before dispatch.
Source-audit angle
1
2
grep -rnE 'union {|typedef.*union' .
grep -rnB2 'reinterpret_cast\|static_cast' .
For each union or cast: trace through the IOCTL path; confirm a type check precedes the cast.
Common failures
| Symptom | Cause |
|---|---|
RIP = 0x????????00XXXXXX | high bits not what you expected; check pool grooming |
BSOD BAD_POOL_CALLER | grooming destabilised pool state |
| Bug works once, fails on rerun | high-bit randomness; need more reliable groom |
Real-world type-confusion case studies
- CVE-2018-8120 (Win32k SetImeInfoEx) — type confusion in win32k.sys, full LPE.
- CVE-2020-1054 (Win32k DrawIconEx) — similar shape.
- Chrome V8 array element kind confusion — recurring class of JIT bugs.
Practice ladder
- HEVD TypeConfusion with controlled high bits.
- HEVD TypeConfusion without helper; full pool groom.
- Win32k LPE (older builds).