Understanding Use-After-Free (UAF) in Windows Kernel Drivers

In this blog post, we’ll explore use-after-free (UAF) vulnerabilities in Windows kernel drivers. We will start by developing a custom vulnerable driver and analyzing how UAF occurs. Additionally, we will explain double free vulnerabilities, their implications, and how they can lead to system crashes or privilege escalation. Finally, we’ll develop a proof-of-concept (PoC) exploit to demonstrate the impact of these vulnerabilities, including triggering a blue screen of death (BSOD).

What is Use-After-Free?

A use-after-free (UAF) vulnerability occurs when a program continues to use a pointer after the associated memory has been freed. This can lead to memory corruption, arbitrary code execution, or system crashes.

Common APIs That Allocate and Free Memory in Windows Kernel Drivers

In Windows kernel development, memory allocation and deallocation are crucial operations. Improper management of allocated memory can lead to use-after-free (UAF) vulnerabilities, resulting in arbitrary code execution, privilege escalation, and system crashes (BSODs).

This section explores various allocation and deallocation functions in Windows kernel drivers, their correct usage, and potential security risks.

1. Use-After-Free Classic Pool-Based

Windows kernel provides paged and non-paged memory pools for allocation. In the case of classic pool-based UAF, the Windows kernel driver allocates memory using ExAllocatePoolWithTag(), deallocates it with ExFreePoolWithTag(), and then mistakenly accesses it. This results in a crash (BSOD) due to accessing invalid memory. Such vulnerabilities are critical, as they can be exploited to execute arbitrary code, escalate privileges, or corrupt kernel memory.

In this example, we implement a custom kernel driver that allocates a pool of memory, frees it, and then intentionally accesses it, triggering a BSOD.

Memory Allocation

The kernel driver uses ExAllocatePoolWithTag() to allocate memory for storing data (in this case, wrenchData). This memory is part of the non-paged pool, meaning it remains in physical memory and isn’t swapped out.

Memory Deallocation

The memory is then freed using ExFreePool(wrenchData). However, the pointer wrenchData still holds the address of the now-freed memory. The problem arises because the pointer is not nullified or reset after freeing the memory.

Use-After-Free

Use-after-free happens when the freed memory is accessed again, as demonstrated by the code RtlCopyMemory(wrenchData->data, L"WKL UAF Attack!", sizeof(L"WKL UAF Attack!")). The kernel tries to copy data into the freed memory, which leads to unpredictable behavior. This memory is no longer valid and accessing it may cause system instability or crashes.

Overwriting the Pointer

The pointer wrenchData is then deliberately set to an invalid address (0x500). This step is crucial because it could lead to further exploitation if this invalid memory location is accessed in the future, causing a crash (BSOD) or other unintended behavior.

Simple BSOD with UAF in a Custom Driver

For now, I’ll take a simple UAF scenario and demonstrate how it can cause a BSOD using IOCTL. This is not full exploit development—just a basic crash to illustrate a use-after-free. We’ll dive deeper into exploitation techniques in future blog posts.

This PoC demonstrates a use-after-free (UAF) vulnerability in a kernel driver. It opens the vulnerable device and sends an IOCTL command (IOCTL_TEST_CODE) that triggers the UAF. The driver attempts to access memory (wrenchData) that has already been freed, leading to invalid memory access, which could cause memory corruption, system instability, or a BSOD. In future posts, we’ll explore how to turn this into a fully working exploit.

The crash occurs when the driver attempts to access freed memory, specifically in the ExFreeHeapPool function. The invalid memory access happens due to a use-after-free (UAF) condition, where a pointer to freed memory is still being dereferenced (mov rbx, qword ptr [rax+10h]). This results in accessing invalid or corrupted memory, leading to a system crash or potential memory corruption, as seen in the stack trace.

2. Use-After-Free in IRP-Based Memory Management

The IRP-based memory management involves several key APIs, such as IoAllocateIrp, which allocates an IRP for processing I/O requests, and IoFreeIrp, which frees the IRP when it’s no longer needed. Additionally, IoCallDriver is used to send the IRP to another driver for further processing, while IoCompleteRequest signals the completion of the request.

In our custom driver, we allocate memory for an IRP using IoAllocateIrp and process the request. However, after completing the request, we mistakenly free the IRP using IoFreeIrp but later attempt to access or modify the buffer that was passed with the IRP. This can lead to a use-after-free vulnerability, as the memory is no longer valid after being freed.

In this code, the driver processes an IOCTL request and allocates memory for the IRP buffer (IRP_BUFFER) located in the system buffer of the IRP. It then copies the string “IRP Data” into the buffer->data. After the IRP is processed, it is freed using IoFreeIrp with the line IoFreeIrp(Irp);. However, the driver proceeds to access the buffer->data after the IRP is freed, which leads to a use-after-free (UAF) vulnerability. Accessing the memory of buffer->data after it has been deallocated results in undefined behavior, such as crashes or potential security exploits.

Simple BSOD with UAF in a Custom Driver

The exploit will follow the same pattern as previously explained. Let’s now examine the issue using WinDbg, as shown below.

The crash appears to be related to a use-after-free (UAF) vulnerability. Specifically, the faulting address ffff860d71dfb9f0 seems to indicate that the IRP (I/O Request Packet) was freed, but the driver or process continued to access the freed memory. The IoFreeIrp call in the kernel driver appears to have been followed by an attempt to access the freed IRP buffer (located at ffff860d71dfb9f0), which caused the system to trigger a bug check (error code 1232).

The stack trace points to the IOCTL handler in the kernel driver (KernelPool!IOCTL+0x90), which is where the memory access occurred after the IRP was freed.

3. Use-After-Free via ObDereferenceObject()

The Windows kernel manages objects like FILE_OBJECT, DEVICE_OBJECT, and ETHREAD using reference counting. When an object is created or accessed, its reference count increases, and when it’s no longer needed, the reference count decreases. The function responsible for this is ObDereferenceObject().

If an object is freed while another part of the system still holds a reference, accessing it afterward causes a use-after-free (UAF) condition, leading to BSOD or potential exploitation.

Mimicking CVE-2018-8120: Use-After-Free in Win32k.sys

This custom kernel driver mimics CVE-2018-8120, a use-after-free (UAF) vulnerability in Win32k.sys, where an improperly managed object is freed but later accessed, leading to BSOD or potential privilege escalation.

This bug is a Use-after-free (UAF) vulnerability caused by dereferencing a FILE_OBJECT using ObDereferenceObject(pFile), which frees the object. However, the driver continues accessing pFile->FsContext after freeing it, leading to a BSOD when the memory is accessed. This mimics CVE-2018-8120, where freed objects were improperly used, causing crashes or potential exploitation.

Simple BSOD with UAF in a Custom Driver

The exploit will follow the same pattern as previously explained. Let’s now examine the issue using WinDbg, as shown below.

The crash occurs because IoGetDeviceObjectPointer returns a valid FILE_OBJECT, but after calling ObDereferenceObject(pFile), the object is freed. However, the driver still accesses pFile->FsContext, leading to a use-after-free (UAF) bug. The instruction mov qword ptr [rdi],rax tries to write to a NULL or freed pointer (rdi=0000000000000000), causing a BSOD when accessing invalid memory inside IoGetDeviceObjectPointer.

Use-After-Free Mitigation

To prevent use-after-free bugs, you should immediately nullify the pointer after freeing memory. This makes accidental reuse safer, since accessing a NULL pointer will result in a clear crash (or avoided entirely with checks). For larger systems, managing ownership with reference counting or smart wrappers is recommended.

Bonus Tip: Spotting UAF in Windows Drivers

To identify use-after-free and double-free (see our next blog for details on that!) vulnerabilities, start by looking for deallocation functions. In user-mode, watch for free, delete, Global Free, or Release. In Windows kernel drivers, key functions include ExFreePoolWithTag, IoFreeMdl, ObDereferenceObject, MmFreeContiguousMemory, and RtlFreeHeap. Many of these calls are wrapped inside internal cleanup functions or callbacks (like CDownloadBuffer::Release or FreeHandle), which can obscure the actual free. Always trace pointer lifecycle: if it’s freed and still accessed or freed again, that’s a bug. Check if the pointer is nullified or checked post-free—if not, it might be reused unsafely.

Reference

  1. https://cwe.mitre.org/data/definitions/416.html