Logo redaa.dev
Hijacking Legitimate Windows Kernel-Mode Drivers

Hijacking Legitimate Windows Kernel-Mode Drivers

September 8, 2024
12 min read
Table of Contents

Abstract

This post details a Proof of Concept (PoC) project I originally developed back in 2021. It represents one of my earliest explorations into the complex worlds of Windows Internals and kernel development.

The only objective of this project was my own education and technical curiosity. The techniques discussed (such as bypassing memory protections and hooking system drivers) are documented here strictly for academic and research purposes. I do not, in any way, encourage or condone the use of these methods to compromise software integrity or for any malicious activity. This is simply a technical look back at a foundational learning project.

1. Introduction: My Journey into the Windows Kernel

Since my first steps in programming, I’ve always been fascinated by the “magic” that happens under the hood. How does the operating system really work? How do software and hardware interact? This curiosity led me down a path that, inevitably, ended in the Windows Kernel.

Today, I want to share what was one of my most challenging and educational personal projects back when I started learning about Windows Internals: a kernel-mode driver designed to intercept a specific function within a legitimate Microsoft driver (win32kbase.sys) using an advanced technique known as Data Pointer Hooking. This project, developed for purely educational purposes, was a deep dive into Windows Internals.

2. The Kernel World: User-Mode vs. Kernel-Mode

To understand this project, we must first grasp the fundamental duality of Windows:

  • User-Mode: This is where all our everyday applications live (browsers, text editors, games). It’s a safe, isolated environment. If an application crashes, the OS can terminate it without compromising global stability.
  • Kernel-Mode: This is the core of the operating system. It’s where critical components like the memory manager, process scheduler, and—crucially—drivers reside. A driver is software that allows the kernel to interact with hardware (graphics card, keyboard, network) and with critical system functions. If something goes wrong here, get ready for a Blue Screen of Death (BSOD)!

2.1. Tracing the Call Flow to Our Hook

So, how does a simple click in a User-Mode application get handled by a driver we want to hook? It follows a precise path designed to safely cross that User-Mode/Kernel-Mode boundary.

Let’s trace the journey:

  1. User-Mode Application: Your code calls a function from a system API, like the Win32 API (e.g., in user32.dll or gdi32.dll).
  2. System DLL Wrapper: These high-level functions often call lower-level functions exported by DLLs like win32u.dll or ntdll.dll. The function you’re calling, which is listed in the DLL’s Export Address Table (EAT), is frequently just a small “stub” or wrapper.
  3. The syscall: This wrapper’s main job is to prepare the arguments (placing them in the correct registers) and then execute a syscall instruction. This instruction is the magic gate—it intentionally triggers a transition from the unprivileged User-Mode (Ring 3) to the privileged Kernel-Mode (Ring 0).
  4. Kernel-Mode Dispatcher: The CPU hands control to the kernel’s central syscall dispatcher. This dispatcher checks the syscall number (an ID for the requested function) and routes the request to the correct kernel module to handle it.
  5. Target Driver (win32kbase.sys): For many GUI and graphics-related syscalls, the handler is win32kbase.sys. The kernel passes the request to this driver.
  6. The Hooked Pointer: This is where our project comes in. As win32kbase.sys processes the request, it will, at some point, call one of its internal helper functions by referencing a function pointer stored in its .data section.

Because we’ve already hijacked that pointer, the driver thinks it’s calling its own legitimate function, but it’s actually calling our hook handler, giving us control of the execution flow.

Image showing the function calling flow

3. The Role of win32kbase.sys

Choosing win32kbase.sys as a target was no accident. This driver is an essential part of the Windows graphics subsystem. It handles critical functions related to the Graphical User Interface (GUI), such as window management, rendering, user input, and more.

It’s an interesting target because its functions are constantly invoked by User-Mode applications, making it an ideal point to intercept certain low-level operations.

4. Types of Hooking in Windows

The interception of functions is known as hooking. Several techniques exist, each with its pros and cons:

  • Inline Hooking (.text section): Directly modifies the binary code of a function (in the .text section) to jump to our code. It’s powerful but more complex to implement and potentially unstable.
  • IRP Hooking (Dispatch Tables): Intercepts the I/O Request Packets (IRPs) that drivers use to communicate. This is a more “official” way to intercept requests, but it only works for drivers that process IRPs.
  • SSDT (System Service Descriptor Table) Hooking: A more complex technique for intercepting system calls (syscalls).
  • Data Pointer Hooking (Our Approach): Our method, which focuses on modifying function pointers stored in a driver’s data sections.

5. The Technique: Data Pointer Hooking

My project centers on this last technique. Instead of modifying code (.text) or dispatch tables (IRP), we seek out a function pointer that the target driver (win32kbase.sys) uses internally and redirect it to our own function.

5.1. What is the .data section?

Every executable module (like a driver) is divided into memory sections. The .data section (or .rdata for read-only data) contains global variables, static data, and often, function pointers that the module itself uses. It’s like the driver’s internal address book.

5.2. Locating the Pointer

The first challenge is finding the exact pointer we want to hijack. This involves:

  1. Loading the target driver: Ensuring win32kbase.sys is loaded into memory.
  2. Scanning its sections: Iterating through the .data or .rdata sections of win32kbase.sys.
  3. Identifying the pattern: Searching for a pattern (signature scanning) that identifies the location of the desired function pointer. In a PoC project, this might be a hardcoded offset if the driver version is known.
src/hook.cpp
NTSTATUS HookCtx::Init( )
{
    // Retrieving the base address of the target driver
	HookCtx::driver_module = Utils::RetrieveModuleBase( xor_string( "\\SystemRoot\\System32\\win32kbase.sys" ) );
	
	if ( HookCtx::driver_module )
	{
        // Find the .data pointer by signature scanning
		HookCtx::data_pointer = Utils::FindPatternImage( ( PCHAR )HookCtx::driver_module, ( PCHAR )xor_string( "\x8B\x8C\x24\xA8\x00\x00\x00\x44\x8B\xCF" ), ( PCHAR )xor_string( "xxxx???xxx" ) );
 
		if ( HookCtx::data_pointer )
		{
			...
		}
	}
	...	
}
Finding the target function pointer

5.3. Bypassing Memory Protection (WP Bit)

As mentioned before, a crucial distinction in memory hooking is where you are writing. By convention, a module’s sections are marked with specific permissions:

  • .data section: Contains global and static writable variables. This section is marked as Read/Write.
  • .rdata section: Contains read-only data (constants, strings). This section is marked as Read-Only to prevent modification.
  • .text section: Contains the executable code. It is marked as Read/Execute.

The CPU enforces these permissions. Even in kernel-mode (Ring 0), the Write Protect (WP) bit in the CR0 register prevents the kernel from writing to pages marked as read-only.

In this PoC the target function pointer resides in the .data section of the legit driver, therefore we do not need to disable the Write Protect bit.

However, if your target pointer was in a read-only section (like .rdata), you would have to bypass this protection. This is achieved by manipulating the processor’s CR0 register, specifically the WP (Write Protect) bit. Disabling it temporarily allows us to write to read-only memory.

Manipulating the CR0 register is an extremely dangerous operation in kernel-mode and can lead to system instability if not handled with extreme care and for very short periods.

  // Disable interrupts to ensure atomicity
  _disable();
 
  // Read current CR0 register
  auto cr0 = __readcr0();
  const auto old_cr0 = cr0;
 
  // Clear the Write Protect bit (bit 16)
  cr0 &= ~(1UL << 16);
 
  // Write the modified value back to CR0
  __writecr0(cr0);
  
  // At this point, kernel write protection is disabled.
  // The write to a read-only section would happen here.
 
  // Restore the original CR0 value to re-enable protection
  __writecr0(old_cr0);
 
  // Re-enable interrupts
  _enable();
Clearing the Write Protect (WP) bit (bit 16) in the CR0 register.

5.4. The Atomic Swap

Once we have located the pointer, and disabled write protection if needed, the final step is the “swap.” We replace the original pointer with the address of our hook function. It’s crucial that this swap is atomic (indivisible) to prevent race conditions on multi-core systems.

src/hook.cpp
  // ...
  if ( HookCtx::data_pointer )
	{
		if ( xor_import(MmIsAddressValid)( HookCtx::data_pointer ) )
		{
            // Resolve the relative virtual address (RVA) of the pointer
			HookCtx::pointer_reference = ( UINT64 )( HookCtx::data_pointer ) - xor_number( 0xC );
			HookCtx::pointer_reference = ( UINT64 )HookCtx::pointer_reference + *( PINT )( ( PBYTE )HookCtx::pointer_reference + xor_number( 3 ) ) + xor_number( 7 );
 
            // Swap the target pointer
			*( PVOID* )&HookCtx::original_pointer = _InterlockedExchangePointer( ( PVOID* )HookCtx::pointer_reference, ( PVOID )HookCtx::hooked_pointer );
 
			return xor_number( STATUS_SUCCESS );
		}
	}
    // ...
Atomically swapping the pointer.

6. Design: A “Shellcode-like” Driver

A crucial detail of this project is that it is not a traditional WDM (Windows Driver Model) driver. Instead of being loaded by the Windows driver manager (NtLoadDriver), it is designed to be manually mapped into the kernel’s address space by an external component (e.g., a kernel manual mapping tool).

This implies several fundamental differences:

  • No classic DriverEntry: It lacks the standard DriverEntry function and does not receive a PDRIVER_OBJECT.
  • No DriverUnload: It does not handle its own unloading or resource cleanup natively. This responsibility falls to the component that maps it.
  • Position-Independent (PIC): Ideally, the code should be Position-Independent Code (PIC) to run from any memory address.

This design makes it more akin to a kernel “payload” or “shellcode,” which is a common technique in the security and anti-cheat fields.

7. Implementation: Key Concepts

Let’s imagine OriginalTargetFunction is the one whose pointer is in the .data section. Our goal is that when the legitimate driver calls it, it actually calls our hook handler (in this case HookCtx::hooked_pointer).

src/hook.cpp
INT64 __fastcall HookCtx::hooked_pointer( PVOID arg1, PVOID arg2, PVOID arg3, PVOID arg4, DWORD arg5, PVOID arg6, PVOID arg7, DWORD arg8 )
{
  // Ensure the caller is in User-Mode, if not, call the original function
  if ( Utils::_ExGetPreviousMode( ) != xor_number( UserMode ) )
    return HookCtx::original_pointer( arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8 );
 
  // Ensure the request comes from our user-mode module by checking the size of 
  // the packet and validating the SECRET_KEY, if not, call the original function
  Communication Request = { };
  if ( !Utils::ReadVirtualMemory( &Request, arg6, sizeof( Communication ) ) || Request.Reason != SECRET_KEY )
    return HookCtx::original_pointer( arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8 );
  
  // At this point we ensured the request comes from our user-mode module,
  // so we can safely execute the logic of the hook handler
 
  return 0;
}
Definition of our hook handler

8. Advanced Features & Evasion

While the project’s core is data pointer hooking, the driver includes several low-level development and evasion techniques common in security software (and malware):

  • Manual Import Resolution: Instead of relying on the standard Import Address Table (IAT), kernel functions (PsLookupProcessByProcessId, etc.) are resolved at runtime. This makes static analysis more difficult.
  • String Obfuscation: All strings are XOR-encrypted to prevent them from being easily detected by static analysis tools.
  • Manual CR3 Resolution: Capable of manually resolving a target process’s CR3 register (which points to its page table) by walking the PML4 structures, without using standard API attachments (KeStackAttachProcess) or EPROCESS lookups.
  • Anti-Stackwalking: Includes an evasion technique using Asynchronous Procedure Calls - APCs and manipulating the KTHREAD structure to make stack tracing by detection tools more difficult, as some of them rely on queueing a kernel-mode APC to a suspicious thread to safely walk its call stack.
  • Physical Memory Access: Contains functionality to read directly from physical memory.

9. Security & Ethical Considerations

It is crucial to emphasize that this project is purely educational and for research purposes. The techniques shown are powerful and can be used for malicious purposes if they fall into the wrong hands.

  • Kernel manipulation and bypassing protections (CR0 WP bit) are inherently risky.
  • Hooking kernel functions must be done with a deep understanding of the system to avoid BSODs and data corruption.

My goal with this type of project is to understand how these techniques work in order to, ultimately, design better defenses and security solutions.

10. Conclusion

This project was a turning point in my journey of learning Windows Internals. It forced me to think about C++ in a completely different way, to dive into low-level Microsoft documentation, and to debug in an environment where one error means a full system reboot.

It was a huge technical challenge, but the reward in knowledge was invaluable. I hope this detailed explanation helps you understand a little more about the fascinating (and dangerous) world of driver development and offensive/defensive kernel security.

11. Additional Resources

— Redaa.