- vmkatz_loader.py moved to tools/vmkatz_loader.py
- Release workflow includes the loader in the esxi-x86_64 tar.gz
- Updated README and docs/esxi.md paths
README trimmed from 524 to 175 lines. Moved to docs/:
- docs/esxi.md — ESXi deployment, VIB bypass, VMFS raw access
- docs/examples.md — Example output (LSASS, hashcat, NTDS, pagefile)
- docs/architecture.md — Module layout and how it works
- docs/tested-targets.md — Test matrix and known limitations
README now focuses on: intro, what it extracts, supported inputs,
quick start, output formats, ESXi basics, build features, and links.
- README: new section explaining vmkatz_loader.py usage when
execInstalledOnly is enabled, with command to check VIB status
- Bug template: add ESXi VIB protection field asking for
execInstalledOnly value and ESXi version
Loads vmkatz into anonymous mmap pages with PROT_EXEC, bypassing
execInstalledOnly on ESXi. VMkernel blocks execve on unsigned binaries
but allows PROT_EXEC on anonymous mappings. Python is VIB-signed.
The loader parses ELF64 headers, maps PT_LOAD segments, applies
R_X86_64_RELATIVE relocations for PIE, builds a proper initial
stack with argc/argv/envp/auxv (AT_PHDR, AT_PHNUM, AT_PAGESZ,
AT_RANDOM for musl TLS init), and jumps to _start via trampoline.
Usage: python3 vmkatz_loader.py /tmp/vmkatz [args...]
Scans LSASS virtual memory for orphaned _KERB_TICKET_INFO structures
that are no longer in the AVL session tree (freed sessions, logged-out
users with memory still resident). Recovers tickets missed by the
normal AVL tree walk.
Approach: scan for valid etype values at ticket_enc_type offsets, then
validate surrounding structure (kvno, ticket_length, flags, service
name pointer) before calling extract_single_ticket for full parsing.
Tries all known TicketOffsets variants (Win7/8/10/11). Deduplicates
against tickets already found by the structured extraction.
Runs automatically after the normal Kerberos provider extraction.
When BitLocker FVEK keys are extracted from a VM memory snapshot,
vmkatz can now use them to decrypt the corresponding encrypted disk
for SAM/NTDS extraction in the same run.
New modules:
- sam/aes_xts.rs: AES-XTS-128/256 sector-level decryption
- sam/bitlocker_decrypt.rs: transparent Read+Seek wrapper that
decrypts sectors on-the-fly, validates FVEK by checking for
NTFS signature in decrypted sector 0
Flow: snapshot → extract FVEK → open disk → detect BitLocker →
try each FVEK candidate → decrypt partition → extract credentials.
Works in directory mode (auto-discovers snapshot + disk) and with
explicit --disk flag.
Scans physical memory for Windows pool tags to extract Full Volume
Encryption Keys, enabling offline decryption of BitLocker volumes:
- FVEc pool tag (Win7): AES-128/256-CBC, Elephant Diffuser modes
- Cngb pool tag (Win8+): AES-128/256-XTS modes
- Dual-copy validation for Cngb, entropy checks for both
- Tries both x64 and x86 structure offsets automatically
Output: hex FVEK in text/csv/hashcat formats, dislocker-compatible
.fvek files via --bitlocker-fvek <dir>. Works in both normal and
--carve modes (physical scan, no LSASS context needed).
MappedFile now has a Pread variant that uses read_exact_at() instead
of memory mapping. When mmap returns EINVAL (unsupported by kernel),
vmkatz falls back to file I/O with a clear warning message.
Layers updated to use read_at() for PhysicalMemory::read_phys:
- VMware: pread for runtime reads, header parsed from first 8MB buffer
- Hyper-V: pread for all reads
- QEMU ELF: pread for runtime reads, ELF header parsed from 1MB buffer
- QEMU savevm: requires mmap (stream parsing needs full slice access)
Zero host RAM impact — pread reads directly from disk to caller buffer.
- qemu/savevm.rs: read_be_u32/read_be_u64 now use .get() instead of
direct indexing — returns 0 instead of panicking on truncated input
- disk/vhdx.rs: guard against chunk_ratio=0 which would cause division
by zero on crafted VHDX files with oversized block_size
- main.rs: suppress dead_code warning for QemuSavevm variant in
non-qemu single-feature builds
When no text descriptor exists and only -sNNN extent files are present,
the discovery code now sorts extents before picking the first one.
Previously it used filesystem iteration order which was non-deterministic.
The Vec<u8> fallback was added for ESXi kernels where mmap failed on
VMFS-5 files. Now that mmap_file() handles block devices and edge cases,
the fallback is unnecessary and dangerous — it would silently try to
allocate hundreds of GB for large VMEM files if mmap ever failed.
Mmap::map() uses fstat which returns 0 for block devices (LVM volumes).
Added utils::mmap_file() that uses seek to get real size and passes it
explicitly to MmapOptions. Replaced all Mmap::map() calls across:
- qemu/savevm.rs (was already fixed inline, now uses shared helper)
- qemu/layer.rs (ELF core dumps)
- hyperv/layer.rs (.bin/.raw dumps)
- vmware/layer.rs (fallback path also used metadata().len())
- hyperv/vmrs.rs (used metadata().len() for size)
- vbox/layer.rs (used metadata().len() for size)
Also added utils::file_size() for non-mmap size queries.
The parser previously rejected VMFS-5 (majorVersion < 24). Now handles
both VMFS-5 and VMFS-6 with version-dependent branching for:
- File descriptor size (2048 vs 2*mdAlignment) and layout
- Block pointers (32-bit vs 64-bit)
- Address encoding (FB vs SFB/LFB, different bit layouts)
- Directory format (flat 140-byte entries vs block-based 288-byte)
- Pointer blocks (4KB/1024 ptrs vs 64KB/8192 ptrs)
- Resource metadata (no rfmd signature in VMFS-5)
- SFD bootstrap offset formula
- TBZ bits (VMFS-6 only, not in 32-bit addresses)
Verified on both VMFS-5 and VMFS-6 datastores.
Block devices (e.g. /dev/pve/vm-*-state-*) were incorrectly routed
to SAM disk extraction instead of QEMU savevm parsing. Two fixes:
- Check QEVM magic on block devices before assuming SAM mode
- Use read_exact instead of mmap for is_qemu_savevm() detection
- Use explicit size via seek(End) for mmap in QemuSavevmLayer::open(),
since fstat returns 0 for block devices
- Show account status in SAM text output: (DISABLED), (NO PASSWORD),
(BLANK PASSWORD) from per-user ACB flags in registry F value
- Add modification date to DPAPI master key text output from NTFS mtime
- Deduplicate DPAPI keys: show only most recent per user (--all for all)
- Filter NTFS DOS 8.3 short names in list_directory() — fixes S-1-5-~1
SID leak from short name aliases
- Add cargo test step to CI workflow
The hashcat mode depends on context (local vs domain user), not just the
crypto version. Domain users use NTLM pre-key derivation (modes 15310/15910)
while local users use SHA1 pre-key (modes 15300/15900). The context is
determined by whether a domain key section exists in the master key file.
Previously all hashes were reported as 15300 or 15900 regardless of context.
Previously, permission errors, mount failures, or I/O issues during VM
file discovery were silently swallowed with if-let-Ok patterns. On a NAS
with partial mounts or restricted permissions, this caused VMs to be
silently missed with no indication of the problem.
Now all I/O errors in discover.rs produce log::warn! messages:
- read_dir failures on scan directories
- metadata/stat failures on individual files
- file open failures in magic-byte checks (ELF, VMDK descriptor)
Added file_size() helper to centralize metadata reads with warnings.
- Add QEMU/Proxmox savevm and VMware embedded .vmsn to supported inputs
- Add ESXi 6.7 and Proxmox savevm test results to tested targets table
- Document BitLocker detection and QEMU savevm limitations
- Update target OS range to include Windows Server 2003
- Add Proxmox example to quick start
- Update module architecture and feature descriptions
Two bugs fixed:
1. LSA key derivation: MD5 input was salt+bootkey×1000 instead of
bootkey+salt×1000 (wrong order per impacket/MS-LSAD spec)
2. Secret decryption: used RC4 instead of DES-ECB with rotating
7-byte key segments (SystemFunction005 / [MS-LSAD] Section 5.1.2)
Implemented:
- DES-ECB decrypt with rotating key (des_ecb_decrypt_rotating)
- transformKey: 7-byte to 8-byte DES key expansion ([MS-LSAD] 5.1.3)
- LSA_SECRET_XP parsing: Length(4) + Version(4) + Secret(Length)
Tested on Windows Server 2003 R2 SP2 VHDX: DPAPI_SYSTEM keys and all
LSA secrets now decrypt correctly.
Windows Server 2003 R2 SP2 reports PolRevision 0x10007 (modern) but uses
the legacy PolSecretEncryptionKey scheme. Try PolEKList first; if not found,
fall back to PolSecretEncryptionKey instead of failing.
Check for the -FVE-FS- OEM ID in partition boot records before
attempting NTFS parsing. When BitLocker is detected, skip the
partition and display a clear error message instead of failing
silently with "No registry hives found".
Applied to SAM extraction, LSA secrets, and NTDS.dit extraction paths.
VMware layer:
- Fix base_offset not set when .vmsn has region tags but no separate
.vmem file (embedded memory). Reads were at wrong file offset.
- Tested on ESXi 6.7 with Win Server 2016 embedded .vmsn snapshot.
Display:
- Don't display Kerberos/WDigest passwords for machine accounts (username
ending with $) — they are binary blobs, not useful plaintext. The
Kerberos keys (AES256, RC4=NT hash) are the exploitable forms.
- Don't count machine passwords in the summary line.
- Truncate long hex passwords to "(hex, N bytes) first32bytes..." when
displayed for other non-printable passwords.
New parser (src/qemu/savevm.rs):
- Parse QEVM magic, RAM block list, and page entries (PAGE/ZERO)
- MMIO gap remapping for q35+UEFI (below_4g=0x80000000)
- Skip non-RAM device sections (dirty-bitmap, etc.) via forward scanning
- HashMap-based deduplication for dirty page iterations (last-write-wins)
- Auto-detection via QEVM magic in format dispatcher
System process discovery improvements:
- Validate candidates by translating Flink VA to physical and checking the
linked EPROCESS has a printable ImageFileName and valid kernel-mode Flink
- Rejects stale EPROCESS remnants with corrupted page tables
Tested on Proxmox VMs: DC25 (Win Server 2025, 4GB) and WKS11 (Win11, 8GB)
- Merge 8 duplicated _x86 functions into single arch-aware implementations:
walk_avl_tree, read_kerb_external_name, extract_kerb_password,
extract_tickets_from_list, extract_single_ticket, detect_kerb_offsets,
extract_kerb_keys, extract_kerberos_credentials
- All structure offsets computed from arch.ptr_size() and arch.ustr_size()
- Delete extract_kerberos_credentials_arch wrapper and all _x86 variants
- Net reduction: -580 lines
- Merge extract_msv_sessions_arch and extract_msv_credentials_arch into the
main functions, selecting offset variants by arch at runtime
- Make all walkers arch-aware: walk_session_buckets, walk_session_list,
walk_msv_list, walk_hash_table, find_inline_hash_table,
find_credentials_ptr_in_entry
- Delete 258 lines of duplicated x86 code (extract_msv_sessions_arch,
extract_msv_credentials_arch, try_extract_primary_cred_x86)
- x86 now gets scoring, enrichment, multi-pass credential scanning, and
SHA1 validation — previously x64-only features
- Add variant_order_for_build_arch for x86 build-aware variant ordering
- Add read_ptr_from_buf, walk_list, read_data_section, scan_data_for_list_head
to types.rs, eliminating ~200 lines of duplicated boilerplate across providers
- Unify kerberos.rs: merge 8 duplicated _x86 functions into arch-aware versions
(walk_avl_tree, extract_single_ticket, read_kerb_external_name, etc.), -440 lines
- Fix CloudAP: read account name from toName_ptr (+0x58) instead of hashName (+0x68)
which is a SHA hash, not the readable UPN. Add 3 offset variants (1507-1607,
1703-1709, 1803+) with auto-detection. Remove useless hashName fallback.
- Fix walk_session_buckets using x64-only read_win_unicode_string and read_virt_u64
for pointers — now uses arch-aware read_ustring and read_ptr
Kerberos:
- Detect Credential Guard ISO-encrypted passwords (type==1 at cred+0x28)
on Win10 1607+. Report as "(Credential Guard ISO)" instead of silently
failing to decrypt.
- Extract SmartCard PINs from SmartcardInfos pointer (CSP_INFOS.PinCode)
when regular password is empty. Stored as "[PIN] <pin>".
- Add smartcard_infos offset to KerbOffsets for Win10 1607+ variants.
MSV:
- Walk the KIWI_MSV1_0_PRIMARY_CREDENTIALS linked list via next pointer
to find the "Primary" entry, instead of assuming the first entry is it.
- Recognize "CredentialKeys" (ANSI_STRING len=14) entries alongside
"Primary" (len=7) during structure scanning and inline byte matching.
- Skip CredentialKeys entries (DPAPI key material already extracted by
DPAPI provider) with a log message.
- Add EPROCESS offset tables for Vista SP2 x86, Win7 SP1 x86, Win8/8.1 x86
- Fix is_prevista_x86 threshold: Vista x86 (PID=0x9C) uses AES/3DES like
Win7+, not DES-X/RC4. Tighten cutoff from 0xA0 to 0x98.
- Update README: 18 offset tables, correct x86 support status
EPT scanning (for VBS/Credential Guard VMs) is now disabled by default
and enabled with --ept, since the vast majority of VMs don't use VBS.
Previously it was enabled by default and disabled with --no-ept.
Also remove examples/ (dev-only test utilities) from tracking.
The PGM struct fields size varies by VirtualBox version:
- v14 (VBox 7.x): 78 bytes — existing behavior
- v12-13 (VBox 5.x-6.x): 74 bytes — no cBalloonedPages field
- v11 (VBox 4.1+): 70 bytes — pre-balloon support
Previously hardcoded to skip(78), which corrupted RAM extraction
for any .sav file not from VBox 7.x. Now reads the PGM unit
version from the unit header and adapts accordingly.
- VDI: search for location= attribute AFTER the UUID match, not in a
window that extends 200 bytes before it. Prevents picking a location
from a different HardDisk entry in multi-disk .vbox files.
- dump: use saturating_add/sub in coalesce_pages to prevent u64
overflow on kernel addresses near 0xFFFFFFFFFFFFF000.
The function is used by both SAM and NTDS output paths. Gating it on
ntds.dit broke compilation when sam was enabled without ntds.dit
(e.g. --features sam or --features vmfs).
- QCOW2: cap l1_table pre-allocation to 1M entries to prevent OOM
from forged l1_size in header (actual entries read from file)
- VMware tags: validate all index bytes are available before parsing,
break out of tag loop instead of producing truncated indices
- Bounds-check tag_start before slicing vmsn_data (prevents panic on
forged memory_group.offset beyond file size)
- Cap Vec::with_capacity for group_count based on actual data size
(prevents OOM from forged u32 in header)
- Cap regions Vec::with_capacity to 4096 (prevents OOM from forged
regionsCount tag value)
- SAM hashes: use saturating_add + checked_add for V-value offset
calculations to prevent overflow on crafted hive data
- ESE: check tag_idx overflow in read_tag before subtraction from
page_size (prevents wrap-around on large tag indices)
- ESE: explicit guard last_var_id < 128 in variable column parsing
to prevent underflow in offset table size calculation
- VMFS flat VMDK: guard block_size == 0 in resolve_position and Read
impl to prevent division by zero on corrupt superblock
- Truncate decrypted password to UNICODE_STRING.Length bytes (was using
MaximumLength, including garbage padding after the actual data)
- Add decode_password_bytes: hex-encode raw bytes directly for binary
passwords instead of round-tripping through lossy UTF-16LE decode
(char::REPLACEMENT_CHARACTER destroyed original bytes)
- Apply to all providers via decrypt_unicode_string_password_arch and
the SSP-specific CFB-8 variant
- Update CSV/text output to use raw hex for binary passwords
Detail the --vmfs-list device discovery with example output, single-VM
vs auto-scan extraction modes, NTFS partition filtering for batch mode,
and the on-disk resolution chain diagram.
Add documentation for two major new features:
- VMFS-6 raw SCSI device parser that bypasses ESXi file locks on running VMs
- Native Hyper-V .vmrs saved state parser (reverse-engineered, no Microsoft DLL)
On Proxmox (and ESXi with VMFS), reading disk images of running VMs
can hit transient I/O errors on individual sectors/clusters that are
temporarily locked by the hypervisor.
Instead of aborting on the first read error, all NTFS and registry
hive reading paths now use block-level resilient I/O:
- read_from_data_runs: reads 4KB blocks individually, zero-fills
blocks that fail, and continues to the next block
- resilient_read_blocks: MFT batch reads zero-fill failing records
instead of skipping entire batches
- read_file_data (ntfs_reader): falls back to chunked 4KB reads
when exact read fails
- Partition table parsing: skips unreadable GPT entries gracefully
- Hive scanning: skips unreadable chunks instead of aborting scan
- MFTMirr: accepts records without FILE signature check on first
extent (live VMs may have transient I/O on header sectors)
- $ATTRIBUTE_LIST parsing for extension MFT records (large hives)
Tested on Proxmox with running Win11 and DC VMs — extracts SAM/SYSTEM
hives and NTDS.dit successfully despite scattered I/O errors.
Self-contained VMFS-6 parser that reads flat VMDKs directly from raw
SCSI partition devices, bypassing VMFS file locks on running VMs.
Key features:
- Full VMFS-6 on-disk format parsing: LVM, superblock, FDC, SBC, PB/PB2
- Directory traversal with allocation map and entry bitmap support
- Block map building with batched PB reads (320 reads vs 262K individual)
- Sub-block resolution for small files via SBC resource metadata
- Auto-scan mode: enumerates all VMs, skips non-Windows VMDKs
- NTFS-only extraction for fast batch scanning
Verified on ESXi 8.0 datastore: 73 VMDKs scanned in <2min, 4 Windows
VMs with SAM hashes + LSA secrets + DCC2 cached credentials extracted.
SHA1 cross-validation only works for machine accounts (where
ShaOwPassword = SHA1(NTHash)). For human accounts, ShaOwPassword =
SHA1(UTF16LE(password)), so the entropy fallback could pick the wrong
offset variant (reading DPAPI Protected field as NT hash).
Two fixes:
- Track SHA1-validated variant across credentials in same LSASS process
(same Windows build → same offsets). Reuse for subsequent credentials.
- DPAPI cross-check: when isDPAPIProtected=1, reject entropy candidates
whose NT hash matches the DPAPIProtected field at offset 0x6A.
Verified: Administrator NT hash now correct (09f6ff15...) on both
Citrix DC snapshots, matching pypykatz ground truth.
- Extract pKeyList keys even when credential substructure is paged out
- Physical scan for KIWI_KERBEROS_KEYS_LIST_6 structures in LSASS pages
- Match key groups to credentials via RC4 key = NT hash correlation
- Merge Kerberos credentials by username/domain when LUID is unknown
- Remove duplicate DPAPI line (was always same as SHA1)
- Hide all-zero LM hashes in output
- --kirbi <DIR>: export individual .kirbi files per ticket
- --ccache <FILE>: export all tickets as MIT Kerberos ccache v4 format
- Uses actual Kerberos name types from ticket data for ccache principals
- Sanitizes filenames for cross-platform compatibility
Read KIWI_KERBEROS_KEYS_LIST_6 from the session entry's pKeyList
pointer, parse KERB_HASHPASSWORD_6[_1607] entries, decrypt and
extract AES128, AES256, RC4 (NTLM), and DES key material.
Offsets for Win10 1607+, Win10 1507, Win8, and Win7 variants.
Keys are displayed in both text and Display formats.
This closes the gap vs pypykatz which extracts these keys from
minidumps. VMkatz now extracts passwords, keys, and tickets.
Add LUID and username validation to reject phantom sessions caused by
wrong MSV struct offsets being applied to valid memory. Filters:
- LUID must have high 32 bits zero (real Windows LUIDs are 32-bit)
- Username must contain at least one alphanumeric char, no file paths
or control characters
Tested on SilverFort-AD (Server 2022 DC, 32GB): eliminates 3 false
sessions while preserving all 10 legitimate ones.
Accept multiple positional arguments and auto-detect file types by magic
bytes (ESE 0xEFCDAB89, registry "regf", minidump "MDMP").
- Raw NTDS.dit + SYSTEM hive: extracts AD hashes without a disk image
- Raw SAM + SYSTEM [+ SECURITY]: extracts local hashes, LSA secrets,
and cached domain credentials from exported registry hives
- Auto-detects which hive is SYSTEM (bootkey extraction), SAM, or
SECURITY — argument order does not matter
- Helpful error messages for incomplete inputs (e.g. NTDS without SYSTEM)
- LSASS minidump detection with guidance to use pypykatz (parser planned)
- Full backward compatibility with existing single-file workflows
- Fix ESE parser for 32KB large pages (Win Server 2025): use 80-byte
page header and 12-bit tag count for pages >=16KB
- Move NTDS code from src/sam/ to src/ntds/ module (ese.rs + mod.rs)
- Enable ntds.dit feature by default in Cargo.toml
- Fix conditional compilation warnings across all feature combinations
- Add test examples for ESE parser and end-to-end NTDS extraction
- Update README with NTDS usage, examples, and test results
Verified against 4 domain controllers:
- 3x Win Server 2019 (8KB pages, GOAD lab): 18/19/15 hashes
- 1x Win Server 2025 (32KB pages): 8 hashes
All hashes match impacket-secretsdump (which itself fails on 32KB pages).
- Fix DCC2 cached credentials: use NL$KM[0:16] as AES key instead of
[16:32]. The secret is already stripped of its LSA_SECRET_BLOB header,
so [16:32] was a double offset producing garbage decryption.
- Fix GMSA secrets: display managed service account passwords as hex
instead of trying to decode binary data as UTF-16LE.
- Add raw block device support for LVM thin volumes (/dev/pve/...).
Auto-detect block devices and route to SAM extraction. Use seek-to-end
for device size since metadata().len() returns 0 for block devices.
- Update README with Proxmox LVM test results and Server 2025 support.
- New --format hashcat: outputs NTLM hashes (mode 1000) and DCC2 (mode 2100)
for direct use with hashcat
- Raw disk support: handles flat VMDKs (-flat.vmdk) and raw images (.raw/.img/.dd)
as simple seek+read without sparse container parsing
- EPT walker: scans for nested hypervisor page tables (VBS/Hyper-V) when
System process not found in L1 physical memory, translates L2→L1 addresses
through Extended Page Tables to access Windows kernel structures
- Debug logging for System process scan near-misses (DTB/Flink rejections)
Win10 1607+ (build 19045) stores a 20-byte SHA/DPAPI field at +0x36 before
the actual hashes, shifting NtOwfPassword from +0x36 to +0x4A. Previous code
extracted the first 16 bytes of ShaOwPassword instead of the real NT hash.
Changes:
- Add DPAPI-shifted layout detection in structural_score(): compares data at
0x36 with data at 0x6A (both contain ShaOwPassword/dppiGenericRandom)
- Add 6 PRIMARY_CRED_OFFSET_VARIANTS: canonical mimikatz (0x36, 0x28, 0x20),
no-unk (0x30), and empirical DPAPI-shifted (0x4A, 0x4C)
- Replace first-match entropy selection with scored candidates, preferring
variants with highest structural_score()
- When DPAPI layout detected: variant 0x36 gets score=0, variant 0x4A gets
score=23 (flags validated + zero LM + DPAPI confirmed)
Verified against pypykatz: NT=bbf7d1528afa8b0fdd40a5b2531bbb6d matches
across all 3 VMware snapshots and VBox snapshot.
- VMware layer: fall back to identity mapping when VMSN has no region
tags (older VMware snapshots), fixes "System process not found" on
VMs with minimal VMSN metadata
- MSV physical scan: replace broken variant-0 fallback with proper
entropy-based validation. When SHA1 cross-validation fails for all
offset variants, check if SHA1 field is zero (not stored on Win7/2012)
and validate hash bytes don't look like UTF-16 text. Prevents garbage
hashes (username/domain text displayed as NT hashes) on older builds.
- Add Win11 24H2 (build 26100+) EPROCESS offsets: UniqueProcessId,
ActiveProcessLinks, ImageFileName shifted +8 from Win10 layout
Tested on ESXi against 12 Windows VMs spanning Win7 through Win11:
Win7, Win8/2012, Win10, Win11, Server 2012 DC, Server 2016,
Server 2016 DC, Server 2019
When WDigest is paged out (common on VBox snapshots), SYSTEM and
service sessions have empty domain. Now fills "NT AUTHORITY" as
domain for LUIDs 0x3e7/0x3e4/0x3e5 when domain is empty, while
preserving WDigest-discovered values (e.g. WORKGROUP) when present.
- Fill in SYSTEM/NETWORK SERVICE/LOCAL SERVICE usernames for well-known
LUIDs when they're empty (common on VBox where WDigest is paged out)
- Ensures DPAPI-only sessions show meaningful identifiers