Windows Kernel Buffer Overflow

In this blog post, we will explore buffer overflows in Windows kernel drivers. We’ll begin with a brief discussion of user-to-kernel interaction via IOCTL (input/output control) requests, which often serve as an entry point for these vulnerabilities. Next, we’ll delve into how buffer overflows occur in kernel-mode code, examining different types such as stack overflow, heap overflow, memset overflow, memcpy overflow, and more. Finally, we’ll analyze real-world buffer overflow cases and demonstrate potential exploitation in vulnerable drivers.

Understanding IOCTL in Windows Kernel Drivers

When working with Windows kernel drivers, understanding communication between user-mode applications and kernel-mode drivers is crucial. One common way to achieve this is through IOCTL (input/output control). IOCTL allows user-mode applications to send commands and data to drivers using the DeviceIoControl() function.

In the kernel, these requests are received as I/O Request Packets (IRPs), specifically handled in the driver’s IRP_MJ_DEVICE_CONTROL function. The driver processes the IRP, performs the requested action, and optionally returns data to the user-mode application.

We won’t dive too deep into the details, but we’ll cover the basics of IOCTL and how it functions through a simple driver example. This diagram is sourced from MatteoMalvica.

Breaking Down IOCTL and IRP in Custom Driver

Define a Custom IOCTL

The line highlighted in red defines a custom IOCTL (input/output control) code using the CTL_CODE macro, which is used by both user-mode applications and kernel-mode drivers to communicate.

Handling IOCTL Requests (IRP_MJ_DEVICE_CONTROL)

In the driver, IOCTL requests are handled inside the IOCTL function, which is assigned to IRP_MJ_DEVICE_CONTROL.

Before calling DeviceIoControl(), a user-mode application must first obtain a handle to the driver using CreateFile(). This handle is necessary to communicate with the driver and ensures that the IOCTL request is sent to the correct device. The handle is passed to DeviceIoControl() with a code and buffer which is processed by the function specified by IRP_MJ_DEVICE_CONTROL (in this case, the IOCTL function).

Retrieving IRP Details

Inside the IOCTL function, the driver extracts details about the request using IoGetCurrentIrpStackLocation(Irp).

  • irpSp→Parameters.DeviceIoControl.IoControlCode: Stores the specific IOCTL command sent from user mode
  • Irp→AssociatedIrp.SystemBuffer: Points to the user-mode buffer containing data for the driver
  • irpSp→Parameters.DeviceIoControl.InputBufferLength: Specifies the size of the received data

The Irp->AssociatedIrp.SystemBuffer parameter is used to access the user-mode buffer because that’s where the I/O manager places the buffer passed in. Meanwhile, irpSp->Parameters.DeviceIoControl.InputBufferLength provides the size of the received data, ensuring we handle it correctly. The stack pointer irpSp (retrieved using IoGetCurrentIrpStackLocation(Irp)) gives access to request-specific parameters, keeping buffer handling separate from other IRP structures to prevent memory corruption.

Custom Function

The IOCTL function processes user-mode requests sent via DeviceIoControl(). It checks the IOCTL code, retrieves the user buffer, and prints the received message if data is available. Finally, it sets the status and completes the request.

Sending an IOCTL from User Mode to a Kernel Driver

This simple program communicates with a Windows kernel driver by issuing an IOCTL (input/output control) request. It begins by opening a handle to the driver (\\.\Hello) and then transmits data using DeviceIoControl with the IOCTL_PROC_DATA code. If the operation succeeds, the driver processes the input; otherwise, an error message is displayed. Finally, the program closes the device handle and terminates.

Running the User-Mode Application to Communicate with the Driver

In our previous blog post, we explored kernel debugging and how to load a custom driver. Now, it’s time to run the user-mode application we just created. Once everything is set up, execute the .exe file, and we should see the message appear in DebugView or WinDbg. I’ll try to demonstrate this using DebugView to show how the communication works between user mode and kernel mode.

Strange! As you can see in the image, the IOCTL code in user mode appears as 0x222000, but in kernel mode, it shows up as 0x800. This happens due to how CTL_CODE generates the full 32-bit IOCTL value. You can decode the IOCTL using OSR’s IOCTL Decoder tool: OSR Online IOCTL Decoder.

Buffer Overflow

A buffer overflow happens when more data is written to a buffer than it can hold, causing it to overflow into adjacent memory.

Example: Imagine a glass designed to hold 250ml of water. If you pour 500ml, the extra water spills over—just like excess data spilling into unintended memory areas, potentially causing crashes or security vulnerabilities.

Memory Allocation in Kernel Drivers and Buffer Overflow Risks

In kernel driver development, proper memory management is even more critical than in user mode as there is no exception handling. When memory operations are not handled carefully, they can lead to buffer overflows, causing severe security vulnerabilities such as kernel crashes, privilege escalation, and even arbitrary code execution.

For this article, I have developed a custom vulnerable driver to demonstrate how buffer overflows occur in kernel mode. Before diving into exploitation, let’s first explore the common memory allocation and manipulation functions used in Windows kernel drivers. Understanding these functions will help us identify how overflows happen and why they can be exploited.

Understanding Kernel Memory Allocation & Vulnerabilities

Memory allocation in kernel-mode drivers typically involves dynamically requesting memory from system pools or handling buffers passed from user-mode applications. Below are some common kernel memory allocation functions:

1. Heap-Based Buffer Overflow

Here, the driver allocates memory from the NonPagedPool and copies user-supplied data into it using RtlCopyMemory without checking the buffer size. If the input is too large, it overflows into adjacent memory, corrupting the kernel heap.

Example Vulnerability: Heap Overflow in Custom Driver

Impact: Memory is allocated using ExAllocatePoolWithTag(NonPagedPool, 128, 'WKL'), but RtlCopyMemory copies inputLength bytes without validation, leading to heap overflow if inputLength is greater than 128.

2. Stack-Based Buffer Overflow

Here, the driver copies data from a user-supplied buffer to a small stack buffer using RtlCopyMemory, without verifying whether the destination buffer is large enough. If the input size is too large, it overwrites stack memory, potentially leading to system crashes or arbitrary code execution.

Example Vulnerability: Stack Overflow in Custom Driver

Impact: A small stack buffer, stackBuffer[100], is used, and RtlCopyMemory copies user data without checking if inputLength exceeds 100 bytes, causing a stack overflow.

3. Overwriting Memory with Memset

Here, the driver fills a kernel buffer with a fixed value using memset, but it does not check the input size. If the input size exceeds the buffer, adjacent memory may be overwritten, potentially corrupting critical kernel structures.

Example Vulnerability: Memset Overflow in Custom Driver

Impact: memset(g_Buffer, 'A', inputLength) writes inputLength bytes, but if inputLength exceeds the buffer size of g_Buffer, it causes overflow.

4. Buffer Overflow via RtlFillMemory

Here, the driver uses RtlFillMemory to fill a kernel buffer with a specific value. However, it does not verify the input size, allowing a buffer overflow that can corrupt adjacent memory.

Example Vulnerability: RtlFillMemory Overflow in Custom Driver

Impact: Fills g_Buffer with a character (‘B’), but if inputLength is too large, it writes beyond the allocated buffer size, leading to overflow.

5. Buffer Overflow via Direct IO

Here, the driver copies data from a user-supplied buffer directly to kernel memory using memmove, without checking the input size. Since Type3InputBuffer is user-controlled, this can lead to memory corruption or arbitrary code execution.

Example Vulnerability: Direct_Io_overflow Overflow in Custom Driver

Impact: The driver retrieves a user-mode buffer (Type3InputBuffer) and copies it into g_Buffer without checking inputLength, leading to a possible buffer overflow.

6. Buffer Overflow via Memcpy

Here, the driver uses memcpy to copy data from user space to a kernel buffer, without verifying whether the destination buffer is large enough. Since memcpy does not allow overlapping memory regions, it can still lead to buffer overflows and memory corruption.

Example Vulnerability: Memcpy Overflow in Custom Driver

Impact: Memcpy directly copies memory without boundary checks, causing buffer overflows when inputLength is larger than g_Buffer.

7. Buffer Overflow via Memmove

Here, the driver copies user-supplied data to a kernel buffer using memmove without verifying if the destination buffer is large enough. Since memmove allows overlapping regions, this can result in memory corruption that is difficult to predict.

Example Vulnerability: Memmove Overflow in Custom Driver

Impact: This copies inputLength bytes from userBuffer to g_Buffer using memmove. If inputLength is too large, it could lead to a possible buffer overflow.

Simple BSOD with Buffer Overflow in a Custom Driver

In my custom driver, I’ve implemented different vulnerabilities using various IOCTL cases.

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

This PoC demonstrates a heap-based buffer overflow in a vulnerable kernel driver. It opens the WKLbof device and sends a 512-byte buffer to an IOCTL handler that only expects 128 bytes. Due to missing input validation, the excess data overflows the allocated heap buffer, potentially causing memory corruption and a BSOD. In future posts, we’ll explore how to turn this into a fully working exploit.

The crash occurs due to a heap-based buffer overflow in our PoC, where a 512-byte user buffer overwrites a 128-byte non-paged pool allocation in the driver. The overflow corrupts adjacent memory, leading to an invalid memory dereference at RtlpHpVsContextAllocateInternal, causing a BSOD.

The pointer in RAX and RCX is invalid because it contains user-controlled data (0x4141414141414141), which is not a valid kernel memory address. When RtlpHpVsContextAllocateInternal attempts to access [RCX] (that is, the data that RCX points to), it tries to read from an unmapped or non-executable memory region, causing an invalid memory to dereference. Since kernel mode cannot handle such invalid accesses, this triggers a bug check (BSOD) due to an access violation. I won’t go into full details here—just a quick glance at how our overflow corrupts execution flow.

Exploring Real-World Kernel Vulnerabilities

In our previous discussions, we explored basic kernel buffer overflow scenarios using IOCTL handlers. Now, let’s dive into a real-world case: CVE-2024-30085, a heap-based buffer overflow vulnerability in cldflt.sys—the Windows Cloud Files Mini Filter Driver.

Understanding CVE-2024-30085

In cldflt.sys, a heap-based buffer overflow occurs due to an unchecked memcpy() operation. The driver allocates a 0x1000-byte buffer in the paged pool using ExAllocatePoolWithTag() but then copies v8 bytes from local_0x70 without validating the size.

The function allocates memory using ExAllocatePoolWithTag(PagedPool, 0x1000, 'HsBm'). The variable local_0x70 points to user-controlled data, while v8 determines the number of bytes copied. If v8 exceeds 0x1000, a heap overflow occurs, potentially corrupting adjacent memory and leading to system instability or exploitation.

I won’t go into stack tracing or deep debugging here—just highlight the core issue: an unsafe memory copy due to improper size validation in a kernel driver, leading to potential crashes or privilege escalation. But if you want to deep dive into root cause analysis, I’ve dropped a link in the references.

Understanding CVE-2023-31096

Now, let’s analyze a stack-based buffer overflow vulnerability in the LSI PCI-SV92EX Soft Modem Kernel Driver (AGRSM64.sys v2.2.100.0). This vulnerability, tracked as CVE-2023-31096, allows for Local Privilege Escalation (LPE) to SYSTEM by exploiting improper input validation in an IOCTL handler.

Specifically, we exploit a stack overflow via RtlCopyMemory() in the IOCTL handler at 0x1b2150 (IOCTL 0x1b2150). The function processes user-supplied input stored in a2, retrieves v72, and copies its value to a destination buffer. Since there are no proper bounds checking on v71, an attacker can trigger a vanilla stack-based buffer overflow, leading to privilege escalation.

Conclusion

In this post, we explored IOCTL handling in Windows kernel drivers and demonstrated how to trigger vulnerabilities through a custom driver. We also examined memory allocation mechanisms in the Windows kernel, highlighting how misconfigurations or lack of proper bounds checking can lead to memory corruption and buffer overflows. Finally, we analyzed two real-world Windows kernel buffer overflow vulnerabilities, reinforcing how these issues can pose serious security risks.

In the next post, we will dive into integer overflows in kernel drivers—how they occur, their impact, and how they can lead to security vulnerabilities. Stay tuned!

Reference

  1. https://ssd-disclosure.com/ssd-advisory-cldflt-heap-based-overflow-pe/