mirror of
https://github.com/nikaiw/VMkatz
synced 2026-09-25 07:51:06 +00:00
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:
@@ -4,3 +4,4 @@ vmkatz
|
|||||||
CLAUDE.md
|
CLAUDE.md
|
||||||
TASKS.md
|
TASKS.md
|
||||||
scripts/
|
scripts/
|
||||||
|
examples/
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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))
|
|
||||||
}
|
|
||||||
@@ -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
@@ -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() => {
|
||||||
|
|||||||
Reference in New Issue
Block a user