mirror of
https://github.com/nikaiw/VMkatz
synced 2026-09-25 07:51:06 +00:00
Add QEMU/KVM/Proxmox ELF core dump and Hyper-V .bin support
- New `qemu` feature: ELF64 core dump reader (from dump-guest-memory / virsh dump) Parses PT_LOAD segments for GPA→file offset mapping with binary search - New `hyperv` feature: Hyper-V legacy .bin raw memory reader (identity mapping) Also handles raw dumps from MemProcFS export - Smart format detection: magic-based (ELF header) with extension fallback .elf → QEMU, .bin/.raw → auto-detect ELF or Hyper-V, .sav → VBox, else VMware - Auto-discovery: .elf, .bin, .raw files in folder mode - Fix MSV physical scan merge: LUID=0 credentials now match existing sessions by username+domain instead of all colliding at HashMap key 0
This commit is contained in:
+3
-1
@@ -4,9 +4,11 @@ version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[features]
|
||||
default = ["vmware", "vbox", "sam"]
|
||||
default = ["vmware", "vbox", "qemu", "hyperv", "sam"]
|
||||
vmware = ["dep:memmap2"]
|
||||
vbox = []
|
||||
qemu = ["dep:memmap2"]
|
||||
hyperv = ["dep:memmap2"]
|
||||
sam = ["dep:ntfs", "dep:md-5", "dep:sha2"]
|
||||
|
||||
[dependencies]
|
||||
|
||||
+39
-2
@@ -60,7 +60,7 @@ pub fn discover_vm_files(dir: &Path) -> Result<VmDiscovery> {
|
||||
})
|
||||
}
|
||||
|
||||
/// Find .vmsn files with matching .vmem, and standalone .sav files.
|
||||
/// Find memory snapshot files: .vmsn+.vmem, .sav, .elf, .bin, .raw
|
||||
fn discover_lsass_files(all_files: &[PathBuf], out: &mut Vec<PathBuf>) {
|
||||
for file in all_files {
|
||||
let ext = file.extension().and_then(|e| e.to_str()).unwrap_or("");
|
||||
@@ -72,16 +72,53 @@ fn discover_lsass_files(all_files: &[PathBuf], out: &mut Vec<PathBuf>) {
|
||||
out.push(file.clone());
|
||||
}
|
||||
} else if ext.eq_ignore_ascii_case("sav") {
|
||||
// Skip empty .sav files (0 bytes = incomplete/placeholder snapshot)
|
||||
// VirtualBox saved state - skip empty files
|
||||
if let Ok(meta) = file.metadata() {
|
||||
if meta.len() > 0 {
|
||||
out.push(file.clone());
|
||||
}
|
||||
}
|
||||
} else if ext.eq_ignore_ascii_case("elf") {
|
||||
// QEMU ELF core dump (from dump-guest-memory / virsh dump --memory-only)
|
||||
if is_elf_core(file) {
|
||||
out.push(file.clone());
|
||||
}
|
||||
} else if ext.eq_ignore_ascii_case("bin") {
|
||||
// Hyper-V legacy .bin or ELF dump with .bin extension
|
||||
if let Ok(meta) = file.metadata() {
|
||||
// Hyper-V .bin files are VM-RAM-sized; skip tiny metadata files
|
||||
if meta.len() > 1024 * 1024 {
|
||||
out.push(file.clone());
|
||||
}
|
||||
}
|
||||
} else if ext.eq_ignore_ascii_case("raw") {
|
||||
// Raw memory dump (from MemProcFS export, etc.)
|
||||
if let Ok(meta) = file.metadata() {
|
||||
if meta.len() > 1024 * 1024 {
|
||||
out.push(file.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Check if a file is an ELF core dump (magic + ET_CORE). Reads only 18 bytes.
|
||||
fn is_elf_core(path: &Path) -> bool {
|
||||
use std::io::Read;
|
||||
let Ok(mut f) = fs::File::open(path) else { return false };
|
||||
let mut buf = [0u8; 18];
|
||||
if f.read_exact(&mut buf).is_err() {
|
||||
return false;
|
||||
}
|
||||
// ELF magic: 7f 45 4c 46
|
||||
if buf[0..4] != [0x7f, b'E', b'L', b'F'] {
|
||||
return false;
|
||||
}
|
||||
// e_type at offset 16 (u16 LE) should be ET_CORE (4)
|
||||
let e_type = u16::from_le_bytes([buf[16], buf[17]]);
|
||||
e_type == 4
|
||||
}
|
||||
|
||||
/// Find the latest VMDK descriptor file for SAM extraction.
|
||||
///
|
||||
/// Strategy: find the highest-numbered snapshot descriptor (`*-NNNNNN.vmdk`),
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
//! Hyper-V memory layer.
|
||||
//!
|
||||
//! Supports:
|
||||
//! - Legacy `.bin` files (Hyper-V 2008/2012): raw physical memory dump (identity mapping)
|
||||
//! - Raw memory dumps from MemProcFS pmem export or vm2dmp conversions
|
||||
//!
|
||||
//! Note: Modern `.vmrs` files (Hyper-V 2016+) use a proprietary undocumented format
|
||||
//! that can only be read via Microsoft's vmsavedstatedumpprovider.dll on Windows.
|
||||
//! For .vmrs files, convert to raw dump first using MemProcFS or the Microsoft API.
|
||||
|
||||
use std::fs;
|
||||
use std::path::Path;
|
||||
|
||||
use memmap2::Mmap;
|
||||
|
||||
use crate::error::{GovmemError, Result};
|
||||
use crate::memory::PhysicalMemory;
|
||||
|
||||
/// Hyper-V memory layer: provides physical memory from .bin or raw dump files.
|
||||
pub struct HypervLayer {
|
||||
mmap: Mmap,
|
||||
size: u64,
|
||||
}
|
||||
|
||||
impl HypervLayer {
|
||||
/// Open a Hyper-V legacy .bin file or raw memory dump.
|
||||
///
|
||||
/// The file is identity-mapped: file offset = guest physical address.
|
||||
/// For .bin files, optionally loads CPU state from a companion .vsv file (future).
|
||||
pub fn open(path: &Path) -> Result<Self> {
|
||||
let file = fs::File::open(path)?;
|
||||
let mmap = unsafe { Mmap::map(&file)? };
|
||||
let size = mmap.len() as u64;
|
||||
|
||||
if size == 0 {
|
||||
return Err(GovmemError::Io(std::io::Error::new(
|
||||
std::io::ErrorKind::InvalidData,
|
||||
"Empty memory dump file",
|
||||
)));
|
||||
}
|
||||
|
||||
// Sanity check: .bin files should be at least a few MB (VM RAM)
|
||||
// Skip check for very small files (< 1MB) that are likely not memory dumps
|
||||
if size < 1024 * 1024 {
|
||||
log::warn!(
|
||||
"File is only {} KB - may not be a valid memory dump",
|
||||
size / 1024
|
||||
);
|
||||
}
|
||||
|
||||
log::info!(
|
||||
"Hyper-V .bin: {} MB identity-mapped",
|
||||
size / (1024 * 1024)
|
||||
);
|
||||
|
||||
Ok(Self { mmap, size })
|
||||
}
|
||||
}
|
||||
|
||||
impl PhysicalMemory for HypervLayer {
|
||||
fn read_phys(&self, phys_addr: u64, buf: &mut [u8]) -> Result<()> {
|
||||
let end = phys_addr + buf.len() as u64;
|
||||
if end > self.size {
|
||||
// Partial read: fill with zeros for out-of-bounds portion
|
||||
buf.fill(0);
|
||||
if phys_addr < self.size {
|
||||
let avail = (self.size - phys_addr) as usize;
|
||||
buf[..avail].copy_from_slice(&self.mmap[phys_addr as usize..self.size as usize]);
|
||||
}
|
||||
return Ok(());
|
||||
}
|
||||
buf.copy_from_slice(&self.mmap[phys_addr as usize..end as usize]);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn phys_size(&self) -> u64 {
|
||||
self.size
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
mod layer;
|
||||
|
||||
pub use layer::HypervLayer;
|
||||
@@ -4,6 +4,10 @@ pub mod memory;
|
||||
pub mod vmware;
|
||||
#[cfg(feature = "vbox")]
|
||||
pub mod vbox;
|
||||
#[cfg(feature = "qemu")]
|
||||
pub mod qemu;
|
||||
#[cfg(feature = "hyperv")]
|
||||
pub mod hyperv;
|
||||
pub mod paging;
|
||||
pub mod windows;
|
||||
pub mod pe;
|
||||
|
||||
+18
-2
@@ -186,9 +186,25 @@ pub fn extract_all_credentials<P: PhysicalMemory>(
|
||||
if !msv_creds.is_empty() {
|
||||
msv_status = "ok";
|
||||
}
|
||||
let mut next_synth_luid = 0x8000_0000_0000_0000u64;
|
||||
for (luid, msv_cred) in msv_creds {
|
||||
let entry = all_creds.entry(luid).or_insert_with(|| {
|
||||
Credential::new_empty(luid, msv_cred.username.clone(), msv_cred.domain.clone())
|
||||
// When physical scan returns LUID=0, match by username+domain to existing session
|
||||
let effective_luid = if luid == 0 {
|
||||
all_creds.iter()
|
||||
.find(|(_, c)| c.username.eq_ignore_ascii_case(&msv_cred.username)
|
||||
&& c.domain.eq_ignore_ascii_case(&msv_cred.domain)
|
||||
&& c.msv.is_none())
|
||||
.map(|(&k, _)| k)
|
||||
.unwrap_or_else(|| {
|
||||
let synth = next_synth_luid;
|
||||
next_synth_luid += 1;
|
||||
synth
|
||||
})
|
||||
} else {
|
||||
luid
|
||||
};
|
||||
let entry = all_creds.entry(effective_luid).or_insert_with(|| {
|
||||
Credential::new_empty(effective_luid, msv_cred.username.clone(), msv_cred.domain.clone())
|
||||
});
|
||||
entry.msv = Some(msv_cred);
|
||||
}
|
||||
|
||||
+190
-75
@@ -1,36 +1,41 @@
|
||||
#[cfg(not(any(feature = "vmware", feature = "vbox", feature = "sam")))]
|
||||
compile_error!("At least one backend must be enabled: --features vmware, vbox, and/or sam");
|
||||
#[cfg(not(any(feature = "vmware", feature = "vbox", feature = "qemu", feature = "hyperv", feature = "sam")))]
|
||||
compile_error!("At least one backend must be enabled: --features vmware, vbox, qemu, hyperv, and/or sam");
|
||||
|
||||
use std::path::Path;
|
||||
|
||||
use anyhow::Context;
|
||||
use clap::Parser;
|
||||
|
||||
#[cfg(any(feature = "vmware", feature = "vbox"))]
|
||||
#[cfg(any(feature = "vmware", feature = "vbox", feature = "qemu", feature = "hyperv"))]
|
||||
use vmkatz::lsass;
|
||||
use vmkatz::lsass::finder::PagefileRef;
|
||||
#[cfg(any(feature = "vmware", feature = "vbox"))]
|
||||
#[cfg(any(feature = "vmware", feature = "vbox", feature = "qemu", feature = "hyperv"))]
|
||||
use vmkatz::lsass::types::Credential;
|
||||
#[cfg(any(feature = "vmware", feature = "vbox"))]
|
||||
#[cfg(any(feature = "vmware", feature = "vbox", feature = "qemu", feature = "hyperv"))]
|
||||
use vmkatz::memory::PhysicalMemory;
|
||||
#[cfg(feature = "vbox")]
|
||||
use vmkatz::vbox::VBoxLayer;
|
||||
#[cfg(feature = "vmware")]
|
||||
use vmkatz::vmware::VmwareLayer;
|
||||
#[cfg(any(feature = "vmware", feature = "vbox"))]
|
||||
#[cfg(feature = "qemu")]
|
||||
use vmkatz::qemu::QemuElfLayer;
|
||||
#[cfg(feature = "hyperv")]
|
||||
use vmkatz::hyperv::HypervLayer;
|
||||
// EPROCESS offsets auto-detected at runtime from ALL_EPROCESS_OFFSETS
|
||||
#[cfg(any(feature = "vmware", feature = "vbox"))]
|
||||
#[cfg(any(feature = "vmware", feature = "vbox", feature = "qemu", feature = "hyperv"))]
|
||||
use vmkatz::windows::process;
|
||||
|
||||
#[derive(Parser, Debug)]
|
||||
#[command(
|
||||
name = "vmkatz",
|
||||
version,
|
||||
about = "VM memory forensics - extract credentials from VMware/VirtualBox/Hyper-V snapshots and disk images",
|
||||
about = "VM memory forensics - extract credentials from VMware/VirtualBox/QEMU/Hyper-V snapshots and disk images",
|
||||
long_about = "vmkatz extracts Windows credentials from virtual machine memory snapshots and disk images.\n\n\
|
||||
Supported inputs:\n \
|
||||
- VMware snapshots (.vmsn + .vmem)\n \
|
||||
- VirtualBox saved states (.sav)\n \
|
||||
- QEMU/KVM/Proxmox ELF core dumps (.elf, from dump-guest-memory / virsh dump)\n \
|
||||
- Hyper-V legacy saved states (.bin) and raw memory dumps (.raw, .dmp)\n \
|
||||
- Disk images for SAM hashes (.vdi, .vmdk, .qcow2, .vhdx, .vhd)\n \
|
||||
- VM directories (auto-discovers all files)\n\n\
|
||||
Target: Windows 7 SP1 through Windows 11 x64",
|
||||
@@ -283,7 +288,7 @@ fn run_directory(dir: &Path, args: &Args) -> anyhow::Result<()> {
|
||||
#[cfg(not(feature = "sam"))]
|
||||
let disk_path: vmkatz::lsass::finder::DiskPathRef<'_> = Default::default();
|
||||
|
||||
#[cfg(any(feature = "vmware", feature = "vbox"))]
|
||||
#[cfg(any(feature = "vmware", feature = "vbox", feature = "qemu", feature = "hyperv"))]
|
||||
for file in &discovery.lsass_files {
|
||||
let name = file.file_name().unwrap_or_default().to_string_lossy();
|
||||
println!("\n[*] LSASS: {}", name);
|
||||
@@ -313,73 +318,183 @@ fn run_lsass(input_path: &Path, args: &Args, pagefile: PagefileRef<'_>, disk_pat
|
||||
let verbose = args.verbose || args.list_processes;
|
||||
let ext = input_path.extension().and_then(|e| e.to_str()).unwrap_or("");
|
||||
|
||||
if ext.eq_ignore_ascii_case("sav") {
|
||||
#[cfg(feature = "vbox")]
|
||||
{
|
||||
run_with_layer(
|
||||
|| {
|
||||
if verbose {
|
||||
println!("[*] Opening VirtualBox saved state: {}", input_path.display());
|
||||
}
|
||||
let layer = VBoxLayer::open(input_path)
|
||||
.context("Failed to open VirtualBox .sav file")?;
|
||||
if verbose {
|
||||
println!("[+] RAM: {} MB ({} pages mapped)", layer.phys_size() / (1024 * 1024), layer.page_count());
|
||||
}
|
||||
Ok(layer)
|
||||
},
|
||||
args,
|
||||
verbose,
|
||||
pagefile,
|
||||
disk_path,
|
||||
)
|
||||
}
|
||||
#[cfg(not(feature = "vbox"))]
|
||||
{
|
||||
let _ = (pagefile, disk_path);
|
||||
anyhow::bail!("VirtualBox .sav support not enabled (compile with --features vbox)")
|
||||
}
|
||||
} else {
|
||||
#[cfg(feature = "vmware")]
|
||||
{
|
||||
run_with_layer(
|
||||
|| {
|
||||
if verbose {
|
||||
println!("[*] Opening VMware memory dump: {}", input_path.display());
|
||||
}
|
||||
let layer = VmwareLayer::open(input_path)
|
||||
.context("Failed to open VMware memory dump")?;
|
||||
if verbose {
|
||||
println!("[+] VMEM mapped: {} MB", layer.phys_size() / (1024 * 1024));
|
||||
println!("[+] Memory regions: {}", layer.regions.len());
|
||||
for (i, region) in layer.regions.iter().enumerate() {
|
||||
println!(
|
||||
" Region {}: guest=0x{:x} vmem=0x{:x} pages=0x{:x} ({}MB)",
|
||||
i,
|
||||
region.guest_page_num,
|
||||
region.vmem_page_num,
|
||||
region.page_count,
|
||||
(region.page_count * 0x1000) / (1024 * 1024)
|
||||
);
|
||||
// Detect format by extension and magic bytes
|
||||
let format = detect_lsass_format(input_path, ext);
|
||||
|
||||
match format {
|
||||
LsassFormat::VBox => {
|
||||
#[cfg(feature = "vbox")]
|
||||
{
|
||||
run_with_layer(
|
||||
|| {
|
||||
if verbose {
|
||||
println!("[*] Opening VirtualBox saved state: {}", input_path.display());
|
||||
}
|
||||
}
|
||||
Ok(layer)
|
||||
},
|
||||
args,
|
||||
verbose,
|
||||
pagefile,
|
||||
disk_path,
|
||||
)
|
||||
let layer = VBoxLayer::open(input_path)
|
||||
.context("Failed to open VirtualBox .sav file")?;
|
||||
if verbose {
|
||||
println!("[+] RAM: {} MB ({} pages mapped)", layer.phys_size() / (1024 * 1024), layer.page_count());
|
||||
}
|
||||
Ok(layer)
|
||||
},
|
||||
args,
|
||||
verbose,
|
||||
pagefile,
|
||||
disk_path,
|
||||
)
|
||||
}
|
||||
#[cfg(not(feature = "vbox"))]
|
||||
{
|
||||
let _ = (pagefile, disk_path);
|
||||
anyhow::bail!("VirtualBox .sav support not enabled (compile with --features vbox)")
|
||||
}
|
||||
}
|
||||
#[cfg(not(feature = "vmware"))]
|
||||
{
|
||||
let _ = (pagefile, disk_path);
|
||||
anyhow::bail!("VMware .vmem/.vmsn support not enabled (compile with --features vmware)")
|
||||
LsassFormat::QemuElf => {
|
||||
#[cfg(feature = "qemu")]
|
||||
{
|
||||
run_with_layer(
|
||||
|| {
|
||||
if verbose {
|
||||
println!("[*] Opening QEMU ELF core dump: {}", input_path.display());
|
||||
}
|
||||
let layer = QemuElfLayer::open(input_path)
|
||||
.context("Failed to open QEMU ELF core dump")?;
|
||||
if verbose {
|
||||
println!("[+] ELF: {} MB physical, {} PT_LOAD segments",
|
||||
layer.phys_size() / (1024 * 1024), layer.segment_count());
|
||||
}
|
||||
Ok(layer)
|
||||
},
|
||||
args,
|
||||
verbose,
|
||||
pagefile,
|
||||
disk_path,
|
||||
)
|
||||
}
|
||||
#[cfg(not(feature = "qemu"))]
|
||||
{
|
||||
let _ = (pagefile, disk_path);
|
||||
anyhow::bail!("QEMU ELF support not enabled (compile with --features qemu)")
|
||||
}
|
||||
}
|
||||
LsassFormat::HypervBin => {
|
||||
#[cfg(feature = "hyperv")]
|
||||
{
|
||||
run_with_layer(
|
||||
|| {
|
||||
if verbose {
|
||||
println!("[*] Opening Hyper-V memory dump: {}", input_path.display());
|
||||
}
|
||||
let layer = HypervLayer::open(input_path)
|
||||
.context("Failed to open Hyper-V .bin memory dump")?;
|
||||
if verbose {
|
||||
println!("[+] RAM: {} MB identity-mapped", layer.phys_size() / (1024 * 1024));
|
||||
}
|
||||
Ok(layer)
|
||||
},
|
||||
args,
|
||||
verbose,
|
||||
pagefile,
|
||||
disk_path,
|
||||
)
|
||||
}
|
||||
#[cfg(not(feature = "hyperv"))]
|
||||
{
|
||||
let _ = (pagefile, disk_path);
|
||||
anyhow::bail!("Hyper-V support not enabled (compile with --features hyperv)")
|
||||
}
|
||||
}
|
||||
LsassFormat::Vmware => {
|
||||
#[cfg(feature = "vmware")]
|
||||
{
|
||||
run_with_layer(
|
||||
|| {
|
||||
if verbose {
|
||||
println!("[*] Opening VMware memory dump: {}", input_path.display());
|
||||
}
|
||||
let layer = VmwareLayer::open(input_path)
|
||||
.context("Failed to open VMware memory dump")?;
|
||||
if verbose {
|
||||
println!("[+] VMEM mapped: {} MB", layer.phys_size() / (1024 * 1024));
|
||||
println!("[+] Memory regions: {}", layer.regions.len());
|
||||
for (i, region) in layer.regions.iter().enumerate() {
|
||||
println!(
|
||||
" Region {}: guest=0x{:x} vmem=0x{:x} pages=0x{:x} ({}MB)",
|
||||
i,
|
||||
region.guest_page_num,
|
||||
region.vmem_page_num,
|
||||
region.page_count,
|
||||
(region.page_count * 0x1000) / (1024 * 1024)
|
||||
);
|
||||
}
|
||||
}
|
||||
Ok(layer)
|
||||
},
|
||||
args,
|
||||
verbose,
|
||||
pagefile,
|
||||
disk_path,
|
||||
)
|
||||
}
|
||||
#[cfg(not(feature = "vmware"))]
|
||||
{
|
||||
let _ = (pagefile, disk_path);
|
||||
anyhow::bail!("VMware .vmem/.vmsn support not enabled (compile with --features vmware)")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(any(feature = "vmware", feature = "vbox"))]
|
||||
/// Format detection for LSASS memory snapshot files.
|
||||
enum LsassFormat {
|
||||
VBox,
|
||||
QemuElf,
|
||||
HypervBin,
|
||||
Vmware,
|
||||
}
|
||||
|
||||
/// Detect the memory snapshot format from extension and magic bytes.
|
||||
fn detect_lsass_format(path: &Path, ext: &str) -> LsassFormat {
|
||||
// Extension-based detection first
|
||||
if ext.eq_ignore_ascii_case("sav") {
|
||||
return LsassFormat::VBox;
|
||||
}
|
||||
if ext.eq_ignore_ascii_case("elf") {
|
||||
return LsassFormat::QemuElf;
|
||||
}
|
||||
if ext.eq_ignore_ascii_case("bin") {
|
||||
// Could be Hyper-V .bin or a raw dump — check for ELF magic
|
||||
if has_elf_magic(path) {
|
||||
return LsassFormat::QemuElf;
|
||||
}
|
||||
return LsassFormat::HypervBin;
|
||||
}
|
||||
if ext.eq_ignore_ascii_case("raw") {
|
||||
// Raw memory dump — check for ELF magic (virsh dump can produce .raw)
|
||||
if has_elf_magic(path) {
|
||||
return LsassFormat::QemuElf;
|
||||
}
|
||||
return LsassFormat::HypervBin;
|
||||
}
|
||||
|
||||
// For unknown extensions, try magic-based detection
|
||||
if has_elf_magic(path) {
|
||||
return LsassFormat::QemuElf;
|
||||
}
|
||||
|
||||
// Default: VMware (.vmem, .vmsn, or anything else)
|
||||
LsassFormat::Vmware
|
||||
}
|
||||
|
||||
/// Check if file starts with ELF magic bytes (reads only 4 bytes).
|
||||
fn has_elf_magic(path: &Path) -> bool {
|
||||
use std::io::Read;
|
||||
let Ok(mut f) = std::fs::File::open(path) else { return false };
|
||||
let mut magic = [0u8; 4];
|
||||
f.read_exact(&mut magic).is_ok() && magic == [0x7f, b'E', b'L', b'F']
|
||||
}
|
||||
|
||||
#[cfg(any(feature = "vmware", feature = "vbox", feature = "qemu", feature = "hyperv"))]
|
||||
fn run_with_layer<L: PhysicalMemory, F: FnOnce() -> anyhow::Result<L>>(
|
||||
make_layer: F,
|
||||
args: &Args,
|
||||
@@ -473,7 +588,7 @@ fn run_with_layer<L: PhysicalMemory, F: FnOnce() -> anyhow::Result<L>>(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(any(feature = "vmware", feature = "vbox"))]
|
||||
#[cfg(any(feature = "vmware", feature = "vbox", feature = "qemu", feature = "hyperv"))]
|
||||
fn find_process_by_name<'a>(
|
||||
processes: &'a [vmkatz::windows::process::Process],
|
||||
name: &str,
|
||||
@@ -491,7 +606,7 @@ fn find_process_by_name<'a>(
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(any(feature = "vmware", feature = "vbox"))]
|
||||
#[cfg(any(feature = "vmware", feature = "vbox", feature = "qemu", feature = "hyperv"))]
|
||||
fn print_text(credentials: &[Credential]) {
|
||||
let with_creds = credentials.iter().filter(|c| c.has_credentials()).count();
|
||||
println!(
|
||||
@@ -504,7 +619,7 @@ fn print_text(credentials: &[Credential]) {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(any(feature = "vmware", feature = "vbox"))]
|
||||
#[cfg(any(feature = "vmware", feature = "vbox", feature = "qemu", feature = "hyperv"))]
|
||||
fn csv_escape(s: &str) -> String {
|
||||
if s.contains(',') || s.contains('"') || s.contains('\n') {
|
||||
format!("\"{}\"", s.replace('"', "\"\""))
|
||||
@@ -513,7 +628,7 @@ fn csv_escape(s: &str) -> String {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(any(feature = "vmware", feature = "vbox"))]
|
||||
#[cfg(any(feature = "vmware", feature = "vbox", feature = "qemu", feature = "hyperv"))]
|
||||
fn print_csv(credentials: &[Credential]) {
|
||||
println!("luid,username,domain,nt_hash,lm_hash,sha1_hash,wdigest_password,kerberos_password,tspkg_password");
|
||||
for cred in credentials.iter().filter(|c| c.has_credentials()) {
|
||||
@@ -557,7 +672,7 @@ fn print_csv(credentials: &[Credential]) {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(any(feature = "vmware", feature = "vbox"))]
|
||||
#[cfg(any(feature = "vmware", feature = "vbox", feature = "qemu", feature = "hyperv"))]
|
||||
fn print_ntlm(credentials: &[Credential]) {
|
||||
let zero_hash = [0u8; 16];
|
||||
for cred in credentials.iter().filter(|c| c.has_credentials()) {
|
||||
|
||||
@@ -0,0 +1,234 @@
|
||||
//! QEMU/KVM/Proxmox ELF core dump reader.
|
||||
//!
|
||||
//! Reads ELF64 core dumps produced by:
|
||||
//! - `dump-guest-memory` in QEMU monitor
|
||||
//! - `virsh dump <domain> <file> --memory-only`
|
||||
//! - Proxmox: `qm monitor <VMID>` then `dump-guest-memory`
|
||||
//!
|
||||
//! The file contains PT_LOAD segments with p_paddr = guest physical address.
|
||||
|
||||
use std::fs;
|
||||
use std::path::Path;
|
||||
|
||||
use memmap2::Mmap;
|
||||
|
||||
use crate::error::{GovmemError, Result};
|
||||
use crate::memory::PhysicalMemory;
|
||||
|
||||
const PAGE_SIZE: u64 = 4096;
|
||||
|
||||
// ELF64 constants
|
||||
const ELF_MAGIC: [u8; 4] = [0x7f, b'E', b'L', b'F'];
|
||||
const ELFCLASS64: u8 = 2;
|
||||
const ELFDATA2LSB: u8 = 1; // little-endian
|
||||
const ET_CORE: u16 = 4;
|
||||
const PT_LOAD: u32 = 1;
|
||||
const ELF64_EHDR_SIZE: usize = 64;
|
||||
const ELF64_PHDR_SIZE: usize = 56;
|
||||
|
||||
/// A PT_LOAD segment: maps guest physical address range to file offset.
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
struct LoadSegment {
|
||||
/// File offset where segment data starts.
|
||||
file_offset: u64,
|
||||
/// Guest physical address (from p_paddr).
|
||||
gpa_start: u64,
|
||||
/// Size of data in file (p_filesz).
|
||||
file_size: u64,
|
||||
}
|
||||
|
||||
/// QEMU ELF core dump memory layer.
|
||||
pub struct QemuElfLayer {
|
||||
mmap: Mmap,
|
||||
segments: Vec<LoadSegment>,
|
||||
phys_end: u64,
|
||||
}
|
||||
|
||||
impl QemuElfLayer {
|
||||
/// Open a QEMU ELF core dump file (.elf or any extension).
|
||||
pub fn open(path: &Path) -> Result<Self> {
|
||||
let file = fs::File::open(path)?;
|
||||
let mmap = unsafe { Mmap::map(&file)? };
|
||||
|
||||
if mmap.len() < ELF64_EHDR_SIZE {
|
||||
return Err(GovmemError::InvalidMagic(0));
|
||||
}
|
||||
|
||||
// Parse ELF64 header
|
||||
let data = &mmap[..];
|
||||
if data[0..4] != ELF_MAGIC {
|
||||
return Err(GovmemError::InvalidMagic(u32::from_le_bytes([
|
||||
data[0], data[1], data[2], data[3],
|
||||
])));
|
||||
}
|
||||
if data[4] != ELFCLASS64 {
|
||||
return Err(GovmemError::PeError(0, "Not ELF64 (only 64-bit supported)".into()));
|
||||
}
|
||||
if data[5] != ELFDATA2LSB {
|
||||
return Err(GovmemError::PeError(0, "Not little-endian ELF".into()));
|
||||
}
|
||||
|
||||
let e_type = u16::from_le_bytes([data[16], data[17]]);
|
||||
if e_type != ET_CORE {
|
||||
return Err(GovmemError::PeError(
|
||||
0,
|
||||
format!("ELF type {} is not ET_CORE (expected {})", e_type, ET_CORE),
|
||||
));
|
||||
}
|
||||
|
||||
let e_phoff = u64::from_le_bytes(data[32..40].try_into().unwrap());
|
||||
let e_phentsize = u16::from_le_bytes([data[54], data[55]]) as usize;
|
||||
let e_phnum = u16::from_le_bytes([data[56], data[57]]) as usize;
|
||||
|
||||
if e_phentsize < ELF64_PHDR_SIZE {
|
||||
return Err(GovmemError::PeError(
|
||||
0,
|
||||
format!("ELF phdr size {} < expected {}", e_phentsize, ELF64_PHDR_SIZE),
|
||||
));
|
||||
}
|
||||
|
||||
// Parse program headers, collect PT_LOAD segments
|
||||
let mut segments = Vec::new();
|
||||
let mut phys_end: u64 = 0;
|
||||
|
||||
for i in 0..e_phnum {
|
||||
let off = e_phoff as usize + i * e_phentsize;
|
||||
if off + ELF64_PHDR_SIZE > data.len() {
|
||||
break;
|
||||
}
|
||||
let ph = &data[off..off + ELF64_PHDR_SIZE];
|
||||
|
||||
let p_type = u32::from_le_bytes(ph[0..4].try_into().unwrap());
|
||||
if p_type != PT_LOAD {
|
||||
continue;
|
||||
}
|
||||
|
||||
let p_offset = u64::from_le_bytes(ph[8..16].try_into().unwrap());
|
||||
let p_paddr = u64::from_le_bytes(ph[24..32].try_into().unwrap());
|
||||
let p_filesz = u64::from_le_bytes(ph[32..40].try_into().unwrap());
|
||||
|
||||
if p_filesz == 0 {
|
||||
continue;
|
||||
}
|
||||
|
||||
let seg_end = p_paddr.saturating_add(p_filesz);
|
||||
if seg_end > phys_end {
|
||||
phys_end = seg_end;
|
||||
}
|
||||
|
||||
segments.push(LoadSegment {
|
||||
file_offset: p_offset,
|
||||
gpa_start: p_paddr,
|
||||
file_size: p_filesz,
|
||||
});
|
||||
}
|
||||
|
||||
if segments.is_empty() {
|
||||
return Err(GovmemError::PeError(0, "No PT_LOAD segments found in ELF".into()));
|
||||
}
|
||||
|
||||
// Sort by GPA for binary search
|
||||
segments.sort_by_key(|s| s.gpa_start);
|
||||
|
||||
// Align phys_end to page boundary
|
||||
phys_end = (phys_end + PAGE_SIZE - 1) & !(PAGE_SIZE - 1);
|
||||
|
||||
log::info!(
|
||||
"QEMU ELF: {} PT_LOAD segments, physical end: 0x{:x} ({} MB)",
|
||||
segments.len(),
|
||||
phys_end,
|
||||
phys_end / (1024 * 1024)
|
||||
);
|
||||
|
||||
Ok(Self {
|
||||
mmap,
|
||||
segments,
|
||||
phys_end,
|
||||
})
|
||||
}
|
||||
|
||||
/// Number of PT_LOAD segments.
|
||||
pub fn segment_count(&self) -> usize {
|
||||
self.segments.len()
|
||||
}
|
||||
|
||||
/// Find the segment containing the given GPA using binary search.
|
||||
fn find_segment(&self, gpa: u64) -> Option<&LoadSegment> {
|
||||
let idx = self
|
||||
.segments
|
||||
.partition_point(|s| s.gpa_start <= gpa);
|
||||
if idx == 0 {
|
||||
return None;
|
||||
}
|
||||
let seg = &self.segments[idx - 1];
|
||||
if gpa < seg.gpa_start + seg.file_size {
|
||||
Some(seg)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl PhysicalMemory for QemuElfLayer {
|
||||
fn read_phys(&self, phys_addr: u64, buf: &mut [u8]) -> Result<()> {
|
||||
let len = buf.len() as u64;
|
||||
if len == 0 {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// Fast path: entire read fits in one segment
|
||||
if let Some(seg) = self.find_segment(phys_addr) {
|
||||
let offset_in_seg = phys_addr - seg.gpa_start;
|
||||
let avail = seg.file_size - offset_in_seg;
|
||||
if avail >= len {
|
||||
let file_off = (seg.file_offset + offset_in_seg) as usize;
|
||||
let end = file_off + buf.len();
|
||||
if end <= self.mmap.len() {
|
||||
buf.copy_from_slice(&self.mmap[file_off..end]);
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Slow path: read may span segments or hit unmapped regions
|
||||
// Fill with zeros first, then overlay mapped data
|
||||
buf.fill(0);
|
||||
let mut pos = 0u64;
|
||||
while pos < len {
|
||||
let cur_gpa = phys_addr + pos;
|
||||
if let Some(seg) = self.find_segment(cur_gpa) {
|
||||
let offset_in_seg = cur_gpa - seg.gpa_start;
|
||||
let avail = seg.file_size - offset_in_seg;
|
||||
let to_copy = std::cmp::min(avail, len - pos) as usize;
|
||||
let file_off = (seg.file_offset + offset_in_seg) as usize;
|
||||
let end = file_off + to_copy;
|
||||
if end <= self.mmap.len() {
|
||||
let dst_start = pos as usize;
|
||||
buf[dst_start..dst_start + to_copy]
|
||||
.copy_from_slice(&self.mmap[file_off..end]);
|
||||
}
|
||||
pos += to_copy as u64;
|
||||
} else {
|
||||
// Skip to next segment or end
|
||||
let next_seg_start = self
|
||||
.segments
|
||||
.iter()
|
||||
.find(|s| s.gpa_start > cur_gpa)
|
||||
.map(|s| s.gpa_start);
|
||||
match next_seg_start {
|
||||
Some(next) => {
|
||||
let skip = std::cmp::min(next - cur_gpa, len - pos);
|
||||
pos += skip;
|
||||
}
|
||||
None => break, // No more segments
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn phys_size(&self) -> u64 {
|
||||
self.phys_end
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
mod layer;
|
||||
|
||||
pub use layer::QemuElfLayer;
|
||||
Reference in New Issue
Block a user