Flip EPT scanning to opt-in and remove dev examples from repo

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.
This commit is contained in:
NK
2026-03-17 23:24:03 +01:00
parent 97d1d18c6f
commit 06397923d2
5 changed files with 8 additions and 170 deletions
+1
View File
@@ -4,3 +4,4 @@ vmkatz
CLAUDE.md CLAUDE.md
TASKS.md TASKS.md
scripts/ scripts/
examples/
+2 -2
View File
@@ -144,8 +144,8 @@ cargo build --release
# Filter output to specific providers # Filter output to specific providers
./vmkatz --provider msv,kerberos snapshot.vmsn ./vmkatz --provider msv,kerberos snapshot.vmsn
# Skip EPT scanning (faster when VBS is not in use) # Enable EPT scanning for VBS/Credential Guard VMs
./vmkatz --no-ept snapshot.vmsn ./vmkatz --ept snapshot.vmsn
# Verbose output (memory regions, process list, debug info) # Verbose output (memory regions, process list, debug info)
./vmkatz -v snapshot.vmsn ./vmkatz -v snapshot.vmsn
-111
View File
@@ -1,111 +0,0 @@
// Quick test of ESE parser
// cargo run --example test_ese -- /tmp/ntds_test.dit
use std::env;
use std::fs;
struct L;
impl log::Log for L {
fn enabled(&self, m: &log::Metadata) -> bool { m.level() <= log::max_level() }
fn log(&self, r: &log::Record) { if self.enabled(r.metadata()) { eprintln!("[{}] {}", r.level(), r.args()); } }
fn flush(&self) {}
}
fn main() {
let _ = log::set_logger(&L).map(|()| log::set_max_level(log::LevelFilter::Info));
let path = env::args().nth(1).expect("Usage: test_ese <ntds.dit path>");
let data = fs::read(&path).expect("Failed to read file");
println!("File: {} ({} bytes)", path, data.len());
let db = vmkatz::ntds::ese::EseDb::open(&data).expect("Failed to open ESE database");
let tables = db.table_names();
println!("Tables found: {} -> {:?}", tables.len(), tables);
if let Some(cols) = db.columns("datatable") {
println!("datatable: {} columns", cols.len());
let interesting = [
("ATTm590045", "sAMAccountName"),
("ATTk589879", "unicodePwd"),
("ATTk589914", "dBCSPwd"),
("ATTr589970", "objectSid"),
("ATTj589832", "userAccountControl"),
("ATTk590689", "pekList"),
];
for (name, desc) in &interesting {
if let Some(col) = cols.iter().find(|c| c.name == *name) {
println!(" {} ({}) -> id={} type={} tagged={}",
name, desc, col.id, col.col_type, col.is_tagged);
} else {
println!(" {} ({}) -> NOT FOUND", name, desc);
}
}
}
// Scan for PEK
println!("\n=== PEK search ===");
db.for_each_row("datatable", |read_col| {
if let Some(pek_data) = read_col("ATTk590689") {
let sam = read_col("ATTm590045").map(|d| decode_string(&d)).unwrap_or_default();
println!(" PEK found! sam='{}' len={} ver=0x{:02x}",
sam, pek_data.len(),
if pek_data.len() >= 4 { u32::from_le_bytes(pek_data[..4].try_into().unwrap()) } else { 0 });
}
}).expect("Failed to iterate datatable for PEK");
// Read user records
println!("\n=== User records ===");
let mut count = 0;
db.for_each_row("datatable", |read_col| {
if let Some(sam_data) = read_col("ATTm590045") {
let name = decode_string(&sam_data);
let sid = read_col("ATTr589970");
let rid = sid.as_ref().and_then(|s| extract_rid(s));
let uac = read_col("ATTj589832")
.and_then(|d| if d.len() >= 4 { Some(u32::from_le_bytes(d[..4].try_into().unwrap())) } else { None });
let nt_len = read_col("ATTk589879").map(|d| d.len()).unwrap_or(0);
let lm_len = read_col("ATTk589914").map(|d| d.len()).unwrap_or(0);
if lm_len > 0 || nt_len > 0 {
println!(" {} | RID={:?} | UAC={:?} | nt_pwd={} | lm_pwd={}",
name, rid, uac, nt_len, lm_len);
}
count += 1;
}
}).expect("Failed to iterate datatable");
println!("\nTotal records with sAMAccountName: {}", count);
}
fn decode_string(data: &[u8]) -> String {
if data.len() >= 2 && data.len().is_multiple_of(2) {
let u16s: Vec<u16> = data.chunks_exact(2)
.map(|c| u16::from_le_bytes([c[0], c[1]]))
.take_while(|&c| c != 0)
.collect();
let s = String::from_utf16_lossy(&u16s);
if !s.is_empty() && s.chars().all(|c| !c.is_control()) {
return s;
}
}
String::from_utf8_lossy(data).trim_end_matches('\0').to_string()
}
fn extract_rid(sid: &[u8]) -> Option<u32> {
if sid.len() < 8 { return None; }
let sub_auth_count = sid[1] as usize;
let expected_len = 8 + sub_auth_count * 4;
if sid.len() < expected_len || sub_auth_count == 0 { return None; }
let rid_offset = 8 + (sub_auth_count - 1) * 4;
let rid_bytes: [u8; 4] = sid[rid_offset..rid_offset + 4].try_into().ok()?;
let rid_le = u32::from_le_bytes(rid_bytes);
let rid_be = u32::from_be_bytes(rid_bytes);
Some(rid_le.min(rid_be))
}
-52
View File
@@ -1,52 +0,0 @@
// End-to-end NTDS.dit hash extraction test
// Usage: cargo run --example test_ntds -- <ntds.dit> <SYSTEM hive>
use std::env;
use std::fs;
struct L;
impl log::Log for L {
fn enabled(&self, m: &log::Metadata) -> bool { m.level() <= log::max_level() }
fn log(&self, r: &log::Record) { if self.enabled(r.metadata()) { eprintln!("[{}] {}", r.level(), r.args()); } }
fn flush(&self) {}
}
fn main() {
let _ = log::set_logger(&L).map(|()| log::set_max_level(log::LevelFilter::Info));
let args: Vec<String> = env::args().collect();
if args.len() < 3 {
eprintln!("Usage: test_ntds <ntds.dit path> <SYSTEM hive path>");
std::process::exit(1);
}
let ntds_data = fs::read(&args[1]).expect("Failed to read NTDS.dit");
let system_data = fs::read(&args[2]).expect("Failed to read SYSTEM hive");
println!("NTDS.dit: {} bytes", ntds_data.len());
println!("SYSTEM: {} bytes", system_data.len());
match vmkatz::ntds::extract_ad_hashes(&ntds_data, &system_data, false) {
Ok(entries) => {
println!("\n=== Extracted {} hash entries ===\n", entries.len());
// Standard empty LM hash (hash of empty password using LanMan algorithm)
let empty_lm = "aad3b435b51404eeaad3b435b51404ee";
for entry in &entries {
let nt_hex = hex::encode(entry.nt_hash);
let lm_hex = if entry.lm_hash == [0u8; 16] {
empty_lm.to_string()
} else {
hex::encode(entry.lm_hash)
};
println!(
"{}:{}:{}:{}:::",
entry.username, entry.rid, lm_hex, nt_hex,
);
}
}
Err(e) => {
eprintln!("Error: {}", e);
std::process::exit(1);
}
}
}
+5 -5
View File
@@ -160,9 +160,9 @@ struct Args {
#[arg(short, long, default_value_t = false)] #[arg(short, long, default_value_t = false)]
all: bool, all: bool,
/// Skip nested EPT scanning (faster for non-VBS VMs) /// Enable nested EPT scanning (for VBS/Credential Guard VMs)
#[arg(long, default_value_t = false)] #[arg(long, default_value_t = false)]
no_ept: bool, ept: bool,
/// Recursively scan directory for VM snapshot and disk image files /// Recursively scan directory for VM snapshot and disk image files
#[arg(short, long, default_value_t = false)] #[arg(short, long, default_value_t = false)]
@@ -1915,12 +1915,12 @@ fn run_with_layer<L: PhysicalMemory, F: FnOnce() -> anyhow::Result<L>>(
) )
} }
#[cfg(feature = "carve")] #[cfg(feature = "carve")]
Err(_) if args.no_ept && args.carve => { Err(_) if !args.ept && args.carve => {
eprintln!("[*] System process not found — falling back to carve mode"); eprintln!("[*] System process not found — falling back to carve mode");
run_carve(&layer, args, pagefile, disk_path) run_carve(&layer, args, pagefile, disk_path)
} }
Err(_) if args.no_ept => { Err(_) if !args.ept => {
anyhow::bail!("System process not found in physical memory (EPT scan disabled with --no-ept)"); anyhow::bail!("System process not found in physical memory (EPT scan disabled, use --ept to enable)");
} }
#[cfg(feature = "carve")] #[cfg(feature = "carve")]
Err(_) if args.carve && layer.is_truncated() => { Err(_) if args.carve && layer.is_truncated() => {