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). 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
Understanding Windows Kernel Pool Memory
This blog covers Windows pool memory from scratch, including memory types, debugging in WinDbg, and analyzing pool tags. We’ll also use a custom tool to enumerate pool tags effortlessly and explore the segment heap. This is the first post in our VR (Vulnerability Research) & XD (Exploit Development) series, laying the foundation for heap overflows, pool spraying, and advanced kernel exploitation. What is the Windows Kernel Pool? The Windows Kernel Pool is a memory region used by the Windows kernel and drivers to store system-critical structures. In short, the Kernel Pool is the kernel-land version of the user-mode “heap”. Unlike user-mode memory, the kernel pool is shared across all processes, meaning any corruption in the kernel pool can crash the entire system (BSOD). Pool Internals Essentially, chunks that are allocated and placed into use or kept free are housed on either a page that is pageable or a page that is non-pageable. It may be interesting to know that two types of page exist. One is paged pool and the other is non-paged pool: To sum up, in order to take advantage of a heap corruption vulnerability, such as a use-after-free (UAF), a researcher will make a distinction as to whether it is a UAF on the non-paged pool, or a UAF on the paged pool. This is important because the paged pool and non-paged pool are different heaps, meaning they are separate locations in memory. In simpler terms, in order to replace the freed chunk, one must trigger the use-after-free event. This means that there are different object structures that can be placed on the non-paged pool or, respectively, the paged pool. Setting Up Kernel Debugging To get started with kernel debugging, you need to set up a Windows VM and configure it using the following admin commands. Typically, this setup requires two machines: a debuggee system that is our target Windows machine and a debugger system that we will be issuing debug commands from. For basic debugging, you can use local kernel debugging (lkd) on a single system. If you haven’t installed it yet, you can download the Windows Debugging Tools from Microsoft’s official website. Now, on your base machine, start WinDbg and try to enter the port number and key. After that, restart the virtual machine. The following screenshot shows kernel debugging on the virtual machine. First, if we want to see basic view pool memory in kernel debugging, we can use the !vm 1 command in WinDbg. This provides a detailed summary of system memory usage, including information about paged pool and non-paged pool allocations. Here, 157 KB represents the current available memory in the system, while 628 KB shows the total committed memory, meaning memory that has been allocated and is in use. This helps in analyzing memory consumption and potential allocation issues in kernel debugging. If you want to explore further, you can use The !vm 2 command in WinDbg. This provides a more detailed breakdown of memory usage across different pool types and memory zones compared to !vm 1. First, Windows provides the API ExAllocatePoolWithTag, which is the primary API used for pool allocations in kernel mode. Drivers use this to allocate dynamic memory, similar to how malloc works in user mode. Note: While ExAllocatePoolWithTag has been deprecated in favor of ExAllocatePool2, it is still widely used in existing drivers so we will examine this function. Later, I will show in detail how to develop a kernel driver by using this API for ExAllocatePoolWithTag. Here’s a short explanation of the key parameters used in Windows pool memory allocation: There’s more than one kind of _POOL_TYPE. If you want to explore more, you can check out Microsoft’s documentation. We are only focusing on paged pool, non-paged pool, and pool tag. It is also worth mentioning that every chunk of memory on a pool has a dedicated pool header structure inline in front of the allocation, which we will examine shortly in WinDbg. Now let’s use the !pool <address> command in WinDbg to analyze a specific memory address. We want to display details about a pool allocation, including its PoolType, PoolTag, BlockSize, and owning process/module. As we can see in the screenshot above, the memory allocation is categorized as paged pool. The details also tell us that the page is ‘Allocated’ or free, and we can discover the pool tag and sometimes the details will also give the binary name, driver name, and other information. Feel free to explore. So, the question arises—how do we find the address of a pool allocation? It’s actually quite simple! If we check the documentation, we can see that ExAllocatePoolWithTag is a function provided by NtosKrnl.exe (the Windows kernel). This means we can set breakpoints in WinDbg to track memory allocations in real-time. So first let’s examine the API with a command called x /D nt!ExAlloca* in debugger and then set a breakpoint. Let’s set a breakpoint at that specific address and see if it gets triggered. As shown below, we’re using the bp <address> command. As soon as we resume our debugger with the g (Go) command, it will automatically hit the breakpoint and we can view the information gathered from register. In WinDbg, when analyzing a call to ExAllocatePoolWithTag, you can check the registers to understand the allocation request: By monitoring these values, you can determine how drivers allocate memory and track specific pool tags in the kernel. We will demonstrate another register rax, but first try to Step Out and use gu. Now, let’s use !pool <address>. But isn’t this strange? We were looking for the tag NDNB. Here’s a handy tip: to find more interesting data, use the command !pool @rax 2. What is a Pool Tag? A Pool Tag is a four-character identifier that helps track memory allocations in Windows kernel pools (PagedPool, NonPagedPool, etc.). Every time memory is allocated using APIs like ExAllocatePoolWithTag, a pool tag is assigned to identify the allocation’s origin. This is useful for debugging memory leaks, analysing kernel memory