Kernel Architecture
Ring 0 vs Ring 3, Windows kernel components (ntoskrnl, HAL, win32k), KPCR, IRQL levels, the syscall dispatch mechanism, and Virtualization-Based Security
A researcher's analysis shows a suspicious driver loaded at ring 0. From that position it has direct access to every process's address space, can read and write arbitrary memory, can suppress EDR callbacks, modify the SSDT, and hide its own loaded module from all kernel enumeration APIs. The entire user-mode security stack is beneath it. Understanding kernel architecture is the prerequisite for understanding both what a kernel rootkit can do and why Virtualization-Based Security (VBS) changes the threat model fundamentally.
CPU Ring Privilege Model
x86/x64 Privilege Rings (Windows uses only 2):
Ring 0 (CPL 0) — Kernel Mode
ntoskrnl.exe, hal.dll, win32k.sys, drivers (.sys)
Unrestricted: access any physical address, execute privileged
instructions (CLI, HLT, LIDT, MOV CRx, RDMSR/WRMSR)
No MMU restrictions on kernel VA space
Ring 1, 2 — Unused by Windows
(Used in older VMM designs; x64 doesn't meaningfully support them)
Ring 3 (CPL 3) — User Mode
All applications, services, Win32 subsystem DLLs
MMU enforces: cannot access kernel VA, cannot execute privileged instructions
Transition to Ring 0 only via syscall/sysenter or exception/interrupt
CPL stored in the low 2 bits of CS register
Syscall instruction: CPL 3 → CPL 0 (via MSR_STAR/MSR_LSTAR config)
IRETQ: return to user mode (CPL 0 → CPL 3)
Windows Kernel Components
| Component | File | Role |
|---|---|---|
| NT Executive / Kernel | ntoskrnl.exe | Core OS: process/thread/memory management, I/O manager, security reference monitor, object manager, configuration manager (registry), plug-and-play manager |
| Hardware Abstraction Layer | hal.dll | Abstracts hardware differences (interrupt controllers, DMA, timers) so ntoskrnl doesn't need hardware-specific code |
| Win32k subsystem | win32k.sys | Win32 GUI kernel code: window management, GDI, DirectX. Historically a large attack surface (many CVEs). Partially moved to user mode in Win10. |
| Kernel-mode drivers | *.sys in Drivers/ | Device drivers, filter drivers, security drivers (EDR, AV). All run at Ring 0 with same privilege as ntoskrnl. |
| Secure Kernel (VBS) | securekernel.exe (VTL1) | Runs in Secure World (VTL1), isolated from even Ring 0 code. Manages VSM, Credential Guard, HVCI enforcement. |
KPCR and IRQL
The Kernel Processor Control Region (KPCR) is a per-CPU data structure pointed to by the gs segment register in kernel mode (same gs register used for TEB in user mode — the base address changes on syscall entry/exit). It contains per-CPU state critical for scheduling and interrupt handling:
// Key KPCR fields (Windows 10/11 x64, offsets approximate)
// gs:[0] in kernel mode = KPCR base
typedef struct _KPCR {
union {
NT_TIB NtTib; // +0x000: ExceptionList etc
};
PVOID GdtBase; // +0x018: Global Descriptor Table
PVOID TssBase; // +0x020: Task State Segment
ULONG64 UserRsp; // +0x038: saved user RSP on syscall
PKPCR Self; // +0x018: self-pointer
PKPRCB CurrentPrcb; // +0x020: processor control block
KIRQL Irql; // +0x264: current IRQL
ULONG IRQLTable[...];
// KPRCB follows immediately after KPCR
KPRCB Prcb; // Current thread, DPC queue, etc.
} KPCR;
IRQL (Interrupt Request Level) is a software-defined priority mechanism for Windows kernel code. Code running at a higher IRQL preempts lower-IRQL code:
| IRQL Level | Value | Context |
|---|---|---|
| PASSIVE_LEVEL | 0 | Normal thread execution (user mode and most kernel code) |
| APC_LEVEL | 1 | APC delivery; code at this level can block APCs |
| DISPATCH_LEVEL | 2 | DPC (Deferred Procedure Call) and scheduler. Cannot wait or take page faults! |
| 3-26 (device IRQLs) | 3-26 | Hardware interrupt levels; device drivers responding to IRQs |
| CLOCK_LEVEL | 13 (APIC) | System timer clock interrupt (drives scheduler quantum) |
| IPI_LEVEL | 29 | Inter-processor interrupt |
| HIGH_LEVEL | 31 | Machine check, NMI; interrupts completely disabled |
Code running at DISPATCH_LEVEL cannot: take page faults (all accessed memory must be non-pageable, in NonPagedPool), call any function that might block or wait, access pageable memory. Many kernel bugs and BSODs come from violating this rule — accessing paged pool or calling WaitForXxx at DISPATCH_LEVEL. Rootkits that run at DISPATCH_LEVEL to disable pre-emption for critical sections must follow these same rules.
Syscall Dispatch
The syscall handler is configured via Model-Specific Registers at boot:
; MSR_LSTAR (0xC0000082) = address of KiSystemCall64
; When user-mode "syscall" executes:
; 1. CPL: 3 → 0 (Ring 3 → Ring 0)
; 2. RIP saved to RCX (user return address)
; 3. RSP swapped to kernel stack (from KPCR.UserRsp / KPCR.KernelRsp)
; 4. RIP = MSR_LSTAR (KiSystemCall64)
;
; KiSystemCall64 flow:
KiSystemCall64:
swapgs ; swap GS base: user TEB ↔ kernel KPCR
mov qword [gs:UserRsp], rsp ; save user RSP
mov rsp, [gs:KernelRsp] ; switch to kernel stack
push rcx ; save user RIP (return address)
push r11 ; save user RFLAGS
; ... save full register context ...
; Dispatch: EAX = syscall number → index into KiServiceTable (SSDT)
and eax, 0xFFF ; mask to 12 bits
call QWORD [KiServiceTable + rax*8] ; call kernel implementation
; ... restore registers ...
swapgs ; swap back to user TEB
sysretq ; return to user mode (RIP from RCX)
Virtualization-Based Security (VBS / VTL)
VBS uses the CPU's hypervisor capabilities (Intel VT-x / AMD-V) to create two "Virtual Trust Levels" (VTLs): VTL0 is the normal world (where Ring 0 kernel runs), and VTL1 is the secure world (where the Secure Kernel runs). VTL1 is more privileged than VTL0's Ring 0:
VBS Trust Level Architecture:
VTL1 (Secure World) ──────────────── securekernel.exe
Manages hypervisor page table overrides (SLAT)
HVCI: marks kernel code pages read-only in VTL0
Credential Guard: keeps NT hashes in VSM secure memory
Cannot be compromised by VTL0 kernel code (even Ring 0)
VTL0 (Normal World)
Ring 0: ntoskrnl.exe, drivers
Ring 3: user applications, EDR DLLs
All ring 0 code is BELOW VTL1 privilege
A kernel rootkit in VTL0 Ring 0 cannot:
- Modify page protections that VTL1 has set
- Access secure memory (VSM regions)
- Disable HVCI enforcement
Hypervisor (Type-1 hypervisor built into Windows)
Manages SLAT (Second Level Address Translation)
Enforces VTL1 access policy
Hypervisor-Protected Code Integrity (HVCI, also called Memory Integrity) enforces that all kernel code must be from Authenticode-signed files. The Secure Kernel (VTL1) controls which pages are executable in Ring 0. Even if a kernel driver tries to allocate RWX memory and write shellcode, VTL1 marks all anonymous kernel allocations as non-executable. Only mapped image sections (from signed PEs) get execute permission. This is why exploiting a vulnerable signed driver (BYOVD — Bring Your Own Vulnerable Driver) to achieve unsigned kernel code execution requires disabling HVCI first, which itself is protected by VTL1. HVCI is enabled by default on modern Windows 11 hardware.
Q & A
If a rootkit has Ring 0 code execution, can it disable HVCI to load unsigned code?
Not without also compromising VTL1. HVCI is enforced by the Secure Kernel in VTL1. The enforcement mechanism uses Second Level Address Translation (SLAT / EPT on Intel): the hypervisor maps all kernel code pages as read-only and non-writable at the hardware page table level. Even Ring 0 code in VTL0 cannot write to these pages — the hardware-level page protection from VTL1 overrides software page table entries in VTL0. To disable HVCI, an attacker would need to: (1) Exploit the hypervisor itself (extremely difficult — Windows' hypervisor is small, well-audited, and verified by Secure Boot). (2) Exploit the Secure Kernel (similarly protected). (3) Perform a TOCTOU attack on the Secure Boot chain at boot time (requires physical access or firmware compromise). In practice, kernel code execution in VTL0 Ring 0 on an HVCI-enabled system is much less powerful than on a non-HVCI system: the rootkit can still DKOM, modify kernel data structures, and interfere with callbacks, but it cannot execute arbitrary unsigned code — it's limited to ROP chains through existing kernel code, or must use gadgets from already-loaded signed drivers. This is a significant constraint. The BYOVD (Bring Your Own Vulnerable Driver) technique partially addresses this: load a legitimate but exploitable signed driver, use it to get kernel read/write primitives, then use those primitives to escalate further. But HVCI still blocks injecting new unsigned code pages.
What is the KPRCB and how does it relate to the KPCR?
The Kernel Processor Control Block (KPRCB) is an embedded structure within the KPCR — it immediately follows the KPCR header in memory. While the KPCR contains CPU-global per-processor settings (GDT base, TSS base, IRQL, user RSP), the KPRCB contains more dynamic per-processor state: (1) CurrentThread — pointer to the KTHREAD currently executing on this CPU. This is how the kernel quickly finds the currently running thread: KeGetCurrentThread() accesses gs:[KPCR.Prcb.CurrentThread]. (2) NextThread — thread scheduled to run next. (3) IdleThread — the idle thread for this CPU. (4) DpcListHead — the list of Deferred Procedure Calls pending for this CPU. (5) QuantumEnd — flag indicating if the current thread's quantum has expired. (6) Various counters and profiling data. (7) On NUMA systems: NUMA node information. For rootkit analysis: a rootkit that reads gs:[KPCR.Prcb.CurrentThread] can find the currently executing KTHREAD, and from there the EPROCESS (thread's process), and from there the token. This is how kernel shellcode typically escalates privileges: read the current process's token, find the SYSTEM process's token, overwrite the current process's token with SYSTEM's token. Security implications: KPCR/KPRCB are the foundation of the "token stealing" privilege escalation primitive that appears in countless kernel exploits.