#Kernel exploitation is not about finding one bug. It is about understanding a layered defense architecture that has been accumulating for two decades — and then finding the gap. This post is a map of that architecture.
#The Privilege Model
Before discussing defenses, you need to understand what is being defended. The x86/x64 architecture defines four protection rings — Ring 0 through Ring 3 — but modern Windows uses only two: Ring 0 (kernel mode) and Ring 3 (user mode). Rings 1 and 2 are vestigial; they exist in the architecture but no mainstream OS has ever used them.
What kernel mode actually means is a CPU mode bit. The Current Privilege Level (CPL) is stored in bits 0–1 of the Code Segment (CS) selector. CPL=0 means kernel mode. CPL=3 means user mode. That is the entire distinction — one two-bit field in one segment register — and it gates everything.
| Privilege Level | CPL | Access |
|---|---|---|
| Ring 0 (Kernel Mode) | 0 | All physical memory, privileged instructions (WRMSR, MOV CR, HLT, INVD), control register manipulation, IDT/GDT/LDT modification, I/O port access, all kernel address space |
| Ring 3 (User Mode) | 3 | Virtual address space only (lower half), no privileged instructions, no direct hardware access, must use syscalls for any kernel operation |
The practical implication is stark: a user-mode attacker who achieves arbitrary code execution still operates at CPL=3. They cannot read kernel memory. They cannot modify page tables. They cannot disable mitigations stored in model-specific registers (MSRs). Every defense mechanism covered in this post exists to prevent the transition from CPL=3 to CPL=0 — or to contain the damage if that transition occurs.
Virtual Trust Levels (VTLs) add a second dimension to this model. With Virtualization-Based Security (VBS) enabled, the hypervisor partitions the system into VTL 0 (the "normal world" where the regular Windows kernel runs) and VTL 1 (the "secure world" hosting securekernel.exe). The hypervisor enforces memory access policy via Second Level Address Translation (SLAT/EPT): VTL 0 cannot read or write VTL 1 memory, even with Ring 0 privileges. This creates what is effectively a "Ring -1" — a privilege level below the kernel that the kernel itself cannot reach.
#The System Call Mechanism
If Ring 0 is the target, the system call is the door. It is the only legitimate mechanism for a user-mode thread to transition into kernel mode. Understanding this mechanism is non-negotiable: every kernel exploit either abuses a vulnerable syscall or finds a way around it.
#The Syscall Path
On x64 Windows, the syscall instruction is the entry point. Here is the full path from user-mode call to kernel function:
The syscall instruction does three things atomically: loads RIP from the LSTAR MSR (which points to KiSystemCall64), saves the current RFLAGS into R11, and saves the return RIP into RCX. The kernel then swaps the GS base register to access the current processor's Processor Control Region (PCR), builds a trap frame capturing the user-mode context, indexes into the System Service Descriptor Table (SSDT) using the syscall number in EAX, and dispatches to the target function.
The SSDT is the kernel's syscall dispatch table. KeServiceDescriptorTable maps syscall numbers to Nt* function addresses for the core NT syscalls. KeServiceDescriptorTableShadow adds a second table for Win32k GDI syscalls (used when a GUI thread is active). Each entry is a 4-byte offset from the SSDT base, encoded as (target - base) >> 4.
The Nt* vs Zw* distinction matters. Both call the same kernel function, but Zw* variants set the PreviousMode field in the thread object to KernelMode (0), while Nt* variants (called from user mode via syscall) set it to UserMode (1). Kernel functions use PreviousMode to decide whether to trust pointer parameters — a UserMode previous mode triggers probe-and-copy validation. This is why a kernel exploit that calls ZwOpenProcess from a kernel context bypasses handle access checks: the kernel trusts itself.
#Direct Syscall Evasion
The syscall path above assumes the call originates from ntdll.dll. But a process can resolve the syscall number independently and issue syscall from its own code:
// Direct syscall stub — bypasses every user-mode hook
// SSN resolved from ntdll.dll on disk or from a running process
__declspec(naked) NTSTATUS NtAllocateVirtualMemory_Direct(
HANDLE ProcessHandle,
PVOID* BaseAddress,
ULONG_PTR ZeroBits,
PSIZE_T RegionSize,
ULONG AllocationType,
ULONG Protect)
{
__asm {
mov r10, rcx // First argument to r10
mov eax, 0x18 // SSN for NtAllocateVirtualMemory (build-dependent)
syscall // Enter kernel — ntdll.dll never touched
ret
}
}When this executes, the user-mode RIP captured in the trap frame is not within ntdll.dll. It is within the calling process's own .text section. This is the fundamental evasion primitive: every user-mode hook (EDR, API monitor, ETW provider) sits above the syscall instruction. Direct syscall invocation goes under all of them.
#Key Syscalls for Exploitation
| Syscall | Typical SSN Range | Relevance |
|---|---|---|
NtAllocateVirtualMemory | 0x18 | Memory allocation for shellcode/injection |
NtWriteVirtualMemory | 0x3A | Cross-process memory write |
NtCreateThreadEx | 0xC2 | Remote thread creation |
NtOpenProcess | 0x26 | Handle acquisition with desired access |
NtQuerySystemInformation | 0x36 | System-wide information (process list, module list, kernel pointers pre-RS4) |
NtDeviceIoControlFile | 0x07 | IOCTL dispatch to kernel drivers — the primary BYOVD interface |
NtQueryInformationProcess | 0x19 | Process-specific information (debug port, PEB address, mitigation flags) |
NtProtectVirtualMemory | 0x50 | Memory protection changes (RWX transitions) |
SSN values are build-dependent and must be resolved dynamically. The canonical approach is to parse ntdll.dll's export table: each Nt* stub begins with mov eax, <SSN>, and the SSN is the immediate operand at offset +1 from the stub address.
#Kernel Address Space
On x64 Windows, the virtual address space is split: user mode owns the lower half (0x0000000000000000 through 0x00007FFFFFFFFFFF), and kernel mode owns the upper half (0xFFFF8000`00000000 and above). This split is not a security boundary in itself — it is a performance optimization. Every process has the kernel mapped into its upper address space, so a context switch does not require a CR3 reload for kernel memory accesses. The kernel is always there.
What lives in kernel address space is everything. The kernel image (ntoskrnl.exe), the HAL, every loaded driver, the SSDT, the IDT, the GDT, all page tables (accessible via the self-referencing PML4 entry at 0xFFFFF6FB...), the PFN database (tracking every physical page), pool allocations, system PTEs, and session space. An arbitrary read primitive in the kernel gives you access to all of it. An arbitrary read in user mode gives you nothing from kernel space.
KVA Shadow (the Windows implementation of the Meltdown mitigation) changes this picture. On affected CPUs, KVA Shadow maintains separate user-mode and kernel-mode page tables. When the CPU is in Ring 3, the kernel's page table entries are unmapped — only a minimal "trampoline" set of kernel pages (the syscall entry/exit code, the GDT, the IDT) remains mapped. This means a Meltdown-style speculative execution attack from user mode cannot leak kernel memory: the pages are not present in the page tables the CPU is walking.
The practical implication for exploitation: KVA Shadow also hardens KASLR. Before KVA Shadow, kernel pointers were present in user-mode page tables (marked as supervisor-only, but the addresses were there). A speculative side-channel could leak them. After KVA Shadow, the kernel's page table entries are simply absent when the CPU is in Ring 3. You cannot leak what is not mapped.
#Token Security and Integrity Levels
The access token is the prize. Every kernel exploit that targets privilege escalation is ultimately trying to obtain a token with higher privileges than the attacker's current process. Understanding the token model is understanding what you are trying to steal.
#The Access Token
A Windows access token is a kernel object (_TOKEN) that contains:
- User SID — who you are
- Group SIDs — which groups you belong to (includes integrity level as a mandatory label SID)
- Privileges — a bitmap of enabled and present privileges (
SeDebugPrivilege,SeLoadDriverPrivilege, etc.) - Token type — Primary (attached to a process) or Impersonation (attached to a thread, representing a different user)
- Session ID — which logon session this token belongs to
- Mandatory label — the integrity level SID (stored as an ACE in the token's default DACL)
- Logon session reference — links to the LSA logon session
Every process has a primary token, accessible via _EPROCESS.Token. Every thread can optionally have an impersonation token, accessible via _ETHREAD.Tcb.ImpersonationInfo. A thread with an impersonation token operates with the impersonated user's identity for access checks.
#Integrity Levels
Windows assigns every securable object and every process token a mandatory integrity label. The integrity level is stored as a SID in the token's mandatory label ACE:
| Integrity Level | SID Suffix | Typical Use |
|---|---|---|
| Untrusted | S-1-16-0x0 | Sandboxed processes with no access |
| Low | S-1-16-0x1000 | Internet-facing processes (browsers) |
| Medium | S-1-16-0x2000 | Standard user processes (default) |
| High | S-1-16-0x3000 | Elevated (administrator) processes |
| System | S-1-16-0x4000 | SYSTEM services and kernel |
| Protected Process | S-1-16-0x5000+ | Protected processes (PPL) |
The Mandatory Integrity Control (MIC) policy is simple: a process can write to objects at or below its own integrity level, and read from objects at or above. This is enforced in addition to discretionary ACL checks. Even an administrator running at High integrity cannot write into a System-integrity process.
#The Privileges That Matter
// Key privileges for exploitation — from winnt.h
#define SE_DEBUG_PRIVILEGE (20L) // Open any process with full access
#define SE_LOAD_DRIVER_PRIVILEGE (10L) // Load kernel drivers (DSE bypass via policy)
#define SE_IMPERSONATE_PRIVILEGE (29L) // Impersonate any user (including SYSTEM)
#define SE_ASSIGNPRIMARYTOKEN_PRIVILEGE (3L) // Replace a process's primary token
#define SE_TCB_PRIVILEGE (7L) // "Act as part of the OS" — bypasses most checksSeDebugPrivilege is the most commonly targeted: it allows opening any process (including SYSTEM and PPL processes below the caller's PPL level) with PROCESS_ALL_ACCESS. A process running as Administrator has this privilege present but disabled. Enabling it requires AdjustTokenPrivileges, which itself requires the privilege to be present. The catch-22 is resolved by running as Administrator.
#Token Stealing — The Classic Endgame
The canonical kernel exploit payload walks the _EPROCESS linked list to find the System process (PID 4), copies its token pointer, and overwrites the attacker's process token:
// Classic token-stealing payload — kernel context
// This is what every kernel exploit ultimately wants to do
PEPROCESS targetProcess; // Attacker's process
PEPROCESS systemProcess; // System (PID 4)
// 1. Locate System process via PsInitialSystemProcess or ActiveProcessLinks
systemProcess = PsInitialSystemProcess; // Exported kernel variable
// 2. Get the System token
PACCESS_TOKEN systemToken = PsReferencePrimaryToken(systemProcess);
// 3. Overwrite target process token
// _EPROCESS.Token is at a version-dependent offset
*(PACCESS_TOKEN*)((PUCHAR)targetProcess + TOKEN_OFFSET) = systemToken;
// 4. The attacker's process is now SYSTEM
// Next user-mode call runs as SYSTEMThis payload is the reason PatchGuard, KDP, SMEP, and HVCI exist. Every defense in this post is designed to prevent an attacker from reaching the point where these four lines of C can execute.
#Protected Processes (PPL)
Protected Process Light (PPL) adds a signer level to _EPROCESS.Protection. The Protection field is a bitfield encoding both the protection type (Protected Light, Protected) and the signer level:
// _PS_PROTECTION structure (embedded in _EPROCESS.Protection)
typedef struct _PS_PROTECTION {
UCHAR Level; // 3 bits: signer level
UCHAR Type; // 2 bits: 0=None, 1=Protected Light, 2=Protected
// ...
} PS_PROTECTION;
// Signer levels (PsProtectedSigner)
// 0 = None, 1 = Authenticode, 2 = CodeGen, 3 = Antimalware,
// 4 = Lsa, 5 = Windows, 6 = WinTcbA process with PPL at signer level WinTcb (6) cannot be opened with PROCESS_VM_READ by any process at a lower signer level — even if that process is SYSTEM. This is enforced in the kernel's ObpIncrementHandleCountEx during handle creation. The practical implication: even a kernel exploit that achieves arbitrary read/write cannot directly open a handle to a PPL-WinTcb process. It must either disable PPL protection (by modifying the _EPROCESS.Protection field directly, which requires a kernel write primitive) or find another way to access the protected process's memory.
#Boot-Time Integrity
Before the kernel runs, it must be loaded. The boot chain is the sequence of components that execute from power-on to the Windows desktop, and each transition in this chain is a potential attack point. Windows implements four overlapping mechanisms to protect the boot chain.
Secure Boot is the enforcement mechanism. UEFI firmware verifies the digital signature of each boot component against a set of trusted certificates before allowing it to execute. The key hierarchy:
Platform Key (PK) ← Owned by the hardware vendor / enterprise
│
▼
Key Exchange Key (KEK) ← Owned by Microsoft / enterprise
│
▼
Signature Database (db) ← Allowed signers (Microsoft, trusted third parties)
Forbidden Database (dbx) ← Explicitly blocked signers (revoked certs, known malware)
The firmware checks the bootloader's signature against db before execution. If the signature is not in db or is in dbx, the firmware refuses to boot. This prevents bootkits — malware that installs itself as a bootloader — and unauthorized OS loaders. What Secure Boot does not prevent: signed but vulnerable drivers loaded post-boot, firmware vulnerabilities below UEFI, and physical attacks on the SPI flash chip storing the firmware.
Measured Boot is the attestation mechanism. As each boot component loads, its hash is sent to a Trusted Platform Module (TPM) Platform Configuration Register (PCR). The TPM accumulates these measurements in PCR banks, creating a cryptographic record of the boot chain. A remote attestation service can later query the TPM (via a signed quote) and verify that the boot chain matches a known-good state. The distinction from Secure Boot is critical: Secure Boot enforces (refuses to boot if verification fails); Measured Boot attests (records what happened for later verification). A system can pass Secure Boot and still fail remote attestation if a measured component was tampered with in a way that did not break the signature chain.
Early Launch Anti-Malware (ELAM) gives a designated anti-malware driver first-mover advantage. ELAM drivers load before any other third-party boot-start drivers, allowing them to inspect and classify every driver as it initializes. The classification API:
// ELAM driver classifies each subsequent driver image
// Return value determines whether the driver is allowed to initialize
typedef enum _ELAM_DRIVER_CLASSIFICATION {
ElamDriverClassificationGood, // Known good — allow
ElamDriverClassificationBad, // Known bad — block
ElamDriverClassificationUnknown // Unknown — depends on policy
} ELAM_DRIVER_CLASSIFICATION;The ELAM driver receives a callback for each driver image before its entry point executes. It can inspect the image's signature, hash, and metadata, and return a classification. The kernel enforces the classification based on policy: "Good" drivers always load, "Bad" drivers never load, and "Unknown" drivers load or not depending on the configured policy. The limitation: ELAM is a policy enforcement point, not a detection mechanism. Its effectiveness depends entirely on the ELAM driver's classification logic. A compromised or outdated ELAM driver provides no protection.
Driver Signature Enforcement (DSE) requires every kernel-mode driver to carry a valid digital signature that chains to a trusted root certificate. The Code Integrity subsystem (ci.dll in user mode, CI.dll in kernel mode) validates signatures at load time via CiValidateImageHeader and CiCheckSignedFile. A driver that fails validation is not mapped into kernel memory.
The signing landscape has evolved:
- Cross-signed certificates (deprecated): A code-signing certificate issued by a third-party CA that chains to Microsoft's root. No longer accepted on Windows 11.
- Attestation Signing: Microsoft's developer portal signs the driver after basic automated checks. The signature is from Microsoft, not the developer. This is the standard path for development and testing.
- WHQL (Windows Hardware Quality Labs): Full certification with Microsoft's hardware compatibility testing. Required for production drivers distributed through Windows Update.
Test-signing mode (bcdedit /set testsigning on) disables DSE for development. A test-signed driver (signed with a self-generated certificate) loads only when test-signing is enabled. This is the standard kernel development workflow, but it requires boot configuration changes and a reboot.
#Kernel Runtime Integrity
Once the kernel is running, a second layer of defenses protects it from modification. These are the mechanisms that make SSDT hooking, inline patching, and kernel code injection detectable or impossible.
#Kernel Patch Protection (PatchGuard)
PatchGuard is the most well-known and most misunderstood Windows defense. It is not a cryptographic guarantee. It is a timer-based probabilistic defense that makes persistent kernel modification unreliable.
What PatchGuard protects: The SSDT, the IDT, the GDT, critical MSR values (LSTAR, CSTAR, SF_MASK), the kernel's .text section (ntoskrnl.exe and HAL), and certain critical processor control structures. It does not protect: third-party driver code, pool allocations, the _EPROCESS linked list, or any data structure not on its protected list.
How PatchGuard works: A kernel thread (context named KeCompactServiceTable, though the name is a decoy) periodically computes cryptographic checksums of protected regions and compares them against known-good values. The checksum algorithm, the check timing, and the thread's location are all randomized and obfuscated. A mismatch triggers KeBugCheckEx with CRITICAL_STRUCTURE_CORRUPTION (bugcheck code 0x109).
The practical implication: an SSDT hook (like the one Nanga installs) will trigger PatchGuard — not immediately, but within a randomized interval (typically seconds to minutes). The system bluescreens. This makes persistent SSDT hooking infeasible on x64 Windows without also disabling PatchGuard, which itself requires kernel code execution (a chicken-and-egg problem) or a BYOVD driver with arbitrary physical memory access.
The BYOVD bypass: PatchGuard protects the running kernel from modification. It does not protect against a signed driver that is itself malicious. A driver with arbitrary physical memory access can locate PatchGuard's data structures in physical memory and disable them, or can modify kernel structures directly without triggering PatchGuard's checks (if it knows which structures are checked and how). This is the BYOVD attack pattern: use a signed vulnerable driver to gain physical memory access, then use that access to bypass PatchGuard.
#Code Integrity (CI)
Code Integrity validates the digital signature of every kernel-mode image before it is mapped into memory, and — with HVCI enabled — validates page hashes at page-in time. The CI subsystem operates at multiple levels:
- User-mode CI (
ci.dll): Validates user-mode executable images before they are loaded. Enforces WDAC policies for user-mode code. - Kernel-mode CI (
CI.dll): Validates kernel-mode images at load time. The kernel-mode CI library is loaded early in boot and is itself protected by PatchGuard. - CI minifilter: A file system minifilter that intercepts image load operations and validates signatures before the image is mapped.
When a driver is loaded, CI computes its Authenticode hash, verifies the signature chain against the trusted root store, and checks the signing certificate against the forbidden database (dbx). If any check fails, the driver is not loaded. With HVCI enabled, CI also verifies page hashes at page-in time: every executable page is checked against the CI catalog before the hypervisor allows it to become executable.
#Kernel Address Space Layout Randomization (KASLR)
KASLR randomizes the base address of ntoskrnl.exe, hal.dll, and boot-start drivers at each boot. On x64 Windows, the kernel image is loaded at a random offset within a 1 GB region, providing approximately 10 bits of entropy (256 possible base addresses).
The entropy is limited by architectural constraints. The kernel must be loaded in the upper half of the virtual address space, and certain structures (like the HAL heap) require predictable relative offsets. The result is that KASLR is a probabilistic defense, not a cryptographic one. A kernel pointer leak defeats it deterministically.
KASLR bypass techniques:
NtQuerySystemInformationwithSystemModuleInformation: Before Windows 10 RS4 (1803), this returned the kernel base address to any caller. Post-RS4, it requiresSeDebugPrivilege.- Side-channel attacks: CPU vulnerabilities (Meltdown, Spectre, and their variants) can leak kernel pointers through speculative execution side-channels. KVA Shadow mitigates the Meltdown vector but not all side-channel variants.
- Physical memory analysis: If an attacker has physical memory access (via BYOVD or DMA), they can locate the kernel image in physical memory and compute its virtual address from the page table structures.
- Kernel pool leaks: A kernel memory disclosure bug (e.g., an uninitialized pool allocation returned to user mode) can leak kernel pointers, including the kernel base.
The practical implication: a kernel ROP chain needs gadget addresses. Without a KASLR bypass, the exploit must either guess the kernel base (1/256 chance per attempt) or find a way to leak it. Most real-world kernel exploits chain a KASLR bypass (via an information leak) with a write primitive.
#Kernel Data Protection (KDP)
KDP uses the hypervisor to mark certain kernel data pages as read-only via Second Level Address Translation (SLAT/EPT). Unlike PatchGuard, which detects modification after the fact, KDP prevents modification at the hardware level.
The mechanism: KDP identifies critical kernel data structures (the SSDT, certain CI policy structures, driver configuration data) and requests the secure kernel (VTL 1) to mark their physical pages as read-only in the EPT. Any attempt to write to these pages — even from Ring 0 in VTL 0 — triggers an EPT violation that the hypervisor handles. The hypervisor can terminate the write, log the attempt, or trigger a bugcheck.
The difference from PatchGuard is fundamental: PatchGuard is a detective control (it catches you after you modify something). KDP is a preventive control (it stops you from modifying it in the first place). The trade-off is that KDP requires VBS to be enabled, which has a performance cost and hardware requirements.
#Kernel Control Flow Guard (kCFG)
kCFG extends user-mode Control Flow Guard to the kernel. Every indirect call target in kernel mode must be a valid function entry point registered in the CFG bitmap. The mechanism:
// Simplified kCFG check — the real implementation is in the kernel's
// __guard_check_icall_fptr, called before every indirect call
void __guard_check_icall_fptr(void* target) {
// Check if 'target' is a valid function entry in the CFG bitmap
// The bitmap is a bit-per-address map of valid indirect call targets
if (!BitMap_IsValidEntry(g_CfgBitmap, (ULONG_PTR)target)) {
// Target is not a valid function entry — fastfail
KeBugCheckEx(KERNEL_SECURITY_CHECK_FAILURE, ...);
}
}kCFG protects forward-edge control flow: indirect calls (call rax) and indirect jumps (jmp rbx). It does not protect return addresses on the stack — that is CET's job. The practical implication: a kernel exploit that overwrites a function pointer must point it at a valid, CFG-registered function entry. This constrains JOP (Jump-Oriented Programming) and COP (Call-Oriented Programming) chains but does not eliminate them: the attacker can still use any valid function entry as a gadget, and many valid functions have useful side effects when called with attacker-controlled arguments.
#CPU-Enforced Mitigations
The most powerful defenses are not in software. They are in silicon. The CPU enforces these mitigations on every instruction, every memory access, every control flow transfer. They cannot be disabled by software running at the same or lower privilege level.
#Supervisor Mode Execution Prevention (SMEP)
SMEP is controlled by bit 20 of CR4. When set, the CPU refuses to fetch instructions from user-mode pages (pages with the User/Supervisor bit set in the page table) while in Ring 0. The check happens at the MMU on every instruction fetch:
Instruction Fetch in Ring 0:
CPL = 0 (kernel mode)
Page U/S bit = 1 (user-mode page)
CR4.SMEP = 1
→ #PF (Page Fault) — instruction fetch blocked
What SMEP kills: the classic technique of allocating shellcode in user mode, obtaining a kernel write primitive, overwriting a kernel function pointer with the shellcode's user-mode address, and triggering the function call. The CPU is in Ring 0, the shellcode is in a user-mode page, and SMEP says no.
The bypass: Return-Oriented Programming (ROP). SMEP prevents execution of user-mode pages; it does not prevent execution of kernel code at attacker-chosen addresses. A ROP chain uses only kernel .text gadgets — short sequences of kernel instructions ending in ret — chained together by placing their addresses on the stack. Every gadget is in a kernel page (U/S=0), so SMEP allows execution. SMEP raised the bar from "write shellcode anywhere" to "find and chain kernel gadgets," but it did not eliminate kernel exploitation.
#Supervisor Mode Access Prevention (SMAP)
SMAP is controlled by bit 21 of CR4. When set, the CPU refuses to read or write user-mode pages while in Ring 0, unless the AC (Alignment Check) flag in RFLAGS is set. The STAC (Set AC) and CLAC (Clear AC) instructions control this flag:
// Kernel's copy_from_user pattern — explicit SMAP bypass
NTSTATUS ReadUserMemory(PVOID userAddress, PVOID kernelBuffer, SIZE_T size) {
STAC(); // Allow access to user-mode pages
__try {
ProbeForRead(userAddress, size, 1);
RtlCopyMemory(kernelBuffer, userAddress, size);
}
__except(EXCEPTION_EXECUTE_HANDLER) {
return GetExceptionCode();
}
CLAC(); // Disallow access again
return STATUS_SUCCESS;
}What SMAP kills: kernel exploits that pass a user-mode pointer to a kernel function expecting a kernel pointer, hoping the kernel will dereference it. With SMAP, the dereference faults. The attacker cannot trick the kernel into reading a fake _EPROCESS structure from user mode. They must find a way to write their data into kernel memory first — which requires a kernel write primitive, which is the thing they are trying to achieve.
The difference from SMEP: SMEP blocks instruction fetch from user pages. SMAP blocks data access (reads and writes) to user pages. Together, they mean the kernel can neither execute nor access user-mode memory unless it explicitly enables access via STAC/CLAC. An attacker who controls kernel RIP cannot simply redirect execution to user-mode shellcode (SMEP) or point the kernel at a user-mode fake structure (SMAP).
#Control-flow Enforcement Technology (CET)
CET is Intel's hardware answer to control-flow hijacking. It has two components, both available on Intel Tiger Lake (11th gen) and later, and AMD Zen 3 and later.
CET Shadow Stack protects return addresses. The CPU maintains a second, hardware-managed stack (the shadow stack) that contains only return addresses. On every call instruction, the CPU pushes the return address to both the regular stack and the shadow stack. On every ret instruction, the CPU pops the return address from both stacks and compares them. A mismatch triggers a #CP (Control Protection) exception.
CET Indirect Branch Tracking (IBT) protects forward-edge control flow. The CPU expects an ENDBR64 instruction at every valid indirect branch target. A jmp or call to an address that does not start with ENDBR64 triggers #CP. This kills JOP and COP chains: every gadget in a JOP chain is a mid-function instruction sequence, not a function entry point, and none of them start with ENDBR64.
The combined effect: CET kills ROP (shadow stack catches return address mismatches) and JOP/COP (IBT catches indirect branch target mismatches). On CET-enabled hardware with CET-aware software, control-flow hijacking via corrupted pointers is dead. The attacker needs a different primitive — data-only attacks, or finding a way to disable CET (which requires a kernel write to the CET MSRs, which requires kernel code execution, which is the thing CET prevents).
Windows CET support: Windows 10 20H2+ and Windows 11 enable kernel-mode CET (branded as "Hardware-enforced Stack Protection") on supported hardware. User-mode CET is opt-in via the /CETCOMPAT linker flag. Microsoft Edge and other critical processes enable it.
#Virtualization-Based Security
VBS is the most significant architectural change to Windows security since the NT kernel itself. It moves the trust boundary from Ring 0 to the hypervisor. On a VBS-enabled system, even full Ring 0 code execution does not give the attacker access to credentials, the ability to load unsigned code, or the ability to disable defenses.
#VBS Architecture
VBS uses the Hyper-V hypervisor to partition the system into Virtual Trust Levels (VTLs). Each VTL is a separate execution environment with its own kernel, memory protections, and trust assumptions:
The hypervisor enforces memory access policy via SLAT/EPT. VTL 0 cannot read or write VTL 1 memory under any circumstances — not even with Ring 0 privileges. VTL 1 can access VTL 0 memory (it is the more privileged world). VTL transitions occur via VMCALL (from VTL 0 to VTL 1) or hypervisor-mediated hypercalls.
The secure kernel (securekernel.exe) runs in VTL 1 and manages security-sensitive operations: KDP page protections, HVCI policy enforcement, and Credential Guard secret isolation. It is a minimal kernel — no third-party drivers, no plug-and-play, no network stack. Its attack surface is orders of magnitude smaller than the normal kernel.
Hardware requirements: VBS requires SLAT/EPT support (Intel VT-x with EPT, AMD-V with NPT), an I/O MMU (Intel VT-d, AMD-Vi), and TPM 2.0. These are mandatory for Windows 11.
#Hypervisor-Protected Code Integrity (HVCI)
HVCI — branded as "Memory Integrity" in the Windows Security app — enforces that all kernel-mode code is signed and unmodified. The mechanism is simple and brutal:
- When kernel-mode code attempts to make a page executable, the hypervisor intercepts the EPT violation.
- The hypervisor verifies the page's contents against the CI catalog. The page must match a known, signed image.
- If the page is unsigned or has been modified, the hypervisor refuses to make it executable. The page remains NX (No-Execute).
- If the page is signed and unmodified, the hypervisor sets the EPT execute permission and the page becomes executable.
This kills every technique that relies on allocating executable kernel memory: shellcode injection, reflective driver loading, JIT compilation in the kernel, and W^X violations. Even with an arbitrary kernel write primitive, you cannot allocate a page, write shellcode to it, and jump to it. HVCI will refuse to make the page executable.
The performance cost is real: HVCI adds 5–15% overhead depending on workload, because every executable page transition requires hypervisor intervention. This is why HVCI is not universally enabled, even on capable hardware.
#Credential Guard
Credential Guard uses VBS to isolate credential material in VTL 1. A separate, minimal instance of LSASS (LsaIso.exe) runs in VTL 1 and holds the actual secrets: NTLM hashes, Kerberos TGTs and service tickets, DPAPI master keys, and Credential Manager blobs. The VTL 0 LSASS process is a proxy — it forwards authentication requests to VTL 1 and receives responses, but never holds the secrets itself.
What Credential Guard kills: Mimikatz-style credential dumping from LSASS memory. The secrets are not in VTL 0 memory to dump. sekurlsa::logonpasswords returns nothing. lsadump::sam returns nothing. The credentials are in VTL 1, and VTL 0 cannot read VTL 1 memory.
What Credential Guard does not prevent:
- Token theft: Stealing an existing token from a running process (the token is in VTL 0 memory, attached to the process)
- Kerberoasting: Requesting service tickets from Active Directory and cracking them offline (the tickets are returned to VTL 0)
- DCSync: Replicating credentials from a domain controller (the DC does the replication; Credential Guard protects local secrets only)
- Keylogging / phishing: Capturing credentials as they are typed
#Windows Defender Application Control (WDAC)
WDAC (formerly Device Guard) enforces a configurable policy of what code is allowed to run. The policy is an XML document, signed and deployed via Group Policy or MDM, that defines:
- Allow rules: Which signers, file hashes, file paths, and file attributes are permitted
- Deny rules: Which signers, hashes, or paths are explicitly blocked
- Managed installer rules: Which installers can write executable files that are automatically trusted
- COM object and script enforcement: Whether WDAC applies to scripting hosts and COM objects
The critical distinction from AppLocker: WDAC is enforced by the kernel's CI subsystem. AppLocker is a user-mode service that can be bypassed by any process with sufficient privileges. WDAC cannot be bypassed from user mode — the enforcement happens in the kernel, at image load time, before the image is mapped.
On a WDAC-locked machine, even a signed but policy-violating binary will not run. This kills the BYOVD attack vector if the vulnerable driver is not in the allow list. Microsoft maintains a hypervisor-enforced vulnerable driver blocklist that WDAC can reference, automatically blocking known-vulnerable drivers even if they are signed.
#Windows Defender System Guard
System Guard is the umbrella term for the hardware-based root of trust that underpins VBS, HVCI, and Credential Guard. It provides:
- Boot-time attestation: TPM measurements of the boot chain, verified against known-good values
- Runtime attestation: A System Guard runtime attestation agent that periodically verifies the integrity of VBS components
- Secure Launch: A mechanism to start the hypervisor and VTL 1 before VTL 0, ensuring the secure world is established before the normal kernel runs
The attestation flow: TPM → boot measurements → Secure Boot verification → VBS initialization → runtime attestation agent starts → periodic integrity checks. If any component fails attestation, System Guard can trigger remediation (up to and including refusing to release secrets to the compromised environment).
#Process-Level Mitigations
While the previous sections focused on protecting the kernel, Windows also implements mitigations that constrain what an attacker can do from user mode. These matter for kernel exploitation because the initial access vector is almost always a user-mode vulnerability.
Arbitrary Code Guard (ACG) prevents a process from allocating new executable memory or modifying existing executable memory. When ACG is enabled on a process (via SetProcessMitigationPolicy with ProcessDynamicCodePolicy), NtAllocateVirtualMemory and NtProtectVirtualMemory fail if they would create or transition to an executable page. The only way to introduce executable code is via the image loader — mapping a signed, CI-verified DLL. This kills shellcode injection, reflective DLL loading, and JIT compilation (unless the process uses a special ACG-compatible JIT API that creates executable memory in a separate, signed JIT process).
Microsoft Edge enables ACG on its content processes. Even if an attacker achieves arbitrary code execution in a renderer, they cannot allocate executable memory for shellcode. They must either escape the sandbox (to a process without ACG) or use a technique that does not require new executable code (such as reusing existing signed DLL code).
Export Address Filtering (EAF) and Import Address Filtering (IAF) are EMET-era mitigations now built into Windows. EAF sets hardware breakpoints on the export table of ntdll.dll and kernel32.dll. Any code that reads these export tables triggers a debug exception. Since most shellcode begins by parsing the PE export table to resolve API addresses, EAF catches shellcode at its first instruction. IAF works similarly but watches the import address table (IAT) of specific modules.
The practical limitation: EAF/IAF are easily bypassed by shellcode that resolves API addresses without touching the export table — for example, by walking the PEB's loaded module list, parsing each module's export directory directly, or using syscall numbers directly (bypassing the API layer entirely). They are a speed bump, not a wall.
Windows Sandbox uses the same hypervisor technology as VBS to create a lightweight, ephemeral, isolated desktop environment. Each sandbox session starts from a clean Windows snapshot and runs in a hardware-isolated container. On close, everything is discarded. The sandbox uses the host's kernel (via a special container type) rather than a separate kernel, making it lighter than a full VM but providing strong isolation for the user. For the kernel exploit developer, Windows Sandbox is a convenient, disposable test environment — you can load drivers, trigger bugchecks, and revert to a clean state instantly.
#Modern Windows 11 Hardening
Windows 11 is not just Windows 10 with a new UI. It is a security baseline reset. The hardware requirements — TPM 2.0, Secure Boot, supported CPUs with CET and SLAT — are the mechanism by which Microsoft enforces that baseline.
Pluton Security Processor is Microsoft's answer to physical TPM attacks. A traditional discrete TPM communicates with the CPU over an SPI bus — a physical interface that can be probed, interposed, or sniffed with moderate hardware skills. Pluton integrates the security processor directly into the CPU die, eliminating the SPI bus attack surface. The root of trust is inside the CPU package, where physical access requires decapping the chip.
Pluton handles the same functions as a discrete TPM — Secure Boot key storage, PCR measurements, attestation — but with stronger physical security guarantees. It is available on AMD Ryzen 6000+ (Microsoft Pluton), Qualcomm Snapdragon X series, and select Intel platforms.
Windows 11 defaults represent a step change from Windows 10:
- HVCI enabled by default on new installations where hardware supports it
- Credential Guard enabled by default on enterprise-joined machines
- Driver cross-signing deprecated — only WHQL-signed or Attestation-signed drivers are accepted
- Legacy components removed — no 32-bit Windows 11, no legacy BIOS boot, no NTVDM (16-bit subsystem)
- Kernel-mode print drivers deprecated — print drivers moving to user mode reduces the kernel attack surface
- Vulnerable driver blocklist enabled by default, updated via Windows Update
The practical implication: the baseline security posture of Windows 11 is significantly higher than Windows 10. Exploits that worked on Windows 10 may not work on Windows 11, not because the bug was fixed, but because the exploitation technique is blocked by a now-default mitigation.
The configurable mitigation surface is important to understand. Many of these defenses are configurable via Exploit Protection settings, Group Policy, or MDM policies. An enterprise might disable HVCI for performance reasons. A gaming machine might have VBS disabled. A developer workstation might have test-signing enabled. The exploit developer must always check which mitigations are actually enabled on the target:
# Check VBS status
Get-ComputerInfo -Property "DeviceGuard*"
# Check HVCI status
Get-CimInstance -ClassName Win32_DeviceGuard -Namespace root\Microsoft\Windows\DeviceGuard
# Check CET / Hardware-enforced Stack Protection
Get-ItemProperty -Path "HKLM:\SYSTEM\CurrentControlSet\Control\Session Manager\Memory Management" `
-Name "FeatureSimulations"
# Check if test-signing is enabled
bcdedit /enum | findstr testsigning
# Check Secure Boot state
Confirm-SecureBootUEFI#The Remaining Attack Surface
After all these defenses, what is left? The honest answer: enough. The bar is dramatically higher than it was a decade ago, but the attack surface has shifted, not disappeared.
#Bring Your Own Vulnerable Driver (BYOVD)
BYOVD is the most practical kernel exploitation technique in 2026. The pattern is simple: load a signed but vulnerable driver that exposes arbitrary physical memory access or MSR read/write via DeviceIoControl, then use that access to bypass every software-enforced defense.
The canonical vulnerable drivers — RTCore64.sys (MSI Afterburner), capcom.sys (Capcom anti-cheat), WinRing0.sys — are signed, pass DSE and CI, run in Ring 0, and their IOCTL handlers are legitimate code (passing kCFG). The vulnerability is in the driver's logic: it exposes a "read physical memory" or "write MSR" IOCTL to any caller, with no access control.
// BYOVD exploitation flow — simplified
// 1. Load the vulnerable driver (requires SeLoadDriverPrivilege or admin)
SC_HANDLE scm = OpenSCManager(NULL, NULL, SC_MANAGER_ALL_ACCESS);
SC_HANDLE svc = CreateService(scm, "VulnDrv", "VulnDrv",
SERVICE_ALL_ACCESS, SERVICE_KERNEL_DRIVER,
SERVICE_DEMAND_START, SERVICE_ERROR_NORMAL,
"C:\\path\\to\\vuln_driver.sys", ...);
StartService(svc, 0, NULL);
// 2. Open a handle to the driver's device
HANDLE hDevice = CreateFile("\\\\.\\VulnDevice", GENERIC_READ | GENERIC_WRITE,
0, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
// 3. Use the driver's IOCTL to read/write physical memory
// Locate kernel structures in physical memory
// Overwrite token / disable mitigations / patch page tables
DWORD bytesReturned;
DeviceIoControl(hDevice, IOCTL_READ_PHYS_MEMORY,
&request, sizeof(request), &result, sizeof(result), &bytesReturned, NULL);Microsoft's response is the Vulnerable Driver Blocklist, enforced by WDAC and HVCI. The blocklist contains hashes of known-vulnerable drivers and prevents them from loading. It is updated via Windows Update. The limitation: the blocklist is reactive. New vulnerable drivers are discovered regularly, and a driver that is not yet on the blocklist loads without issue.
#Firmware-Level Attacks
UEFI firmware vulnerabilities execute before the OS loads, outside the scope of all Windows defenses. The firmware is stored on an SPI flash chip on the motherboard. Physical access allows rewriting it with a flash programmer. Logical access (from Ring 0) allows reading and potentially writing it via the SPI controller.
SMM (System Management Mode) is a CPU mode more privileged than Ring 0. SMM code runs in SMRAM, a region of physical memory that is locked and invisible to the OS. SMM handlers are installed by the firmware and persist across OS reboots. A vulnerability in an SMM handler can be exploited for a persistent implant that survives OS reinstallation. SMM attacks are the "Ring -2" threat — they are outside the scope of kernel exploitation but represent the next frontier.
#Supply Chain Compromises
A malicious but properly signed driver bypasses DSE, CI, and HVCI. The signature is valid. The code is signed. The CI catalog entry is correct. The driver is simply malicious. This is the SolarWinds scenario applied to kernel mode: a compromised build pipeline produces a signed driver that includes a backdoor.
The defense against supply chain attacks is not technical but procedural: WHQL certification requires submitting the driver to Microsoft for testing, and Microsoft's testing includes behavioral analysis. But a sufficiently sophisticated attacker can hide malicious behavior behind legitimate functionality, or trigger it only under specific conditions that testing does not exercise.
#Data-Only Attacks
Data-only attacks are the frontier of kernel exploitation research. They do not modify code or hijack control flow. They modify data that the kernel interprets as authoritative:
- Token pointer overwrite: Directly overwrite
_EPROCESS.Tokenwith the System token pointer. No code execution, no control flow hijacking — just a data write. CET, kCFG, and HVCI do not prevent this. - Reference count corruption: Modify a kernel object's reference count to trigger a use-after-free. The freed object's memory is reallocated for a different purpose, creating a type confusion.
- Synchronization primitive manipulation: Corrupt a mutex, event, or semaphore to create a race condition that leads to a double-fetch or time-of-check-to-time-of-use (TOCTOU) vulnerability.
- Page table manipulation: Modify page table entries directly (via physical memory access from BYOVD) to make kernel pages accessible from user mode, bypassing SMEP and SMAP at the hardware level.
Data-only attacks are harder to develop than traditional control-flow hijacking because they require deep understanding of the kernel's data structures and their invariants. But they are also harder to defend against because they do not violate any of the control-flow or code-integrity invariants that the defenses enforce.
#Logic Bugs in Allowed Code
A buffer overflow in a signed, allowed kernel driver's IOCTL handler is not stopped by any of the defenses covered in this post. DSE ensures the driver is signed. CI ensures it has not been modified. HVCI ensures its pages are from the signed image. kCFG ensures indirect calls go to valid function entries. CET ensures return addresses are not corrupted. But none of these prevent the driver from having a bug that allows an attacker to write beyond a buffer boundary.
The defense-in-depth argument: each mitigation makes exploitation harder, but none makes it impossible. A buffer overflow in a driver still gives the attacker a write primitive. KASLR makes it harder to know what to write. SMEP/SMAP constrain what can be written where. CET constrains how control flow can be redirected. But a sufficiently skilled attacker can chain these constraints into a working exploit — it just takes more work than it did in 2015.
#Practical Prerequisites
This section is the bridge from theory to practice. The previous sections explained what the defenses are. This section explains what you need to start working with them.
#The Lab Environment
Kernel exploitation requires a dedicated environment. You will crash the machine hundreds of times. You will corrupt kernel state in ways that survive a reboot. You will need to attach a kernel debugger, which requires boot configuration changes. Do not do this on your primary machine.
The two-machine kernel debugging setup is the standard workflow:
- Host machine runs WinDbg (WinDbg Preview from the Microsoft Store)
- Target machine runs the code being debugged
- The two are connected via KDNET (kernel debugger over network) — an Ethernet or Wi-Fi connection between the two machines
KDNET setup on the target:
bcdedit /debug on
bcdedit /dbgsettings net hostip:<HOST_IP> port:50000 key:<KEY>On the host, WinDbg connects via: File → Attach to Kernel → Net → Port: 50000, Key: <KEY>
VM-based alternatives are more convenient for most work:
- VMware: Add a virtual serial port to the VM, configure the host to listen on a named pipe, connect WinDbg to the pipe
- Hyper-V: Enable COM port redirection, connect WinDbg to the redirected port
- VirtualBox: Similar serial port configuration
VMs have a critical limitation: they do not support VBS, HVCI, or Credential Guard (the hypervisor is already in use by the VM). For testing VBS-aware exploits, you need a physical machine.
Test-signing is required for loading custom drivers:
bcdedit /set testsigning on
bcdedit /set nointegritychecks on # Disables DSE entirely (use with caution)Snapshot workflow: Take a VM snapshot before loading a driver. After a bugcheck, revert to the snapshot. This is faster than rebooting and preserves your debugging session state.
Windows build selection: Choose a specific Windows build and obtain its exact symbols. Kernel structures change between builds, and offsets shift. The Windows 10 22H2 (build 19045) and Windows 11 24H2 (build 26100) are common targets. Download the symbol package from the Microsoft Symbol Server or use WinDbg's automatic symbol download.
#Essential Tools
| Tool | Purpose |
|---|---|
| WinDbg Preview | Kernel debugging. Commands you must know: !analyze -v, !process 0 0, !pte, !pool, !irql, !drvobj, !devobj, dt, u, bp/ba/bm, dq/dd/db, r, k/kb/kv, .reload, .sympath |
| IDA Pro / Ghidra / Binary Ninja | Static analysis of ntoskrnl.exe, hal.dll, and target drivers. You will spend more time reading code than writing it. |
| OSR Driver Loader | Loading test drivers without writing a service installer. Essential for rapid iteration. |
| Sysinternals Suite | Process Explorer, Process Monitor, Autoruns, WinObj, LiveKd. WinObj is particularly useful for understanding the kernel object namespace. |
| Visual Studio + WDK | Building kernel drivers. The WDK includes headers, libraries, and build tools for kernel-mode development. |
| Python 3 | Exploit development and automation. Most kernel exploit prototypes are written in Python with ctypes for IOCTL interaction. |
| C/C++ toolchain | For writing kernel drivers and exploit payloads. You need both user-mode (MSVC) and kernel-mode (WDK) compilation. |
#Knowledge Prerequisites
- x86/x64 assembly: You must read disassembly fluently. The kernel is written in C but debugged in assembly. You will trace through
KiSystemCall64, read trap frames, and analyze ROP gadgets — all in assembly. - C programming: The kernel is C. You will read kernel code (via the Windows Research Kernel, leaked source, or reverse-engineered pseudocode) and write exploit code in C.
- Operating system concepts: Virtual memory (page tables, TLBs, self-referencing PML4 entries), interrupt handling (IDT, IRQLs, DPCs), synchronization (spinlocks, mutexes, events, IRQL-based synchronization), and I/O (IRPs, IOCTLs, device stacks).
- Windows Internals: The Windows Internals book (Yosifovich, Ionescu, Russinovich, and Solomon) is the canonical reference. Read Part 1 (system architecture, processes, threads, memory management) and Part 2 (security, storage, networking). This is not optional.
- Reverse engineering: You will spend more time reading code than writing it. You must be comfortable with IDA or Ghidra, able to navigate large disassemblies, and able to reconstruct C pseudocode from optimized assembly.
#The Mindset Shift
Kernel exploitation is not about finding one bug. It is about understanding a system well enough to find the gap in its defenses. The defenses are not obstacles to be bypassed — they are the specification of what your exploit must achieve:
- SMEP means "you cannot execute user-mode code in the kernel." Your exploit must not do that.
- CET means "you cannot corrupt return addresses." Your exploit must not do that.
- HVCI means "you cannot create new executable code." Your exploit must not do that.
#The Defense Matrix
This table is the cheat sheet. It maps every defense mechanism covered in this post to what it protects, what attack it defeats, and what bypasses it. Return to this table when planning an exploit chain.
| Defense | Layer | Protects | Defeats | Bypassed By |
|---|---|---|---|---|
| Secure Boot | Boot | Bootloader integrity | Bootkits, unauthorized bootloaders | Firmware vulnerabilities, signed shim loaders, physical SPI flash access |
| Measured Boot | Boot | Boot chain attestation | Undetected boot tampering | TPM attacks (SPI bus sniffing), Pluton mitigates this |
| ELAM | Boot | Early driver classification | Malicious boot-start drivers | ELAM driver compromise, policy set to allow Unknown |
| DSE | Boot | Driver signature verification | Unsigned kernel drivers | BYOVD (signed vulnerable driver), stolen signing certificates, test-signing mode |
| PatchGuard | Kernel Runtime | SSDT, IDT, GDT, MSRs, kernel .text | Persistent kernel hooking and patching | BYOVD (physical memory access), hypervisor-level attacks |
| KASLR | Kernel Runtime | Kernel base address randomization | Hardcoded gadget addresses | Kernel pointer leaks, side-channel attacks, physical memory analysis |
| KDP | Hypervisor | Critical kernel data pages | Kernel data tampering | Hypervisor escape, physical memory access below EPT |
| kCFG | Kernel Runtime | Indirect call target validation | JOP/COP chains | Data-only attacks, valid function entry gadgets |
| SMEP | CPU | Instruction fetch from user pages in Ring 0 | User-mode shellcode executed in kernel | ROP (kernel gadgets), page table manipulation |
| SMAP | CPU | Data access to user pages in Ring 0 | Fake user-mode structures passed to kernel | ROP, kernel data corruption, STAC/CLAC gadgets |
| CET Shadow Stack | CPU | Return address integrity | ROP chains | Data-only attacks, non-control-flow hijacking |
| CET IBT | CPU | Indirect branch target validation | JOP/COP chains | Data-only attacks, ENDBR64 gadgets |
| HVCI | Hypervisor | Kernel executable page creation | Unsigned/modified kernel code execution | Hypervisor escape, BYOVD (signed code reuse) |
| Credential Guard | Hypervisor | LSASS secrets in VTL 1 | Credential dumping from LSASS memory | Token theft, Kerberoasting, DCSync, keylogging |
| WDAC | Kernel Runtime | Code execution policy | Unauthorized binary execution | Policy misconfiguration, policy bypass via signed trusted binaries |
| ACG | Process | Dynamic code generation | Shellcode injection, reflective DLL loading | Signed DLL side-loading, JIT process escape |
| VBS | Hypervisor | VTL 0 / VTL 1 isolation | Kernel compromise reaching secure world | Hypervisor escape, hardware vulnerabilities |
| Pluton | Hardware | TPM key storage, physical bus security | Physical TPM bus attacks | CPU decapping (impractical at scale) |
#The Exploitation Decision Tree
Given a target with a specific mitigation configuration, what techniques are available?
Further reading:
- Windows Internals, Part 1 (Yosifovich, Ionescu, Russinovich, Solomon) — System architecture, processes, threads, memory management
- Windows Internals, Part 2 (Russinovich, Solomon, Ionescu, Yosifovich) — Security, storage, networking
- Windows Kernel Programming (Pavel Yosifovich) — Practical kernel driver development
- The Art of Memory Forensics (Ligh, Case, Levy, Walters) — Understanding kernel data structures through Volatility
- Offensive Security's Windows Kernel Exploitation course (OSEE / EXP-401)
- Public exploit writeups on GitHub, the Zero Day Initiative blog, and Project Zero