Module 1: Kernel vs User-Mode Evasion
Why ring 0 access is the ultimate evasion primitive and the cost of getting there.
Module Objective
Understand the architectural separation between user mode (ring 3) and kernel mode (ring 0) on Windows, why kernel-level rootkits like Nidhogg can evade virtually all user-mode security products, what Driver Signature Enforcement (DSE) is, and the realistic attack paths for loading unsigned drivers.
1. The x86 Privilege Ring Model
Intel and AMD processors implement a hardware privilege system with four rings (0 through 3). Windows uses only two of these:
| Ring | Name | Runs What | Access Level |
|---|---|---|---|
| Ring 0 | Kernel Mode | ntoskrnl.exe, HAL, kernel drivers (.sys files) | Full hardware access, all memory, all instructions |
| Ring 3 | User Mode | Applications, services, DLLs, user-mode EDR agents | Restricted: no direct hardware, isolated address space, filtered syscalls |
The transition between ring 3 and ring 0 happens through system calls (the syscall instruction on x64). When a user-mode application calls a Windows API like NtOpenProcess, execution transfers to the kernel through the SSDT (System Service Descriptor Table), runs in ring 0, and returns the result.
Ring Transition via Syscall
Ring 3
CPU trap
Ring 0 entry
Kernel routine
2. Why User-Mode Evasion Is a Losing Battle
Most offensive tools (Cobalt Strike, Meterpreter, custom loaders) operate in user mode. They must evade user-mode EDR hooks, API monitoring, ETW tracing, and memory scanning — all from the same privilege level. This creates an inherent disadvantage:
| Evasion Challenge | User-Mode (Ring 3) | Kernel-Mode (Ring 0) |
|---|---|---|
| API Hooking | Must unhook ntdll.dll, risk detection by hook integrity checks | Can prevent hooks from being installed, or silently bypass them |
| ETW Telemetry | Can patch EtwEventWrite in own process, but EDR gets kernel-level ETW | Can disable ETW providers at the source in kernel memory |
| Process Enumeration | Cannot hide from NtQuerySystemInformation | Can unlink EPROCESS from ActiveProcessLinks, invisible to all user-mode queries |
| Memory Scanning | Cannot prevent EDR kernel driver from reading process memory | Can manipulate VAD entries and PTEs to hide memory regions |
| File Protection | ACLs can be overridden by SYSTEM/admin | Can intercept IRP_MJ_CREATE at the filesystem level, denying all access |
The Core Asymmetry
A kernel driver runs at the same privilege level as the EDR kernel driver. It can read, modify, or nullify anything the EDR does. User-mode tools can never reach up to touch kernel data structures. This asymmetry is why kernel rootkits remain the most powerful evasion mechanism on Windows.
3. What a Kernel Rootkit Can Do
Nidhogg demonstrates the full range of capabilities available to a kernel-mode rootkit. Each capability maps to a specific kernel mechanism:
Nidhogg Capability Map
| Capability | Kernel Mechanism | Module |
|---|---|---|
| Hide processes | DKOM: unlink EPROCESS from ActiveProcessLinks | 3 |
| Protect processes from termination | ObRegisterCallbacks: strip PROCESS_TERMINATE from handles | 3, 6 |
| Hide/protect files | IRP major function hooking on filesystem driver | 4 |
| Protect registry keys | CmRegisterCallbackEx: block registry operations | 4 |
| Blind EDR telemetry | Disable ETW trace providers in kernel memory | 5 |
| Remove EDR callbacks | Locate and null callback arrays (PsSetCreateProcessNotifyRoutine, etc.) | 6 |
| Evade memory scanners | VAD manipulation (PTE manipulation is a general kernel technique, not a core Nidhogg feature) | 7 |
| Elevate process tokens | Direct token pointer replacement in EPROCESS | 3 |
4. The Kernel Address Space
On 64-bit Windows, the virtual address space is divided cleanly between user mode and kernel mode:
TextVirtual Address Space (x64 Windows)
=============================================
0x0000000000000000 - 0x00007FFFFFFFFFFF User space (128 TB)
- Per-process, isolated
- Accessible from ring 3
- Contains: EXE, DLLs, heap, stack, TEB/PEB
0xFFFF800000000000 - 0xFFFFFFFFFFFFFFFF Kernel space (128 TB)
- Shared across all processes
- Accessible ONLY from ring 0
- Contains: ntoskrnl, HAL, kernel drivers,
pool allocations, system PTEs, EPROCESS/ETHREAD
Key insight: kernel address space is shared across all processes. When Nidhogg's driver modifies an EPROCESS structure, that change is visible system-wide because every process maps the same kernel memory. There is no per-process isolation in kernel space.
5. Driver Signature Enforcement (DSE)
Microsoft introduced Driver Signature Enforcement to prevent unauthorized kernel code from loading. Starting with Windows Vista x64, all kernel drivers must be signed with a valid certificate:
| Signing Level | Requirement | Who Can Get It |
|---|---|---|
| WHQL | Microsoft Hardware Quality Labs certification | Hardware vendors through Microsoft's portal |
| Attestation | EV code signing certificate + Microsoft attestation signature | Developers with EV certificates (purchased from CAs) |
| Test Signing | Self-signed with test certificate (requires test mode boot) | Anyone (but system must boot with TESTSIGNING ON) |
DSE is enforced by the Code Integrity (CI) module (ci.dll). When NtLoadDriver is called, CI validates the driver's Authenticode signature against the minimum signing level before allowing the image to load into kernel space.
C// Simplified view of what CI checks during driver load
// ci!CiValidateImageHeader is called from ntoskrnl
NTSTATUS CiValidateImageHeader(
PVOID ImageBase,
ULONG ImageSize,
ULONG ImageSigningLevel, // Minimum required level
// ... additional parameters
);
// Returns STATUS_INVALID_IMAGE_HASH if signature check fails
6. Bypassing DSE: Attack Paths
Despite DSE, attackers have several paths to load unsigned kernel drivers. These are the realistic vectors that red teams use:
BYOVD — Bring Your Own Vulnerable Driver
Load a legitimately signed but vulnerable driver (e.g., one with an arbitrary read/write primitive), then exploit it to map unsigned code into kernel memory. The signed driver passes DSE; the payload never goes through NtLoadDriver. Tools like kdmapper automate this using drivers such as Intel's iqvw64e.sys.
| Bypass Method | How It Works | Detectability |
|---|---|---|
| BYOVD (kdmapper) | Exploit a signed vulnerable driver to manually map unsigned code | Moderate: vulnerable driver loads are logged, Microsoft maintains a blocklist |
| Test Signing Mode | Boot with bcdedit /set testsigning on | High: watermark on desktop, NtQuerySystemInformation exposes it |
| Leaked/Stolen Certificates | Sign with a compromised legitimate code signing cert | Low initially, high once cert is revoked and added to CRL |
| Boot Configuration Abuse | Modify BCD to disable integrity checks during boot | High: requires admin + reboot, Secure Boot blocks this |
| HyperV/VBS Weaknesses | Exploit pre-VBS systems that lack HVCI enforcement | Varies: older Windows 10 builds without HVCI are vulnerable |
Nidhogg and DSE
Nidhogg itself is distributed as source code. To use it operationally, the red team must compile it and find a way to load it. The project does not ship a DSE bypass — that is left to the operator. During development and testing, test signing mode or kdmapper are common choices. In a real engagement, BYOVD is the most practical path.
7. HVCI and Kernel Code Integrity
Hypervisor-protected Code Integrity (HVCI), part of Virtualization-Based Security (VBS), adds a second layer of defense beyond DSE:
TextWithout HVCI:
Ring 0 code can allocate RWX kernel memory and execute arbitrary code
With HVCI:
The hypervisor (ring -1) enforces W^X in kernel space
- Pages can be writable OR executable, never both
- New kernel code pages must be validated by Secure Kernel
- Even a kernel driver cannot simply allocate and run shellcode
HVCI significantly raises the bar for kernel rootkits. On systems with HVCI enabled, manually mapped drivers (like kdmapper output) may fail because the hypervisor blocks execution of unsigned kernel pages. However, many enterprise environments still lack HVCI, and older Windows 10 builds do not enable it by default.
8. PatchGuard (Kernel Patch Protection)
PatchGuard (KPP) is a kernel integrity monitoring system that periodically checks critical kernel structures for unauthorized modifications:
What PatchGuard Monitors
- SSDT (System Service Descriptor Table) — syscall hooks
- IDT (Interrupt Descriptor Table) — interrupt handler hooks
- GDT (Global Descriptor Table) — segment descriptor modifications
- Critical kernel code sections — inline hooks in ntoskrnl
- Processor MSRs — LSTAR (syscall entry point) modifications
- Certain callback arrays — some notification routine tables
When PatchGuard detects a modification, it triggers CRITICAL_STRUCTURE_CORRUPTION (bug check 0x109), crashing the system with a blue screen. This means Nidhogg must carefully avoid techniques that PatchGuard monitors. For example, Nidhogg uses DKOM (modifying linked list pointers) rather than SSDT hooking, because SSDT modifications are reliably detected by PatchGuard. However, DKOM is not guaranteed to be PatchGuard-safe: PatchGuard's monitoring scope varies between Windows builds and can include checks on kernel data structures like EPROCESS linked lists. Nidhogg's own documentation acknowledges that PatchGuard can potentially detect DKOM modifications. In practice, DKOM is less likely to be caught than SSDT hooking, but it is not undetectable.
9. Nidhogg Architecture Overview
Nidhogg is structured as two components that communicate via IOCTLs:
Nidhogg Two-Component Architecture
User-mode C++ client
Ring 3
Kernel driver
Ring 0
EPROCESS, callbacks,
ETW, IRP hooks
The user-mode client sends commands (hide process, protect file, disable ETW, etc.) through DeviceIoControl calls. The kernel driver receives these as I/O Request Packets (IRPs) with IOCTL codes, executes the requested operation using kernel APIs and data structure manipulation, and returns the result. This architecture is covered in detail in Module 2 (driver basics) and Module 8 (full IOCTL table).
Knowledge Check
Q1: Why can a kernel-mode rootkit evade user-mode EDR agents?
Q2: What is BYOVD?
Q3: Why does Nidhogg use DKOM instead of SSDT hooking to hide processes?