Add pagefile.sys resolution for paged-out LSASS memory

When a VM directory contains both memory snapshots and disk images,
open pagefile.sys from the VMDK/VDI/QCOW2 disk to resolve pages
that Windows swapped out. Uses pre-built NTFS data run map with
RefCell<Box<dyn DiskImage>> for interior mutability.

- PTE pagefile detection (bits 0/10/11 + pagefile number/offset)
- PageFileFault error variant in page table walker
- PagefileReader: opens disk, extracts pagefile.sys data runs,
  binary-search page resolution
- --disk CLI option for explicit disk image in single-file mode
- Folder mode auto-discovers disk and opens pagefile automatically
- Feature-gated behind "sam" (requires NTFS + disk image support)
This commit is contained in:
NK
2026-02-09 03:44:41 +01:00
parent fdf88e6c64
commit 00802654c4
8 changed files with 359 additions and 13 deletions
+3
View File
@@ -20,6 +20,9 @@ pub enum GovmemError {
#[error("Page fault at 0x{0:x} (level: {1})")]
PageFault(u64, &'static str),
#[error("Pagefile fault at 0x{0:x} (PTE: 0x{1:x})")]
PageFileFault(u64, u64),
#[error("System process not found")]
SystemProcessNotFound,
+17 -2
View File
@@ -18,14 +18,29 @@ pub struct LsassDlls {
pub cloudap: Option<LoadedModule>,
}
/// Pagefile reference type: wraps Option<&PagefileReader> when sam feature is enabled,
/// or () when not. Allows a unified function signature across feature configurations.
#[cfg(feature = "sam")]
pub type PagefileRef<'a> = Option<&'a crate::paging::pagefile::PagefileReader>;
#[cfg(not(feature = "sam"))]
pub type PagefileRef<'a> = ();
/// Find LSASS and extract all credentials.
/// When a pagefile reader is provided, paged-out memory is resolved from disk.
pub fn extract_all_credentials<P: PhysicalMemory>(
phys: &P,
lsass: &Process,
_kernel_dtb: u64,
pagefile: PagefileRef<'_>,
) -> Result<Vec<Credential>> {
// Create virtual memory reader for LSASS
let lsass_vmem = ProcessMemory::new(phys, lsass.dtb);
// Create virtual memory reader for LSASS (with optional pagefile resolution)
#[cfg(feature = "sam")]
let lsass_vmem = ProcessMemory::with_pagefile(phys, lsass.dtb, pagefile);
#[cfg(not(feature = "sam"))]
let lsass_vmem = {
let _ = pagefile;
ProcessMemory::new(phys, lsass.dtb)
};
log::info!(
"LSASS: PID={}, DTB=0x{:x}, PEB=0x{:x}",
+75 -5
View File
@@ -8,6 +8,7 @@ use clap::Parser;
#[cfg(any(feature = "vmware", feature = "vbox"))]
use vmkatz::lsass;
use vmkatz::lsass::finder::PagefileRef;
#[cfg(any(feature = "vmware", feature = "vbox"))]
use vmkatz::lsass::types::Credential;
#[cfg(any(feature = "vmware", feature = "vbox"))]
@@ -36,6 +37,7 @@ use vmkatz::windows::process;
after_help = "EXAMPLES:\n \
vmkatz snapshot.vmsn Extract LSASS credentials\n \
vmkatz --format ntlm snapshot.vmsn Output as NTLM hashes\n \
vmkatz --disk disk.vmdk snapshot.vmsn Resolve paged-out creds from disk\n \
vmkatz disk.vdi Extract SAM hashes + LSA secrets\n \
vmkatz /path/to/vm/directory/ Auto-discover and process all files\n \
vmkatz --list-processes snapshot.vmsn List running processes only\n \
@@ -55,6 +57,11 @@ struct Args {
#[arg(long, default_value_t = false)]
sam: bool,
/// Disk image for pagefile.sys resolution (resolves paged-out memory from disk)
#[cfg(feature = "sam")]
#[arg(long, value_name = "DISK_IMAGE")]
disk: Option<String>,
/// Output format
#[arg(long, default_value = "text", value_name = "FORMAT", value_parser = ["text", "csv", "ntlm"])]
format: String,
@@ -101,7 +108,27 @@ fn main() -> anyhow::Result<()> {
}
// LSASS credential extraction mode
run_lsass(input_path, &args)
#[cfg(feature = "sam")]
{
let pagefile_reader = args.disk.as_ref().and_then(|d| {
match vmkatz::paging::pagefile::PagefileReader::open(Path::new(d)) {
Ok(pf) => {
println!(
"[+] Pagefile: {:.1} MB",
pf.pagefile_size() as f64 / (1024.0 * 1024.0),
);
Some(pf)
}
Err(e) => {
eprintln!("[!] Failed to open pagefile from {}: {}", d, e);
None
}
}
});
return run_lsass(input_path, &args, pagefile_reader.as_ref());
}
#[cfg(not(feature = "sam"))]
run_lsass(input_path, &args, Default::default())
}
#[cfg(feature = "sam")]
@@ -191,11 +218,39 @@ fn run_directory(dir: &Path, args: &Args) -> anyhow::Result<()> {
return Ok(());
}
// Try to open pagefile.sys from the first available disk image
#[cfg(feature = "sam")]
let pagefile_reader = if !discovery.lsass_files.is_empty() {
discovery.disk_files.first().and_then(|d| {
match vmkatz::paging::pagefile::PagefileReader::open(d) {
Ok(pf) => {
println!(
"[+] Pagefile: {:.1} MB from {}",
pf.pagefile_size() as f64 / (1024.0 * 1024.0),
d.file_name().unwrap_or_default().to_string_lossy()
);
Some(pf)
}
Err(e) => {
log::info!("No pagefile from disk: {}", e);
None
}
}
})
} else {
None
};
#[cfg(feature = "sam")]
let pagefile: PagefileRef<'_> = pagefile_reader.as_ref();
#[cfg(not(feature = "sam"))]
let pagefile: PagefileRef<'_> = Default::default();
#[cfg(any(feature = "vmware", feature = "vbox"))]
for file in &discovery.lsass_files {
let name = file.file_name().unwrap_or_default().to_string_lossy();
println!("\n[*] LSASS: {}", name);
if let Err(e) = run_lsass(file, args) {
if let Err(e) = run_lsass(file, args, pagefile) {
eprintln!("[!] {}: {}", name, e);
}
}
@@ -217,7 +272,7 @@ fn run_directory(dir: &Path, args: &Args) -> anyhow::Result<()> {
Ok(())
}
fn run_lsass(input_path: &Path, args: &Args) -> anyhow::Result<()> {
fn run_lsass(input_path: &Path, args: &Args, pagefile: PagefileRef<'_>) -> anyhow::Result<()> {
let verbose = args.verbose || args.list_processes;
let ext = input_path.extension().and_then(|e| e.to_str()).unwrap_or("");
@@ -238,10 +293,12 @@ fn run_lsass(input_path: &Path, args: &Args) -> anyhow::Result<()> {
},
args,
verbose,
pagefile,
)
}
#[cfg(not(feature = "vbox"))]
{
let _ = pagefile;
anyhow::bail!("VirtualBox .sav support not enabled (compile with --features vbox)")
}
} else {
@@ -272,10 +329,12 @@ fn run_lsass(input_path: &Path, args: &Args) -> anyhow::Result<()> {
},
args,
verbose,
pagefile,
)
}
#[cfg(not(feature = "vmware"))]
{
let _ = pagefile;
anyhow::bail!("VMware .vmem/.vmsn support not enabled (compile with --features vmware)")
}
}
@@ -286,6 +345,7 @@ fn run_with_layer<L: PhysicalMemory, F: FnOnce() -> anyhow::Result<L>>(
make_layer: F,
args: &Args,
verbose: bool,
pagefile: PagefileRef<'_>,
) -> anyhow::Result<()> {
let layer = make_layer()?;
@@ -325,8 +385,18 @@ fn run_with_layer<L: PhysicalMemory, F: FnOnce() -> anyhow::Result<L>>(
}
// Extract credentials
let credentials = lsass::finder::extract_all_credentials(&layer, lsass_proc, system.dtb)
.context("Credential extraction failed")?;
let credentials =
lsass::finder::extract_all_credentials(&layer, lsass_proc, system.dtb, pagefile)
.context("Credential extraction failed")?;
// Report pagefile resolution stats
#[cfg(feature = "sam")]
if let Some(pf) = pagefile {
let resolved = pf.pages_resolved();
if resolved > 0 {
println!("[+] Pagefile: {} pages resolved from disk", resolved);
}
}
match args.format.as_str() {
"csv" => print_csv(&credentials),
+20
View File
@@ -27,4 +27,24 @@ impl PageTableEntry {
pub fn raw(&self) -> u64 {
self.0
}
/// Windows pagefile PTE: not present (bit 0=0), not transition (bit 10=0),
/// not prototype (bit 11=0), and non-zero (has pagefile info).
/// Bits 1-4 = pagefile number, bits 32-63 = page offset in pagefile.
pub fn is_pagefile(&self) -> bool {
self.0 != 0
&& (self.0 & 1) == 0
&& (self.0 & (1 << 10)) == 0
&& (self.0 & (1 << 11)) == 0
}
/// Pagefile number from bits 1-4 (usually 0 for primary pagefile.sys).
pub fn pagefile_number(&self) -> u8 {
((self.0 >> 1) & 0xF) as u8
}
/// Byte offset into pagefile from bits 32-63 (page index * 4096).
pub fn pagefile_offset(&self) -> u64 {
((self.0 >> 32) & 0xFFFF_FFFF) * 4096
}
}
+2
View File
@@ -1,2 +1,4 @@
pub mod entry;
#[cfg(feature = "sam")]
pub mod pagefile;
pub mod translate;
+196
View File
@@ -0,0 +1,196 @@
use std::cell::RefCell;
use std::io::{Read, Seek, SeekFrom};
use std::path::Path;
use ntfs::attribute_value::NtfsAttributeValue;
use crate::disk::{self, DiskImage};
use crate::error::{GovmemError, Result};
use crate::paging::entry::PageTableEntry;
/// Pre-built data run map entry mapping pagefile byte ranges to absolute disk positions.
struct PagefileDataRun {
file_offset: u64,
disk_offset: u64,
length: u64,
}
/// Reads pages from pagefile.sys on a virtual disk image.
///
/// Pre-extracts NTFS data runs at construction time to avoid keeping ntfs crate
/// types alive (which would create self-referential struct issues). Uses RefCell
/// for interior mutability since read_virt(&self) is immutable but disk seeks need &mut.
pub struct PagefileReader {
disk: RefCell<Box<dyn DiskImage>>,
data_runs: Vec<PagefileDataRun>,
pagefile_size: u64,
pages_resolved: std::cell::Cell<u64>,
}
impl PagefileReader {
/// Open pagefile.sys from a disk image, extracting its NTFS data runs.
pub fn open(disk_path: &Path) -> Result<Self> {
let mut disk = disk::open_disk(disk_path)?;
let (data_runs, pagefile_size) = extract_pagefile_data_runs(&mut disk)?;
log::info!(
"Pagefile: {:.1} MB, {} data runs",
pagefile_size as f64 / (1024.0 * 1024.0),
data_runs.len()
);
Ok(Self {
disk: RefCell::new(disk),
data_runs,
pagefile_size,
pages_resolved: std::cell::Cell::new(0),
})
}
pub fn pagefile_size(&self) -> u64 {
self.pagefile_size
}
pub fn pages_resolved(&self) -> u64 {
self.pages_resolved.get()
}
/// Read a 4KB page from the pagefile at the given byte offset.
pub fn read_page(&self, byte_offset: u64) -> Result<[u8; 4096]> {
if byte_offset + 4096 > self.pagefile_size {
return Err(GovmemError::DecryptionError(format!(
"Pagefile offset 0x{:x} + 4096 exceeds size 0x{:x}",
byte_offset, self.pagefile_size
)));
}
// Binary search for the data run containing this offset
let idx = match self.data_runs.binary_search_by(|run| {
if byte_offset < run.file_offset {
std::cmp::Ordering::Greater
} else if byte_offset >= run.file_offset + run.length {
std::cmp::Ordering::Less
} else {
std::cmp::Ordering::Equal
}
}) {
Ok(i) => i,
Err(_) => {
// Sparse region: return zeros
return Ok([0u8; 4096]);
}
};
let run = &self.data_runs[idx];
let run_offset = byte_offset - run.file_offset;
let disk_pos = run.disk_offset + run_offset;
let mut disk = self.disk.borrow_mut();
disk.seek(SeekFrom::Start(disk_pos))?;
let mut buf = [0u8; 4096];
disk.read_exact(&mut buf)?;
self.pages_resolved.set(self.pages_resolved.get() + 1);
Ok(buf)
}
/// Resolve a pagefile PTE: check if it points to pagefile #0 and read the page.
pub fn resolve_pte(&self, raw_pte: u64) -> Option<[u8; 4096]> {
let pte = PageTableEntry(raw_pte);
if !pte.is_pagefile() || pte.pagefile_number() != 0 {
return None;
}
self.read_page(pte.pagefile_offset()).ok()
}
}
/// Extract pagefile.sys data runs from the disk image.
fn extract_pagefile_data_runs(
disk: &mut Box<dyn DiskImage>,
) -> Result<(Vec<PagefileDataRun>, u64)> {
let partitions = crate::sam::find_ntfs_partitions(disk)?;
for &partition_offset in &partitions {
match try_extract_from_partition(disk, partition_offset) {
Ok(result) => return Ok(result),
Err(e) => {
log::debug!("No pagefile at partition 0x{:x}: {}", partition_offset, e);
}
}
}
Err(GovmemError::DecryptionError(
"pagefile.sys not found on any NTFS partition".to_string(),
))
}
/// Try to extract pagefile.sys data runs from a specific NTFS partition.
fn try_extract_from_partition(
disk: &mut Box<dyn DiskImage>,
partition_offset: u64,
) -> Result<(Vec<PagefileDataRun>, u64)> {
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 parse error: {}", e))
})?;
let root = ntfs.root_directory(&mut part_reader).map_err(|e| {
GovmemError::DecryptionError(format!("NTFS root dir error: {}", e))
})?;
let pagefile = crate::sam::find_entry(&ntfs, &root, &mut part_reader, "pagefile.sys")?;
let data_item = pagefile
.data(&mut part_reader, "")
.ok_or_else(|| {
GovmemError::DecryptionError("pagefile.sys: no $DATA attribute".to_string())
})?
.map_err(|e| {
GovmemError::DecryptionError(format!("pagefile.sys $DATA error: {}", e))
})?;
let data_attr = data_item.to_attribute().map_err(|e| {
GovmemError::DecryptionError(format!("pagefile.sys to_attribute error: {}", e))
})?;
let data_value = data_attr.value(&mut part_reader).map_err(|e| {
GovmemError::DecryptionError(format!("pagefile.sys value error: {}", e))
})?;
let pagefile_size = data_value.len();
// Extract data runs from non-resident attribute
match data_value {
NtfsAttributeValue::NonResident(nr) => {
let mut runs = Vec::new();
let mut cumulative_offset = 0u64;
for run_result in nr.data_runs() {
let run = run_result.map_err(|e| {
GovmemError::DecryptionError(format!(
"pagefile.sys data run error: {}",
e
))
})?;
let allocated = run.allocated_size();
if let Some(pos) = run.data_position().value() {
runs.push(PagefileDataRun {
file_offset: cumulative_offset,
disk_offset: partition_offset + pos.get(),
length: allocated,
});
}
cumulative_offset += allocated;
}
Ok((runs, pagefile_size))
}
_ => Err(GovmemError::DecryptionError(
"pagefile.sys: $DATA is not non-resident (unexpected for a pagefile)".to_string(),
)),
}
}
+42 -2
View File
@@ -58,6 +58,14 @@ impl<'a, P: PhysicalMemory> PageTableWalker<'a, P> {
if pte.is_transition() {
return Ok(pte.frame_addr() | (vaddr & 0xFFF));
}
// Check for pagefile PTE (non-zero, not transition, not prototype)
if pte.is_pagefile() {
log::trace!(
"PageFileFault: VA=0x{:x} PTE=0x{:016x} pfn={} offset=0x{:x}",
vaddr, pte.raw(), pte.pagefile_number(), pte.pagefile_offset()
);
return Err(GovmemError::PageFileFault(vaddr, pte.raw()));
}
return Err(GovmemError::PageFault(vaddr, "PT"));
}
@@ -143,10 +151,13 @@ impl<'a, P: PhysicalMemory> PageTableWalker<'a, P> {
}
/// Process virtual memory: combines a DTB (CR3) with physical memory for address translation.
/// Optional pagefile reader resolves pages swapped to pagefile.sys on disk.
pub struct ProcessMemory<'a, P: PhysicalMemory> {
phys: &'a P,
walker: PageTableWalker<'a, P>,
dtb: u64,
#[cfg(feature = "sam")]
pagefile: Option<&'a crate::paging::pagefile::PagefileReader>,
}
impl<'a, P: PhysicalMemory> ProcessMemory<'a, P> {
@@ -155,6 +166,22 @@ impl<'a, P: PhysicalMemory> ProcessMemory<'a, P> {
phys,
walker: PageTableWalker::new(phys),
dtb,
#[cfg(feature = "sam")]
pagefile: None,
}
}
#[cfg(feature = "sam")]
pub fn with_pagefile(
phys: &'a P,
dtb: u64,
pagefile: Option<&'a crate::paging::pagefile::PagefileReader>,
) -> Self {
Self {
phys,
walker: PageTableWalker::new(phys),
dtb,
pagefile,
}
}
@@ -182,12 +209,25 @@ impl<'a, P: PhysicalMemory> VirtualMemory for ProcessMemory<'a, P> {
match self.walker.translate(self.dtb, current_vaddr) {
Ok(phys_addr) => {
if self.phys.read_phys(phys_addr, &mut buf[offset..offset + chunk]).is_err() {
// Physical read failed, zero-fill
buf[offset..offset + chunk].fill(0);
}
}
#[cfg(feature = "sam")]
Err(GovmemError::PageFileFault(_vaddr, raw_pte)) => {
// Try to resolve from pagefile.sys on disk
if let Some(pf) = self.pagefile {
if let Some(page_data) = pf.resolve_pte(raw_pte) {
let page_off = (current_vaddr & 0xFFF) as usize;
buf[offset..offset + chunk]
.copy_from_slice(&page_data[page_off..page_off + chunk]);
} else {
buf[offset..offset + chunk].fill(0);
}
} else {
buf[offset..offset + chunk].fill(0);
}
}
Err(_) => {
// Page not present (paged out / demand paging), zero-fill
buf[offset..offset + chunk].fill(0);
}
}
+4 -4
View File
@@ -128,7 +128,7 @@ fn process_hive_data(
}
/// Parse MBR/GPT and find all NTFS partitions, returning their byte offsets.
fn find_ntfs_partitions<R: Read + Seek>(reader: &mut R) -> Result<Vec<u64>> {
pub(crate) fn find_ntfs_partitions<R: Read + Seek>(reader: &mut R) -> Result<Vec<u64>> {
use std::io::SeekFrom;
reader.seek(SeekFrom::Start(0))?;
let mut mbr = [0u8; 512];
@@ -281,7 +281,7 @@ fn read_hive_files<R: Read + Seek>(
}
/// Find a directory entry by name (case-insensitive).
fn find_entry<'n, R: Read + Seek>(
pub(crate) fn find_entry<'n, R: Read + Seek>(
ntfs: &'n ntfs::Ntfs,
dir: &ntfs::NtfsFile<'n>,
reader: &mut R,
@@ -351,13 +351,13 @@ fn read_file_data<R: Read + Seek>(
}
/// Wraps a Read+Seek with a partition offset.
struct PartitionReader<'a, R: Read + Seek> {
pub(crate) struct PartitionReader<'a, R: Read + Seek> {
inner: &'a mut R,
offset: u64,
}
impl<'a, R: Read + Seek> PartitionReader<'a, R> {
fn new(inner: &'a mut R, offset: u64) -> Self {
pub(crate) fn new(inner: &'a mut R, offset: u64) -> Self {
Self { inner, offset }
}
}