use crate::{
backend::MemoryOps,
error::{Error, Result},
memory::{self, AddressSpace, DTB_IDENTITY, PAGE_SIZE},
phys::PhysMem,
symbols::{
DownloadJob, FieldInfo, ModuleSymbolDiscovery, ModuleSymbolLoad, ModuleSymbolSource,
ModuleSymbolStatus, ParsedType, SymbolIndexDiagnostic, SymbolStore, TypeInfo,
download_jobs_parallel,
},
types::*,
};
use indicatif::{ProgressBar, ProgressStyle};
use pelite::pe64::{Pe, PeFile, PeView};
use rayon::prelude::*;
use std::collections::HashSet;
use std::ops::Range;
use std::path::Path;
use std::sync::Arc;
use zerocopy::{FromBytes, IntoBytes};
#[derive(Debug, Clone)]
pub struct ProcessInfo {
pub pid: u64,
pub name: String,
pub dtb: Dtb,
pub eprocess_va: VirtAddr,
}
#[derive(Debug, Clone)]
pub struct ModuleInfo {
pub name: String,
pub short_name: String,
pub base_address: VirtAddr,
pub size: u32,
pub time_date_stamp: Option<u32>,
pub checksum: Option<u32>,
pub file_version: Option<String>,
pub product_version: Option<String>,
}
impl ModuleInfo {
pub fn new(name: String, base_address: VirtAddr, size: u32) -> Self {
let short_name = Self::derive_short_name(&name);
Self {
name,
short_name,
base_address,
size,
time_date_stamp: None,
checksum: None,
file_version: None,
product_version: None,
}
}
pub fn with_time_date_stamp(mut self, tds: u32) -> Self {
self.time_date_stamp = Some(tds);
self
}
pub fn with_checksum(mut self, cs: u32) -> Self {
self.checksum = Some(cs);
self
}
pub fn with_version_info(mut self, file_ver: String, product_ver: String) -> Self {
self.file_version = Some(file_ver);
self.product_version = Some(product_ver);
self
}
pub fn derive_short_name(name: &str) -> String {
let filename = name.rsplit(['\\', '/']).next().unwrap_or(name);
let without_ext = filename
.rsplit_once('.')
.map(|(base, _)| base)
.unwrap_or(filename);
let lowered = without_ext.to_lowercase();
match lowered.as_str() {
"ntoskrnl" | "ntkrnlmp" | "ntkrnlpa" | "ntkrpamp" => "nt".to_string(),
_ => lowered,
}
}
pub fn end_address(&self) -> VirtAddr {
VirtAddr(self.base_address.0.saturating_add(self.size as u64))
}
pub fn contains_address(&self, address: VirtAddr) -> bool {
address.0 >= self.base_address.0 && address.0 < self.end_address().0
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ModuleSymbolDiagnostic {
pub module: String,
pub phase: &'static str,
pub compiland: Option<String>,
pub message: String,
}
#[derive(Debug, Clone, Default)]
pub struct ModuleSymbolLoadReport {
pub total: usize,
pub loaded: usize,
pub unloaded: usize,
pub no_pdb: usize,
pub skipped: usize,
pub failed: usize,
pub diagnostic_count: usize,
pub diagnostics: Vec<ModuleSymbolDiagnostic>,
}
impl ModuleSymbolLoadReport {
fn new(total: usize) -> Self {
Self {
total,
..Self::default()
}
}
fn record_status(&mut self, status: &ModuleSymbolStatus) {
match status {
ModuleSymbolStatus::Loaded => {
self.loaded += 1;
}
ModuleSymbolStatus::MissingDebugInfo => {
self.no_pdb += 1;
}
ModuleSymbolStatus::Skipped => {
self.skipped += 1;
}
ModuleSymbolStatus::Failed(_) => {
self.failed += 1;
}
}
}
fn record_diagnostics(&mut self, module: &str, diagnostics: Vec<SymbolIndexDiagnostic>) {
const REPORT_DIAGNOSTIC_LIMIT: usize = 64;
self.diagnostic_count += diagnostics.len();
let remaining = REPORT_DIAGNOSTIC_LIMIT.saturating_sub(self.diagnostics.len());
self.diagnostics
.extend(diagnostics.into_iter().take(remaining).map(|diagnostic| {
ModuleSymbolDiagnostic {
module: module.to_string(),
phase: diagnostic.phase,
compiland: diagnostic.compiland,
message: diagnostic.message,
}
}));
}
pub fn failed_count(&self) -> usize {
self.failed
}
}
#[derive(Debug)]
pub struct PeImage {
bytes: Vec<u8>,
holes: Vec<Range<usize>>,
}
impl PeImage {
pub fn complete(bytes: Vec<u8>) -> Self {
Self {
bytes,
holes: Vec::new(),
}
}
pub fn as_slice(&self) -> &[u8] {
&self.bytes
}
pub fn is_complete(&self) -> bool {
self.holes.is_empty()
}
pub fn is_present(&self, at: usize, len: usize) -> bool {
let Some(end) = at.checked_add(len) else {
return false;
};
end <= self.bytes.len() && !self.holes.iter().any(|h| at < h.end && h.start < end)
}
pub fn present_slice(&self, at: usize, len: usize) -> Option<&[u8]> {
self.is_present(at, len).then(|| &self.bytes[at..at + len])
}
}
pub fn read_pe_image<'a, B: MemoryOps<PhysAddr>>(
base_address: VirtAddr,
memory: &memory::AddressSpace<'a, B>,
) -> Result<PeImage> {
let mut header_buf = [0u8; 0x1000];
memory.read_bytes(base_address, &mut header_buf)?;
let view = PeView::from_bytes(&header_buf)?;
let optional_header = view.optional_header();
let sections = view.section_headers();
let total_size = optional_header.SizeOfImage as usize;
let mut image_buffer = vec![0u8; total_size];
let mut holes: Vec<Range<usize>> = Vec::new();
let header_len = std::cmp::min(header_buf.len(), total_size);
image_buffer[..header_len].copy_from_slice(&header_buf[..header_len]);
for section in sections {
let v_addr = section.VirtualAddress as usize;
let v_size = section.VirtualSize as usize;
let raw_size = section.SizeOfRawData as usize;
let copy_size = std::cmp::max(v_size, raw_size);
if copy_size == 0 || v_addr + copy_size > total_size {
continue;
}
let target_slice = &mut image_buffer[v_addr..v_addr + copy_size];
match memory.read_bytes(VirtAddr(base_address.0 + v_addr as u64), target_slice) {
Ok(()) => {}
Err(Error::PartialRead(read)) => holes.push((v_addr + read)..(v_addr + copy_size)),
Err(_) => holes.push(v_addr..(v_addr + copy_size)),
}
}
Ok(PeImage {
bytes: image_buffer,
holes,
})
}
pub fn section_name_at<'a, B: MemoryOps<PhysAddr>>(
memory: &memory::AddressSpace<'a, B>,
base: VirtAddr,
address: VirtAddr,
) -> Option<String> {
let rva = u32::try_from(address.0.checked_sub(base.0)?).ok()?;
let mut header_buf = [0u8; 0x1000];
memory.read_bytes(base, &mut header_buf).ok()?;
let view = PeView::from_bytes(&header_buf).ok()?;
for section in view.section_headers() {
let va = section.VirtualAddress;
let size = section.VirtualSize.max(section.SizeOfRawData);
if rva >= va && rva < va.saturating_add(size) {
return section.name().ok().map(|s| s.to_string());
}
}
None
}
pub fn read_pe_version_info<B: MemoryOps<PhysAddr>>(
base: VirtAddr,
memory: &memory::AddressSpace<'_, B>,
) -> Option<(String, String)> {
use pelite::image::IMAGE_DIRECTORY_ENTRY_RESOURCE;
let mut header_buf = [0u8; 0x1000];
memory.read_bytes(base, &mut header_buf).ok()?;
let view = PeView::from_bytes(&header_buf).ok()?;
let rsrc_dir = view.data_directory().get(IMAGE_DIRECTORY_ENTRY_RESOURCE)?;
let rsrc_rva = rsrc_dir.VirtualAddress;
let rsrc_size = (rsrc_dir.Size as usize).min(256 * 1024);
if rsrc_size < 16 {
return None;
}
let mut rsrc_buf = vec![0u8; rsrc_size];
memory
.read_bytes(VirtAddr(base.0 + rsrc_rva as u64), &mut rsrc_buf)
.ok()?;
let data_entry_rva = find_rt_version_data_entry(&rsrc_buf, rsrc_rva)?;
let ver_rva = read_u32_at(&rsrc_buf, data_entry_rva)?;
let ver_size = read_u32_at(&rsrc_buf, data_entry_rva + 4)? as usize;
if !(52..=32 * 1024).contains(&ver_size) {
return None;
}
let ver_offset_in_rsrc = (ver_rva as usize).checked_sub(rsrc_rva as usize)?;
let ver_data = rsrc_buf.get(ver_offset_in_rsrc..ver_offset_in_rsrc + ver_size)?;
parse_vs_fixedfileinfo(ver_data)
}
const RT_VERSION: u32 = 16;
const VS_FIXEDFILEINFO_SIGNATURE: u32 = 0xFEEF04BD;
fn find_rt_version_data_entry(rsrc: &[u8], rsrc_rva: u32) -> Option<usize> {
let type_entry = find_resource_id_entry(rsrc, 0, RT_VERSION)?;
let name_entry = first_resource_entry(rsrc, type_entry)?;
let lang_entry = first_resource_entry(rsrc, name_entry)?;
if lang_entry & 0x8000_0000 != 0 {
return None;
}
let data_entry_off = lang_entry as usize;
if data_entry_off + 16 > rsrc.len() {
return None;
}
let rva = read_u32_at(rsrc, data_entry_off)?;
if rva < rsrc_rva || (rva as usize - rsrc_rva as usize) >= rsrc.len() {
return None;
}
Some(data_entry_off)
}
fn find_resource_id_entry(rsrc: &[u8], dir_off: usize, target_id: u32) -> Option<u32> {
if dir_off + 16 > rsrc.len() {
return None;
}
let num_named = read_u16_at(rsrc, dir_off + 12)? as usize;
let num_id = read_u16_at(rsrc, dir_off + 14)? as usize;
let entries_start = dir_off + 16;
for i in num_named..(num_named + num_id) {
let entry_off = entries_start + i * 8;
let id = read_u32_at(rsrc, entry_off)?;
if id == target_id {
return read_u32_at(rsrc, entry_off + 4);
}
}
None
}
fn first_resource_entry(rsrc: &[u8], parent_entry: u32) -> Option<u32> {
if parent_entry & 0x8000_0000 == 0 {
return None;
}
let dir_off = (parent_entry & 0x7FFF_FFFF) as usize;
if dir_off + 16 > rsrc.len() {
return None;
}
let num_named = read_u16_at(rsrc, dir_off + 12)? as usize;
let num_id = read_u16_at(rsrc, dir_off + 14)? as usize;
if num_named + num_id == 0 {
return None;
}
let first_entry_off = dir_off + 16;
read_u32_at(rsrc, first_entry_off + 4)
}
fn parse_vs_fixedfileinfo(data: &[u8]) -> Option<(String, String)> {
let sig_bytes = VS_FIXEDFILEINFO_SIGNATURE.to_le_bytes();
let pos = data.windows(4).position(|w| w == sig_bytes)?;
if pos + 52 > data.len() {
return None;
}
let info = &data[pos..];
let file_minor = u16::from_le_bytes([info[8], info[9]]);
let file_major = u16::from_le_bytes([info[10], info[11]]);
let file_revision = u16::from_le_bytes([info[12], info[13]]);
let file_build = u16::from_le_bytes([info[14], info[15]]);
let prod_minor = u16::from_le_bytes([info[16], info[17]]);
let prod_major = u16::from_le_bytes([info[18], info[19]]);
let prod_revision = u16::from_le_bytes([info[20], info[21]]);
let prod_build = u16::from_le_bytes([info[22], info[23]]);
let file_ver = format!(
"{}.{}.{}.{}",
file_major, file_minor, file_build, file_revision
);
let prod_ver = format!(
"{}.{}.{}.{}",
prod_major, prod_minor, prod_build, prod_revision
);
Some((file_ver, prod_ver))
}
fn read_u16_at(buf: &[u8], off: usize) -> Option<u16> {
buf.get(off..off + 2)
.map(|b| u16::from_le_bytes([b[0], b[1]]))
}
fn read_u32_at(buf: &[u8], off: usize) -> Option<u32> {
buf.get(off..off + 4)
.map(|b| u32::from_le_bytes([b[0], b[1], b[2], b[3]]))
}
fn populate_module_versions<B: MemoryOps<PhysAddr>>(
modules: &mut [ModuleInfo],
memory: &memory::AddressSpace<'_, B>,
) {
for module in modules.iter_mut() {
if let Some((file_ver, prod_ver)) = read_pe_version_info(module.base_address, memory) {
module.file_version = Some(file_ver);
module.product_version = Some(prod_ver);
}
}
}
pub fn read_pe_image_from_file(path: &Path) -> Result<PeImage> {
let data = std::fs::read(path)?;
let file = PeFile::from_bytes(&data)?;
let optional_header = file.optional_header();
let total_size = optional_header.SizeOfImage as usize;
let mut image_buffer = vec![0u8; total_size];
let headers_size = (optional_header.SizeOfHeaders as usize)
.min(total_size)
.min(data.len());
image_buffer[..headers_size].copy_from_slice(&data[..headers_size]);
for section in file.section_headers() {
let v_addr = section.VirtualAddress as usize;
let raw_ptr = section.PointerToRawData as usize;
let raw_size = section.SizeOfRawData as usize;
if raw_size == 0 || v_addr + raw_size > total_size || raw_ptr + raw_size > data.len() {
continue;
}
image_buffer[v_addr..v_addr + raw_size].copy_from_slice(&data[raw_ptr..raw_ptr + raw_size]);
}
Ok(PeImage::complete(image_buffer))
}
pub struct SymbolRef<'a> {
obj: &'a WinObject,
rva: u32,
}
impl SymbolRef<'_> {
pub fn address(&self) -> VirtAddr {
self.obj.address_of(self.rva)
}
pub fn read<T>(&self) -> Result<T>
where
T: IntoBytes + FromBytes + Copy,
{
self.obj.memory().read(self.address())
}
}
pub struct WinObject {
pub base_address: VirtAddr,
dtb: Dtb,
arch: Arch,
kernel_dtb: Dtb,
binary_snapshot: Vec<u8>,
pub guid: Option<u128>,
phys: Arc<PhysMem>,
symbols: Arc<SymbolStore>,
}
impl WinObject {
pub fn new(
phys: Arc<PhysMem>,
symbols: Arc<SymbolStore>,
dtb: Dtb,
base_address: VirtAddr,
) -> Self {
Self::new_with_arch(phys, symbols, dtb, base_address, Arch::Amd64)
}
pub fn new_with_arch(
phys: Arc<PhysMem>,
symbols: Arc<SymbolStore>,
dtb: Dtb,
base_address: VirtAddr,
arch: Arch,
) -> Self {
Self {
base_address,
dtb,
arch,
kernel_dtb: dtb,
binary_snapshot: Vec::new(),
guid: None,
phys,
symbols,
}
}
pub fn arch(&self) -> Arch {
self.arch
}
pub fn load_symbols(mut self) -> Result<Self> {
let symbols = Arc::clone(&self.symbols);
self.guid = symbols.load_from_binary(&mut self, "ntoskrnl.exe")?;
Ok(self)
}
pub fn load_symbols_from_module_info(
mut self,
name: &str,
time_date_stamp: u32,
size_of_image: u32,
) -> Result<Self> {
let symbols = Arc::clone(&self.symbols);
self.guid = symbols.load_from_module_info(
name,
self.base_address,
self.dtb,
time_date_stamp,
size_of_image,
)?;
Ok(self)
}
pub fn dtb(&self) -> Dtb {
self.dtb
}
pub fn register_as_kernel(&self) {
self.symbols.set_kernel_guid(self.guid);
}
pub fn binary_size(&self) -> usize {
self.binary_snapshot.len()
}
pub fn sibling(&self, dtb: Dtb, base_address: VirtAddr) -> WinObject {
WinObject {
base_address,
dtb,
arch: self.arch,
kernel_dtb: self.kernel_dtb,
binary_snapshot: Vec::new(),
guid: None,
phys: Arc::clone(&self.phys),
symbols: Arc::clone(&self.symbols),
}
}
pub fn address_of(&self, rva: impl Into<u64>) -> VirtAddr {
self.base_address + rva.into()
}
fn address_space<'a>(
&self,
phys: &'a Arc<PhysMem>,
dtb: Dtb,
) -> AddressSpace<'a, Arc<PhysMem>> {
match self.arch {
Arch::Amd64 => AddressSpace::new(phys, dtb),
Arch::Arm64 => AddressSpace::new_arm64(phys, dtb, self.kernel_dtb),
}
}
pub fn memory(&self) -> AddressSpace<'_, Arc<PhysMem>> {
self.address_space(&self.phys, self.dtb)
}
pub fn symbol<S>(&self, name: S) -> Result<SymbolRef<'_>>
where
S: Into<String>,
{
let name = name.into();
let guid = self.guid.ok_or(Error::ExpectedSymbols)?;
let rva = self
.symbols
.symbol_rva(guid, &name)?
.ok_or(Error::SymbolNotFound(name))?;
Ok(SymbolRef { obj: self, rva })
}
pub fn closest_symbol(&self, address: VirtAddr) -> Result<(String, u32)> {
let guid = self.guid.ok_or(Error::ExpectedSymbols)?;
let result = self
.symbols
.closest_symbol(guid, self.base_address, address)
.ok_or(Error::UnknownAddress(address))?;
Ok(result)
}
pub fn view(&mut self) -> Option<PeView<'_>> {
if self.binary_snapshot.is_empty() {
let phys = Arc::clone(&self.phys);
let memory = self.address_space(&phys, self.dtb);
self.binary_snapshot = read_pe_image(self.base_address, &memory).ok()?.bytes;
}
PeView::from_bytes(&self.binary_snapshot).ok()
}
pub fn types(&self) -> Types<'_> {
Types {
obj: self,
dtb: self.dtb,
}
}
pub fn types_in(&self, dtb: Dtb) -> Types<'_> {
Types { obj: self, dtb }
}
}
#[derive(Clone, Copy)]
pub struct Types<'a> {
obj: &'a WinObject,
dtb: Dtb,
}
impl<'a> Types<'a> {
pub fn layout<S>(self, name: S) -> Result<TypeInfo>
where
S: Into<String> + AsRef<str>,
{
let guid = self.obj.guid.ok_or(Error::ExpectedSymbols)?;
self.obj
.symbols
.dump_struct_with_types(guid, name.as_ref())
.ok_or_else(|| Error::StructNotFound(name.into()))
}
pub fn struct_at(self, name: &str, base: VirtAddr) -> Result<StructRef<'a>> {
let ti = self.layout(name)?;
Ok(StructRef {
obj: self.obj,
dtb: self.dtb,
ti,
base,
})
}
pub fn list_at(
self,
head: VirtAddr,
record_type: &str,
link_field: &str,
) -> Result<impl Iterator<Item = Result<StructRef<'a>>> + 'a> {
let (obj, dtb) = (self.obj, self.dtb);
let record_ti = self.layout(record_type)?;
let link_offset = record_ti.field_offset(link_field)?;
let list_memory = |dtb: Dtb| obj.address_space(&obj.phys, dtb);
let mut current: VirtAddr = list_memory(dtb).read(head)?;
let mut count = 0usize;
const MAX: usize = 1000;
Ok(std::iter::from_fn(move || {
if current.is_zero() || current == head || count >= MAX {
return None;
}
count += 1;
let record = StructRef {
obj,
dtb,
ti: record_ti.clone(),
base: current - link_offset,
};
match list_memory(dtb).read::<VirtAddr>(current) {
Ok(next) if next == current => current = head, Ok(next) => current = next,
Err(e) => {
current = head;
return Some(Err(e));
}
}
Some(Ok(record))
}))
}
}
pub struct StructRef<'a> {
obj: &'a WinObject,
dtb: Dtb,
ti: TypeInfo,
base: VirtAddr,
}
impl<'a> StructRef<'a> {
fn memory(&self) -> AddressSpace<'a, Arc<PhysMem>> {
self.obj.address_space(&self.obj.phys, self.dtb)
}
pub fn addr(&self) -> VirtAddr {
self.base
}
fn field(&self, name: &str) -> Result<&FieldInfo> {
self.ti
.fields
.get(name)
.ok_or_else(|| Error::FieldNotFound(name.to_string()))
}
fn with(&self, ti: TypeInfo, base: VirtAddr) -> StructRef<'a> {
StructRef {
obj: self.obj,
dtb: self.dtb,
ti,
base,
}
}
pub fn read_field<T: Copy + zerocopy::FromZeros + FromBytes + IntoBytes>(
&self,
name: &str,
) -> Result<T> {
let offset = self.field(name)?.offset as u64;
self.memory().read(self.base + offset)
}
pub fn follow(&self, name: &str) -> Result<StructRef<'a>> {
let field = self.field(name)?;
let ParsedType::Pointer(inner) = &field.type_data else {
return Err(Error::FieldTypeMismatch(name.to_string(), "pointer".into()));
};
let ParsedType::Struct(struct_name) = inner.as_ref() else {
return Err(Error::FieldTypeMismatch(
name.to_string(),
"pointer to struct".into(),
));
};
let struct_name = struct_name.clone();
let target: VirtAddr = self.memory().read(self.base + field.offset as u64)?;
let ti = self.obj.types().layout(&struct_name)?;
Ok(self.with(ti, target))
}
pub fn embedded(&self, name: &str) -> Result<StructRef<'a>> {
let field = self.field(name)?;
let type_name = match &field.type_data {
ParsedType::Struct(n) | ParsedType::Union(n) => n.clone(),
_ => {
return Err(Error::FieldTypeMismatch(
name.to_string(),
"struct or union".into(),
));
}
};
let base = self.base + field.offset as u64;
let ti = self.obj.types().layout(&type_name)?;
Ok(self.with(ti, base))
}
pub fn read_unicode_string(&self) -> Result<String> {
let length: u16 = self.read_field("Length")?;
let buffer: VirtAddr = self.read_field("Buffer")?;
if length == 0 || buffer.is_zero() {
return Ok(String::new());
}
let mut buf = vec![0u8; length as usize];
self.memory().read_bytes(buffer, &mut buf)?;
let u16s: Vec<u16> = buf
.chunks_exact(2)
.map(|c| u16::from_le_bytes([c[0], c[1]]))
.collect();
Ok(String::from_utf16_lossy(&u16s))
}
pub fn unicode_string(&self, name: &str) -> Result<String> {
self.embedded(name)?.read_unicode_string()
}
pub fn list(
&self,
head_field: &str,
record_type: &str,
link_field: &str,
) -> Result<impl Iterator<Item = Result<StructRef<'a>>> + 'a> {
let head = self.base + self.field(head_field)?.offset as u64;
self.obj
.types_in(self.dtb)
.list_at(head, record_type, link_field)
}
}
fn module_info_from_record(record: &StructRef<'_>) -> Result<Option<ModuleInfo>> {
let dll_base: VirtAddr = record.read_field("DllBase")?;
if dll_base.is_zero() {
return Ok(None);
}
let size_of_image: u32 = record.read_field("SizeOfImage")?;
let name = record
.unicode_string("BaseDllName")
.ok()
.filter(|s| !s.is_empty())
.unwrap_or_else(|| "<unknown>".to_string());
let mut info = ModuleInfo::new(name, dll_base, size_of_image);
if let Ok(tds) = record.read_field::<u32>("TimeDateStamp") {
info = info.with_time_date_stamp(tds);
}
if let Ok(cs) = record.read_field::<u32>("CheckSum") {
info = info.with_checksum(cs);
}
Ok(Some(info))
}
pub struct Guest {
pub ntoskrnl: WinObject,
}
fn is_valid_kernel_dtb_amd64(phys: &PhysMem, dtb: Dtb) -> Result<bool> {
let Ok(kernel_pml4) = phys.read::<[PageTableEntry; 256]>(dtb + 8 * 256) else {
return Ok(false);
};
if kernel_pml4
.into_iter()
.filter(|e| e.page_frame() == dtb)
.count()
!= 1
{
return Ok(false);
}
const KUSER_SHARED_DATA_VA: VirtAddr = VirtAddr::from_u64(0xfffff78000000000);
let addr_space = AddressSpace::new(phys, dtb);
if let Some(xlat) = addr_space.virt_to_phys(KUSER_SHARED_DATA_VA)?
&& !xlat.user
&& xlat.nx
{
Ok(true)
} else {
Ok(false)
}
}
fn find_kernel_dtb_amd64(phys: &PhysMem) -> Result<Option<Dtb>> {
let base = phys.ram_base();
for dtb in (base + 0x1000..base + 0x1000000).step_by(PAGE_SIZE) {
if is_valid_kernel_dtb_amd64(phys, dtb)? {
return Ok(Some(dtb));
}
}
Ok(None)
}
fn is_valid_kernel_dtb_arm64(phys: &PhysMem, dtb: Dtb) -> Result<bool> {
const KUSER_SHARED_DATA_VA: VirtAddr = VirtAddr::from_u64(0xfffff78000000000);
let addr_space = AddressSpace::new_arm64(phys, dtb, dtb);
let Some(xlat) = addr_space.virt_to_phys(KUSER_SHARED_DATA_VA)? else {
return Ok(false);
};
let mut buf = [0u8; 0x40];
if phys.read_bytes(xlat.address, &mut buf).is_err() {
return Ok(false);
}
Ok(buf[0x2C..0x30] == [0x64, 0xaa, 0x64, 0xaa]
&& buf[0x30] == b'C'
&& buf[0x32] == b':'
&& buf[0x34] == b'\\')
}
fn kernel_machine_at(dtb: Dtb, phys: &PhysMem, arch: Arch) -> Result<Option<u16>> {
let base = match arch {
Arch::Amd64 => find_ntoskrnl_va(dtb, phys)?,
Arch::Arm64 => find_ntoskrnl_va_arm64(dtb, phys)?,
};
let Some(base) = base else {
return Ok(None);
};
let space = match arch {
Arch::Amd64 => AddressSpace::new(phys, dtb),
Arch::Arm64 => AddressSpace::new_arm64(phys, dtb, dtb),
};
let mut dos = [0u8; 4];
if space.read_bytes(base, &mut dos).is_err() || dos != [0x4d, 0x5a, 0x90, 0x00] {
return Ok(None);
}
let lfanew: u32 = match space.read(base + 0x3Cu64) {
Ok(v) => v,
Err(_) => return Ok(None),
};
if lfanew == 0 || lfanew > 0x1000 {
return Ok(None);
}
let mut sig = [0u8; 6];
if space.read_bytes(base + lfanew as u64, &mut sig).is_err() || &sig[..4] != b"PE\0\0" {
return Ok(None);
}
Ok(Some(u16::from_le_bytes([sig[4], sig[5]])))
}
pub fn find_kernel(phys: &PhysMem) -> Result<Option<(Dtb, Arch)>> {
if let Some(dtb) = find_kernel_dtb_amd64(phys)?
&& matches!(kernel_machine_at(dtb, phys, Arch::Amd64)?, Some(0x8664))
{
return Ok(Some((dtb, Arch::Amd64)));
}
for dtb in find_kernel_dtb_arm64_candidates(phys)? {
if matches!(kernel_machine_at(dtb, phys, Arch::Arm64)?, Some(0xaa64)) {
return Ok(Some((dtb, Arch::Arm64)));
}
}
Ok(None)
}
fn find_kernel_dtb_arm64_candidates(phys: &PhysMem) -> Result<Vec<Dtb>> {
const KUSER_L0_INDEX: u64 = 495;
const MAX_CANDIDATES: usize = 32;
let base = phys.ram_base();
let ram_end = base.saturating_add(phys.ram_size());
let mut out = Vec::new();
for dtb in (base.saturating_add(0x1000)..ram_end).step_by(PAGE_SIZE) {
let Ok(entry) = phys.read::<PageTableEntry>(dtb + 8 * KUSER_L0_INDEX) else {
continue;
};
if entry.0 & 0b11 != 0b11 {
continue;
}
let frame = entry.arm64_page_frame();
if frame < base || frame >= ram_end {
continue;
}
if is_valid_kernel_dtb_arm64(phys, dtb)? {
out.push(dtb);
if out.len() >= MAX_CANDIDATES {
break;
}
}
}
Ok(out)
}
fn is_ntoskrnl_header(header: &[u8]) -> bool {
header.len() >= 4
&& header[..4] == [0x4d, 0x5a, 0x90, 0x00]
&& header.chunks_exact(8).any(|c| c == b"POOLCODE")
}
fn is_ntoskrnl_pte(phys: &PhysMem, pte: PageTableEntry) -> Result<bool> {
if pte.is_user() || !pte.is_nx() {
return Ok(false);
}
let Ok(header) = phys.read::<[u8; 0x1000]>(pte.page_frame()) else {
return Ok(false);
};
Ok(is_ntoskrnl_header(&header))
}
fn find_ntoskrnl_va(kernel_dtb: Dtb, phys: &PhysMem) -> Result<Option<VirtAddr>> {
const KERNEL_VA_MIN: VirtAddr = VirtAddr::from_u64(0xfffff80000000000);
const KERNEL_VA_MAX: VirtAddr = VirtAddr::from_u64(0xfffff80800000000);
let pml4e_count = KERNEL_VA_MAX.pml4_index() - KERNEL_VA_MIN.pml4_index() + 1;
let Ok(kernel_pml4) = phys.read::<[PageTableEntry; 256]>(kernel_dtb + 8 * 256) else {
return Ok(None);
};
for (rel_pml4_index, pml4e) in kernel_pml4
.into_iter()
.enumerate()
.skip(KERNEL_VA_MIN.pml4_index() - 256)
.take(pml4e_count)
{
let pml4_index = 256 + rel_pml4_index;
if !pml4e.is_present() {
continue;
}
let Ok(pdpt) = phys.read::<[PageTableEntry; 512]>(pml4e.page_frame()) else {
continue;
};
let pdpte_count = if pml4_index == pml4e_count - 1 {
KERNEL_VA_MAX.pdpt_index() + 1
} else {
512
};
for (pdpt_index, pdpte) in pdpt.into_iter().take(pdpte_count).enumerate() {
if !pdpte.is_present() {
continue;
}
if pdpte.is_large_page() {
if let Ok(true) = is_ntoskrnl_pte(phys, pdpte) {
return Ok(Some(VirtAddr::construct(pml4_index, pdpt_index, 0, 0)));
}
continue;
}
let Ok(pd) = phys.read::<[PageTableEntry; 512]>(pdpte.page_frame()) else {
continue;
};
let pde_count = if pdpt_index == pdpte_count - 1 {
KERNEL_VA_MAX.pd_index() + 1
} else {
512
};
for (pd_index, pde) in pd.into_iter().take(pde_count).enumerate() {
if !pde.is_present() {
continue;
}
if pde.is_large_page() {
if let Ok(true) = is_ntoskrnl_pte(phys, pde) {
return Ok(Some(VirtAddr::construct(
pml4_index, pdpt_index, pd_index, 0,
)));
}
continue;
}
let Ok(pt) = phys.read::<[PageTableEntry; 512]>(pde.page_frame()) else {
continue;
};
let pte_count = if pd_index == pde_count - 1 {
KERNEL_VA_MAX.pt_index() + 1
} else {
512
};
for (pt_index, pte) in pt.into_iter().take(pte_count).enumerate() {
if !pte.is_present() {
continue;
}
if let Ok(true) = is_ntoskrnl_pte(phys, pte) {
return Ok(Some(VirtAddr::construct(
pml4_index, pdpt_index, pd_index, pt_index,
)));
}
}
}
}
}
Ok(None)
}
fn is_ntoskrnl_pte_arm64(phys: &PhysMem, pte: PageTableEntry) -> Result<bool> {
if pte.arm64_is_user() || !pte.arm64_is_pxn() {
return Ok(false);
}
is_ntoskrnl_header_at(phys, pte.arm64_page_frame())
}
fn is_ntoskrnl_header_at(phys: &PhysMem, frame: u64) -> Result<bool> {
let Ok(header) = phys.read::<[u8; 0x1000]>(frame) else {
return Ok(false);
};
Ok(is_ntoskrnl_header(&header))
}
fn find_ntoskrnl_va_arm64(kernel_dtb: Dtb, phys: &PhysMem) -> Result<Option<VirtAddr>> {
const KERNEL_VA_MIN: VirtAddr = VirtAddr::from_u64(0xfffff80000000000);
const KERNEL_VA_MAX: VirtAddr = VirtAddr::from_u64(0xffffff8000000000);
let pml4e_count = KERNEL_VA_MAX.pml4_index() - KERNEL_VA_MIN.pml4_index() + 1;
let Ok(kernel_table) = phys.read::<[PageTableEntry; 256]>(kernel_dtb + 8 * 256) else {
return Ok(None);
};
for (rel_index, l0) in kernel_table
.into_iter()
.enumerate()
.skip(KERNEL_VA_MIN.pml4_index() - 256)
.take(pml4e_count)
{
let pml4_index = 256 + rel_index;
if !l0.arm64_is_valid() || l0.arm64_is_block() {
continue;
}
let Ok(l1_table) = phys.read::<[PageTableEntry; 512]>(l0.arm64_page_frame()) else {
continue;
};
let on_upper_l0 = pml4_index == KERNEL_VA_MAX.pml4_index();
let l1_count = if on_upper_l0 {
KERNEL_VA_MAX.pdpt_index() + 1
} else {
512
};
for (l1_index, l1) in l1_table.into_iter().take(l1_count).enumerate() {
if !l1.arm64_is_valid() {
continue;
}
if l1.arm64_is_block() {
let block = l1.arm64_page_frame();
for l2_index in 0..512u64 {
if is_ntoskrnl_header_at(phys, block + l2_index * (2 << 20))? {
return Ok(Some(VirtAddr::construct(
pml4_index,
l1_index,
l2_index as usize,
0,
)));
}
}
continue;
}
let Ok(l2_table) = phys.read::<[PageTableEntry; 512]>(l1.arm64_page_frame()) else {
continue;
};
let on_upper_l1 = on_upper_l0 && l1_index == KERNEL_VA_MAX.pdpt_index();
let l2_count = if on_upper_l1 {
KERNEL_VA_MAX.pd_index() + 1
} else {
512
};
for (l2_index, l2) in l2_table.into_iter().take(l2_count).enumerate() {
if !l2.arm64_is_valid() {
continue;
}
if l2.arm64_is_block() {
let block = l2.arm64_page_frame();
for pt_index in 0..512u64 {
if is_ntoskrnl_header_at(phys, block + pt_index * 0x1000)? {
return Ok(Some(VirtAddr::construct(
pml4_index,
l1_index,
l2_index,
pt_index as usize,
)));
}
}
continue;
}
let Ok(l3_table) = phys.read::<[PageTableEntry; 512]>(l2.arm64_page_frame()) else {
continue;
};
let on_upper_l2 = on_upper_l1 && l2_index == KERNEL_VA_MAX.pd_index();
let l3_count = if on_upper_l2 {
KERNEL_VA_MAX.pt_index() + 1
} else {
512
};
for (l3_index, l3) in l3_table.into_iter().take(l3_count).enumerate() {
if l3.0 & 0b11 != 0b11 {
continue;
}
if let Ok(true) = is_ntoskrnl_pte_arm64(phys, l3) {
return Ok(Some(VirtAddr::construct(
pml4_index, l1_index, l2_index, l3_index,
)));
}
}
}
}
}
Ok(None)
}
fn find_ntoskrnl_va_triage(kernel_dtb: Dtb, phys: &PhysMem) -> Result<Option<VirtAddr>> {
let space = AddressSpace::new(phys, kernel_dtb);
if let Some(dmp_info) = phys.dmp_info() {
let mut header = vec![0u8; 0x1000];
for driver in &dmp_info.triage_drivers {
let candidate = VirtAddr(driver.base);
if space.read_bytes(candidate, &mut header).is_err() {
continue;
}
if is_ntoskrnl_header(&header) {
return Ok(Some(candidate));
}
}
let ps_loaded = dmp_info.ps_loaded_module_list;
if ps_loaded >= 0xfffff80000000000 {
let page_base = ps_loaded & !0xFFF;
for offset in (0..0x100_0000u64).step_by(0x1000) {
let candidate = page_base - offset;
if space.read_bytes(VirtAddr(candidate), &mut header).is_err() {
continue;
}
if is_ntoskrnl_header(&header) {
return Ok(Some(VirtAddr(candidate)));
}
}
}
}
Ok(None)
}
fn find_ntoskrnl(phys: Arc<PhysMem>, symbols: Arc<SymbolStore>) -> Result<Option<WinObject>> {
let Some((kernel_dtb, arch)) = find_kernel(&phys)? else {
return Ok(None);
};
let ntoskrnl_va = match arch {
Arch::Amd64 => find_ntoskrnl_va(kernel_dtb, &phys)?,
Arch::Arm64 => find_ntoskrnl_va_arm64(kernel_dtb, &phys)?,
};
let Some(ntoskrnl_va) = ntoskrnl_va else {
return Ok(None);
};
Ok(Some(WinObject::new_with_arch(
phys,
symbols,
kernel_dtb,
ntoskrnl_va,
arch,
)))
}
impl Guest {
fn queue_module_symbol_load(
symbols: &SymbolStore,
downloads: &mut Vec<ModuleSymbolLoad>,
ready: &mut Vec<ModuleSymbolLoad>,
load: ModuleSymbolLoad,
) {
if symbols.has_matching_pdb(&load.job) {
ready.push(load);
} else {
downloads.push(load);
}
}
fn apply_module_symbol_status(
symbols: &SymbolStore,
report: &mut ModuleSymbolLoadReport,
dtb: Dtb,
module: &ModuleInfo,
status: ModuleSymbolStatus,
) {
symbols.set_module_symbol_status(dtb, module.base_address, status.clone());
report.record_status(&status);
}
pub fn new_with_kernel_base_hint(
phys: Arc<PhysMem>,
symbols: Arc<SymbolStore>,
kernel_base_hint: Option<VirtAddr>,
) -> Result<Self> {
let ntoskrnl = if let Some(kernel_base) = kernel_base_hint {
let (kernel_dtb, arch) = find_kernel(&phys)?.ok_or(Error::NtoskrnlNotFound)?;
WinObject::new_with_arch(phys, symbols, kernel_dtb, kernel_base, arch)
} else {
find_ntoskrnl(phys, symbols)?.ok_or(Error::NtoskrnlNotFound)?
}
.load_symbols()?;
ntoskrnl.register_as_kernel();
Ok(Self { ntoskrnl })
}
pub fn new(phys: Arc<PhysMem>, symbols: Arc<SymbolStore>) -> Result<Self> {
Self::new_with_kernel_base_hint(phys, symbols, None)
}
pub fn new_with_dtb(
phys: Arc<PhysMem>,
symbols: Arc<SymbolStore>,
kernel_dtb: Dtb,
) -> Result<Self> {
let is_triage = kernel_dtb == DTB_IDENTITY;
let arch = phys
.dmp_info()
.and_then(|info| info.system_info.as_ref())
.and_then(|si| Arch::from_machine_type(si.machine_image_type as u16))
.unwrap_or(Arch::Amd64);
let ntoskrnl_va = if is_triage {
find_ntoskrnl_va_triage(kernel_dtb, &phys)?
} else {
match arch {
Arch::Amd64 => find_ntoskrnl_va(kernel_dtb, &phys)?,
Arch::Arm64 => find_ntoskrnl_va_arm64(kernel_dtb, &phys)?,
}
};
let ntoskrnl_va = match ntoskrnl_va {
Some(va) => va,
None if is_triage => phys
.dmp_info()
.and_then(|i| i.kern_base)
.map(VirtAddr)
.ok_or(Error::NtoskrnlNotFound)?,
None => return Err(Error::NtoskrnlNotFound),
};
let obj = WinObject::new_with_arch(
Arc::clone(&phys),
Arc::clone(&symbols),
kernel_dtb,
ntoskrnl_va,
arch,
);
let ntoskrnl = match obj.load_symbols() {
Ok(loaded) => loaded,
Err(_) if is_triage => {
let driver = phys
.dmp_info()
.and_then(|info| info.triage_drivers.iter().find(|d| d.base == ntoskrnl_va.0))
.cloned()
.ok_or(Error::NtoskrnlNotFound)?;
WinObject::new_with_arch(phys, symbols, kernel_dtb, ntoskrnl_va, arch)
.load_symbols_from_module_info(
&driver.name,
driver.time_date_stamp,
driver.size,
)?
}
Err(e) => return Err(e),
};
ntoskrnl.register_as_kernel();
Ok(Self { ntoskrnl })
}
pub fn enumerate_processes(&self) -> Result<Vec<ProcessInfo>> {
let memory = self.ntoskrnl.memory();
let eprocess_info = self.ntoskrnl.types().layout("_EPROCESS")?;
let active_process_links_offset = eprocess_info.field_offset("ActiveProcessLinks")?;
let pcb_offset = eprocess_info.field_offset("Pcb")?;
let kprocess_info = self.ntoskrnl.types().layout("_KPROCESS")?;
let dir_table_base_offset =
pcb_offset + kprocess_info.field_offset("DirectoryTableBase")?;
let unique_process_id_offset = eprocess_info.field_offset("UniqueProcessId")?;
let image_filename_offset = eprocess_info.field_offset("ImageFileName")?;
let ps_initial_system_process: VirtAddr =
self.ntoskrnl.symbol("PsInitialSystemProcess")?.read()?;
let ps_active_process_head = self
.ntoskrnl
.symbol("PsActiveProcessHead")
.ok()
.map(|s| s.address());
let mut processes = Vec::new();
let mut visited = HashSet::new();
let mut current_eprocess = ps_initial_system_process;
loop {
if current_eprocess.0 == 0 || visited.contains(¤t_eprocess.0) {
break;
}
visited.insert(current_eprocess.0);
let pid = memory.read::<u64>(current_eprocess + unique_process_id_offset)?;
let dtb = memory.read::<Dtb>(current_eprocess + dir_table_base_offset)? & !0xfff;
if dtb == 0 {
break;
}
let name = self
.full_process_name(current_eprocess, dtb)
.unwrap_or_else(|_| {
let mut name_buf = [0u8; 15];
if memory
.read_bytes(current_eprocess + image_filename_offset, &mut name_buf)
.is_ok()
{
String::from_utf8_lossy(
&name_buf[..name_buf.iter().position(|&c| c == 0).unwrap_or(15)],
)
.to_string()
} else {
"<unknown>".to_string()
}
});
processes.push(ProcessInfo {
pid,
name,
dtb,
eprocess_va: current_eprocess,
});
let flink = memory.read::<VirtAddr>(current_eprocess + active_process_links_offset)?;
if flink.0 == 0 || Some(flink) == ps_active_process_head {
break;
}
current_eprocess = flink - active_process_links_offset;
if current_eprocess == ps_initial_system_process {
break;
}
}
Ok(processes)
}
pub fn process_image_name(&self, eprocess_va: VirtAddr) -> Option<String> {
let memory = self.ntoskrnl.memory();
let offset = self
.ntoskrnl
.types()
.layout("_EPROCESS")
.ok()?
.field_offset("ImageFileName")
.ok()?;
let mut name_buf = [0u8; 15];
memory
.read_bytes(eprocess_va + offset, &mut name_buf)
.ok()?;
let len = name_buf.iter().position(|&c| c == 0).unwrap_or(15);
if len == 0 {
return None;
}
Some(String::from_utf8_lossy(&name_buf[..len]).to_string())
}
fn full_process_name(&self, eprocess_va: VirtAddr, dtb: Dtb) -> Result<String> {
let eprocess = self
.ntoskrnl
.types_in(dtb)
.struct_at("_EPROCESS", eprocess_va)?;
let peb = eprocess.follow("Peb")?;
let image_base: VirtAddr = peb.read_field("ImageBaseAddress")?;
if image_base.is_zero() {
return Err(Error::MissingImageBase);
}
for record in peb.follow("Ldr")?.list(
"InLoadOrderModuleList",
"_LDR_DATA_TABLE_ENTRY",
"InLoadOrderLinks",
)? {
let record = record?;
let dll_base: VirtAddr = record.read_field("DllBase")?;
if dll_base == image_base {
return record.unicode_string("BaseDllName");
}
}
Err(Error::MissingImage)
}
pub fn winobj_from_process_info(&self, info: &ProcessInfo) -> Result<WinObject> {
let eprocess = self
.ntoskrnl
.types_in(info.dtb)
.struct_at("_EPROCESS", info.eprocess_va)?;
let peb = eprocess.follow("Peb")?;
if peb.addr().is_zero() {
return Err(Error::MissingPEB);
}
let base_address: VirtAddr = peb.read_field("ImageBaseAddress")?;
Ok(self.ntoskrnl.sibling(info.dtb, base_address))
}
pub fn process_modules(&self, info: &ProcessInfo) -> Result<Vec<ModuleInfo>> {
let eprocess = self
.ntoskrnl
.types_in(info.dtb)
.struct_at("_EPROCESS", info.eprocess_va)?;
let peb = eprocess.follow("Peb")?;
if peb.addr().is_zero() {
return Err(Error::MissingPEB);
}
let ldr = peb.follow("Ldr")?;
if ldr.addr().is_zero() {
return Ok(Vec::new());
}
let mut modules = Vec::new();
for record in ldr.list(
"InLoadOrderModuleList",
"_LDR_DATA_TABLE_ENTRY",
"InLoadOrderLinks",
)? {
if let Some(module) = module_info_from_record(&record?)? {
modules.push(module);
}
}
Ok(modules)
}
pub fn kernel_modules(&self) -> Result<Vec<ModuleInfo>> {
let head = self.ntoskrnl.symbol("PsLoadedModuleList")?.address();
let record_type = if self
.ntoskrnl
.types()
.layout("_KLDR_DATA_TABLE_ENTRY")
.is_ok()
{
"_KLDR_DATA_TABLE_ENTRY"
} else {
"_LDR_DATA_TABLE_ENTRY"
};
let mut modules = Vec::new();
for record in self
.ntoskrnl
.types()
.list_at(head, record_type, "InLoadOrderLinks")?
{
if let Some(module) = module_info_from_record(&record?)? {
modules.push(module);
}
}
Ok(modules)
}
pub fn populate_kernel_module_versions(&self, modules: &mut [ModuleInfo]) {
let memory = self.ntoskrnl.memory();
populate_module_versions(modules, &memory);
}
pub fn populate_process_module_versions(&self, modules: &mut [ModuleInfo], info: &ProcessInfo) {
let process_mem = self.ntoskrnl.sibling(info.dtb, VirtAddr(0));
let memory = process_mem.memory();
populate_module_versions(modules, &memory);
}
fn is_session_space(addr: VirtAddr) -> bool {
let prefix = addr.0 >> 44;
prefix == 0xFFFF8 || prefix == 0xFFFF9 || prefix == 0xFFFFA
}
pub fn load_module_symbols(
phys: &PhysMem,
symbols: &SymbolStore,
modules: Vec<ModuleInfo>,
dtb: Dtb,
skip_session_space: bool,
arch: Arch,
) -> Result<ModuleSymbolLoadReport> {
let mut report = ModuleSymbolLoadReport::new(modules.len());
let mut jobs_with_info: Vec<ModuleSymbolLoad> = Vec::new();
let mut image_jobs: Vec<(DownloadJob, ModuleInfo)> = Vec::new();
let mut ready_to_load: Vec<ModuleSymbolLoad> = Vec::new();
for module in modules {
if skip_session_space && Self::is_session_space(module.base_address) {
Self::apply_module_symbol_status(
symbols,
&mut report,
dtb,
&module,
ModuleSymbolStatus::Skipped,
);
continue;
}
match symbols.extract_download_job(phys, dtb, &module.name, module.base_address, arch) {
Ok(ModuleSymbolDiscovery::Ready { job, guid, source }) => {
Self::queue_module_symbol_load(
symbols,
&mut jobs_with_info,
&mut ready_to_load,
ModuleSymbolLoad::new(job, guid, source, module, dtb),
);
}
Ok(ModuleSymbolDiscovery::NeedsImage { image_job }) => {
image_jobs.push((image_job, module));
}
Err(_e) if module.time_date_stamp.is_some() => {
let tds = module.time_date_stamp.unwrap();
match SymbolStore::build_image_download_job(&module.name, tds, module.size) {
Ok(image_job) => image_jobs.push((image_job, module)),
Err(e) => Self::apply_module_symbol_status(
symbols,
&mut report,
dtb,
&module,
ModuleSymbolStatus::Failed(e.to_string()),
),
}
}
Err(e) => {
Self::apply_module_symbol_status(
symbols,
&mut report,
dtb,
&module,
ModuleSymbolStatus::Failed(e.to_string()),
);
}
}
}
let image_results =
download_jobs_parallel(image_jobs.iter().map(|(job, _)| job.clone()).collect());
for ((image_job, module), result) in image_jobs.into_iter().zip(image_results) {
match result {
Ok(_) => match symbols.extract_download_job_from_image_file(&image_job.path) {
Ok(Some((job, guid))) => {
Self::queue_module_symbol_load(
symbols,
&mut jobs_with_info,
&mut ready_to_load,
ModuleSymbolLoad::new(
job,
guid,
ModuleSymbolSource::Image,
module,
dtb,
),
);
}
Ok(None) => {
Self::apply_module_symbol_status(
symbols,
&mut report,
dtb,
&module,
ModuleSymbolStatus::MissingDebugInfo,
);
}
Err(e) => {
Self::apply_module_symbol_status(
symbols,
&mut report,
dtb,
&module,
ModuleSymbolStatus::Failed(e.to_string()),
);
}
},
Err(e) => {
Self::apply_module_symbol_status(
symbols,
&mut report,
dtb,
&module,
ModuleSymbolStatus::Failed(e.to_string()),
);
}
}
}
let download_results =
download_jobs_parallel(jobs_with_info.iter().map(|load| load.job.clone()).collect());
for (load, result) in jobs_with_info.into_iter().zip(download_results) {
match result {
Ok(_) => ready_to_load.push(load),
Err(e) => {
Self::apply_module_symbol_status(
symbols,
&mut report,
dtb,
&load.module,
ModuleSymbolStatus::Failed(e.to_string()),
);
}
}
}
if !ready_to_load.is_empty() {
let pb = ProgressBar::new(ready_to_load.len() as u64);
pb.set_style(
ProgressStyle::with_template("Indexing [{bar:40}] {pos}/{len}")
.unwrap()
.progress_chars("#-"),
);
let results = ready_to_load
.into_par_iter()
.map(|load| {
let module = load.module.clone();
let guid = load.guid;
let result = symbols.load_downloaded_pdb(&load);
pb.inc(1);
(module, guid, result)
})
.collect::<Vec<_>>();
pb.finish_and_clear();
for (module, guid, result) in results {
match result {
Ok(_) => {
report.record_status(&ModuleSymbolStatus::Loaded);
report.record_diagnostics(&module.name, symbols.index_diagnostics(guid));
}
Err(e) => {
Self::apply_module_symbol_status(
symbols,
&mut report,
dtb,
&module,
ModuleSymbolStatus::Failed(e.to_string()),
);
}
}
}
}
Ok(report)
}
pub fn load_all_kernel_module_symbols(
&self,
phys: &PhysMem,
symbols: &SymbolStore,
) -> Result<ModuleSymbolLoadReport> {
let mut modules = self.kernel_modules()?;
if !modules
.iter()
.any(|module| module.base_address == self.ntoskrnl.base_address)
{
let size = self.ntoskrnl.binary_size().try_into().unwrap_or(u32::MAX);
if size != 0 {
modules.insert(
0,
ModuleInfo::new("ntoskrnl.exe".to_string(), self.ntoskrnl.base_address, size),
);
}
}
let dtb = self.ntoskrnl.dtb();
Self::load_module_symbols(phys, symbols, modules, dtb, true, self.ntoskrnl.arch())
}
pub fn load_missing_kernel_module_symbols(
&self,
phys: &PhysMem,
symbols: &SymbolStore,
) -> Result<ModuleSymbolLoadReport> {
let dtb = self.ntoskrnl.dtb();
let modules = self.kernel_modules()?;
if modules.is_empty() {
return Ok(ModuleSymbolLoadReport::new(0));
}
let unloaded = symbols.retain_modules_for_dtb(dtb, &modules);
let missing = modules
.into_iter()
.filter(|module| {
symbols
.module_symbol_status(dtb, module.base_address)
.is_none()
})
.collect::<Vec<_>>();
let mut report =
Self::load_module_symbols(phys, symbols, missing, dtb, true, self.ntoskrnl.arch())?;
report.unloaded = unloaded;
Ok(report)
}
pub fn load_all_process_module_symbols(
&self,
phys: &PhysMem,
symbols: &SymbolStore,
info: &ProcessInfo,
) -> Result<ModuleSymbolLoadReport> {
let modules = self.process_modules(info)?;
let dtb = info.dtb;
Self::load_module_symbols(phys, symbols, modules, dtb, false, self.ntoskrnl.arch())
}
pub fn load_symbols_for_modules(
&self,
phys: &PhysMem,
symbols: &SymbolStore,
modules: Vec<ModuleInfo>,
dtb: Dtb,
) -> Result<ModuleSymbolLoadReport> {
Self::load_module_symbols(phys, symbols, modules, dtb, false, self.ntoskrnl.arch())
}
}
#[cfg(test)]
mod tests {
use super::{ModuleSymbolLoadReport, PeImage};
use crate::symbols::SymbolIndexDiagnostic;
#[test]
fn pe_image_present_respects_holes_and_bounds() {
let hole = 0x40..0x80;
let image = PeImage {
bytes: vec![0u8; 0x100],
holes: vec![hole],
};
assert!(image.is_present(0x00, 0x40));
assert!(image.is_present(0x80, 0x80));
assert!(image.present_slice(0x10, 0x10).is_some());
assert!(!image.is_present(0x40, 0x01));
assert!(!image.is_present(0x3f, 0x02));
assert!(!image.is_present(0x7f, 0x02));
assert!(image.present_slice(0x38, 0x10).is_none());
assert!(!image.is_present(0xf0, 0x20));
assert!(!image.is_present(usize::MAX, 1));
}
#[test]
fn symbol_report_preserves_index_diagnostics_and_total_count() {
let mut report = ModuleSymbolLoadReport::new(1);
let diagnostics = (0..70)
.map(|index| SymbolIndexDiagnostic {
phase: "line iteration",
compiland: Some(format!("{index}.obj")),
message: "malformed line record".to_string(),
})
.collect();
report.record_diagnostics("driver.sys", diagnostics);
assert_eq!(report.diagnostic_count, 70);
assert_eq!(report.diagnostics.len(), 64);
assert_eq!(report.diagnostics[0].module, "driver.sys");
assert_eq!(report.diagnostics[0].compiland.as_deref(), Some("0.obj"));
}
}