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:
NK
2026-02-10 17:39:37 +01:00
parent dc41b621ff
commit f801d4f830
9 changed files with 573 additions and 80 deletions
+3 -1
View File
@@ -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
View File
@@ -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`),
+79
View File
@@ -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
}
}
+3
View File
@@ -0,0 +1,3 @@
mod layer;
pub use layer::HypervLayer;
+4
View File
@@ -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
View File
@@ -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
View File
@@ -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()) {
+234
View File
@@ -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
}
}
+3
View File
@@ -0,0 +1,3 @@
mod layer;
pub use layer::QemuElfLayer;