Understanding how user-mode applications communicate with kernel drivers is fundamental to Windows internals and security research. This post explores the architecture of kernel drivers and demonstrates how function hooking enables powerful user-to-kernel communication.
Windows Architecture Overview
Windows operates in two modes:
- User Mode (Ring 3) - Where applications run with limited privileges
- Kernel Mode (Ring 0) - Where the OS kernel and drivers run with full system access
Applications cannot directly access kernel memory or execute privileged instructions. They must use well-defined interfaces to communicate with kernel components.
Kernel Driver Basics
A Windows kernel driver is a special type of executable that runs in kernel mode. Here's a minimal driver structure:
#include <ntddk.h>
DRIVER_UNLOAD DriverUnload;
DRIVER_DISPATCH DriverCreate;
DRIVER_DISPATCH DriverClose;
DRIVER_DISPATCH DriverDeviceControl;
void DriverUnload(PDRIVER_OBJECT DriverObject) {
UNICODE_STRING symLink = RTL_CONSTANT_STRING(L"\\??\\MyDriver");
IoDeleteSymbolicLink(&symLink);
IoDeleteDevice(DriverObject->DeviceObject);
}
NTSTATUS DriverEntry(PDRIVER_OBJECT DriverObject, PUNICODE_STRING RegistryPath) {
UNREFERENCED_PARAMETER(RegistryPath);
UNICODE_STRING devName = RTL_CONSTANT_STRING(L"\\Device\\MyDriver");
UNICODE_STRING symLink = RTL_CONSTANT_STRING(L"\\??\\MyDriver");
PDEVICE_OBJECT DeviceObject;
NTSTATUS status = IoCreateDevice(
DriverObject,
0,
&devName,
FILE_DEVICE_UNKNOWN,
0,
FALSE,
&DeviceObject
);
if (!NT_SUCCESS(status)) {
return status;
}
IoCreateSymbolicLink(&symLink, &devName);
DriverObject->DriverUnload = DriverUnload;
DriverObject->MajorFunction[IRP_MJ_CREATE] = DriverCreate;
DriverObject->MajorFunction[IRP_MJ_CLOSE] = DriverClose;
DriverObject->MajorFunction[IRP_MJ_DEVICE_CONTROL] = DriverDeviceControl;
return STATUS_SUCCESS;
}
User-Mode to Kernel Communication
Method 1: DeviceIoControl
The standard way to communicate with a driver is through DeviceIoControl:
#include <Windows.h>
#include <iostream>
#define IOCTL_MY_OPERATION CTL_CODE(FILE_DEVICE_UNKNOWN, 0x800, METHOD_BUFFERED, FILE_ANY_ACCESS)
struct RequestData {
ULONG64 targetAddress;
ULONG64 value;
};
int main() {
HANDLE hDevice = CreateFileW(
L"\\\\.\\MyDriver",
GENERIC_READ | GENERIC_WRITE,
0,
nullptr,
OPEN_EXISTING,
FILE_ATTRIBUTE_NORMAL,
nullptr
);
if (hDevice == INVALID_HANDLE_VALUE) {
std::cerr << "Failed to open driver\n";
return 1;
}
RequestData request = { 0x12345678, 42 };
DWORD bytesReturned;
BOOL success = DeviceIoControl(
hDevice,
IOCTL_MY_OPERATION,
&request,
sizeof(request),
&request,
sizeof(request),
&bytesReturned,
nullptr
);
CloseHandle(hDevice);
return success ? 0 : 1;
}
Driver-Side Handler:
NTSTATUS DriverDeviceControl(PDEVICE_OBJECT DeviceObject, PIRP Irp) {
UNREFERENCED_PARAMETER(DeviceObject);
PIO_STACK_LOCATION stack = IoGetCurrentIrpStackLocation(Irp);
NTSTATUS status = STATUS_SUCCESS;
ULONG bytesTransferred = 0;
switch (stack->Parameters.DeviceIoControl.IoControlCode) {
case IOCTL_MY_OPERATION: {
RequestData* data = (RequestData*)Irp->AssociatedIrp.SystemBuffer;
data->value = PerformKernelOperation(data->targetAddress);
bytesTransferred = sizeof(RequestData);
break;
}
default:
status = STATUS_INVALID_DEVICE_REQUEST;
}
Irp->IoStatus.Status = status;
Irp->IoStatus.Information = bytesTransferred;
IoCompleteRequest(Irp, IO_NO_INCREMENT);
return status;
}
Function Hooking for Kernel Communication
Function hooking intercepts calls to Windows API functions, redirecting them through your code. This technique is powerful for both legitimate purposes (monitoring, debugging) and understanding system internals.
Hooking Architecture
User App calls NtReadFile()
|
v
ntdll.dll (User-mode stub)
|
v [syscall instruction]
Kernel: nt!NtReadFile
|
v [Your Hook]
Your Driver Code
|
v
Original Function
Implementing a Kernel Hook
Here's how to hook a kernel function using inline patching:
#include <ntddk.h>
typedef NTSTATUS (*NtReadFilePtr)(
HANDLE FileHandle,
HANDLE Event,
PIO_APC_ROUTINE ApcRoutine,
PVOID ApcContext,
PIO_STATUS_BLOCK IoStatusBlock,
PVOID Buffer,
ULONG Length,
PLARGE_INTEGER ByteOffset,
PULONG Key
);
NtReadFilePtr OriginalNtReadFile = nullptr;
UCHAR OriginalBytes[12];
NTSTATUS HookedNtReadFile(
HANDLE FileHandle,
HANDLE Event,
PIO_APC_ROUTINE ApcRoutine,
PVOID ApcContext,
PIO_STATUS_BLOCK IoStatusBlock,
PVOID Buffer,
ULONG Length,
PLARGE_INTEGER ByteOffset,
PULONG Key
) {
NTSTATUS status = OriginalNtReadFile(
FileHandle, Event, ApcRoutine, ApcContext,
IoStatusBlock, Buffer, Length, ByteOffset, Key
);
return status;
}
void InstallHook(PVOID TargetFunction, PVOID HookFunction) {
UCHAR jumpPatch[12] = {
0x48, 0xB8,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0xFF, 0xE0
};
*(PVOID*)(jumpPatch + 2) = HookFunction;
KIRQL oldIrql;
KeRaiseIrql(HIGH_LEVEL, &oldIrql);
CR0 cr0 = __readcr0();
cr0.WP = 0;
__writecr0(cr0.Value);
RtlCopyMemory(OriginalBytes, TargetFunction, sizeof(OriginalBytes));
RtlCopyMemory(TargetFunction, jumpPatch, sizeof(jumpPatch));
cr0.WP = 1;
__writecr0(cr0.Value);
KeLowerIrql(oldIrql);
}
User-Mode Hook Communication Pattern
A common pattern is to hook a rarely-used Windows function and use it as a communication channel:
Driver Side:
NTSTATUS HookedNtQueryIntervalProfile(
KPROFILE_SOURCE ProfileSource,
PULONG Interval
) {
if (ProfileSource == MAGIC_SIGNATURE) {
CommunicationData* data = (CommunicationData*)Interval;
switch (data->operation) {
case OP_READ_MEMORY:
return HandleReadMemory(data);
case OP_WRITE_MEMORY:
return HandleWriteMemory(data);
case OP_GET_MODULE_BASE:
return HandleGetModuleBase(data);
}
}
return OriginalNtQueryIntervalProfile(ProfileSource, Interval);
}
User-Mode Client:
#include <Windows.h>
#define MAGIC_SIGNATURE 0x1337
typedef NTSTATUS(NTAPI* NtQueryIntervalProfilePtr)(
ULONG ProfileSource,
PULONG Interval
);
class DriverCommunication {
NtQueryIntervalProfilePtr NtQueryIntervalProfile;
public:
DriverCommunication() {
HMODULE ntdll = GetModuleHandleW(L"ntdll.dll");
NtQueryIntervalProfile = (NtQueryIntervalProfilePtr)
GetProcAddress(ntdll, "NtQueryIntervalProfile");
}
bool ReadKernelMemory(ULONG64 address, PVOID buffer, SIZE_T size) {
CommunicationData data = {};
data.operation = OP_READ_MEMORY;
data.address = address;
data.buffer = buffer;
data.size = size;
NTSTATUS status = NtQueryIntervalProfile(
MAGIC_SIGNATURE,
(PULONG)&data
);
return NT_SUCCESS(status);
}
ULONG64 GetKernelModuleBase(const wchar_t* moduleName) {
CommunicationData data = {};
data.operation = OP_GET_MODULE_BASE;
wcscpy_s(data.moduleName, moduleName);
NtQueryIntervalProfile(MAGIC_SIGNATURE, (PULONG)&data);
return data.result;
}
};
Security Considerations
Kernel hooking is a double-edged sword:
Legitimate Uses:
- Security software (antivirus, EDR)
- System monitoring tools
- Debugging and reverse engineering
- Research and education
Risks:
- System instability if implemented incorrectly
- Can be detected by anti-cheat and security software
- Requires kernel-mode code signing (on modern Windows)
- PatchGuard (KPP) protects critical kernel structures
Modern Alternatives
Microsoft provides legitimate APIs for kernel communication:
- Filter Drivers - For file system and network monitoring
- ETW (Event Tracing for Windows) - For system-wide tracing
- Callbacks -
PsSetCreateProcessNotifyRoutine,ObRegisterCallbacks - Hypervisor-Based Solutions - VBS, HVCI
Conclusion
Understanding kernel driver communication and function hooking provides deep insight into Windows internals. While these techniques are powerful, they should be used responsibly and with full awareness of their implications. Modern Windows security features like PatchGuard, HVCI, and Secure Boot have made traditional hooking more challenging, pushing developers toward Microsoft-supported APIs for legitimate use cases.
Always ensure you have proper authorization before implementing kernel-level modifications, and never use these techniques for malicious purposes.