mirror of
https://github.com/nikaiw/VMkatz
synced 2026-09-25 07:51:06 +00:00
Add file-backed DLL page resolution from disk images
When Windows drops DLL .text pages from the working set, it zeros the PTE knowing the data can be re-read from the DLL file. This commit reads DLL files from the disk image via NTFS and serves those pages when a zero-PTE fault occurs in a known non-writable DLL section. Architecture: - FileBackedResolver reads PE files from disk, extracts non-writable sections (.text, .rdata), maps them to module_base + VirtualAddress - Binary search resolves VA to on-disk section data - Integrated into ProcessMemory::read_virt() as fallback on PageFault - Works synergistically with pagefile resolution (DLL .text enables pattern scans that discover structures whose data pages are in pagefile) Results on VMware test snapshots: - 472 sections loaded from 93 DLLs (~40 MB) - 12,020 DLL pages resolved from disk per snapshot - 2,235 pagefile pages resolved (up from 0 without file-backed) - New --disk flag for single-file mode, auto-discovered in folder mode
This commit is contained in:
+51
-4
@@ -25,20 +25,30 @@ pub type PagefileRef<'a> = Option<&'a crate::paging::pagefile::PagefileReader>;
|
|||||||
#[cfg(not(feature = "sam"))]
|
#[cfg(not(feature = "sam"))]
|
||||||
pub type PagefileRef<'a> = ();
|
pub type PagefileRef<'a> = ();
|
||||||
|
|
||||||
|
/// Disk path reference type: wraps Option<&Path> when sam feature is enabled,
|
||||||
|
/// or () when not. Allows a unified function signature across feature configurations.
|
||||||
|
#[cfg(feature = "sam")]
|
||||||
|
pub type DiskPathRef<'a> = Option<&'a std::path::Path>;
|
||||||
|
#[cfg(not(feature = "sam"))]
|
||||||
|
pub type DiskPathRef<'a> = ();
|
||||||
|
|
||||||
/// Find LSASS and extract all credentials.
|
/// Find LSASS and extract all credentials.
|
||||||
/// When a pagefile reader is provided, paged-out memory is resolved from disk.
|
/// When a pagefile reader is provided, paged-out memory is resolved from disk.
|
||||||
|
/// When a disk path is provided, demand-paged DLL sections are resolved from DLL files.
|
||||||
pub fn extract_all_credentials<P: PhysicalMemory>(
|
pub fn extract_all_credentials<P: PhysicalMemory>(
|
||||||
phys: &P,
|
phys: &P,
|
||||||
lsass: &Process,
|
lsass: &Process,
|
||||||
_kernel_dtb: u64,
|
_kernel_dtb: u64,
|
||||||
pagefile: PagefileRef<'_>,
|
pagefile: PagefileRef<'_>,
|
||||||
|
disk_path: DiskPathRef<'_>,
|
||||||
) -> Result<Vec<Credential>> {
|
) -> Result<Vec<Credential>> {
|
||||||
// Create virtual memory reader for LSASS (with optional pagefile resolution)
|
// Create initial virtual memory reader for module enumeration
|
||||||
#[cfg(feature = "sam")]
|
#[cfg(feature = "sam")]
|
||||||
let lsass_vmem = ProcessMemory::with_pagefile(phys, lsass.dtb, pagefile);
|
let lsass_vmem_init = ProcessMemory::with_resolvers(phys, lsass.dtb, pagefile, None);
|
||||||
#[cfg(not(feature = "sam"))]
|
#[cfg(not(feature = "sam"))]
|
||||||
let lsass_vmem = {
|
let lsass_vmem_init = {
|
||||||
let _ = pagefile;
|
let _ = pagefile;
|
||||||
|
let _ = disk_path;
|
||||||
ProcessMemory::new(phys, lsass.dtb)
|
ProcessMemory::new(phys, lsass.dtb)
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -50,7 +60,7 @@ pub fn extract_all_credentials<P: PhysicalMemory>(
|
|||||||
);
|
);
|
||||||
|
|
||||||
// Enumerate DLLs in LSASS
|
// Enumerate DLLs in LSASS
|
||||||
let modules = peb::enumerate_modules(&lsass_vmem, lsass.peb_vaddr, &X64_LDR)?;
|
let modules = peb::enumerate_modules(&lsass_vmem_init, lsass.peb_vaddr, &X64_LDR)?;
|
||||||
|
|
||||||
log::debug!("LSASS modules:");
|
log::debug!("LSASS modules:");
|
||||||
for m in &modules {
|
for m in &modules {
|
||||||
@@ -62,6 +72,36 @@ pub fn extract_all_credentials<P: PhysicalMemory>(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Build file-backed resolver from disk to serve demand-paged DLL sections
|
||||||
|
#[cfg(feature = "sam")]
|
||||||
|
let filebacked = disk_path.and_then(|p| {
|
||||||
|
match crate::paging::filebacked::FileBackedResolver::from_disk_and_modules(p, &modules) {
|
||||||
|
Ok(fb) if fb.section_count() > 0 => {
|
||||||
|
log::info!(
|
||||||
|
"File-backed: {} sections, {:.1} MB from {} DLLs",
|
||||||
|
fb.section_count(),
|
||||||
|
fb.total_bytes() as f64 / (1024.0 * 1024.0),
|
||||||
|
modules.len()
|
||||||
|
);
|
||||||
|
Some(fb)
|
||||||
|
}
|
||||||
|
Ok(_) => {
|
||||||
|
log::info!("File-backed: no DLL sections loaded from disk");
|
||||||
|
None
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
log::info!("File-backed resolver failed: {}", e);
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Create enhanced vmem with file-backed resolution for DLL sections
|
||||||
|
#[cfg(feature = "sam")]
|
||||||
|
let lsass_vmem = ProcessMemory::with_resolvers(phys, lsass.dtb, pagefile, filebacked.as_ref());
|
||||||
|
#[cfg(not(feature = "sam"))]
|
||||||
|
let lsass_vmem = lsass_vmem_init;
|
||||||
|
|
||||||
let dlls = LsassDlls {
|
let dlls = LsassDlls {
|
||||||
lsasrv: find_module(&modules, "lsasrv.dll"),
|
lsasrv: find_module(&modules, "lsasrv.dll"),
|
||||||
msv1_0: find_module(&modules, "msv1_0.dll"),
|
msv1_0: find_module(&modules, "msv1_0.dll"),
|
||||||
@@ -295,6 +335,13 @@ pub fn extract_all_credentials<P: PhysicalMemory>(
|
|||||||
msv_status, wdigest_status, kerberos_status, tspkg_status, dpapi_status,
|
msv_status, wdigest_status, kerberos_status, tspkg_status, dpapi_status,
|
||||||
ssp_status, livessp_status, credman_status, cloudap_status,
|
ssp_status, livessp_status, credman_status, cloudap_status,
|
||||||
);
|
);
|
||||||
|
#[cfg(feature = "sam")]
|
||||||
|
if let Some(fb) = &filebacked {
|
||||||
|
let resolved = fb.pages_resolved();
|
||||||
|
if resolved > 0 {
|
||||||
|
println!("[+] File-backed: {} DLL pages resolved from disk", resolved);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Merge MSV credentials with unknown LUID (0) into matching credentials by username+domain
|
// Merge MSV credentials with unknown LUID (0) into matching credentials by username+domain
|
||||||
if let Some(orphan) = all_creds.remove(&0) {
|
if let Some(orphan) = all_creds.remove(&0) {
|
||||||
|
|||||||
+21
-9
@@ -110,7 +110,8 @@ fn main() -> anyhow::Result<()> {
|
|||||||
// LSASS credential extraction mode
|
// LSASS credential extraction mode
|
||||||
#[cfg(feature = "sam")]
|
#[cfg(feature = "sam")]
|
||||||
{
|
{
|
||||||
let pagefile_reader = args.disk.as_ref().and_then(|d| {
|
let disk_path_str = args.disk.clone();
|
||||||
|
let pagefile_reader = disk_path_str.as_ref().and_then(|d| {
|
||||||
match vmkatz::paging::pagefile::PagefileReader::open(Path::new(d)) {
|
match vmkatz::paging::pagefile::PagefileReader::open(Path::new(d)) {
|
||||||
Ok(pf) => {
|
Ok(pf) => {
|
||||||
println!(
|
println!(
|
||||||
@@ -120,15 +121,16 @@ fn main() -> anyhow::Result<()> {
|
|||||||
Some(pf)
|
Some(pf)
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
eprintln!("[!] Failed to open pagefile from {}: {}", d, e);
|
log::info!("No pagefile from {}: {}", d, e);
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
return run_lsass(input_path, &args, pagefile_reader.as_ref());
|
let disk_ref = disk_path_str.as_ref().map(|d| Path::new(d.as_str()));
|
||||||
|
return run_lsass(input_path, &args, pagefile_reader.as_ref(), disk_ref);
|
||||||
}
|
}
|
||||||
#[cfg(not(feature = "sam"))]
|
#[cfg(not(feature = "sam"))]
|
||||||
run_lsass(input_path, &args, Default::default())
|
run_lsass(input_path, &args, Default::default(), Default::default())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(feature = "sam")]
|
#[cfg(feature = "sam")]
|
||||||
@@ -246,11 +248,18 @@ fn run_directory(dir: &Path, args: &Args) -> anyhow::Result<()> {
|
|||||||
#[cfg(not(feature = "sam"))]
|
#[cfg(not(feature = "sam"))]
|
||||||
let pagefile: PagefileRef<'_> = Default::default();
|
let pagefile: PagefileRef<'_> = Default::default();
|
||||||
|
|
||||||
|
// Disk path for file-backed DLL resolution
|
||||||
|
#[cfg(feature = "sam")]
|
||||||
|
let disk_path: vmkatz::lsass::finder::DiskPathRef<'_> =
|
||||||
|
discovery.disk_files.first().map(|p| p.as_path());
|
||||||
|
#[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"))]
|
||||||
for file in &discovery.lsass_files {
|
for file in &discovery.lsass_files {
|
||||||
let name = file.file_name().unwrap_or_default().to_string_lossy();
|
let name = file.file_name().unwrap_or_default().to_string_lossy();
|
||||||
println!("\n[*] LSASS: {}", name);
|
println!("\n[*] LSASS: {}", name);
|
||||||
if let Err(e) = run_lsass(file, args, pagefile) {
|
if let Err(e) = run_lsass(file, args, pagefile, disk_path) {
|
||||||
eprintln!("[!] {}: {}", name, e);
|
eprintln!("[!] {}: {}", name, e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -272,7 +281,7 @@ fn run_directory(dir: &Path, args: &Args) -> anyhow::Result<()> {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn run_lsass(input_path: &Path, args: &Args, pagefile: PagefileRef<'_>) -> anyhow::Result<()> {
|
fn run_lsass(input_path: &Path, args: &Args, pagefile: PagefileRef<'_>, disk_path: vmkatz::lsass::finder::DiskPathRef<'_>) -> anyhow::Result<()> {
|
||||||
let verbose = args.verbose || args.list_processes;
|
let verbose = args.verbose || args.list_processes;
|
||||||
let ext = input_path.extension().and_then(|e| e.to_str()).unwrap_or("");
|
let ext = input_path.extension().and_then(|e| e.to_str()).unwrap_or("");
|
||||||
|
|
||||||
@@ -294,11 +303,12 @@ fn run_lsass(input_path: &Path, args: &Args, pagefile: PagefileRef<'_>) -> anyho
|
|||||||
args,
|
args,
|
||||||
verbose,
|
verbose,
|
||||||
pagefile,
|
pagefile,
|
||||||
|
disk_path,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
#[cfg(not(feature = "vbox"))]
|
#[cfg(not(feature = "vbox"))]
|
||||||
{
|
{
|
||||||
let _ = pagefile;
|
let _ = (pagefile, disk_path);
|
||||||
anyhow::bail!("VirtualBox .sav support not enabled (compile with --features vbox)")
|
anyhow::bail!("VirtualBox .sav support not enabled (compile with --features vbox)")
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -330,11 +340,12 @@ fn run_lsass(input_path: &Path, args: &Args, pagefile: PagefileRef<'_>) -> anyho
|
|||||||
args,
|
args,
|
||||||
verbose,
|
verbose,
|
||||||
pagefile,
|
pagefile,
|
||||||
|
disk_path,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
#[cfg(not(feature = "vmware"))]
|
#[cfg(not(feature = "vmware"))]
|
||||||
{
|
{
|
||||||
let _ = pagefile;
|
let _ = (pagefile, disk_path);
|
||||||
anyhow::bail!("VMware .vmem/.vmsn support not enabled (compile with --features vmware)")
|
anyhow::bail!("VMware .vmem/.vmsn support not enabled (compile with --features vmware)")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -346,6 +357,7 @@ fn run_with_layer<L: PhysicalMemory, F: FnOnce() -> anyhow::Result<L>>(
|
|||||||
args: &Args,
|
args: &Args,
|
||||||
verbose: bool,
|
verbose: bool,
|
||||||
pagefile: PagefileRef<'_>,
|
pagefile: PagefileRef<'_>,
|
||||||
|
disk_path: vmkatz::lsass::finder::DiskPathRef<'_>,
|
||||||
) -> anyhow::Result<()> {
|
) -> anyhow::Result<()> {
|
||||||
let layer = make_layer()?;
|
let layer = make_layer()?;
|
||||||
|
|
||||||
@@ -386,7 +398,7 @@ fn run_with_layer<L: PhysicalMemory, F: FnOnce() -> anyhow::Result<L>>(
|
|||||||
|
|
||||||
// Extract credentials
|
// Extract credentials
|
||||||
let credentials =
|
let credentials =
|
||||||
lsass::finder::extract_all_credentials(&layer, lsass_proc, system.dtb, pagefile)
|
lsass::finder::extract_all_credentials(&layer, lsass_proc, system.dtb, pagefile, disk_path)
|
||||||
.context("Credential extraction failed")?;
|
.context("Credential extraction failed")?;
|
||||||
|
|
||||||
// Report pagefile resolution stats
|
// Report pagefile resolution stats
|
||||||
|
|||||||
@@ -0,0 +1,251 @@
|
|||||||
|
//! File-backed page resolution for demand-paged DLL sections.
|
||||||
|
//!
|
||||||
|
//! When Windows removes DLL .text pages from the working set, it zeros the PTE
|
||||||
|
//! knowing the data can be re-read from the DLL file on disk. This module reads
|
||||||
|
//! DLL files from a disk image via NTFS and serves those pages on zero-PTE faults.
|
||||||
|
|
||||||
|
use std::cell::Cell;
|
||||||
|
use std::io::{Read, Seek};
|
||||||
|
use std::path::Path;
|
||||||
|
|
||||||
|
use crate::disk;
|
||||||
|
use crate::error::{GovmemError, Result};
|
||||||
|
use crate::windows::peb::LoadedModule;
|
||||||
|
|
||||||
|
/// IMAGE_SCN_MEM_WRITE — skip writable sections (in-memory content differs from disk).
|
||||||
|
const SCN_MEM_WRITE: u32 = 0x8000_0000;
|
||||||
|
|
||||||
|
/// Pre-read DLL section mapped to a virtual address range.
|
||||||
|
struct BackedSection {
|
||||||
|
va_start: u64,
|
||||||
|
data: Vec<u8>, // page-aligned size
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Resolves demand-paged DLL pages by serving data from on-disk PE files.
|
||||||
|
pub struct FileBackedResolver {
|
||||||
|
sections: Vec<BackedSection>, // sorted by va_start
|
||||||
|
total_bytes: usize,
|
||||||
|
pages_resolved: Cell<u64>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FileBackedResolver {
|
||||||
|
/// Build a resolver by reading DLL files from a disk image's NTFS System32.
|
||||||
|
pub fn from_disk_and_modules(disk_path: &Path, modules: &[LoadedModule]) -> Result<Self> {
|
||||||
|
let mut disk = disk::open_disk(disk_path)?;
|
||||||
|
let partitions = crate::sam::find_ntfs_partitions(&mut disk)?;
|
||||||
|
|
||||||
|
let mut sections = Vec::new();
|
||||||
|
|
||||||
|
for &partition_offset in &partitions {
|
||||||
|
match Self::try_load_from_partition(&mut disk, partition_offset, modules, &mut sections)
|
||||||
|
{
|
||||||
|
Ok(()) => break,
|
||||||
|
Err(e) => {
|
||||||
|
log::debug!(
|
||||||
|
"File-backed: partition 0x{:x}: {}",
|
||||||
|
partition_offset,
|
||||||
|
e
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let total_bytes: usize = sections.iter().map(|s| s.data.len()).sum();
|
||||||
|
sections.sort_by_key(|s| s.va_start);
|
||||||
|
|
||||||
|
Ok(Self {
|
||||||
|
sections,
|
||||||
|
total_bytes,
|
||||||
|
pages_resolved: Cell::new(0),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn try_load_from_partition(
|
||||||
|
disk: &mut Box<dyn disk::DiskImage>,
|
||||||
|
partition_offset: u64,
|
||||||
|
modules: &[LoadedModule],
|
||||||
|
sections: &mut Vec<BackedSection>,
|
||||||
|
) -> Result<()> {
|
||||||
|
let mut part_reader = crate::sam::PartitionReader::new(disk, partition_offset);
|
||||||
|
|
||||||
|
let ntfs = ntfs::Ntfs::new(&mut part_reader)
|
||||||
|
.map_err(|e| GovmemError::DecryptionError(format!("NTFS: {}", e)))?;
|
||||||
|
let root = ntfs
|
||||||
|
.root_directory(&mut part_reader)
|
||||||
|
.map_err(|e| GovmemError::DecryptionError(format!("NTFS root: {}", e)))?;
|
||||||
|
|
||||||
|
// Navigate to Windows\System32
|
||||||
|
let windows = crate::sam::find_entry(&ntfs, &root, &mut part_reader, "Windows")?;
|
||||||
|
let sys32 = crate::sam::find_entry(&ntfs, &windows, &mut part_reader, "System32")?;
|
||||||
|
|
||||||
|
let mut loaded_count = 0usize;
|
||||||
|
for module in modules {
|
||||||
|
let dll_name = &module.base_name;
|
||||||
|
if dll_name.is_empty() {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
match crate::sam::find_entry(&ntfs, &sys32, &mut part_reader, dll_name) {
|
||||||
|
Ok(file) => {
|
||||||
|
match Self::read_pe_sections(&file, &mut part_reader, module.base) {
|
||||||
|
Ok(secs) => {
|
||||||
|
let bytes: usize = secs.iter().map(|s| s.data.len()).sum();
|
||||||
|
log::debug!(
|
||||||
|
"File-backed: {} @ 0x{:x}: {} sections, {} KB",
|
||||||
|
dll_name,
|
||||||
|
module.base,
|
||||||
|
secs.len(),
|
||||||
|
bytes / 1024
|
||||||
|
);
|
||||||
|
loaded_count += 1;
|
||||||
|
sections.extend(secs);
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
log::debug!("File-backed: {} PE parse: {}", dll_name, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(_) => {
|
||||||
|
log::debug!("File-backed: {} not in System32", dll_name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if loaded_count > 0 {
|
||||||
|
Ok(())
|
||||||
|
} else {
|
||||||
|
Err(GovmemError::DecryptionError(
|
||||||
|
"No DLLs found on disk".to_string(),
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Read PE file from NTFS and extract non-writable section data.
|
||||||
|
fn read_pe_sections<R: Read + Seek>(
|
||||||
|
file: &ntfs::NtfsFile,
|
||||||
|
reader: &mut R,
|
||||||
|
module_base: u64,
|
||||||
|
) -> Result<Vec<BackedSection>> {
|
||||||
|
let pe_data = crate::sam::read_file_data(file, reader)?;
|
||||||
|
Self::parse_pe_sections(&pe_data, module_base)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Parse PE headers, return non-writable sections mapped to module_base.
|
||||||
|
fn parse_pe_sections(pe_data: &[u8], module_base: u64) -> Result<Vec<BackedSection>> {
|
||||||
|
if pe_data.len() < 0x40 {
|
||||||
|
return Err(GovmemError::DecryptionError("PE too small".to_string()));
|
||||||
|
}
|
||||||
|
|
||||||
|
// DOS header
|
||||||
|
if u16::from_le_bytes([pe_data[0], pe_data[1]]) != 0x5A4D {
|
||||||
|
return Err(GovmemError::DecryptionError("Not a PE (no MZ)".to_string()));
|
||||||
|
}
|
||||||
|
let e_lfanew = u32::from_le_bytes(pe_data[0x3C..0x40].try_into().unwrap()) as usize;
|
||||||
|
if e_lfanew + 24 > pe_data.len() {
|
||||||
|
return Err(GovmemError::DecryptionError("Invalid e_lfanew".to_string()));
|
||||||
|
}
|
||||||
|
|
||||||
|
// PE signature
|
||||||
|
if u32::from_le_bytes(pe_data[e_lfanew..e_lfanew + 4].try_into().unwrap()) != 0x0000_4550
|
||||||
|
{
|
||||||
|
return Err(GovmemError::DecryptionError(
|
||||||
|
"Invalid PE signature".to_string(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
// COFF header
|
||||||
|
let num_sections =
|
||||||
|
u16::from_le_bytes(pe_data[e_lfanew + 6..e_lfanew + 8].try_into().unwrap()) as usize;
|
||||||
|
let opt_hdr_size = u16::from_le_bytes(
|
||||||
|
pe_data[e_lfanew + 20..e_lfanew + 22].try_into().unwrap(),
|
||||||
|
) as usize;
|
||||||
|
|
||||||
|
let section_table = e_lfanew + 24 + opt_hdr_size;
|
||||||
|
let mut sections = Vec::new();
|
||||||
|
|
||||||
|
for i in 0..num_sections {
|
||||||
|
let off = section_table + i * 40;
|
||||||
|
if off + 40 > pe_data.len() {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
let virt_size =
|
||||||
|
u32::from_le_bytes(pe_data[off + 8..off + 12].try_into().unwrap()) as usize;
|
||||||
|
let virt_addr =
|
||||||
|
u32::from_le_bytes(pe_data[off + 12..off + 16].try_into().unwrap()) as u64;
|
||||||
|
let raw_size =
|
||||||
|
u32::from_le_bytes(pe_data[off + 16..off + 20].try_into().unwrap()) as usize;
|
||||||
|
let raw_offset =
|
||||||
|
u32::from_le_bytes(pe_data[off + 20..off + 24].try_into().unwrap()) as usize;
|
||||||
|
let characteristics =
|
||||||
|
u32::from_le_bytes(pe_data[off + 36..off + 40].try_into().unwrap());
|
||||||
|
|
||||||
|
// Skip writable sections (in-memory content modified by process)
|
||||||
|
if characteristics & SCN_MEM_WRITE != 0 {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if raw_size == 0 || raw_offset == 0 || virt_size == 0 {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if raw_offset.saturating_add(raw_size) > pe_data.len() {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Copy raw data, page-aligned to VirtualSize
|
||||||
|
let copy_size = std::cmp::min(raw_size, virt_size);
|
||||||
|
let padded_size = (virt_size + 0xFFF) & !0xFFF;
|
||||||
|
let mut data = vec![0u8; padded_size];
|
||||||
|
data[..copy_size].copy_from_slice(&pe_data[raw_offset..raw_offset + copy_size]);
|
||||||
|
|
||||||
|
sections.push(BackedSection {
|
||||||
|
va_start: module_base + virt_addr,
|
||||||
|
data,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(sections)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Resolve a page from file-backed DLL data. Returns page contents if the
|
||||||
|
/// virtual address falls within a known non-writable DLL section.
|
||||||
|
pub fn resolve_page(&self, vaddr: u64) -> Option<[u8; 4096]> {
|
||||||
|
let page_base = vaddr & !0xFFF;
|
||||||
|
|
||||||
|
let idx = match self.sections.binary_search_by(|s| {
|
||||||
|
let s_end = s.va_start + s.data.len() as u64;
|
||||||
|
if page_base < s.va_start {
|
||||||
|
std::cmp::Ordering::Greater
|
||||||
|
} else if page_base >= s_end {
|
||||||
|
std::cmp::Ordering::Less
|
||||||
|
} else {
|
||||||
|
std::cmp::Ordering::Equal
|
||||||
|
}
|
||||||
|
}) {
|
||||||
|
Ok(i) => i,
|
||||||
|
Err(_) => return None,
|
||||||
|
};
|
||||||
|
|
||||||
|
let section = &self.sections[idx];
|
||||||
|
let offset = (page_base - section.va_start) as usize;
|
||||||
|
if offset + 4096 > section.data.len() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut page = [0u8; 4096];
|
||||||
|
page.copy_from_slice(§ion.data[offset..offset + 4096]);
|
||||||
|
self.pages_resolved.set(self.pages_resolved.get() + 1);
|
||||||
|
Some(page)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn pages_resolved(&self) -> u64 {
|
||||||
|
self.pages_resolved.get()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn total_bytes(&self) -> usize {
|
||||||
|
self.total_bytes
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn section_count(&self) -> usize {
|
||||||
|
self.sections.len()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +1,6 @@
|
|||||||
pub mod entry;
|
pub mod entry;
|
||||||
#[cfg(feature = "sam")]
|
#[cfg(feature = "sam")]
|
||||||
|
pub mod filebacked;
|
||||||
|
#[cfg(feature = "sam")]
|
||||||
pub mod pagefile;
|
pub mod pagefile;
|
||||||
pub mod translate;
|
pub mod translate;
|
||||||
|
|||||||
+19
-1
@@ -367,12 +367,15 @@ impl<'a, P: PhysicalMemory> PageTableWalker<'a, P> {
|
|||||||
|
|
||||||
/// Process virtual memory: combines a DTB (CR3) with physical memory for address translation.
|
/// Process virtual memory: combines a DTB (CR3) with physical memory for address translation.
|
||||||
/// Optional pagefile reader resolves pages swapped to pagefile.sys on disk.
|
/// Optional pagefile reader resolves pages swapped to pagefile.sys on disk.
|
||||||
|
/// Optional file-backed resolver serves demand-paged DLL sections from disk.
|
||||||
pub struct ProcessMemory<'a, P: PhysicalMemory> {
|
pub struct ProcessMemory<'a, P: PhysicalMemory> {
|
||||||
phys: &'a P,
|
phys: &'a P,
|
||||||
walker: PageTableWalker<'a, P>,
|
walker: PageTableWalker<'a, P>,
|
||||||
dtb: u64,
|
dtb: u64,
|
||||||
#[cfg(feature = "sam")]
|
#[cfg(feature = "sam")]
|
||||||
pagefile: Option<&'a crate::paging::pagefile::PagefileReader>,
|
pagefile: Option<&'a crate::paging::pagefile::PagefileReader>,
|
||||||
|
#[cfg(feature = "sam")]
|
||||||
|
filebacked: Option<&'a crate::paging::filebacked::FileBackedResolver>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a, P: PhysicalMemory> ProcessMemory<'a, P> {
|
impl<'a, P: PhysicalMemory> ProcessMemory<'a, P> {
|
||||||
@@ -383,20 +386,24 @@ impl<'a, P: PhysicalMemory> ProcessMemory<'a, P> {
|
|||||||
dtb,
|
dtb,
|
||||||
#[cfg(feature = "sam")]
|
#[cfg(feature = "sam")]
|
||||||
pagefile: None,
|
pagefile: None,
|
||||||
|
#[cfg(feature = "sam")]
|
||||||
|
filebacked: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(feature = "sam")]
|
#[cfg(feature = "sam")]
|
||||||
pub fn with_pagefile(
|
pub fn with_resolvers(
|
||||||
phys: &'a P,
|
phys: &'a P,
|
||||||
dtb: u64,
|
dtb: u64,
|
||||||
pagefile: Option<&'a crate::paging::pagefile::PagefileReader>,
|
pagefile: Option<&'a crate::paging::pagefile::PagefileReader>,
|
||||||
|
filebacked: Option<&'a crate::paging::filebacked::FileBackedResolver>,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
Self {
|
Self {
|
||||||
phys,
|
phys,
|
||||||
walker: PageTableWalker::new(phys),
|
walker: PageTableWalker::new(phys),
|
||||||
dtb,
|
dtb,
|
||||||
pagefile,
|
pagefile,
|
||||||
|
filebacked,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -455,6 +462,17 @@ impl<'a, P: PhysicalMemory> VirtualMemory for ProcessMemory<'a, P> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
Err(ref e) => {
|
Err(ref e) => {
|
||||||
|
// Try file-backed resolution for demand-paged DLL sections
|
||||||
|
#[cfg(feature = "sam")]
|
||||||
|
if let Some(fb) = self.filebacked {
|
||||||
|
if let Some(page_data) = fb.resolve_page(current_vaddr) {
|
||||||
|
let page_off = (current_vaddr & 0xFFF) as usize;
|
||||||
|
buf[offset..offset + chunk]
|
||||||
|
.copy_from_slice(&page_data[page_off..page_off + chunk]);
|
||||||
|
offset += chunk;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
log::trace!("Page fault: {} at VA 0x{:x}", e, current_vaddr);
|
log::trace!("Page fault: {} at VA 0x{:x}", e, current_vaddr);
|
||||||
buf[offset..offset + chunk].fill(0);
|
buf[offset..offset + chunk].fill(0);
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
@@ -356,7 +356,7 @@ pub(crate) fn find_entry<'n, R: Read + Seek>(
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Read file data ($DATA attribute) into a Vec<u8>.
|
/// Read file data ($DATA attribute) into a Vec<u8>.
|
||||||
fn read_file_data<R: Read + Seek>(
|
pub(crate) fn read_file_data<R: Read + Seek>(
|
||||||
file: &ntfs::NtfsFile,
|
file: &ntfs::NtfsFile,
|
||||||
reader: &mut R,
|
reader: &mut R,
|
||||||
) -> Result<Vec<u8>> {
|
) -> Result<Vec<u8>> {
|
||||||
|
|||||||
Reference in New Issue
Block a user