DPCs: Deferred Procedure Calls and Interrupt Deferral
Objective: Understand how the Windows kernel uses Deferred Procedure Calls (DPCs) to move work out of high-IRQL interrupt service routines down to
DISPATCH_LEVEL, covering theKDPCstructure, IRQL mechanics, the full queue-to-callback lifecycle, threaded and timer DPCs, the DPC watchdog, and how defenders detect kernel-mode abuse of the DPC mechanism.
1. The Interrupt Deferral Problem
When a hardware device raises an interrupt, the kernel dispatches to an Interrupt Service Routine (ISR) running at DIRQL — a device IRQL higher than the scheduler itself. At that level the processor cannot wait, cannot touch pageable memory, and blocks all lower-priority interrupts on that CPU. An ISR that lingers degrades the entire system; the guidance is that ISRs should not run longer than 25 microseconds.
Windows therefore uses a two-phase interrupt model. The ISR does the minimum work needed to quiesce the device (acknowledge the interrupt, snapshot status), then schedules a Deferred Procedure Call to perform the heavier processing later, at a gentler IRQL. The DPC executes at DISPATCH_LEVEL, which is still too high for anything that touches pageable memory — but it is low enough to run the bulk of device servicing without starving other interrupts.
The essence of the DPC is deferring execution to gentler circumstances. It is the kernel’s primary tool for keeping ISRs short.
2. IRQL Levels: A Precise Map
The Interrupt Request Level (IRQL) is a per-processor priority that determines what code may run and what it may do. Any routine running at DISPATCH_LEVEL or above is not preemptable, runs to completion, and must reside in non-paged memory.
| IRQL Name | Value | Notes |
|---|---|---|
PASSIVE_LEVEL | 0 | Normal user/kernel thread execution; paging and waiting allowed |
APC_LEVEL | 1 | Asynchronous Procedure Calls |
DISPATCH_LEVEL | 2 | DPC execution, scheduler, spin locks; no paging, no waiting |
DIRQL | 3–11 (device-dependent) | Hardware ISRs run here |
An ISR at DIRQL cannot call functions that require PASSIVE_LEVEL. It instead schedules a DPC, which the kernel later runs at DISPATCH_LEVEL. Because DISPATCH_LEVEL still forbids page faults and blocking waits, a DPC routine and all data it touches must be non-paged.

3. The KDPC Structure Dissected
The KDPC is the structure in which the kernel keeps the state of a Deferred Procedure Call. It has always been explicitly undocumented — Microsoft labels it an opaque structure and warns drivers not to set members directly. The published layout from WDK/OSR headers is:
typedef struct _KDPC {
UCHAR Type; // DpcObject or ThreadedDpcObject
UCHAR Importance; // Low / Medium / High
USHORT Number; // target processor (directed DPCs)
LIST_ENTRY DpcListEntry; // links into per-processor DPC queue
PKDEFERRED_ROUTINE DeferredRoutine; // pointer to the callback function
PVOID DeferredContext; // driver-supplied context value
PVOID SystemArgument1; // extra arg passed to callback
PVOID SystemArgument2; // extra arg passed to callback
__volatile PVOID DpcData; // internal; pointer to KDPC_DATA
} KDPC, *PKDPC, *PRKDPC;| Field | Purpose |
|---|---|
Type | Distinguishes a normal DpcObject from a ThreadedDpcObject |
Importance | Controls queue insertion: MediumImportance = tail, HighImportance = head |
Number | Target logical processor, set via KeSetTargetProcessorDpc |
DeferredRoutine | Pointer to the KDEFERRED_ROUTINE callback |
DeferredContext | Opaque context the driver receives back in the callback |
SystemArgument1/2 | Caller-supplied arguments passed through to the callback |
DpcData | Volatile internal pointer to the per-processor KDPC_DATA; non-NULL while queued |
The DpcData field is the kernel’s bookkeeping anchor: before Windows 8.1 it pointed directly at a KDPC_DATA structure, and its non-NULL state indicates the DPC is currently queued. Because DeferredRoutine is a raw function pointer inside a writable structure, it is also a corruption target — covered in §10.
4. The DPC Lifecycle: From ISR to Callback
A DPC moves through four stages: allocate → initialize → queue → drain.
| API Function | Purpose |
|---|---|
KeInitializeDpc | Initializes a KDPC, binding a DeferredRoutine and DeferredContext |
KeInsertQueueDpc | Inserts the KDPC into the per-processor queue; returns FALSE if already queued |
IoRequestDpc | Convenience wrapper called from ISR context for the DpcForIsr pattern |
KeRemoveQueueDpc | Removes a pending (not-yet-fired) DPC from the queue |
Kernel code first allocates a KDPC in non-paged pool (or the device extension) so the object is resident when referenced from the ISR.
// C1 — allocate and initialize a DPC object
PKDPC pDpc = ExAllocatePool2(POOL_FLAG_NON_PAGED, sizeof(KDPC), 'cpDD');
if (pDpc) {
KeInitializeDpc(pDpc, MyCustomDpc, DeviceContext); // routine + context
}The callback must match the KDEFERRED_ROUTINE signature and runs at DISPATCH_LEVEL:
// C2 — DPC callback stub
VOID MyCustomDpc(
_In_ PKDPC Dpc,
_In_opt_ PVOID DeferredContext,
_In_opt_ PVOID SystemArgument1,
_In_opt_ PVOID SystemArgument2)
{
UNREFERENCED_PARAMETER(Dpc);
ASSERT(KeGetCurrentIrql() == DISPATCH_LEVEL); // invariant
// Non-paged, bounded work only — no waits, no page faults.
}The ISR queues the DPC. The return value of KeInsertQueueDpc enforces the single-instantiation guarantee: only one instance of a given KDPC can be queued at a time, so queuing it twice before it fires runs the routine once.
// C3 — queue from a mock ISR
BOOLEAN queued = KeInsertQueueDpc(pDpc, Arg1, Arg2);
if (!queued) {
// Already pending on a queue — the earlier request still stands.
}Device drivers commonly use the wrapper from inside their InterruptService routine:
// C4 — DpcForIsr pattern
BOOLEAN MyIsr(_In_ PKINTERRUPT Interrupt, _In_ PVOID Context) {
PDEVICE_OBJECT devObj = (PDEVICE_OBJECT)Context;
// ...acknowledge hardware quickly...
IoRequestDpc(devObj, devObj->CurrentIrp, NULL); // schedules DpcForIsr
return TRUE;
}When the processor returns from the interrupt, it checks its DPC queue; if entries are pending, the kernel raises IRQL to DISPATCH_LEVEL, drains the queue by invoking each DeferredRoutine, then lowers IRQL back down.

5. Per-Processor DPC Queues and KPRCB
Each logical processor owns a separate DPC queue, stored as a KDPC_DATA structure inside the processor’s KPRCB (Kernel Processor Control Block). This avoids cross-CPU locking on the common path.
KDPC_DATA carries the queue head, depth, count, and a spin lock:
typedef struct _KDPC_DATA {
LIST_ENTRY DpcListHead; // queued KDPC objects
ULONG DpcLock; // spin lock protecting the list
volatile ULONG DpcQueueDepth; // pending DPCs
ULONG DpcCount; // running total
} KDPC_DATA, *PKDPC_DATA;Exact
KDPC_DATAfield names vary by kernel build — confirm against a live PDB withdt nt!_KDPC_DATAbefore relying on offsets.
Because each queue is per-processor, the target processor of a DPC determines which CPU drains it. By default a DPC runs on the CPU that queued it, but it can be pinned elsewhere (§6) — a property attackers exploit to manipulate specific cores.

6. Controlling DPC Behaviour
| API Function | Purpose |
|---|---|
KeSetImportanceDpc | Sets Importance; HighImportance inserts at the queue head |
KeSetTargetProcessorDpc | Pins the DPC to a specific logical processor (directed DPC) |
KeRemoveQueueDpc | Dequeues a pending DPC; fails once the routine is already running |
DPCs have three priority levels — low, medium, high. Importance influences KeInsertQueueDpc: high-importance DPCs go to the head of the queue and are serviced first.
A directed DPC is created by binding it to a CPU before queuing. The pattern below — iterating over KeNumberProcessors and targeting each core — is the same primitive a rootkit weaponizes for CPU lockdown, so treat it as an educational illustration only:
// C5 — directed DPC setup (educational pattern)
for (CCHAR cpu = 0; cpu < KeNumberProcessors; cpu++) {
KeInitializeDpc(&pDpcArray[cpu], MyCustomDpc, NULL);
KeSetTargetProcessorDpc(&pDpcArray[cpu], cpu); // pin to logical CPU
KeSetImportanceDpc(&pDpcArray[cpu], HighImportance);
}Once a DPC begins executing it cannot be removed; KeRemoveQueueDpc only rescinds a still-pending entry.
7. Threaded DPCs
Since Windows Server 2003, a KDPC can represent either a normal DPC or a threaded DPC. In the threaded variant, the kernel — if it can arrange it — calls the routine back at PASSIVE_LEVEL from a highest-priority thread, allowing more flexible work. Support can be disabled, in which case the threaded DPC falls back to running at DISPATCH_LEVEL exactly like a normal DPC.
You initialize one with KeInitializeThreadedDpc and a CustomThreadedDpc routine. Because that routine can run at either PASSIVE_LEVEL or DISPATCH_LEVEL, it must synchronize correctly at both IRQLs:
// C7 — threaded DPC with dual-IRQL guard
KeInitializeThreadedDpc(&g_ThreadedDpc, MyThreadedDpc, NULL);
VOID MyThreadedDpc(_In_ PKDPC Dpc, _In_opt_ PVOID Ctx,
_In_opt_ PVOID A1, _In_opt_ PVOID A2) {
ASSERT(KeGetCurrentIrql() <= DISPATCH_LEVEL); // may be PASSIVE or DISPATCH
// Use locks valid at both levels.
}Threaded DPCs should be preferred over ordinary DPCs unless a particular DPC must never be preempted — not even by another DPC.
8. Timer DPCs and KTIMER
A DPC is also the callback mechanism for kernel timers. You associate a KDPC with a KTIMER and arm it; on expiry the kernel queues the DPC. KeSetTimerEx supports both one-shot and periodic timers.
// C6 — periodic timer DPC
KeInitializeTimerEx(&g_Timer, NotificationTimer);
KeInitializeDpc(&g_TimerDpc, MyCustomDpc, NULL);
LARGE_INTEGER due;
due.QuadPart = -10LL * 1000 * 1000; // 1 second, relative
KeSetTimerEx(&g_Timer, due, 1000 /* ms period */, &g_TimerDpc);Windows uses special timer DPCs internally for timer expiration and context switching. The same primitive — a recurring timer pointed at a non-paged callback — is the cleanest way a driver schedules background work, and the cleanest way a malicious driver re-enters its payload (§10).
9. The DPC Watchdog and Debugging
The kernel runs a DPC watchdog. Bug Check 0x00000133 (DPC_WATCHDOG_VIOLATION) fires when the watchdog detects either a single long-running DPC or a prolonged time spent at DISPATCH_LEVEL or above. The timing budgets are 100 microseconds for a DPC and 25 microseconds for an ISR. A malicious DPC spin-loop can therefore inadvertently trip the watchdog and crash the host.
Inspect live DPC state in the kernel debugger:
kd> !dpcs ; list pending DPCs per processor
kd> dt nt!_KDPC ; KDPC layout for this build
kd> dt nt!_KDPC_DATA ; per-processor queue structure
kd> !prcb ; processor control block (contains DpcData)
kd> !pcr ; processor control region!dpcs reveals each queued DPC’s DeferredRoutine address — the single most useful artifact, since an unknown or non-image-backed routine address is a strong anomaly.
10. Common Attacker Techniques
DPCs give kernel-mode malware a high-IRQL execution surface. Because code at DISPATCH_LEVEL is non-preemptable and runs to completion, it is ideal cover for Direct Kernel Object Manipulation (DKOM).
| Technique | Description |
|---|---|
| CPU lockdown / freeze-other-CPUs | Queue a directed KDPC to every non-current CPU via KeSetTargetProcessorDpc and spin, raising all secondary cores to DISPATCH_LEVEL to block interruption during a DKOM patch |
| Timer DPC payload | Arm a KTIMER whose DeferredRoutine points at attacker-controlled non-paged code, for recurring stealth execution |
| KDPC hijacking | Overwrite DeferredRoutine in a legitimate queued KDPC to redirect execution to a payload |
| Driver-based persistence | Load a malicious signed/BYOVD driver that registers a recurring timer DPC at load time |
The CPU-lockdown pattern is especially relevant to defenders: by parking every other core at DISPATCH_LEVEL, the rootkit can unlink processes, patch EDR callbacks, or hide drivers while no scheduler or AV thread can run.

11. Defensive Strategies & Detection
DPC objects live entirely in kernel memory and are not directly observable from user mode, so detection focuses on the driver that installs them and on kernel ETW timing telemetry.
Sysmon and Windows event telemetry:
| Event ID | Source | Relevance |
|---|---|---|
6 | Sysmon — Driver Loaded | Fires on every driver load; primary signal for kernel modules that register DPC routines |
7 | Sysmon — Image Loaded | Catches unsigned/anomalous modules entering kernel space |
7045 | Service Control Manager | New kernel-mode driver, especially from a non-standard path |
7040 | Service Control Manager | Service start-type change — driver persistence |
ETW providers: The NT Kernel Logger session with EVENT_TRACE_FLAG_DPC and EVENT_TRACE_FLAG_INTERRUPT records per-DPC timing and the routine address, exposing abnormally long-running or unknown-address DPC routines. Microsoft-Windows-Kernel-Processor-Power surfaces IRQL/watchdog events. Verify the exact flag constants against the current WDK evntrace.h.
Sigma anchor — unsigned/expired driver load:
title: Suspicious Kernel Driver Load (Unsigned or Expired)
logsource:
product: windows
service: sysmon
detection:
selection_unsigned:
EventID: 6
Signed: 'false'
selection_expired:
EventID: 6
SignatureStatus: 'Expired'
selection_path:
EventID: 6
ImageLoaded|contains: '\Temp\'
condition: selection_unsigned or selection_expired or selection_path
level: highHunt additionally for EventID 6 where ImageLoaded resolves outside \SystemRoot\System32\drivers\.
Hardening:
| Mitigation | Description |
|---|---|
| Driver Signature Enforcement (DSE) | Default on 64-bit Windows; blocks unsigned drivers that would install DPC routines |
| HVCI | Protects kernel code pages, raising the bar for DPC shellcode and DeferredRoutine overwrite |
| Kernel CET | Hardware shadow stack mitigates ROP-based DPC hijacking |
| DPC Watchdog | Built-in; Bug Check 0x133 catches long-running DPC loops, including malicious spin-locks |
| Vulnerable Driver Blocklist | HKLM\SYSTEM\CurrentControlSet\Control\CI\Config\VulnerableDriverBlocklistEnable blocks known BYOVD primitives |
| WDAC / Memory Integrity | Restrict which drivers may load, shrinking the DPC-abuse attack surface |
12. Tools for DPC Analysis
| Tool | Description | Link |
|---|---|---|
| WinDbg | !dpcs, dt nt!_KDPC, !prcb, !pcr live queue inspection | microsoft.com |
| Process Hacker | Driver/service enumeration and kernel module listing | processhacker.sourceforge.io |
| Windows Performance Recorder / xperf | Captures DPC/ISR ETW timing and routine addresses | microsoft.com |
| Sysmon | Driver-load (EID 6) and image-load (EID 7) telemetry | sysinternals.com |
| Volatility | Memory-forensic enumeration of drivers and kernel callbacks | volatilityfoundation.org |
| Ghidra | Static analysis of suspect drivers for KeInsertQueueDpc usage | ghidra-sre.org |
13. MITRE ATT&CK Mapping
| Technique | MITRE ID | Detection |
|---|---|---|
| Rootkit | T1014 | ETW DPC routine-address anomalies; !dpcs unknown routines |
| Boot/Logon Autostart: Kernel Modules | T1547.006 | Sysmon EID 6 / Event 7045 driver loads |
| Exploitation for Privilege Escalation | T1068 | HVCI/CET violations; KDPC.DeferredRoutine corruption |
| Impair Defenses: Disable/Modify Tools | T1562.001 | CPU-freeze DPC pattern halting EDR threads; watchdog 0x133 |
| Native API | T1106 | Driver use of KeInitializeDpc / KeInsertQueueDpc |
No dedicated ATT&CK sub-technique exists for DPC abuse as of ATT&CK v15; the techniques above are the parents. Verify current IDs at attack.mitre.org before publishing.
Summary
- DPCs are the kernel’s mechanism for deferring interrupt work from high-IRQL ISRs down to
DISPATCH_LEVEL, keeping ISRs under their 25 µs budget. - The opaque
KDPCstructure carries theDeferredRoutine, context, arguments, and aDpcDatapointer that marks whether it is queued on a per-processorKDPC_DATAlist in theKPRCB. - The lifecycle runs allocate →
KeInitializeDpc→KeInsertQueueDpc/IoRequestDpc→ per-CPU drain atDISPATCH_LEVEL, with a single-instantiation guarantee per object. - Rootkits abuse directed DPCs for CPU lockdown, timer DPCs for stealth re-entry, and
DeferredRoutinecorruption for hijacking — mapping toT1014,T1547.006, andT1562.001. - Detect via Sysmon Event ID 6 driver loads, NT Kernel Logger DPC timing telemetry, and the DPC watchdog (
0x133); harden with DSE, HVCI, Kernel CET, and the vulnerable driver blocklist.
Related Tutorials
- APCs: Asynchronous Procedure Calls and Thread Hijacking Surface
- IRQL Levels: Interrupt Request Priorities Explained
- System Calls and SSDT: How User Mode Reaches the Kernel
- Access Tokens and Privileges: The Kernel’s Security Context
- SIDs and Security Descriptors: Identity in Windows Security
References
- Introduction to DPC Objects – Windows Kernel Drivers | Microsoft Learn
- Managing Hardware Priorities (IRQL) – Windows Kernel Drivers | Microsoft Learn
- Deferred Procedure Call (DPC) – Kernel API Reference | Geoff Chappell, Software Analyst
- Deferred Procedure Call Details (KDPC Internals & Queuing Mechanisms) | OSR Online NT Insider
- Understanding Deferred Procedure Calls (DPCs) for Windows Vulnerability Research & Exploit Development | Medium / WaterBucket
- IRQLs: Close Encounters of the Rootkit Kind (DPCs & IRQL Abuse) | OffSec Blog
HAL and Ntoskrnl: The Kernel Core Components
Objective: Understand the architecture and division of labor between
hal.dll(the Hardware Abstraction Layer) andntoskrnl.exe(the NT kernel and Executive), how they are loaded during boot, the structures and routines each exposes, and how defenders inspect, detect tampering against, and harden these Ring 0 core components.
1. HAL and Ntoskrnl Overview
Two binaries sit at the bottom of Windows kernel mode and everything else builds on them. ntoskrnl.exe is the NT kernel plus the Executive — the policy and service layer of the OS. hal.dll is the Hardware Abstraction Layer — a thin platform shim that hides interrupt controllers, bus topology, timers, and DMA behind a uniform interface so the rest of the kernel stays hardware-independent.
| Binary | Full name | Loaded by | Ring |
|---|---|---|---|
ntoskrnl.exe | NT OS Kernel + Executive | winload.efi | Ring 0 |
hal.dll | Hardware Abstraction Layer | winload.efi | Ring 0 |
Both reside in %SystemRoot%\System32\. On multiprocessor systems the SMP-aware image ntkrnlmp.exe is selected by the loader and presented as ntoskrnl.exe; modern Windows 10/11 ships only the SMP variant. Verify image identity and signature on a live host with sigcheck, dumpbin /headers, or the WinDbg lm command. The separation exists for portability (HAL absorbs platform differences) and layering (the kernel implements scheduling and policy, not chipset quirks).
2. Boot Handoff: From Bootloader to KiSystemStartup
winload.efi loads ntoskrnl.exe and hal.dll into memory, then transfers control to the kernel entry point KiSystemStartup, passing a pointer to a LOADER_PARAMETER_BLOCK. That structure carries the memory descriptor list, the ARC hardware tree, NLS data, and other boot-time state the kernel needs before it can manage its own memory.
winload.efi
└─ loads ntoskrnl.exe + hal.dll
└─ ntoskrnl!KiSystemStartup(PLOADER_PARAMETER_BLOCK)
├─ HalInitializeProcessor() ; HAL brings up per-CPU hardware
├─ KiInitializeKernel() ; KPCR/KPRCB, IDT, GDT
├─ Executive phase init:
│ Mm/Ob/Se/Io/Cm/Ps InitSystem()
└─ PsInitialSystemProcess() ; System process (PID 4)
└─ Phase 1: smss.exe launchedHAL initializes the processor before the Executive runs a single line of policy code. Secure Boot validates the winload.efi → ntoskrnl.exe / hal.dll chain in firmware, so tampering with either binary on disk breaks the boot chain on a properly configured machine.

3. The HAL: Abstracting the Hardware
The HAL translates abstract requests into platform-specific operations: programming the APIC, translating bus-relative addresses, allocating DMA-coherent buffers, and calibrating the stall timer. Drivers and the kernel call HAL routines instead of touching hardware registers directly.
| Routine | Purpose |
|---|---|
HalGetInterruptVector | Translate a bus IRQ to a system interrupt vector and required IRQL |
HalTranslateBusAddress | Convert a bus-relative address to a logical address |
HalAllocateCommonBuffer | Allocate DMA-coherent memory visible to CPU and device |
KeStallExecutionProcessor | Calibrated busy-wait (HAL-implemented on most platforms) |
HalRequestSoftwareInterrupt | Request a software interrupt at a given IRQL to trigger DPC delivery |
On modern ACPI systems the HAL is far thinner than in the NT 4 era. Many classic Hal* exports such as HalGetInterruptVector are deprecated; the PnP/ACPI stack and IoConnectInterruptEx now handle interrupt wiring. Since Windows 8, HAL Extensions (halextpcat.dll, halextintc.dll, and similar PE images loaded by HAL itself) carry SoC- and OEM-specific code without replacing the whole HAL.
4. IRQL: The Kernel’s Preemption Ladder
Interrupt Request Level (IRQL) is the central arbitration mechanism shared by HAL and the kernel. The HAL programs the interrupt controller to enforce IRQL in hardware; running at an IRQL masks all interrupts at or below that level on the current CPU.
| IRQL (x64) | Symbolic name | Used for |
|---|---|---|
| 0 | PASSIVE_LEVEL | Normal thread execution |
| 1 | APC_LEVEL | APC delivery; paging allowed |
| 2 | DISPATCH_LEVEL | Scheduler, spin locks; no paging, no blocking |
| 3–12 | Device IRQLs | Hardware ISRs |
| 13 | CLOCK_LEVEL | Clock interrupt |
| 14 | PROFILE_LEVEL | Profiling interrupt |
| 15 | HIGH_LEVEL | NMI, machine check |
The cardinal rule: at DISPATCH_LEVEL or above you may not touch pageable memory or block, because the scheduler and page fault handler cannot run. A driver that dereferences paged-out memory at elevated IRQL produces the classic IRQL_NOT_LESS_OR_EQUAL bug check. Query the current level with KeGetCurrentIrql(). IRQL numeric values are architecture-specific; the table above is the canonical x64 mapping.

5. The Kernel Layer (Ke): Scheduling and Synchronization
The Ke layer sits directly above HAL and implements thread scheduling, interrupt and exception dispatch, and the low-level synchronization primitives the rest of the system depends on.
| Routine | What it does |
|---|---|
KeInitializeSpinLock | Initialize a spin-lock object |
KeAcquireSpinLock | Raise IRQL to DISPATCH_LEVEL and acquire the lock |
KeReleaseSpinLock | Release the lock and restore the saved IRQL |
KeInsertQueueDpc | Queue a Deferred Procedure Call |
KeWaitForSingleObject | Wait on a dispatcher object (event, mutex, timer, thread) |
KeSetEvent | Set a kernel event to the signaled state |
Dispatcher objects — events, mutexes, semaphores, timers, threads — share a common DISPATCHER_HEADER carrying Type, SignalState, and WaitListHead. The wait machinery keys off that header. The synchronization pattern below runs at PASSIVE_LEVEL, where blocking is legal:
KEVENT readyEvent;
KeInitializeEvent(&readyEvent, NotificationEvent, FALSE);
// ... another thread eventually calls KeSetEvent(&readyEvent, IO_NO_INCREMENT, FALSE);
NTSTATUS status = KeWaitForSingleObject(
&readyEvent, // dispatcher object
Executive, // wait reason
KernelMode, // processor mode
FALSE, // non-alertable
NULL); // no timeoutPer-CPU scheduler state lives in the KPCR (Kernel Processor Control Region), reachable via gs:[0] on x64, with an embedded KPRCB holding CurrentThread, NextThread, IdleThread, and the DPC queue.
6. The Executive Layer (Ex and Friends)
The Executive comprises the higher-level managers, each identified by a two-letter prefix. They build on Ke primitives and HAL services.
| Manager | Prefix | Responsibilities |
|---|---|---|
| Object Manager | Ob | Object lifecycle, handles, reference counting |
| Process/Thread Manager | Ps | EPROCESS/ETHREAD creation and teardown |
| Memory Manager | Mm | VAD trees, PTEs, page faults, pool |
| I/O Manager | Io | IRP lifecycle, driver loading |
| Security Reference Monitor | Se | Access checks, tokens, privileges |
| Configuration Manager | Cm | Registry hive management |
| Executive Support | Ex | Pool allocation, lookaside lists, callbacks |
Correct pool usage on modern Windows uses ExAllocatePool2 (the successor to ExAllocatePoolWithTag, deprecated starting Windows 10 build 19041) paired with ExFreePoolWithTag:
// Allocate non-paged pool with a 4-byte tag (read in WinDbg as 'XgAT').
PVOID buffer = ExAllocatePool2(POOL_FLAG_NON_PAGED, 0x1000, 'TAgX');
if (buffer != NULL) {
// ... use buffer at IRQL <= DISPATCH_LEVEL ...
ExFreePoolWithTag(buffer, 'TAgX');
}The Object Manager exposes ObReferenceObjectByHandle to convert a handle into a referenced kernel object pointer — the gateway every component crosses when validating access.
7. Key Kernel Structures
A handful of structures are the backbone of process, thread, and CPU state. Defenders and rootkit authors alike walk these every day.
| Structure | Key fields |
|---|---|
EPROCESS | UniqueProcessId, ActiveProcessLinks, Token, VadRoot, Peb, ImageFileName[15], ThreadListHead |
ETHREAD | Cid (CLIENT_ID), ThreadListEntry, Win32StartAddress, embedded KTHREAD |
KTHREAD | Header (DISPATCHER_HEADER), KernelStack, State, WaitIrql, Teb |
KPCR | Per-CPU; IRQL, IDT/GDT pointers, pointer to KPRCB |
KPRCB | CurrentThread, NextThread, IdleThread, DPC queue |
KDPC | DeferredRoutine, DeferredContext, DpcListEntry |
ActiveProcessLinks is a doubly linked LIST_ENTRY chaining every EPROCESS. The Task Manager view of “all processes” is, at bottom, a walk of this list. That makes it a prime DKOM target: unlinking an EPROCESS hides the process from list-based enumeration while it continues to run and be scheduled — covered in Section 10.
8. The SSDT and System Call Dispatch
A user-mode SYSCALL instruction transfers Ring 3 → Ring 0 and lands in ntoskrnl!KiSystemCall64. The dispatcher indexes the System Service Dispatch Table via KeServiceDescriptorTable, which points at KiServiceTable (an array of service routine offsets) and KiArgumentTable (argument byte counts). GUI calls into win32k.sys route through the shadow table KeServiceDescriptorTableShadow.
Patching KiServiceTable so a service index points at attacker code is the classic SSDT hook, historically used by rootkits to intercept NtQuerySystemInformation, NtOpenProcess, and similar. On x64 this is exactly the kind of structure modification PatchGuard validates, so SSDT hooking is loud and largely obsolete on modern systems — but understanding the dispatch path is essential for reading both live disassembly and integrity-check telemetry.

9. Live Analysis with WinDbg and Volatility
Load Microsoft symbols and the entire layout becomes navigable. List the core modules and dump structures directly:
0: kd> lm m nt ; ntoskrnl base, range, symbols
0: kd> lm m hal ; hal.dll base and range
0: kd> dt nt!_EPROCESS ; full EPROCESS field layout
0: kd> !process 0 0 ; enumerate processes via ActiveProcessLinks
0: kd> !pcr 0 ; KPCR for CPU 0
0: kd> !prcb 0 ; KPRCB: CurrentThread / IdleThread
0: kd> dps nt!KeServiceDescriptorTable ; SSDT pointer + service count
0: kd> !idt ; IDT vectors (HAL-programmed interrupt routing)For dead-box memory forensics, Volatility 3 reconstructs the same view from a dump and is the natural cross-check against a possibly compromised live host:
# Enumerate processes and loaded kernel modules from a memory image.
vol -f memory.dmp windows.pslist
vol -f memory.dmp windows.modules
# psscan walks pool tags instead of ActiveProcessLinks; a process that
# appears in psscan but NOT in pslist is a candidate DKOM-unlinked process.
vol -f memory.dmp windows.psscanA delta between windows.pslist (list-based) and windows.psscan (pool-scan-based) is a high-fidelity indicator of ActiveProcessLinks tampering.
10. Common Attacker Techniques
Kernel-core abuse turns on either modifying ntoskrnl structures from a loaded driver or exploiting a vulnerability to reach Ring 0 in the first place.
| Technique | Description |
|---|---|
| SSDT hooking | Patch KiServiceTable entries to intercept syscalls |
| DKOM unlinking | Splice an EPROCESS out of ActiveProcessLinks to hide a process |
| Kernel callback removal | Strip PsSetCreateProcessNotifyRoutine entries to blind EDR |
| BYOVD | Load a vulnerable signed driver to gain a Ring 0 primitive |
| Kernel exploitation | Abuse an ntoskrnl/HAL bug to escalate Ring 3 → Ring 0 |
| In-memory image patch | Patch ntoskrnl.exe code pages at runtime |
A malicious driver is still loaded through the documented path — a Services registry key of Type = 1 followed by a load — which is exactly where detection begins. Bring-Your-Own-Vulnerable-Driver remains popular precisely because it sidesteps the need to find a fresh kernel bug.

11. Defensive Strategies & Detection
Detection centers on driver loads, integrity events, and kernel structure cross-checks.
| Sysmon Event ID | Name | Relevance |
|---|---|---|
6 | Driver Loaded | Kernel driver load with Signed, Hashes, Signature fields |
7 | Image Loaded | Module loads in unusual contexts |
13 | Registry Value Set | New Services driver entries |
Pair Sysmon with Windows event sources: System Event ID 7045 (new kernel-mode service installed), Security Event ID 5038 (image hash invalid — DSE failure), and Event ID 6281 (page hash mismatch). The Microsoft-Windows-Kernel-Memory ETW provider surfaces pool allocations useful for hunting pool-based implants.
title: Suspicious Unsigned Kernel Driver Load
logsource:
product: windows
service: sysmon
detection:
selection:
EventID: 6
Signed: 'false'
filter_legit:
ImageLoaded|startswith:
- 'C:\Windows\System32\drivers\'
- 'C:\Windows\System32\DriverStore\'
condition: selection and not filter_legit
level: high| Mechanism | Description |
|---|---|
| PatchGuard (KPP) | Validates SSDT, IDT, GDT, KPCR, and kernel code; bug check 0x109 on tampering |
| Driver Signature Enforcement | ci.dll requires Authenticode-signed drivers |
| HVCI | VTL1 enforces signed Ring 0 code; blunts BYOVD and runtime patching |
| Secure Boot | Validates the winload → ntoskrnl/hal chain in firmware |
Operational hardening: enable HVCI (Core Isolation → Memory Integrity), confirm Secure Boot in msinfo32, audit SeLoadDriverPrivilege use, deploy the Microsoft Vulnerable Driver Blocklist (DriverSiPolicy.p7b), monitor HKLM\SYSTEM\CurrentControlSet\Services\ for new Type = 1 entries, and baseline loaded-module hashes against periodic WinPmem/Volatility snapshots.
12. MITRE ATT&CK Mapping
| Technique | MITRE ID | Detection |
|---|---|---|
| Rootkit | T1014 | Volatility pslist/psscan delta; PatchGuard bug check 0x109 |
| Kernel Modules and Extensions | T1547.006 | Sysmon EID 6; Event ID 7045; Services key writes |
| Exploitation for Privilege Escalation | T1068 | Crash telemetry, anomalous Ring 0 transitions |
| Impair Defenses | T1562.001 | Missing kernel callbacks; EDR self-protection alerts |
| Process Injection | T1055 | Kernel KeStackAttachProcess/MmCopyVirtualMemory use |
| Modify System Image | T1601.001 | Code integrity Event ID 5038/6281; PatchGuard |
13. Tools for Kernel Analysis
| Tool | Description | Link |
|---|---|---|
| WinDbg | Live and dump kernel debugging, structure walks | microsoft.com |
| Volatility 3 | Memory forensics, pslist/psscan/modules | volatilityfoundation.org |
| WinPmem | Live memory acquisition | github.com |
| Process Hacker | Driver and handle inspection | processhacker.sourceforge.io |
| Sysmon | Driver-load and registry telemetry | sysinternals.com |
| sigcheck | Image signature and hash verification | sysinternals.com |
| Ghidra | Static analysis of drivers and ntoskrnl | ghidra-sre.org |
14. Summary
- HAL and ntoskrnl are the two Ring 0 binaries every other Windows component is built on — HAL abstracts hardware, ntoskrnl implements the kernel and Executive policy layers.
- The kernel layer (
Ke) supplies scheduling and synchronization; the Executive (Ob,Ps,Mm,Io,Se,Cm,Ex) builds managers on top, all arbitrated by IRQL that the HAL enforces in hardware. - Core structures —
EPROCESS,ETHREAD,KPCR, the SSDT — are the backbone of process and CPU state and the prime targets for SSDT hooks, DKOM unlinking, and callback removal. - Detect kernel tampering via Sysmon Event ID
6, Event IDs7045/5038/6281, and Volatility pslist-vs-psscan deltas; prevent it with HVCI, DSE, Secure Boot, and the vulnerable-driver blocklist.
Related Tutorials
- Access Tokens and Privileges: The Kernel’s Security Context
- System Calls and SSDT: How User Mode Reaches the Kernel
- User Mode vs Kernel Mode: Privilege Rings and the Boundary
- SIDs and Security Descriptors: Identity in Windows Security
- Fibers: User-Mode Cooperative Threads
References
- Windows Kernel-Mode HAL Library – Microsoft Learn (Windows Drivers)
- Windows Kernel-Mode Kernel Library – Microsoft Learn (Windows Drivers)
- Overview of Windows Components (Kernel-Mode) – Microsoft Learn
- User Mode and Kernel Mode – Microsoft Learn (Windows Drivers)
- Boot or Logon Autostart Execution: Kernel Modules and Extensions (T1547.006) – MITRE ATT&CK
- Deeper into Windows Architecture (HAL, ntoskrnl, Executive) – Microsoft Learn Archive