mirror of
https://github.com/nikaiw/VMkatz
synced 2026-09-25 07:51:06 +00:00
Fix DCC2 key offset, GMSA display, and add block device support
- 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.
This commit is contained in:
@@ -54,9 +54,10 @@ All 9 SSP credential providers that mimikatz implements:
|
||||
| VirtualBox virtual disks | `.vdi` | VirtualBox |
|
||||
| QEMU/KVM virtual disks | `.qcow2` | QEMU, Proxmox |
|
||||
| Hyper-V virtual disks | `.vhdx`, `.vhd` | Hyper-V |
|
||||
| LVM block devices | `/dev/...` | Proxmox LVM-thin, raw LVs |
|
||||
| VM directories | any folder | Auto-discovers all processable files |
|
||||
|
||||
**Target OS**: Windows 7 SP1 through Windows 11 x64 (auto-detected).
|
||||
**Target OS**: Windows 7 SP1 through Windows Server 2025 x64 (auto-detected).
|
||||
|
||||
## Quick Start
|
||||
|
||||
@@ -200,7 +201,7 @@ cargo build --release --no-default-features --features "sam ntds.dit"
|
||||
|
||||
## Tested Targets
|
||||
|
||||
20 VMs tested across 6 Windows versions and 3 hypervisors.
|
||||
Tested across 7 Windows versions and 4 hypervisors.
|
||||
|
||||
| Hypervisor | Guest OS | Artifact | Result | Notes |
|
||||
| --- | --- | --- | --- | --- |
|
||||
@@ -219,6 +220,9 @@ cargo build --release --no-default-features --features "sam ntds.dit"
|
||||
| ESXi 8.0 | Windows 11 x64 | LSASS (`.vmsn`) | PASS | 2 VMs, no VBS |
|
||||
| ESXi 8.0 | Windows 11 x64 | SAM (flat `.vmdk`) | PASS | Powered-off VM |
|
||||
| ESXi 8.0 | Windows 11 x64 (VBS) | LSASS (`.vmsn`) | FAIL | Credential Guard / VBS |
|
||||
| Proxmox 8 | Windows Server 2016 x64 | SAM / LSA / DCC2 (LVM block device) | PASS | Live + stopped VMs |
|
||||
| Proxmox 8 | Windows Server 2019 x64 | SAM / LSA / DCC2 (LVM block device) | PASS | 3 VMs, incl. DCs |
|
||||
| Proxmox 8 | Windows Server 2025 x64 | SAM / LSA (LVM block device) | PASS | Template VM |
|
||||
|
||||
### Known limitations
|
||||
- **VBS / Credential Guard**: VMs with Virtualization-Based Security enabled use nested Hyper-V page tables. The VMEM captured by ESXi is 99% zero pages because the actual kernel memory is behind Hyper-V's SLAT. An EPT walker is implemented but cannot yet recover credentials from these VMs. SAM extraction from the virtual disk still works when the VM is powered off.
|
||||
|
||||
+6
-5
@@ -25,7 +25,7 @@ pub fn open_disk(path: &Path) -> Result<Box<dyn DiskImage>> {
|
||||
.to_lowercase();
|
||||
let stem = path.file_stem().and_then(|s| s.to_str()).unwrap_or("");
|
||||
|
||||
// Detect flat VMDK (e.g., "name-flat.vmdk") — raw disk, not sparse
|
||||
// Detect flat VMDK (e.g., "name-flat.vmdk") - raw disk, not sparse
|
||||
if ext == "vmdk" && stem.ends_with("-flat") {
|
||||
let disk = raw::RawDisk::open(path)?;
|
||||
return Ok(Box::new(disk));
|
||||
@@ -56,9 +56,10 @@ pub fn open_disk(path: &Path) -> Result<Box<dyn DiskImage>> {
|
||||
let disk = raw::RawDisk::open(path)?;
|
||||
Ok(Box::new(disk))
|
||||
}
|
||||
_ => Err(crate::error::GovmemError::ProcessNotFound(format!(
|
||||
"Unsupported disk format: .{}",
|
||||
ext
|
||||
))),
|
||||
_ => {
|
||||
// No recognized extension - try as raw disk (block devices, etc.)
|
||||
let disk = raw::RawDisk::open(path)?;
|
||||
Ok(Box::new(disk))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+7
-4
@@ -5,7 +5,8 @@ use std::path::Path;
|
||||
use crate::error::{GovmemError, Result};
|
||||
|
||||
/// Raw flat disk image — no container format, just raw sectors.
|
||||
/// Handles flat VMDKs (`-flat.vmdk`), raw dumps (`.raw`, `.img`, `.dd`).
|
||||
/// Handles flat VMDKs (`-flat.vmdk`), raw dumps (`.raw`, `.img`, `.dd`),
|
||||
/// and block devices (`/dev/...`).
|
||||
pub struct RawDisk {
|
||||
file: File,
|
||||
size: u64,
|
||||
@@ -13,11 +14,13 @@ pub struct RawDisk {
|
||||
|
||||
impl RawDisk {
|
||||
pub fn open(path: &Path) -> Result<Self> {
|
||||
let file = File::open(path).map_err(GovmemError::Io)?;
|
||||
let size = file.metadata().map_err(GovmemError::Io)?.len();
|
||||
let mut file = File::open(path).map_err(GovmemError::Io)?;
|
||||
// metadata().len() returns 0 for block devices, so seek to end instead
|
||||
let size = file.seek(SeekFrom::End(0)).map_err(GovmemError::Io)?;
|
||||
file.seek(SeekFrom::Start(0)).map_err(GovmemError::Io)?;
|
||||
log::info!(
|
||||
"Raw disk: {} ({} MB)",
|
||||
path.file_name().unwrap_or_default().to_string_lossy(),
|
||||
path.display(),
|
||||
size / (1024 * 1024)
|
||||
);
|
||||
Ok(Self { file, size })
|
||||
|
||||
+26
-16
@@ -150,30 +150,24 @@ fn main() -> anyhow::Result<()> {
|
||||
return run_directory(input_path, &args);
|
||||
}
|
||||
|
||||
// Auto-detect SAM mode for disk images, or explicit --sam flag
|
||||
// Auto-detect SAM mode for disk images / block devices, or explicit --sam flag
|
||||
#[cfg(feature = "sam")]
|
||||
{
|
||||
let ext = input_path
|
||||
.extension()
|
||||
.and_then(|e| e.to_str())
|
||||
.unwrap_or("");
|
||||
let is_disk_ext = ext.eq_ignore_ascii_case("vdi")
|
||||
|| ext.eq_ignore_ascii_case("vmdk")
|
||||
|| ext.eq_ignore_ascii_case("qcow2")
|
||||
|| ext.eq_ignore_ascii_case("qcow")
|
||||
|| ext.eq_ignore_ascii_case("vhdx")
|
||||
|| ext.eq_ignore_ascii_case("vhd");
|
||||
let is_block_device = is_block_dev(input_path);
|
||||
#[cfg(feature = "ntds.dit")]
|
||||
let sam_mode = args.sam
|
||||
|| args.ntds
|
||||
|| ext.eq_ignore_ascii_case("vdi")
|
||||
|| ext.eq_ignore_ascii_case("vmdk")
|
||||
|| ext.eq_ignore_ascii_case("qcow2")
|
||||
|| ext.eq_ignore_ascii_case("qcow")
|
||||
|| ext.eq_ignore_ascii_case("vhdx")
|
||||
|| ext.eq_ignore_ascii_case("vhd");
|
||||
let sam_mode = args.sam || args.ntds || is_disk_ext || is_block_device;
|
||||
#[cfg(not(feature = "ntds.dit"))]
|
||||
let sam_mode = args.sam
|
||||
|| ext.eq_ignore_ascii_case("vdi")
|
||||
|| ext.eq_ignore_ascii_case("vmdk")
|
||||
|| ext.eq_ignore_ascii_case("qcow2")
|
||||
|| ext.eq_ignore_ascii_case("qcow")
|
||||
|| ext.eq_ignore_ascii_case("vhdx")
|
||||
|| ext.eq_ignore_ascii_case("vhd");
|
||||
let sam_mode = args.sam || is_disk_ext || is_block_device;
|
||||
if sam_mode {
|
||||
return run_sam(input_path, &args);
|
||||
}
|
||||
@@ -668,6 +662,22 @@ fn run_lsass(
|
||||
}
|
||||
}
|
||||
|
||||
/// Check if a path is a block device (Linux /dev/...).
|
||||
fn is_block_dev(path: &Path) -> bool {
|
||||
#[cfg(unix)]
|
||||
{
|
||||
use std::os::unix::fs::FileTypeExt;
|
||||
std::fs::metadata(path)
|
||||
.map(|m| m.file_type().is_block_device())
|
||||
.unwrap_or(false)
|
||||
}
|
||||
#[cfg(not(unix))]
|
||||
{
|
||||
let _ = path;
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
/// Format detection for LSASS memory snapshot files.
|
||||
enum LsassFormat {
|
||||
VBox,
|
||||
|
||||
+4
-2
@@ -95,8 +95,10 @@ pub fn extract_cached_credentials(
|
||||
|
||||
log::info!("DCC2 iteration count: {}", iteration_count);
|
||||
|
||||
// AES key = NL$KM[16..32]
|
||||
let aes_key = &nlkm_key[16..32];
|
||||
// AES key = first 16 bytes of the NL$KM secret.
|
||||
// Note: pypykatz/impacket use [16:32] on the RAW blob (including the 16-byte
|
||||
// LSA_SECRET_BLOB header), which is equivalent to [0:16] on the stripped secret.
|
||||
let aes_key = &nlkm_key[..16];
|
||||
|
||||
let mut credentials = Vec::new();
|
||||
|
||||
|
||||
@@ -447,6 +447,13 @@ fn parse_secret(name: &str, data: &[u8]) -> LsaSecretType {
|
||||
}
|
||||
|
||||
if let Some(service_name) = name.strip_prefix("_SC_") {
|
||||
// GMSA managed passwords are binary blobs, not UTF-16LE text
|
||||
if service_name.starts_with("GMSA_") || service_name.starts_with("GMSA{") {
|
||||
return LsaSecretType::ServicePassword {
|
||||
service: service_name.to_string(),
|
||||
password: hex::encode(data),
|
||||
};
|
||||
}
|
||||
let password = decode_utf16le(data);
|
||||
return LsaSecretType::ServicePassword {
|
||||
service: service_name.to_string(),
|
||||
|
||||
Reference in New Issue
Block a user