Files
nikaiw-VMkatz/tests/minidump.rs
T
NK d616a62775 v1.0.0: major rewrite — minidump support, pre-Vista, verified offsets, carve mode
LSASS credential extraction:
- Minidump (.dmp) support: full MSV/Kerberos/DPAPI/WDigest/TsPkg/SSP/
  LiveSSP/CredMan/CloudAP extraction from LSASS minidumps
- Pre-Vista (WinXP/Win2003): 32-bit EPROCESS, PAE paging, DES-X-CBC/RC4
- Win11 24H2 (26100+): correct EPROCESS offsets, MSV LIST_64/LIST_65,
  Kerberos variant with shifted offsets
- All MSV/SSP/LiveSSP/CloudAP/CredMan/WDigest offsets verified against
  mimikatz C structs and pypykatz templates
- EPROCESS offsets verified against Vergilius Project (13 variants)
- AES-CFB-128 cipher for non-8-aligned LSASS blobs
- DPAPI extraction from dpapisrv.dll (Win10 19041+ moved g_MasterKeyCacheList)
- Adaptive MSV offset discovery with build-number-aware variant ordering
- MSV NT hash fix: validated variant tracking + DPAPI cross-check for
  human accounts (SHA1 validation only works for machine accounts)
- Kerberos: AES/DES/RC4 key extraction, kirbi/ccache export, ticket
  quality validation, false positive filtering
- CloudAP: PRT blob extraction, 7 patterns covering Win10 1507–Win11 24H2
- CredMan: correct 2-level navigation (SET_LIST→STARTER→entry)
- Garbage filtering: repeating pattern detection, structural score
  validation, unknown etype rejection

Architecture:
- Carve mode: two-level degraded extraction for truncated memory files
- sam/mod.rs split into 4 submodules (partition, ntfs_reader,
  disk_fallbacks, vmdk_scan)
- ProviderStatus enum replacing string-based status tracking
- Safe read helpers (utils.rs) replacing 86 try_into().unwrap() calls
- GovmemError renamed to VmkatzError across all 35 source files
- Named paging constants (PAGE_PHYS_MASK, LARGE_*_MASK)

Performance:
- TLB cache for page table translation (256-entry direct-mapped)
- QCOW2 L2 table caching (64 tables, amortized I/O)
- VMware region binary search (partition_point)
- Stack-allocated ASN.1 length encoding, IV entropy histogram
- Single-pass System process + EPT scanning
- memchr::memmem for pattern matching

Robustness:
- Minidump parser hardening (bounds checks, overflow protection)
- PE32 validation (machine type, section count, optional header size)
- Multiple pagefile support (PTE pagefile_number routing)
- VMware embedded memory support (.vmss/.vmsn without .vmem)
- EPT false positive prevention (reserved bits, PDPT validation)
- VMEM truncation detection with user warning

Testing:
- 8 automated tests (4 unit + 4 integration)
- Non-regression framework: compare.py + esxi_test.sh
- All credentials verified against pypykatz on 10+ minidumps

CLI:
- --all/-a: show empty sessions (hidden by default)
- --no-ept: skip EPT scanning
- --kirbi/--ccache: Kerberos ticket export
- Silent output modes: ntlm, hashcat, text summary
- Hex display for non-printable machine account passwords
2026-03-09 16:21:24 +01:00

171 lines
6.1 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
use std::path::Path;
use vmkatz::minidump::Minidump;
use vmkatz::memory::VirtualMemory;
/// Synthetic minidump: header + 1 SystemInfoStream + 1 Memory64ListStream with 1 region.
fn make_test_minidump() -> Vec<u8> {
let mut data = Vec::new();
// Header (32 bytes)
data.extend_from_slice(&0x504D_444Du32.to_le_bytes()); // Signature "MDMP"
data.extend_from_slice(&0x0000_A793u32.to_le_bytes()); // Version
data.extend_from_slice(&2u32.to_le_bytes()); // NumberOfStreams
data.extend_from_slice(&32u32.to_le_bytes()); // StreamDirectoryRva
data.extend_from_slice(&0u32.to_le_bytes()); // CheckSum
data.extend_from_slice(&0u32.to_le_bytes()); // TimeDateStamp
data.extend_from_slice(&2u64.to_le_bytes()); // Flags
// Stream directory (2 entries × 12 bytes = 24 bytes, starts at offset 32)
let sysinfo_rva = 32 + 24; // 56
let sysinfo_size = 56u32;
data.extend_from_slice(&7u32.to_le_bytes()); // STREAM_TYPE_SYSTEM_INFO
data.extend_from_slice(&sysinfo_size.to_le_bytes());
data.extend_from_slice(&(sysinfo_rva as u32).to_le_bytes());
let mem64_rva = sysinfo_rva + sysinfo_size as usize; // 112
let mem64_size = 16 + 16; // header(16) + 1 descriptor(16) = 32
data.extend_from_slice(&9u32.to_le_bytes()); // STREAM_TYPE_MEMORY64_LIST
data.extend_from_slice(&(mem64_size as u32).to_le_bytes());
data.extend_from_slice(&(mem64_rva as u32).to_le_bytes());
// SystemInfoStream (56 bytes at offset 56)
data.extend_from_slice(&9u16.to_le_bytes()); // ProcessorArchitecture = AMD64
data.extend_from_slice(&0u16.to_le_bytes()); // ProcessorLevel
data.extend_from_slice(&0u16.to_le_bytes()); // ProcessorRevision
data.extend_from_slice(&[1u8]); // NumberOfProcessors
data.extend_from_slice(&[1u8]); // ProductType
data.extend_from_slice(&10u32.to_le_bytes()); // MajorVersion
data.extend_from_slice(&0u32.to_le_bytes()); // MinorVersion
data.extend_from_slice(&19045u32.to_le_bytes()); // BuildNumber
data.extend_from_slice(&2u32.to_le_bytes()); // PlatformId
data.extend_from_slice(&0u32.to_le_bytes()); // CSDVersionRva
data.extend_from_slice(&0u16.to_le_bytes()); // SuiteMask
data.extend_from_slice(&0u16.to_le_bytes()); // Reserved2
data.extend_from_slice(&[0u8; 24]); // CPU_INFORMATION
// Memory64ListStream at offset 112
let memory_data_start = mem64_rva + mem64_size; // 144
data.extend_from_slice(&1u64.to_le_bytes()); // NumberOfMemoryRanges
data.extend_from_slice(&(memory_data_start as u64).to_le_bytes()); // BaseRva
data.extend_from_slice(&0x1000u64.to_le_bytes()); // StartOfMemoryRange
data.extend_from_slice(&8u64.to_le_bytes()); // DataSize
// Memory data: 8 bytes at offset 144
data.extend_from_slice(&0xDEAD_BEEF_CAFE_BABEu64.to_le_bytes());
data
}
#[test]
fn parse_synthetic_minidump() {
let data = make_test_minidump();
let mdmp = Minidump::parse(data).expect("parse failed");
assert_eq!(mdmp.build_number, 19045);
assert_eq!(mdmp.major_version, 10);
assert_eq!(mdmp.minor_version, 0);
assert_eq!(mdmp.modules.len(), 0);
// Read back the memory region
let val = mdmp.read_virt_u64(0x1000).expect("read failed");
assert_eq!(val, 0xDEAD_BEEF_CAFE_BABE);
}
#[test]
fn read_outside_region_fails() {
let data = make_test_minidump();
let mdmp = Minidump::parse(data).expect("parse failed");
// Address outside any region should fail
assert!(mdmp.read_virt_u64(0x2000).is_err());
}
#[test]
fn parse_real_minidump() {
let path = Path::new("lsass.dmp");
if !path.exists() {
return;
}
let mdmp = Minidump::open(path).expect("failed to parse lsass.dmp");
assert!(mdmp.build_number > 0, "build number should be > 0");
assert!(!mdmp.modules.is_empty(), "should have modules");
assert!(
mdmp.modules.iter().any(|m| !m.full_name.is_empty()),
"at least some modules should have names"
);
let has_lsasrv = mdmp.modules.iter().any(|m| m.base_name == "lsasrv.dll");
assert!(has_lsasrv, "lsasrv.dll should be in module list");
let lsasrv = mdmp.modules.iter().find(|m| m.base_name == "lsasrv.dll").unwrap();
let mz = mdmp.read_virt_u16(lsasrv.base).expect("should read MZ header");
assert_eq!(mz, 0x5A4D, "lsasrv.dll should start with MZ");
}
/// End-to-end credential extraction from lsass.dmp.
/// Validates that vmkatz extracts the same NT hash as pypykatz.
#[test]
fn extract_credentials_from_real_minidump() {
let path = Path::new("lsass.dmp");
if !path.exists() {
return;
}
let mdmp = Minidump::open(path).expect("failed to parse lsass.dmp");
let region_ranges = mdmp.region_ranges();
let credentials = vmkatz::lsass::finder::extract_credentials_from_minidump(
&mdmp,
&mdmp.modules,
mdmp.build_number,
&region_ranges,
mdmp.arch,
)
.expect("credential extraction should succeed");
// Should find at least one credential with MSV data
let msv_creds: Vec<_> = credentials.iter().filter(|c| c.msv.is_some()).collect();
assert!(
!msv_creds.is_empty(),
"should extract at least one MSV credential from lsass.dmp"
);
// Validate known-good NT hash for user@SECLAB (Win10 19045)
let expected_nt = "bbf7d1528afa8b0fdd40a5b2531bbb6d";
let expected_sha1 = "abdcba254aec226bb7762f709753084d3985b7d3";
let found_nt = msv_creds.iter().any(|c| {
if let Some(msv) = &c.msv {
hex::encode(msv.nt_hash) == expected_nt
} else {
false
}
});
assert!(
found_nt,
"should find NT hash {} (user@SECLAB)",
expected_nt
);
let found_sha1 = msv_creds.iter().any(|c| {
if let Some(msv) = &c.msv {
hex::encode(msv.sha1_hash) == expected_sha1
} else {
false
}
});
assert!(
found_sha1,
"should find SHA1 hash {} (matches pypykatz)",
expected_sha1
);
// Verify the user's session metadata
let user_cred = credentials
.iter()
.find(|c| c.username == "user" && c.domain == "SECLAB" && c.msv.is_some());
assert!(user_cred.is_some(), "should find user@SECLAB session with MSV data");
}