IRQL Levels: Interrupt Request Priorities Explained
Objective: Understand the Windows kernel’s Interrupt Request Level (IRQL) priority system — what each level means numerically and symbolically, how the HAL arbitrates hardware and software interrupts, which APIs query and change the IRQL, what kernel operations are legal at each level, and how malicious kernel code abuses IRQL semantics to evade defenders.
1. What Is an IRQL?
An Interrupt Request Level (IRQL) is a per-processor priority value that determines which kernel-mode support routines the currently executing code may legally call. It is an integer in the range 0–31, stored as type KIRQL (a typedef for UCHAR). Three levels — PASSIVE_LEVEL, APC_LEVEL, and DISPATCH_LEVEL — are referred to symbolically; the rest are usually named by value.
IRQL is per-processor, not per-thread. On x86 it lives in the Irql field of the _KPCR (Kernel Processor Control Region); on x64 it is mapped to the CR8 register (Task Priority Register). When the processor raises its IRQL, all interrupts at or below that level are masked. Higher-numbered interrupts preempt all lower-IRQL processing; once handled, the processor returns to the previous level. Raising and lowering must follow strict stack discipline — you only lower back to a level you previously raised from.
2. The IRQL Hierarchy
The Hardware Abstraction Layer (HAL) maps physical interrupt vectors to software IRQLs. The count of levels is architecture-dependent: x64 and Itanium expose 16 IRQLs; x86 exposes 32, owing to differences in interrupt-controller hardware. The canonical wdm.h symbolic definitions differ across architectures.
| Symbolic Name | x64 Value | x86 Value | Description |
|---|---|---|---|
PASSIVE_LEVEL / LOW_LEVEL | 0 | 0 | Normal thread execution; nothing masked |
APC_LEVEL | 1 | 1 | APC delivery and page-fault handling |
DISPATCH_LEVEL | 2 | 2 | Thread scheduler / DPC queue |
CMC_LEVEL | 3 | — | Correctable Machine Check |
| Device IRQLs (DIRQL) | 4–11 | 3–26 | Hardware device interrupts |
CLOCK_LEVEL | 13 | 28 | System clock timer |
IPI_LEVEL / DRS_LEVEL | 14 | 29 | Inter-Processor Interrupt |
POWER_LEVEL | 15 | 30 | Power failure |
PROFILE_LEVEL / HIGH_LEVEL | 15 | 31 | Profiling / highest maskable |
Higher value = higher priority. A device interrupt at DIRQL 8 preempts a DPC at DISPATCH_LEVEL (2), which itself preempts ordinary thread code at PASSIVE_LEVEL (0).

3. Software IRQLs: PASSIVE, APC, and DISPATCH
The lowest three levels are software IRQLs — the kernel raises and lowers them without involving the interrupt controller.
PASSIVE_LEVEL (0) masks nothing. This is where normal kernel-mode thread code runs: DriverEntry, AddDevice, Unload, most dispatch routines, and driver-created worker threads. All blocking, paging, and synchronization primitives are available.
APC_LEVEL (1) masks Asynchronous Procedure Call interrupts only. The sole functional difference from PASSIVE_LEVEL is that APCs cannot interrupt the running code. Both levels imply a valid thread context and both permit access to pageable memory. Page-fault handling itself runs at APC_LEVEL.
DISPATCH_LEVEL (2) masks DISPATCH_LEVEL and APC_LEVEL. Critically, the thread scheduler is disabled — code here owns the processor until it lowers IRQL. Routines such as StartIo, DpcForIsr, IoTimer, Cancel (holding the cancel spin lock), and all DPC callbacks run here. Two hard rules apply: no access to paged memory, and no blocking waits.
| Feature | PASSIVE_LEVEL | APC_LEVEL | DISPATCH_LEVEL |
|---|---|---|---|
| Thread context | Yes | Yes | Not guaranteed |
| Scheduler active | Yes | Yes | No |
| Paged pool access | Yes | Yes | No |
| Blocking waits allowed | Yes | Yes | No |
4. Hardware IRQLs: DIRQL and Above
Levels at or above the device range are hardware IRQLs driven by the interrupt controller. A driver’s Device IRQL (DIRQL) is the SynchronizeIrql stored in its _KINTERRUPT object. When a device fires, the processor raises to that DIRQL and invokes the Interrupt Service Routine (ISR), a KSERVICE_ROUTINE.
At DIRQL, all interrupts at or below the driver’s level are masked, but higher-DIRQL devices, the clock, and power-failure interrupts may still preempt. Because the scheduler and lower-priority interrupts are blocked, ISRs must be minimal — they acknowledge the hardware, capture volatile state, and queue a DPC for the heavy lifting at DISPATCH_LEVEL.
Above DIRQL sit CLOCK_LEVEL, IPI_LEVEL (used by one processor to interrupt another), POWER_LEVEL, and HIGH_LEVEL. The general principle: the higher the IRQL, the shorter the code must run. Sustained work at high IRQL starves the entire processor.
// KSERVICE_ROUTINE - runs at DIRQL; must be minimal
BOOLEAN MyInterruptServiceRoutine(
PKINTERRUPT Interrupt, PVOID ServiceContext) {
// Acknowledge hardware, then defer heavy work to a DPC.
// Do NOT touch paged memory here.
IoRequestDpc(MyDeviceObject, MyDeviceObject->CurrentIrp, ServiceContext);
return TRUE;
}5. Kernel APIs for IRQL Management
Drivers query and adjust IRQL through a small, exported API surface in wdm.h.
| API Function | Purpose |
|---|---|
KeGetCurrentIrql() | Returns the current processor IRQL; callable at any IRQL |
KeRaiseIrql(NewIrql, &OldIrql) | Raises to NewIrql; saves prior level. NewIrql must be ≥ current |
KeLowerIrql(OldIrql) | Restores a previously saved IRQL — only after a matching raise |
KeRaiseIrqlToDpcLevel() | Raises to DISPATCH_LEVEL, returns old IRQL |
KeAcquireSpinLock(&Lock, &OldIrql) | Acquires spin lock, raising to DISPATCH_LEVEL |
KeReleaseSpinLock(&Lock, OldIrql) | Releases lock, restoring saved IRQL |
KeAcquireSpinLockAtDpcLevel(&Lock) | Acquires lock without raising (caller already at DISPATCH_LEVEL) |
The exact signatures:
KIRQL KeGetCurrentIrql(void);
void KeRaiseIrql(
_In_ KIRQL NewIrql,
_Out_ PKIRQL OldIrql
);
void KeLowerIrql(_In_ KIRQL NewIrql); // restore saved old IRQL
KIRQL KeRaiseIrqlToDpcLevel(void);The raise/lower discipline is enforced: calling KeRaiseIrql with a value lower than the current IRQL is a fatal error, and KeLowerIrql may only restore the level a prior KeRaiseIrql saved.
// Demonstrates the raise/lower stack discipline
VOID MyFunctionNeedingDispatchLevel(VOID) {
KIRQL oldIrql;
KeRaiseIrql(DISPATCH_LEVEL, &oldIrql);
// --- Critical section: no paged pool access here ---
KeLowerIrql(oldIrql);
}Spin locks couple mutual exclusion with IRQL: acquiring one raises to DISPATCH_LEVEL so the holder cannot be preempted by the scheduler on its processor.
KSPIN_LOCK MySpinLock;
KIRQL oldIrql;
KeInitializeSpinLock(&MySpinLock);
// KeAcquireSpinLock raises to DISPATCH_LEVEL internally
KeAcquireSpinLock(&MySpinLock, &oldIrql);
// ... protected shared-data access (non-paged only) ...
KeReleaseSpinLock(&MySpinLock, oldIrql); // restores oldIrqlA driver inspecting its own context queries the level directly:
// Demonstrates KeGetCurrentIrql() usage and KIRQL type
NTSTATUS DriverDispatchCreate(PDEVICE_OBJECT DeviceObject, PIRP Irp) {
KIRQL currentIrql = KeGetCurrentIrql();
// Expected: PASSIVE_LEVEL (0) in a dispatch routine
DbgPrint("[MyDriver] Current IRQL: %u\n", (ULONG)currentIrql);
// ...complete IRP...
}6. Memory Access Rules at Each IRQL
The single most consequential IRQL rule concerns paged memory. Any routine running above APC_LEVEL that touches paged pool causes a fatal page fault. Resolving a page fault requires the file-system driver to read from disk — an operation that needs a context switch, which is impossible once the scheduler is disabled at DISPATCH_LEVEL.
| Memory Pool | PASSIVE_LEVEL | APC_LEVEL | DISPATCH_LEVEL+ |
|---|---|---|---|
| Paged pool | Accessible | Accessible | Fatal page fault |
| Non-paged pool | Accessible | Accessible | Accessible |
Code at or above DISPATCH_LEVEL must therefore allocate from non-paged pool and operate only on locked or non-pageable memory (for example, buffers locked with MmProbeAndLockPages). Violating this rule produces the most common driver bug check — IRQL_NOT_LESS_OR_EQUAL (0x0000000A), or its driver-attributed variant 0x000000D1.
7. DPCs: The DISPATCH_LEVEL Workhorses
A Deferred Procedure Call (DPC) moves work out of the time-critical ISR into DISPATCH_LEVEL. The ISR queues a _KDPC object (via IoRequestDpc or KeInsertQueueDpc); the kernel drains the DPC queue as IRQL drops below DISPATCH_LEVEL. DpcForIsr handles per-IRP completion; CustomDpc and CustomTimerDpc serve driver-specific needs.
// KDEFERRED_ROUTINE - runs at DISPATCH_LEVEL
VOID MyDpcRoutine(
PKDPC Dpc, PVOID DeferredContext,
PVOID SystemArgument1, PVOID SystemArgument2) {
// Safe: non-paged pool only.
// Do NOT call KeWaitForSingleObject with a nonzero timeout.
DbgPrint("[MyDpc] Running at DISPATCH_LEVEL\n");
}A DPC that runs too long throttles the whole system and triggers DPC_WATCHDOG_VIOLATION (0x00000133) once sustained execution exceeds the watchdog threshold.

8. APCs: The APC_LEVEL Mechanism
An Asynchronous Procedure Call (APC) executes a function in the context of a specific thread. Kernel APCs run at APC_LEVEL; user APCs are delivered when a thread returns to PASSIVE_LEVEL in a user-mode alertable wait. Drivers initialize them with KeInitializeApc and queue them with KeInsertQueueApc. Because APC_LEVEL still implies a valid thread context and permits paged access, certain dispatch routines raise to APC_LEVEL to serialize against APC delivery while remaining able to page in data.
9. Debugging IRQL With WinDbg
WinDbg exposes IRQL state on both live kernels and crash dumps.
; Check current IRQL on each processor
!irql
; Examine the KPCR for processor 0
!pcr 0
; List pending DPCs
!dpcs
; Analyze a 0x0000000A bugcheck
!analyze -vOn x64 the IRQL is the CR8 register; you can read it and the _KPCR directly:
; dt = display type; shows _KPCR struct at GS base
dt nt!_KPCR @$pcr
; On x64, IRQL maps to CR8 (Task Priority Register)
r cr8The IRQL contract is also expressed statically through SAL annotations in wdm.h, which static-analysis tooling verifies at build time:
// Illustrates IRQL annotation macros from wdm.h
_IRQL_requires_max_(DISPATCH_LEVEL)
VOID MyRoutineSafeAtOrBelowDispatch(VOID);
_IRQL_requires_(PASSIVE_LEVEL)
VOID MyRoutineRequiresPassive(VOID);
_IRQL_raises_(DISPATCH_LEVEL)
_IRQL_saves_
KIRQL MyRaiseRoutine(VOID);10. IRQL in a Security Context
IRQL semantics become a security concern the moment attacker code reaches ring 0. Code running at DISPATCH_LEVEL owns its processor and is invisible to user-mode EDR hooks — an ideal vantage point for unhooking the SSDT, overwriting kernel callbacks, or hiding objects before defensive software can react. Because paged access above APC_LEVEL is fatal, IRQL violations also serve as a crude denial-of-service primitive: a single bad page touch produces an IRQL_NOT_LESS_OR_EQUAL blue screen.
The dominant delivery vector is Bring Your Own Vulnerable Driver (BYOVD) — loading a legitimately signed but exploitable driver to obtain kernel-IRQL execution without writing a new signed driver. Missing or incorrect IRQL SAL annotations frequently mask the very bugs these attacks exploit.

11. Common Attacker Techniques
| Technique | Description |
|---|---|
| BYOVD kernel execution | Load a signed-but-vulnerable driver (e.g. RTCore64.sys, dbutil_2_3.sys) to run code at kernel IRQL |
EDR unhooking at DISPATCH_LEVEL | Patch SSDT entries or kernel callbacks while the scheduler is disabled, beating re-hook races |
| Rootkit concealment | Hide processes, files, and connections from DIRQL/DISPATCH_LEVEL, below user-mode visibility |
| Spin-lock starvation | Hold a spin lock at DISPATCH_LEVEL to monopolize a processor — driver-stack DoS |
| Deliberate IRQL fault | Force paged access above APC_LEVEL to bug-check the host (0x0000000A DoS) |
| DSE downgrade | Flip test-signing or pre-release flags to load unsigned kernel code |
12. Defensive Strategies & Detection
Driver loads are the chokepoint. Sysmon Event ID 6 (Driver Loaded) records ImageLoaded, Hashes, Signed, Signature, and SignatureStatus — the fields that expose unsigned or anomalously signed drivers and known-vulnerable BYOVD payloads. Event ID 7045 (and System log 7036/7040) surface drivers registered as services. PatchGuard violations of _KPCR/IDT/SSDT raise bug check 0x00000109 (CRITICAL_STRUCTURE_CORRUPTION); HVCI/Code-Integrity blocks land in Microsoft-Windows-CodeIntegrity/Operational (Event IDs 3001–3089) and Security Event ID 5038.
A starting Sigma rule for vulnerable-driver loads:
title: Suspicious Vulnerable Driver Load (Possible BYOVD)
logsource:
product: windows
service: sysmon
detection:
selection_unsigned:
EventID: 6
Signed: 'false'
selection_known_vuln:
EventID: 6
ImageLoaded|endswith:
- '\RTCore64.sys'
- '\dbutil_2_3.sys'
condition: selection_unsigned or selection_known_vuln
level: highISR/DPC behavior can be traced through the NT Kernel Logger ETW provider with interrupt and DPC flags enabled:
xperf -on Base+Interrupt+DPC
xperf -d trace.etlHardening layers: enforce Driver Signature Enforcement and HVCI (M1048) so unsigned or tampered drivers cannot load even on a compromised kernel; enable the Microsoft Vulnerable Driver Blocklist (HKLM\SYSTEM\CurrentControlSet\Control\CI\Config\VulnerableDriverBlocklistEnable); restrict SeLoadDriverPrivilege to administrators (M1026); and run suspect drivers under Driver Verifier in a VM to force IRQL checks. Monitor bcdedit test-signing changes and the CI\Config registry path for downgrade attempts.
MITRE ATT&CK Mapping
| Technique | MITRE ID | Detection |
|---|---|---|
| Rootkit | T1014 | Sysmon EID 6 unsigned/anomalous drivers; HVCI logs |
| Create System Process: Service | T1543.003 | EID 7045 / System 7036 driver-service install |
| Impair Defenses: Disable Tools | T1562.001 | EDR callback integrity, PatchGuard 0x109 |
| Impair Defenses: Downgrade | T1562.010 | CI\Config registry + bcdedit test-signing audit |
| Exploitation for Priv-Esc | T1068 | BYOVD load (EID 6) preceding kernel-write activity |
| Escape to Host | T1611 | Kernel-IRQL execution from container context |
13. Tools for IRQL Analysis
| Tool | Description | Link |
|---|---|---|
| WinDbg | !irql, !pcr, !dpcs, !analyze -v on bug checks | microsoft.com |
| Driver Verifier | Forces IRQL/pool/deadlock checks on a target driver | microsoft.com |
| Sysmon | Driver-load (EID 6) and service (7045) telemetry | microsoft.com |
| xperf / WPA | ETW interrupt and DPC tracing | microsoft.com |
| Process Hacker | Live driver and kernel-module enumeration | processhacker.sourceforge.io |
| Volatility | Memory-forensic driver and callback inspection | volatilityfoundation.org |
| Ghidra | Static analysis of suspect driver binaries | ghidra-sre.org |
Summary
- IRQL is a per-processor priority register that gates which kernel routines code may legally call and which interrupts are masked.
- The HAL maps hardware vectors onto 16 IRQLs on x64 and 32 on x86; higher value preempts lower, and raising/lowering must follow strict stack discipline.
- Above
APC_LEVELthe scheduler is disabled and paged memory is off-limits — touching it triggersIRQL_NOT_LESS_OR_EQUAL(0x0000000A). - Attackers reach kernel IRQL through BYOVD to unhook EDR, conceal rootkits, or bug-check the host as a DoS — mapped to
T1014,T1543.003,T1562.001, andT1068. - Detect via Sysmon Event ID 6, the vulnerable-driver blocklist, HVCI/DSE enforcement, and
SeLoadDriverPrivilegerestriction.
Related Tutorials
- Windows Scheduler Internals: Priority Levels, Quantum, and Thread Selection
- DPCs: Deferred Procedure Calls and Interrupt Deferral
- Access Tokens and Privileges: The Kernel’s Security Context
- SIDs and Security Descriptors: Identity in Windows Security
- Fibers: User-Mode Cooperative Threads
References
- Managing Hardware Priorities (IRQL Levels) — Windows Kernel Driver Docs | Microsoft Learn
- Always Preemptible and Always Interruptible — Windows Kernel Driver Docs | Microsoft Learn
- IRQL Annotations for Drivers — Windows Driver Testing | Microsoft Learn
- !irql Extension Command (WinDbg Kernel Debugger) | Microsoft Learn
- Dispatch Routines and IRQLs — Windows Kernel Driver Docs | Microsoft Learn
- Guidelines for Writing DPC Routines (DISPATCH_LEVEL IRQL) | Microsoft Learn
User Mode vs Kernel Mode: Privilege Rings and the Boundary
Objective: Understand the architectural separation between user mode (Ring 3) and kernel mode (Ring 0) on Windows — how Intel hardware enforces it, how the Windows OS layers process isolation and the system call dispatch path on top, and why this boundary is the central battleground for rootkits, EDR, and modern kernel hardening.
1. Why Rings Exist — The Hardware Contract
Intel x86/x64 CPUs define four hardware privilege levels — called rings — numbered 0 (most privileged) through 3 (least privileged). The currently executing privilege level is encoded in the low two bits of the CS segment register and is referred to as the Current Privilege Level (CPL). Every memory access, every instruction fetch, and every attempt at a privileged instruction is checked against this value by the CPU itself, before any OS code runs.
Windows collapses Intel’s four rings into two:
| Feature | User Mode | Kernel Mode |
|---|---|---|
| Ring / CPL | Ring 3 (CPL = 3) | Ring 0 (CPL = 0) |
| Memory access | User VA only | Full kernel + user VA |
| Privileged instructions | Faults with #GP | Allowed |
| Address space isolation | Per-process private | Single shared VA across all drivers |
| Crash blast radius | Process termination | Bug check (BSOD) |
| Entry mechanism | Native execution | SYSCALL / interrupt / exception |
Rings 1 and 2 exist in hardware but are unused by Windows — using only Ring 0 and Ring 3 maps cleanly to the “supervisor vs. user” model and is portable to architectures (ARM64, older RISC) that don’t expose intermediate levels. The instant Ring 3 code attempts to execute LGDT, LIDT, RDMSR, WRMSR, HLT, CLI, STI, or any I/O instruction outside its IOPB, the CPU raises a General Protection Fault (#GP) and the kernel terminates the offending thread.
This single hardware guarantee — CPL is checked by silicon, not software — is what makes the user/kernel boundary trustworthy in the first place.

2. User Mode: The Sandboxed World
When Windows launches an application, it creates a process with its own private virtual address space, its own handle table, and a security token. On x64, the user-mode half of the address space spans 0x0000000000000000 – 0x00007FFFFFFFFFFF (128 TB). Anything above that canonical boundary is kernel territory and is unmapped from user mode page tables (especially under KVA Shadow).
User-mode code can:
- Allocate memory in its own VA via
VirtualAlloc. - Open handles to kernel objects through documented APIs.
- Spawn threads and processes via the Win32 subsystem (
csrss.exe).
User-mode code cannot:
- Read or write another process’s memory without explicit handle access.
- Touch kernel VA, modify page tables, or read MSRs directly.
- Service interrupts, install drivers, or hook the IDT/GDT.
Every meaningful operation that touches hardware, files, networking, or kernel objects must therefore traverse the user/kernel boundary through a system call.
3. Kernel Mode: The Shared Kingdom
In contrast to user mode’s per-process isolation, all kernel-mode code shares a single virtual address space. ntoskrnl.exe, the HAL, file system drivers, network stack drivers, and every third-party driver loaded on the system all coexist in the same address space, on the same privilege level, with no memory protection between them.
| Region | Purpose |
|---|---|
| Non-paged pool | Kernel allocations that must remain resident (DPC/ISR code, kernel objects) |
| Paged pool | Kernel allocations that can be paged out |
| System PTE region | Kernel-managed page table entries for I/O mapping |
| HAL / driver image range | Loaded driver .text/.data sections |
A buggy driver writing to the wrong pointer can corrupt another driver’s structures or the kernel’s own state. A crash in any kernel component triggers a bug check (BSOD) because, unlike user mode, there is no isolation boundary to contain the damage. This is also exactly why attackers want Ring 0: once executing in kernel mode, malicious code has the same authority over the OS as the OS itself.
4. Crossing the Boundary — The SYSCALL Path
Every Win32 API that touches the kernel eventually reaches an ntdll.dll stub. On x64 those stubs all have the same shape:
; ntdll!NtReadFile (representative)
mov r10, rcx ; preserve arg1 (RCX is clobbered by SYSCALL)
mov eax, 0x06 ; syscall number (build-specific; illustrative)
syscall ; user -> kernel transition
retThe SYSCALL instruction is the choreography of the boundary crossing. The CPU performs all of the following atomically:
| Step | CPU action |
|---|---|
| 1 | Saves user RIP into RCX |
| 2 | Saves user RFLAGS into R11 |
| 3 | Masks RFLAGS per IA32_FMASK (MSR 0xC0000084) — clears IF so interrupts are off at kernel entry |
| 4 | Loads new CS/SS selectors from IA32_STAR (MSR 0xC0000081) |
| 5 | Loads RIP from IA32_LSTAR (MSR 0xC0000082) — points to nt!KiSystemCall64 |
| 6 | Transitions CPL from 3 to 0 |
From here, control is in Windows. The kernel-side dispatch chain is:
| Function | Role |
|---|---|
nt!KiSystemCall64 | Entry point loaded from IA32_LSTAR. Executes swapgs to swap user GS for kernel GS, switches to the kernel stack, allocates and populates a _KTRAP_FRAME with the saved user-mode register state. With KVA Shadow (KPTI) enabled, the variant KiSystemCall64Shadow is used to swap page tables first. |
nt!KiSystemServiceUser | Locates the current _KTHREAD via GS:[0x188] and sets KTHREAD.PreviousMode = UserMode (1) so the kernel knows arguments came from Ring 3 and must be probed. |
nt!KiSystemServiceStart | Splits the syscall number in EAX into a table identifier (high bits) and a service index (low bits). |
nt!KiSystemServiceRepeat | Selects KeServiceDescriptorTable (Nt* executive calls) or KeServiceDescriptorTableShadow (Win32k GUI calls), validates the argument count, and dispatches. |
Service routine (e.g. nt!NtReadFile) | Validates user pointers (ProbeForRead / ProbeForWrite) and performs the work. |
SYSRET | Restores RIP from RCX, RFLAGS from R11, transitions CPL from 0 back to 3, and the caller returns from ntdll. |
The key takeaway for defenders: every user-mode action eventually appears in EAX as a syscall number — and EDR products that hook only in user space (in ntdll) can be bypassed by re-implementing this exact stub in attacker code (direct/indirect syscalls).

5. The SSDT — Routing Calls Inside the Kernel
The System Service Descriptor Table (SSDT) is the array of function pointers that turns EAX into a kernel routine address.
| Symbol | Description |
|---|---|
KeServiceDescriptorTable | Exported; primary SSDT for Nt* executive system calls |
KeServiceDescriptorTableShadow | Not exported; adds the Win32k.sys GUI calls used by threads with a Win32 subsystem context |
ServiceTable | Field inside each descriptor — pointer to an array of encoded function offsets (on x64 these are relative offsets right-shifted by 4) |
NumberOfServices | Count of valid entries |
Patching SSDT entries to redirect kernel calls was the classic 32-bit rootkit technique (and the canonical kernel hook for early HIPS products). On x64, PatchGuard (KPP) periodically verifies the SSDT and several other critical structures; modification triggers Bug Check 0x109 — CRITICAL_STRUCTURE_CORRUPTION.
6. Key Kernel Structures at the Boundary
The kernel maintains per-CPU and per-thread state that defenders inspect to understand mode transitions.
// Conceptual layout — verify offsets against your build's symbols.
typedef struct _KPCR {
// ...
struct _KPRCB Prcb; // at +0x180 on x64; embedded
} KPCR, *PKPCR;
typedef struct _KPRCB {
// ...
struct _KTHREAD *CurrentThread; // GS:[0x188] in kernel mode
} KPRCB, *PKPRCB;
typedef struct _KTHREAD {
// ...
UCHAR PreviousMode; // 0 = KernelMode, 1 = UserMode
PKTRAP_FRAME TrapFrame; // saved register state from SYSCALL
} KTHREAD, *PKTHREAD;PreviousMode is one of the most consequential bytes in the system: kernel routines branch on it to decide whether to probe and capture caller-supplied pointers (user mode) or trust them directly (kernel mode). Bugs in that check have been the root cause of multiple Windows LPE CVEs.
Inspect any of these live in WinDbg on a kernel debug target:
0: kd> rdmsr 0xC0000082 ; IA32_LSTAR -> KiSystemCall64
0: kd> dg cs ; show CS selector + CPL
0: kd> dt nt!_KPCR @$pcr
0: kd> dt nt!_KTHREAD @$thread PreviousMode TrapFrame
0: kd> dt nt!_KTRAP_FRAME @$thread->TrapFrame
0: kd> dps KeServiceDescriptorTable L47. Hardening the Boundary
Microsoft has spent two decades hardening the user/kernel boundary in layers. Each mechanism closes a class of attacks against Ring 0.
| Mechanism | What it enforces |
|---|---|
| PatchGuard (KPP) | Periodic integrity checks on SSDT, IDT, GDT, KPCR, MSRs, and kernel code sections. Tampering triggers Bug Check 0x109. |
| Driver Signature Enforcement (DSE) | All kernel drivers must be signed. Enforced by ci.dll. Disabling DSE (bcdedit /set testsigning on) is a strong adversary indicator. |
| Secure Boot | UEFI-rooted trust chain prevents unsigned bootloaders/drivers from loading before Windows starts. |
| HVCI (Memory Integrity) | A VTL1 hypervisor enforces W^X on kernel pages — unsigned code cannot execute even from Ring 0. |
| KVA Shadow (KPTI) | User page tables contain only minimal kernel mappings; full mapping is installed only while CPL = 0. Mitigates Meltdown-class speculative leaks. |
| Microsoft Vulnerable Driver Blocklist | Maintained list of known-abused drivers; enforced by HVCI/CI. |
Together these turn Ring 0 from “anything goes once you’re in” into a far more constrained environment — and explain why modern attackers gravitate toward Bring Your Own Vulnerable Driver (BYOVD) as their cleanest path to kernel code execution.

8. Common Attacker Techniques
The boundary is a target precisely because Ring 0 sits underneath every defensive product. Attackers care about three categories of abuse:
| Technique | Description |
|---|---|
| Direct / indirect syscalls | Rebuild the ntdll stub (mov r10, rcx; mov eax, <N>; syscall) inside the implant to bypass user-mode hooks placed by EDR. |
| BYOVD | Load a legitimately signed but vulnerable driver, then exploit it to gain arbitrary Ring 0 read/write — used to disable EDR, blank tokens, or clear callbacks. |
| Kernel exploitation (LPE) | Exploit a kernel vulnerability (write-what-where, type confusion, double-fetch on user pointers when PreviousMode == UserMode) to escalate Ring 3 → Ring 0. |
| SSDT hooking (legacy) | Patch entries in KeServiceDescriptorTable to intercept syscalls — blocked on x64 by PatchGuard but still relevant for 32-bit forensics. |
| DKOM (Direct Kernel Object Manipulation) | Unlink _EPROCESS entries from ActiveProcessLinks to hide processes; clear PsActiveProcessHead linkages. |
| Callback removal | Walk PsSetCreateProcessNotifyRoutine / PsSetLoadImageNotifyRoutine arrays and null EDR callbacks. |
PreviousMode overwrite | Set KTHREAD.PreviousMode = KernelMode (0) to make subsequent Nt* calls skip user-pointer validation. |
9. Defensive Strategies & Detection
The fact that all roads cross the boundary is a defender’s leverage: even attackers using direct syscalls leave telemetry at driver load, privilege use, and kernel object access layers.
Sysmon coverage
| Event ID | Name | Relevance |
|---|---|---|
1 | Process Create | Parent/child + command line; catches bcdedit, sc.exe create … type= kernel |
6 | Driver Loaded | Fires on every kernel driver load; fields include ImageLoaded, Hashes, Signed, Signature — primary BYOVD signal |
7 | Image Loaded | DLL loads; detect ntdll.dll loaded from non-standard paths |
10 | Process Access | Cross-process handle opens with sensitive GrantedAccess masks (precursor to injection) |
255 | Sysmon Error | Tampering with the Sysmon kernel driver may surface here |
Windows audit policies
| Policy | Event IDs | Detects |
|---|---|---|
Audit Sensitive Privilege Use | 4673 | Use of SeLoadDriverPrivilege — required to load any kernel driver |
Audit Security System Extension | 4697, 7045 | New service / kernel driver installed |
Audit Kernel Object | 4656, 4663 | Access to kernel objects via SACL-tagged handles |
Audit Policy Change | 4719 | Audit-policy tampering (a common pre-attack step) |
High-value ETW providers
Microsoft-Windows-Kernel-Process— process/thread/image events at the kernel boundary.Microsoft-Windows-Kernel-File/Microsoft-Windows-Kernel-Registry— kernel-side file and registry ops, useful for catching driver-stage persistence.Microsoft-Windows-Threat-Intelligence(ETWTI) — emits high-fidelity events forReadProcessMemory,WriteProcessMemory,MapViewOfSection,QueueUserApc. Consumption requires a PPL or kernel consumer; verify provider availability on your build withlogman query providers.
Sigma — BYOVD pattern
title: Suspicious Kernel Driver Load - BYOVD Pattern
logsource:
product: windows
category: driver_load
detection:
selection:
EventID: 6
Signed: 'false'
filter_legit_path:
ImageLoaded|startswith:
- 'C:\Windows\System32\drivers\'
- 'C:\Windows\SysWOW64\drivers\'
condition: selection and not filter_legit_path
fields:
- ImageLoaded
- Hashes
- Signature
- SignatureStatus
level: highSigma — SeLoadDriverPrivilege exercised by non-system principal
title: SeLoadDriverPrivilege Use by Non-System Account
logsource:
product: windows
service: security
detection:
selection:
EventID: 4673
PrivilegeList|contains: 'SeLoadDriverPrivilege'
filter_machine_accounts:
SubjectUserName|endswith: '$'
condition: selection and not filter_machine_accounts
level: mediumHardening checklist
- Enable HVCI / Memory Integrity (
HKLM\SYSTEM\CurrentControlSet\Control\DeviceGuard). - Enable Secure Boot in UEFI.
- Apply the Microsoft Vulnerable Driver Blocklist (HVCI-enforced).
- Verify Meltdown mitigations / KVA Shadow via
Get-SpeculationControlSettings. - Alert on
bcdedit /set testsigning onand on driver loads whereSigned=falseor hashes matchloldrivers.io. - Enable Kernel DMA Protection for laptops with Thunderbolt/USB4.
- Limit
SeLoadDriverPrivilegeassignment and monitor every use via Event4673.
10. Tools for Boundary Analysis
| Tool | Description | Link |
|---|---|---|
| WinDbg | Kernel debugger; inspect _KPCR, _KTHREAD, _KTRAP_FRAME, MSRs, SSDT | aka.ms/windbg |
| Sysmon | Process/driver/handle telemetry — EIDs 1/6/7/10 | sysinternals.com |
| Process Hacker | View loaded drivers, handles, tokens, KPP-safe inspection | processhacker.sourceforge.io |
| Process Monitor | File/registry/thread activity at the boundary | sysinternals.com |
| Volatility 3 | Memory forensics; walk _EPROCESS, hidden processes via DKOM | volatilityfoundation.org |
| DriverView / DriverQuery | Enumerate loaded kernel drivers and signing state | nirsoft.net |
ETW / logman | Enumerate and capture kernel-mode ETW providers | built-in |
| loldrivers.io | Catalog of known-vulnerable signed drivers | loldrivers.io |
11. MITRE ATT&CK Mapping
| Technique | MITRE ID | Detection |
|---|---|---|
| Rootkit | T1014 | Volatility scans for unlinked _EPROCESS; PatchGuard bug checks 0x109 |
| Process Injection | T1055 | Sysmon EID 8/10; ETWTI WriteProcessMemory / QueueUserApc |
| Exploitation for Privilege Escalation | T1068 | Bug check telemetry, unusual PreviousMode transitions, EDR kernel callbacks |
| Create or Modify System Process: Service | T1543.003 | Security EID 4697, System EID 7045, Sysmon EID 6 |
| Impair Defenses | T1562.001 | Driver loads correlated with subsequent loss of EDR telemetry; EID 4673 with SeLoadDriverPrivilege |
| Exploitation for Defense Evasion (BYOVD) | T1211 | Sysmon EID 6 with unsigned driver or known-vulnerable hash; loldrivers.io match |
Summary
- The user/kernel boundary is enforced by silicon —
CPLinCS— not by software, which is what makes it trustworthy. - Windows uses only Ring 0 and Ring 3; user mode runs in a per-process private VA, kernel mode runs in a single shared VA where any bug is a BSOD.
- Every user→kernel transition flows through
SYSCALL→IA32_LSTAR→KiSystemCall64→ SSDT dispatch, leavingEAXandKTHREAD.PreviousModeas the canonical fingerprints. - Modern hardening — PatchGuard, DSE, HVCI, KVA Shadow, and the vulnerable driver blocklist — has pushed attackers toward BYOVD and direct syscalls.
- Defenders watch the boundary through Sysmon EID 6, Security EID 4673 (
SeLoadDriverPrivilege), ETWTI, and kernel-callback EDR telemetry — every Ring 0 attack eventually touches one of them.
Related Tutorials
- System Calls and SSDT: How User Mode Reaches the Kernel
- Fibers: User-Mode Cooperative Threads
- Access Tokens and Privileges: The Kernel’s Security Context
- HAL and Ntoskrnl: The Kernel Core Components
- SIDs and Security Descriptors: Identity in Windows Security
References
- User Mode and Kernel Mode – Windows Drivers | Microsoft Learn
- Kernel-Mode Driver Architecture Design Guide – Windows Drivers | Microsoft Learn
- Exploitation for Privilege Escalation, Technique T1068 – Enterprise | MITRE ATT&CK®
- Privilege Escalation, Tactic TA0004 – Enterprise | MITRE ATT&CK®
- CPU Rings, Privilege, and Protection | Many But Finite (Gustavo Duarte)