use std::collections::{HashMap, HashSet};
use std::sync::Arc;
use std::{fmt, result};
use crate::unwind::{
RecoveredFrame, frame_base_for_register_values, return_address_for_register_values,
};
use crate::{
backend::MemoryOps,
bugchecks::looks_like_kernel_pointer,
debugger_data::{
DebuggerDataBlock, DebuggerDataCandidate, MetadataSource, MetadataValue,
locate_debugger_data_block, read_counter_from_getter,
},
diagnostics,
error::{Error, Result},
guest::{
Guest, ModuleInfo, ModuleSymbolLoadReport, ProcessInfo, StructRef, WinObject,
section_name_at,
},
memory::{AddressSpace, DTB_IDENTITY, PAGE_SIZE},
phys::PhysMem,
symbols::{
LocalVariableLocation, ParsedType, ProcedureLocal, SourceLineExtent, SourceLocation,
SymbolCandidate, SymbolIndex, SymbolStore, TypeInfo, format_symbol_with_offset,
},
types::{Arch, Dtb, PageTableEntry, Value, VirtAddr},
};
pub struct Target {
pub phys: Arc<PhysMem>,
pub symbols: Arc<SymbolStore>,
pub guest: Option<Guest>,
debugger_data: Option<DebuggerDataBlock>,
pub current_process: Option<WinObject>,
pub current_process_info: Option<ProcessInfo>,
triage_fallback: Option<WinObject>,
triage_modules_cache: Option<Vec<ModuleInfo>>,
context_dtb_override: Option<Dtb>,
pub registers: Option<HashMap<String, u64>>,
pub selected_frame: Option<SelectedFrame>,
pub windows_thread_selection: Option<ThreadInfo>,
pub user_vars: HashMap<String, UserVar>,
pub results: Vec<u64>,
pub results_origin: Option<String>,
pub last_exception_code: Option<u32>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SelectedFrame {
pub index: usize,
pub ip: u64,
pub sp: u64,
pub frame_base: Option<u64>,
pub registers: HashMap<String, u64>,
pub seed_registers: HashMap<String, u64>,
}
impl SelectedFrame {
pub fn from_recovered(
frame: &RecoveredFrame,
index: usize,
seed_registers: Option<&HashMap<String, u64>>,
) -> Self {
Self {
index,
ip: frame.frame.ip,
sp: frame.frame.sp,
frame_base: frame.frame_base,
registers: frame.registers.clone(),
seed_registers: seed_registers
.filter(|registers| !registers.is_empty())
.cloned()
.unwrap_or_else(|| frame.registers.clone()),
}
}
pub fn from_registers(index: usize, registers: HashMap<String, u64>) -> Self {
let ip = registers
.get("rip")
.copied()
.or_else(|| registers.get("pc").copied())
.unwrap_or(0);
let sp = registers
.get("rsp")
.copied()
.or_else(|| registers.get("sp").copied())
.unwrap_or(0);
let seed_registers = registers.clone();
Self {
index,
ip,
sp,
frame_base: None,
registers,
seed_registers,
}
}
}
#[derive(Debug, Clone)]
pub struct UserVar {
pub value: u64,
pub source: String,
}
pub struct BuiltinVar {
pub name: &'static str,
pub value: u64,
pub source: &'static str,
}
#[derive(Debug, Clone)]
pub struct DriverObjectInfo {
pub name: String,
pub object: VirtAddr,
pub driver_start: VirtAddr,
pub driver_size: u64,
pub device_object: VirtAddr,
pub driver_unload: VirtAddr,
}
#[derive(Debug, Clone)]
pub struct IrpInfo {
pub address: VirtAddr,
pub irp_type: u16,
pub size: u16,
pub stack_count: u8,
pub current_location: u8,
pub pending_returned: bool,
pub requestor_mode: u8,
pub io_status: Option<u32>,
pub user_event: VirtAddr,
pub user_buffer: VirtAddr,
pub mdl_address: VirtAddr,
pub thread: VirtAddr,
pub current_stack: Option<IoStackLocationInfo>,
}
#[derive(Debug, Clone)]
pub struct IoStackLocationInfo {
pub address: VirtAddr,
pub major_function: u8,
pub minor_function: u8,
pub device_object: VirtAddr,
pub file_object: VirtAddr,
pub completion_routine: VirtAddr,
pub context: VirtAddr,
}
#[derive(Debug, Clone)]
pub struct DriverObjectDetail {
pub object: VirtAddr,
pub via_pointer: bool,
pub name: Option<String>,
pub driver_start: VirtAddr,
pub driver_size: u64,
pub driver_section: VirtAddr,
pub driver_unload: VirtAddr,
pub device_chain: Vec<DeviceLink>,
pub dispatch: Vec<VirtAddr>,
}
#[derive(Debug, Clone)]
pub struct DeviceLink {
pub device: VirtAddr,
pub device_type: u32,
pub flags: u32,
pub characteristics: u32,
pub attached: VirtAddr,
pub next: VirtAddr,
}
#[derive(Debug, Clone)]
pub struct DeviceObjectDetail {
pub object: VirtAddr,
pub via_pointer: bool,
pub device_type: u32,
pub flags: u32,
pub characteristics: u32,
pub driver_object: VirtAddr,
pub attached_device: VirtAddr,
pub next_device: VirtAddr,
pub current_irp: VirtAddr,
pub device_extension: VirtAddr,
pub attached_stack: Vec<DeviceStackEntry>,
}
#[derive(Debug, Clone)]
pub struct DeviceStackEntry {
pub device: VirtAddr,
pub driver_object: VirtAddr,
pub device_type: u32,
pub flags: u32,
}
#[derive(Debug, Clone)]
pub struct ObjectHeaderDetail {
pub input: VirtAddr,
pub mode: &'static str,
pub header: VirtAddr,
pub body: VirtAddr,
pub pointer_count: i64,
pub handle_count: i64,
pub type_index: Option<u64>,
pub type_object: Option<VirtAddr>,
pub type_name: Option<String>,
pub info_mask: Option<u8>,
pub name_info: Option<VirtAddr>,
pub name: Option<String>,
}
#[derive(Debug, Clone)]
pub struct NotifyCallback {
pub kind: &'static str,
pub index: usize,
pub function: VirtAddr,
pub block: VirtAddr,
pub raw: VirtAddr,
pub context: VirtAddr,
}
#[derive(Debug, Clone)]
pub struct SsdtEntry {
pub index: u32,
pub target: VirtAddr,
pub symbol: Option<String>,
pub module: Option<String>,
}
#[derive(Debug, Clone)]
pub struct SsdtTable {
pub label: String,
pub base: VirtAddr,
pub limit: u32,
pub entries: Vec<SsdtEntry>,
}
#[derive(Debug, Clone)]
pub struct IrpHit {
pub irp: VirtAddr,
pub source: &'static str,
pub stack_count: u8,
pub current_location: u8,
pub pid: Option<u64>,
pub tid: Option<u64>,
pub ethread: Option<VirtAddr>,
pub state: Option<u8>,
pub wait_reason: Option<u8>,
pub driver: Option<String>,
pub device: Option<VirtAddr>,
}
#[derive(Debug, Clone)]
pub struct AddressDescription {
pub address: VirtAddr,
pub dtb: Dtb,
pub kind: &'static str,
pub module: Option<AddressModule>,
pub section: Option<String>,
pub va_type: Option<String>,
pub region: Option<MemoryRegionInfo>,
}
#[derive(Debug, Clone)]
pub struct AddressModule {
pub name: String,
pub base: VirtAddr,
pub size: u32,
pub offset: u64,
}
#[derive(Debug, Clone)]
pub struct MemorySearchMatch {
pub address: VirtAddr,
pub offset: u64,
pub symbol: Option<String>,
pub description: AddressDescription,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SymbolSearchMatch {
pub name: String,
pub address: Option<VirtAddr>,
pub module: Option<String>,
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct Arm64SavedRegisters {
pub x: [Option<u64>; 31],
pub sp: Option<u64>,
pub pc: Option<u64>,
pub cpsr: Option<u64>,
pub fp: Option<u64>,
pub lr: Option<u64>,
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct SavedThreadRegisters {
pub rip: Option<u64>,
pub rsp: Option<u64>,
pub rax: Option<u64>,
pub rcx: Option<u64>,
pub rdx: Option<u64>,
pub rbx: Option<u64>,
pub rbp: Option<u64>,
pub rsi: Option<u64>,
pub rdi: Option<u64>,
pub r8: Option<u64>,
pub r9: Option<u64>,
pub r10: Option<u64>,
pub r11: Option<u64>,
pub r12: Option<u64>,
pub r13: Option<u64>,
pub r14: Option<u64>,
pub r15: Option<u64>,
pub rflags: Option<u64>,
pub arm64: Option<Arm64SavedRegisters>,
}
impl SavedThreadRegisters {
pub fn get(&self, name: &str) -> Option<u64> {
let name = name.to_ascii_lowercase();
if let Some(index) = name
.strip_prefix('x')
.and_then(|index| index.parse::<usize>().ok())
.filter(|&index| index < 31)
{
return self.arm64.as_ref()?.x[index];
}
match name.as_str() {
"rip" => self.rip,
"rsp" => self.rsp,
"rax" => self.rax,
"rcx" => self.rcx,
"rdx" => self.rdx,
"rbx" => self.rbx,
"rbp" => self.rbp,
"rsi" => self.rsi,
"rdi" => self.rdi,
"r8" => self.r8,
"r9" => self.r9,
"r10" => self.r10,
"r11" => self.r11,
"r12" => self.r12,
"r13" => self.r13,
"r14" => self.r14,
"r15" => self.r15,
"rflags" | "eflags" => self.rflags,
"sp" => self.arm64.as_ref()?.sp,
"pc" => self.arm64.as_ref()?.pc,
"cpsr" => self.arm64.as_ref()?.cpsr,
"fp" => self.arm64.as_ref()?.fp,
"lr" => self.arm64.as_ref()?.lr,
_ => None,
}
}
}
#[derive(Debug, Clone)]
pub struct ThreadInfo {
pub ethread: VirtAddr,
pub kthread: VirtAddr,
pub tid: Option<u64>,
pub pid: Option<u64>,
pub process_name: Option<String>,
pub eprocess: Option<VirtAddr>,
pub state: Option<u8>,
pub wait_reason: Option<u8>,
pub priority: Option<u8>,
pub base_priority: Option<u8>,
pub wait_irql: Option<u8>,
pub kernel_stack_resident: Option<bool>,
pub start_address: Option<VirtAddr>,
pub win32_start_address: Option<VirtAddr>,
pub teb: Option<VirtAddr>,
pub kernel_stack: Option<VirtAddr>,
pub stack_base: Option<VirtAddr>,
pub stack_limit: Option<VirtAddr>,
pub trap_frame: Option<VirtAddr>,
pub pending_irps: Option<Vec<VirtAddr>>,
}
impl ThreadInfo {
pub fn pseudo_register(&self, name: &str) -> Option<Option<u64>> {
let value = match name.to_ascii_lowercase().as_str() {
"thread" | "ethread" => Some(self.ethread.0),
"kthread" => Some(self.kthread.0),
"tid" => self.tid,
"pid" => self.pid,
"proc" | "process" | "eprocess" => self.eprocess.map(|addr| addr.0),
"teb" => self.teb.map(|addr| addr.0),
"threadstart" | "startaddress" => self.start_address.map(|addr| addr.0),
"win32start" | "win32startaddress" => self.win32_start_address.map(|addr| addr.0),
"kernelstack" => self.kernel_stack.map(|addr| addr.0),
"stackbase" => self.stack_base.map(|addr| addr.0),
"stacklimit" => self.stack_limit.map(|addr| addr.0),
"trapframe" => self.trap_frame.map(|addr| addr.0),
"priority" => self.priority.map(u64::from),
"basepriority" => self.base_priority.map(u64::from),
"waitirql" => self.wait_irql.map(u64::from),
"stackresident" | "kernelstackresident" => {
self.kernel_stack_resident.map(|resident| resident as u64)
}
_ => return None,
};
Some(value)
}
pub fn pseudo_register_value(&self, name: &str) -> Option<u64> {
self.pseudo_register(name).flatten()
}
}
fn thread_owner_matches(thread: &ThreadInfo, process: &ProcessInfo) -> bool {
match thread.eprocess {
Some(eprocess) => eprocess == process.eprocess_va,
None => thread.pid.is_some_and(|pid| pid == process.pid),
}
}
fn select_thread_process_dtb(
thread: &ThreadInfo,
current: Option<&ProcessInfo>,
processes: &[ProcessInfo],
kernel_dtb: Dtb,
) -> Option<Dtb> {
if thread.pid == Some(0) {
return Some(kernel_dtb);
}
current
.filter(|process| thread_owner_matches(thread, process))
.or_else(|| {
processes
.iter()
.find(|process| thread_owner_matches(thread, process))
})
.map(|process| process.dtb)
}
pub fn irp_major_function_name(major: u8) -> &'static str {
match major {
0x00 => "CREATE",
0x01 => "CREATE_NAMED_PIPE",
0x02 => "CLOSE",
0x03 => "READ",
0x04 => "WRITE",
0x05 => "QUERY_INFORMATION",
0x06 => "SET_INFORMATION",
0x07 => "QUERY_EA",
0x08 => "SET_EA",
0x09 => "FLUSH_BUFFERS",
0x0a => "QUERY_VOLUME_INFORMATION",
0x0b => "SET_VOLUME_INFORMATION",
0x0c => "DIRECTORY_CONTROL",
0x0d => "FILE_SYSTEM_CONTROL",
0x0e => "DEVICE_CONTROL",
0x0f => "INTERNAL_DEVICE_CONTROL",
0x10 => "SHUTDOWN",
0x11 => "LOCK_CONTROL",
0x12 => "CLEANUP",
0x13 => "CREATE_MAILSLOT",
0x14 => "QUERY_SECURITY",
0x15 => "SET_SECURITY",
0x16 => "POWER",
0x17 => "SYSTEM_CONTROL",
0x18 => "DEVICE_CHANGE",
0x19 => "QUERY_QUOTA",
0x1a => "SET_QUOTA",
0x1b => "PNP",
_ => "?",
}
}
fn irp_thread(irp: &StructRef) -> VirtAddr {
if let Ok(t) = irp.read_field::<VirtAddr>("Thread") {
return t;
}
irp.embedded("Tail")
.and_then(|tail| tail.embedded("Overlay"))
.and_then(|ov| ov.read_field::<VirtAddr>("Thread"))
.unwrap_or(VirtAddr(0))
}
pub fn process_matches(process: &ProcessInfo, filter: &str) -> bool {
match filter.parse::<u64>() {
Ok(pid) => process.pid == pid,
Err(_) => process
.name
.to_ascii_lowercase()
.contains(&filter.to_ascii_lowercase()),
}
}
pub fn decimal_pid_literal(text: &str) -> Option<u64> {
if text.is_empty() || !text.bytes().all(|byte| byte.is_ascii_digit()) {
return None;
}
text.parse().ok()
}
pub fn kthread_state_name(state: u8) -> &'static str {
match state {
0 => "Initialized",
1 => "Ready",
2 => "Running",
3 => "Standby",
4 => "Terminated",
5 => "Waiting",
6 => "Transition",
7 => "DeferredReady",
8 => "GateWaitObsolete",
9 => "WaitingForProcessInSwap",
_ => "?",
}
}
pub fn lookup_register(registers: &HashMap<String, u64>, name: &str) -> Option<u64> {
let direct = |name: &str| {
registers.get(name).copied().or_else(|| {
registers
.iter()
.find(|(candidate, _)| candidate.eq_ignore_ascii_case(name))
.map(|(_, value)| *value)
})
};
if let Some(value) = direct(name) {
return Some(value);
}
const ALIASES: &[(&str, &str, u32, u32)] = &[
("eax", "rax", 0, 32),
("ax", "rax", 0, 16),
("al", "rax", 0, 8),
("ah", "rax", 8, 8),
("ebx", "rbx", 0, 32),
("bx", "rbx", 0, 16),
("bl", "rbx", 0, 8),
("bh", "rbx", 8, 8),
("ecx", "rcx", 0, 32),
("cx", "rcx", 0, 16),
("cl", "rcx", 0, 8),
("ch", "rcx", 8, 8),
("edx", "rdx", 0, 32),
("dx", "rdx", 0, 16),
("dl", "rdx", 0, 8),
("dh", "rdx", 8, 8),
("esi", "rsi", 0, 32),
("si", "rsi", 0, 16),
("sil", "rsi", 0, 8),
("edi", "rdi", 0, 32),
("di", "rdi", 0, 16),
("dil", "rdi", 0, 8),
("esp", "rsp", 0, 32),
("sp", "rsp", 0, 16),
("spl", "rsp", 0, 8),
("ebp", "rbp", 0, 32),
("bp", "rbp", 0, 16),
("bpl", "rbp", 0, 8),
("eip", "rip", 0, 32),
];
if let Some((_, parent, shift, width)) = ALIASES
.iter()
.find(|(alias, ..)| alias.eq_ignore_ascii_case(name))
{
return direct(parent).map(|value| (value >> shift) & ((1u64 << width) - 1));
}
let bytes = name.as_bytes();
if bytes.len() >= 3 && bytes[0].eq_ignore_ascii_case(&b'r') {
let suffix = bytes[bytes.len() - 1].to_ascii_lowercase();
let width = match suffix {
b'd' => 32,
b'w' => 16,
b'b' => 8,
_ => return None,
};
let number = name[1..name.len() - 1].parse::<u8>().ok()?;
if (8..=15).contains(&number) {
return direct(&name[..name.len() - 1]).map(|value| value & ((1u64 << width) - 1));
}
}
if bytes.len() >= 2 && bytes[0].eq_ignore_ascii_case(&b'w') {
let number = name[1..].parse::<u8>().ok()?;
if number <= 30 {
return direct(&format!("x{number}")).map(|value| value & 0xffff_ffff);
}
}
None
}
pub fn wait_reason_name(reason: u8) -> &'static str {
match reason {
0 => "Executive",
1 => "FreePage",
2 => "PageIn",
3 => "PoolAllocation",
4 => "DelayExecution",
5 => "Suspended",
6 => "UserRequest",
7 => "WrExecutive",
8 => "WrFreePage",
9 => "WrPageIn",
10 => "WrPoolAllocation",
11 => "WrDelayExecution",
12 => "WrSuspended",
13 => "WrUserRequest",
14 => "WrEventPair",
15 => "WrQueue",
16 => "WrLpcReceive",
17 => "WrLpcReply",
18 => "WrVirtualMemory",
19 => "WrPageOut",
20 => "WrRendezvous",
21 => "WrKeyedEvent",
22 => "WrTerminated",
23 => "WrProcessInSwap",
24 => "WrCpuRateControl",
25 => "WrCalloutStack",
26 => "WrKernel",
27 => "WrResource",
28 => "WrPushLock",
29 => "WrMutex",
30 => "WrQuantumEnd",
31 => "WrDispatchInt",
32 => "WrPreempted",
33 => "WrYieldExecution",
34 => "WrFastMutex",
35 => "WrGuardedMutex",
36 => "WrRundown",
37 => "WrAlertByThreadId",
38 => "WrDeferredPreempt",
_ => "?",
}
}
#[derive(Debug, Clone)]
pub struct MemoryRegionInfo {
pub node_address: VirtAddr,
pub level: usize,
pub start: VirtAddr,
pub end: VirtAddr,
pub protection: Option<u64>,
pub vad_type: Option<u64>,
pub private_memory: Option<bool>,
pub commit_charge: Option<u64>,
pub details: Option<String>,
}
impl MemoryRegionInfo {
pub fn size(&self) -> u64 {
self.end.0.saturating_sub(self.start.0)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum DiagnosticValue<T> {
Available(T),
Unavailable(String),
}
impl<T> DiagnosticValue<T> {
fn from_result(result: Result<T>) -> Self {
match result {
Ok(value) => Self::Available(value),
Err(error) => Self::Unavailable(error.to_string()),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct DiagnosticMetric<T> {
pub value: DiagnosticValue<T>,
pub source: Option<MetadataSource>,
}
impl<T> DiagnosticMetric<T> {
fn available(value: MetadataValue<T>) -> Self {
Self {
value: DiagnosticValue::Available(value.value),
source: Some(value.source),
}
}
fn unavailable(errors: Vec<String>) -> Self {
Self {
value: DiagnosticValue::Unavailable(errors.join("; ")),
source: None,
}
}
}
#[derive(Debug, Clone)]
pub struct HandleEntryDetail {
pub handle: u64,
pub entry: VirtAddr,
pub object: DiagnosticValue<VirtAddr>,
pub type_name: DiagnosticValue<Option<String>>,
pub name: DiagnosticValue<Option<String>>,
pub granted_access: DiagnosticValue<u32>,
pub attributes: DiagnosticValue<u32>,
}
#[derive(Debug, Clone)]
pub struct HandleTableSummary {
pub process: ProcessInfo,
pub table: VirtAddr,
pub table_level: u8,
pub advertised_handles: usize,
pub scanned_handles: usize,
pub skipped_entries: usize,
pub truncated: bool,
pub entries: Vec<HandleEntryDetail>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SidAndAttributes {
pub sid: String,
pub attributes: u32,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct PrivilegeInfo {
pub luid: u64,
pub attributes: u32,
}
const SE_PRIVILEGE_ENABLED_BY_DEFAULT: u32 = 0x1;
const SE_PRIVILEGE_ENABLED: u32 = 0x2;
fn decode_token_privilege_bitmaps(
present: u64,
enabled: u64,
enabled_by_default: u64,
) -> Vec<PrivilegeInfo> {
let mut remaining = present;
let mut privileges = Vec::with_capacity(present.count_ones() as usize);
while remaining != 0 {
let bit = remaining.trailing_zeros();
let mask = 1u64 << bit;
let mut attributes = 0;
if enabled_by_default & mask != 0 {
attributes |= SE_PRIVILEGE_ENABLED_BY_DEFAULT;
}
if enabled & mask != 0 {
attributes |= SE_PRIVILEGE_ENABLED;
}
privileges.push(PrivilegeInfo {
luid: u64::from(bit),
attributes,
});
remaining &= remaining - 1;
}
privileges
}
#[derive(Debug, Clone)]
pub struct TokenDetail {
pub process: ProcessInfo,
pub token: VirtAddr,
pub token_id: DiagnosticValue<u64>,
pub authentication_id: DiagnosticValue<u64>,
pub token_type: DiagnosticValue<u32>,
pub impersonation_level: DiagnosticValue<u32>,
pub flags: DiagnosticValue<u32>,
pub user: DiagnosticValue<Option<SidAndAttributes>>,
pub groups: DiagnosticValue<Vec<SidAndAttributes>>,
pub privileges: DiagnosticValue<Vec<PrivilegeInfo>>,
}
#[derive(Debug, Clone)]
pub struct FileObjectDetail {
pub address: VirtAddr,
pub file_type: DiagnosticValue<i16>,
pub size: DiagnosticValue<i16>,
pub device_object: DiagnosticValue<VirtAddr>,
pub device_type: DiagnosticValue<u32>,
pub device_name: DiagnosticValue<Option<String>>,
pub file_name: DiagnosticValue<String>,
pub related_file_object: DiagnosticValue<VirtAddr>,
pub flags: DiagnosticValue<u32>,
pub current_byte_offset: DiagnosticValue<i64>,
pub fs_context: DiagnosticValue<VirtAddr>,
pub fs_context2: DiagnosticValue<VirtAddr>,
pub section_object_pointer: DiagnosticValue<VirtAddr>,
pub private_cache_map: DiagnosticValue<VirtAddr>,
pub final_status: DiagnosticValue<i32>,
pub lock_operation: DiagnosticValue<bool>,
pub delete_pending: DiagnosticValue<bool>,
pub read_access: DiagnosticValue<bool>,
pub write_access: DiagnosticValue<bool>,
pub delete_access: DiagnosticValue<bool>,
pub shared_read: DiagnosticValue<bool>,
pub shared_write: DiagnosticValue<bool>,
pub shared_delete: DiagnosticValue<bool>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ResourceOwner {
pub thread: VirtAddr,
pub count: i32,
}
#[derive(Debug, Clone)]
pub struct ResourceDetail {
pub address: VirtAddr,
pub active_count: DiagnosticValue<i16>,
pub flags: DiagnosticValue<u16>,
pub contention_count: DiagnosticValue<u32>,
pub shared_waiters: DiagnosticValue<u32>,
pub exclusive_waiters: DiagnosticValue<u32>,
pub owners: DiagnosticValue<Vec<ResourceOwner>>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ListTermination {
Head,
Null,
Cycle(VirtAddr),
Bound,
Corrupt(String),
}
impl ListTermination {
pub fn diagnostic(&self) -> Option<String> {
match self {
Self::Head => None,
Self::Null => Some("null link".to_string()),
Self::Cycle(address) => Some(format!("non-head cycle at {:#x}", address.0)),
Self::Bound => Some("entry bound reached".to_string()),
Self::Corrupt(error) => Some(format!("unreadable link: {error}")),
}
}
}
pub struct ListCursor {
head: VirtAddr,
limit: usize,
current: Option<VirtAddr>,
visited: HashSet<u64>,
yielded: usize,
first_entry: bool,
termination: Option<ListTermination>,
}
impl ListCursor {
pub fn new(head: VirtAddr, limit: usize) -> Self {
Self {
head,
limit,
current: None,
visited: HashSet::with_capacity(16),
yielded: 0,
first_entry: false,
termination: None,
}
}
pub fn from_first(first: VirtAddr, limit: usize) -> Self {
Self {
head: first,
limit,
current: Some(first),
visited: HashSet::with_capacity(16),
yielded: 0,
first_entry: true,
termination: None,
}
}
pub fn next(&mut self) -> Option<VirtAddr> {
if self.termination.is_some() {
return None;
}
let current = self.current.take()?;
if !self.first_entry && current == self.head {
self.termination = Some(ListTermination::Head);
return None;
}
if current.is_zero() {
self.termination = Some(ListTermination::Null);
return None;
}
if !self.visited.insert(current.0) {
self.termination = Some(ListTermination::Cycle(current));
return None;
}
if self.yielded >= self.limit {
self.termination = Some(ListTermination::Bound);
return None;
}
self.first_entry = false;
self.yielded += 1;
Some(current)
}
pub fn advance(&mut self, next: result::Result<VirtAddr, String>) {
if self.termination.is_some() {
return;
}
self.current = match next {
Ok(next) => Some(next),
Err(error) => {
self.termination = Some(ListTermination::Corrupt(error));
None
}
};
}
pub fn finish(self) -> ListTermination {
self.termination
.expect("ListCursor::finish called before next() returned None")
}
}
#[derive(Debug, Clone)]
pub struct ResourceListSummary {
pub head: VirtAddr,
pub resources: Vec<ResourceDetail>,
pub termination: ListTermination,
}
#[derive(Debug, Clone)]
pub struct ProcessMemoryUsage {
pub process: ProcessInfo,
pub virtual_size: DiagnosticValue<u64>,
pub peak_virtual_size: DiagnosticValue<u64>,
pub working_set_size: DiagnosticValue<u64>,
pub peak_working_set_size: DiagnosticValue<u64>,
pub pagefile_usage: DiagnosticValue<u64>,
pub peak_pagefile_usage: DiagnosticValue<u64>,
pub private_usage: DiagnosticValue<u64>,
}
#[derive(Debug, Clone)]
pub struct SystemMemorySummary {
pub physical_pages: DiagnosticMetric<u64>,
pub available_pages: DiagnosticMetric<u64>,
pub committed_pages: DiagnosticMetric<u64>,
pub commit_limit_pages: DiagnosticMetric<u64>,
pub paged_pool_pages: DiagnosticMetric<u64>,
pub nonpaged_pool_bytes: DiagnosticMetric<u64>,
pub processes: Vec<ProcessMemoryUsage>,
pub process_count: usize,
pub truncated: bool,
}
fn bounded_list_walk<F>(
head: VirtAddr,
limit: usize,
mut read_next: F,
) -> (Vec<VirtAddr>, ListTermination)
where
F: FnMut(VirtAddr) -> Result<VirtAddr>,
{
let mut links = Vec::new();
let mut cursor = ListCursor::new(head, limit);
cursor.advance(read_next(head).map_err(|error| error.to_string()));
while let Some(current) = cursor.next() {
links.push(current);
cursor.advance(read_next(current).map_err(|error| error.to_string()));
}
(links, cursor.finish())
}
struct ObjectNameLayout {
body_offset: u64,
info_mask_offset: u64,
creator_info_size: Option<u64>,
name_info_size: u64,
name_offset: u64,
}
impl ObjectNameLayout {
fn name_info_address(&self, header: VirtAddr, info_mask: u8) -> Result<Option<VirtAddr>> {
const CREATOR_INFO_BIT: u8 = 0x01;
const NAME_INFO_BIT: u8 = 0x02;
if info_mask & NAME_INFO_BIT == 0 {
return Ok(None);
}
let creator_size = if info_mask & CREATOR_INFO_BIT != 0 {
self.creator_info_size
.ok_or_else(|| Error::StructNotFound("_OBJECT_HEADER_CREATOR_INFO".to_string()))?
} else {
0
};
let offset = self
.name_info_size
.checked_add(creator_size)
.ok_or_else(|| Error::DebugInfo("object name-info offset overflow".to_string()))?;
let address =
header.0.checked_sub(offset).map(VirtAddr).ok_or_else(|| {
Error::DebugInfo("object name-info address underflow".to_string())
})?;
Ok(Some(address))
}
}
fn select_object_header_candidate(
input: VirtAddr,
body_offset: u64,
body_candidate: Option<VirtAddr>,
direct_candidate: Option<VirtAddr>,
body_has_type: bool,
direct_has_type: bool,
) -> Option<(VirtAddr, VirtAddr, &'static str)> {
match (body_candidate, direct_candidate) {
(Some(header), Some(_)) if body_has_type => Some((header, input, "body")),
(Some(_), Some(header)) if direct_has_type => {
Some((header, header + body_offset, "header"))
}
(Some(_), Some(_)) => None,
(Some(header), None) => Some((header, input, "body")),
(None, Some(header)) => Some((header, header + body_offset, "header")),
(None, None) => None,
}
}
struct ObjectDirectoryLayout {
buckets_offset: u64,
bucket_count: u64,
chain_offset: u64,
object_offset: u64,
name_offset: Option<u64>,
}
pub struct StartupMessage {
pub build_number: Value<u16>,
pub base_address: VirtAddr,
pub loaded_module_list: VirtAddr,
}
pub struct ReloadReport {
pub previous_base_address: VirtAddr,
pub startup: Option<StartupMessage>,
pub symbol_report: Option<ModuleSymbolLoadReport>,
pub symbol_error: Option<String>,
}
pub struct AttachReport {
pub name: String,
pub symbol_report: ModuleSymbolLoadReport,
}
pub struct PteLevel {
pub name: String, pub address: VirtAddr,
pub value: PageTableEntry,
}
pub struct PteWalk {
pub address: VirtAddr,
pub dtb: Dtb,
pub pxe: PteLevel,
pub ppe: PteLevel,
pub pde: Option<PteLevel>,
pub pte: Option<PteLevel>,
}
impl Target {
pub fn guest(&self) -> Result<&Guest> {
self.guest.as_ref().ok_or(Error::NtoskrnlNotFound)
}
pub fn kernel_base(&self) -> Option<VirtAddr> {
self.guest
.as_ref()
.map(|g| g.ntoskrnl.base_address)
.or_else(|| {
self.triage_modules_cache.as_ref().and_then(|mods| {
mods.iter()
.find(|m| m.name.to_ascii_lowercase().contains("ntoskrnl"))
.map(|m| m.base_address)
})
})
}
pub fn kernel_dtb(&self) -> Dtb {
self.guest
.as_ref()
.map(|g| g.ntoskrnl.dtb())
.unwrap_or(DTB_IDENTITY)
}
pub fn new() -> Result<Self> {
Self::with_phys(Arc::new(PhysMem::live()?))
}
pub fn with_phys(phys: Arc<PhysMem>) -> Result<Self> {
let symbols = Arc::new(SymbolStore::new());
let guest = if let Some(info) = phys.dmp_info() {
let dtb = if info.is_triage {
DTB_IDENTITY
} else {
info.directory_table_base
};
match Guest::new_with_dtb(phys.clone(), symbols.clone(), dtb) {
Ok(g) => Some(g),
Err(Error::NtoskrnlNotFound) if info.is_triage => None,
Err(e) if info.is_triage => {
diagnostics::eprint_warning(format!(
"kernel discovery failed ({e}); continuing without kernel context"
));
None
}
Err(e) => return Err(e),
}
} else {
Some(Guest::new(phys.clone(), symbols.clone())?)
};
let triage_modules: Option<Vec<ModuleInfo>> = phys
.dmp_info()
.filter(|info| info.is_triage && !info.triage_drivers.is_empty())
.map(|info| {
info.triage_drivers
.iter()
.map(|d| d.to_module_info())
.collect()
});
if let Some(ref guest) = guest {
let _ = guest.load_all_kernel_module_symbols(&phys, &symbols);
} else if let Some(ref modules) = triage_modules {
let dtb = DTB_IDENTITY;
let _ = Guest::load_module_symbols(
&phys,
&symbols,
modules.clone(),
dtb,
false,
Arch::Amd64,
);
}
let triage_fallback = if guest.is_none() {
Some(WinObject::new(
phys.clone(),
symbols.clone(),
DTB_IDENTITY,
VirtAddr(0),
))
} else {
None
};
let triage_modules_cache = triage_modules;
Ok(Self {
phys,
symbols,
guest,
debugger_data: None,
current_process: None,
current_process_info: None,
triage_fallback,
triage_modules_cache,
context_dtb_override: None,
registers: None,
selected_frame: None,
windows_thread_selection: None,
user_vars: HashMap::new(),
results: Vec::new(),
results_origin: None,
last_exception_code: None,
})
}
pub fn with_remote_phys(
phys: Arc<PhysMem>,
kernel_dtb: Dtb,
kernel_base: VirtAddr,
arch: Arch,
) -> Result<Self> {
let symbols = Arc::new(SymbolStore::new());
let ntoskrnl =
WinObject::new_with_arch(phys.clone(), symbols.clone(), kernel_dtb, kernel_base, arch)
.load_symbols()?;
ntoskrnl.register_as_kernel();
let guest = Guest::from_kernel(ntoskrnl);
let _ = guest.load_all_kernel_module_symbols(&phys, &symbols);
Ok(Self {
phys,
symbols,
guest: Some(guest),
debugger_data: None,
current_process: None,
current_process_info: None,
triage_fallback: None,
triage_modules_cache: None,
context_dtb_override: None,
registers: None,
selected_frame: None,
windows_thread_selection: None,
user_vars: HashMap::new(),
results: Vec::new(),
results_origin: None,
last_exception_code: None,
})
}
pub fn current_process(&self) -> Result<&WinObject> {
match &self.current_process {
Some(p) => Ok(p),
None => match &self.guest {
Some(g) => Ok(&g.ntoskrnl),
None => self.triage_fallback.as_ref().ok_or(Error::NtoskrnlNotFound),
},
}
}
pub fn kernel_modules(&self) -> Result<Vec<ModuleInfo>> {
match self.guest() {
Ok(g) => g.kernel_modules().or_else(|e| {
if self.triage_modules_cache.is_some() {
self.triage_modules()
} else {
Err(e)
}
}),
Err(_) => self.triage_modules(),
}
}
pub fn kernel_modules_with_versions(&self) -> Result<Vec<ModuleInfo>> {
let mut mods = self.kernel_modules()?;
if let Ok(g) = self.guest() {
g.populate_kernel_module_versions(&mut mods);
}
Ok(mods)
}
pub fn modules(&self) -> Result<Vec<ModuleInfo>> {
match &self.current_process_info {
Some(process) => self.guest()?.process_modules(process),
None => self.kernel_modules(),
}
}
pub fn modules_with_versions(&self) -> Result<Vec<ModuleInfo>> {
let mut mods = self.modules()?;
if let Ok(g) = self.guest() {
match &self.current_process_info {
Some(info) => g.populate_process_module_versions(&mut mods, info),
None => g.populate_kernel_module_versions(&mut mods),
}
}
Ok(mods)
}
fn triage_modules(&self) -> Result<Vec<ModuleInfo>> {
self.triage_modules_cache
.clone()
.ok_or(Error::NtoskrnlNotFound)
}
pub fn search(&self, start: VirtAddr, pattern: &[u8], length: usize) -> Result<Vec<u64>> {
if pattern.is_empty() || pattern.len() > length {
return Ok(Vec::new());
}
let mut buf = vec![0u8; length];
self.current_process()?
.memory()
.read_bytes(start, &mut buf)?;
Ok((0..=buf.len() - pattern.len())
.filter(|&i| &buf[i..i + pattern.len()] == pattern)
.map(|i| start.0.wrapping_add(i as u64))
.collect())
}
pub fn describe_search_matches(
&self,
start: VirtAddr,
matches: &[u64],
) -> Result<Vec<MemorySearchMatch>> {
matches
.iter()
.copied()
.map(|addr| {
let address = VirtAddr(addr);
Ok(MemorySearchMatch {
address,
offset: addr.wrapping_sub(start.0),
symbol: self.closest_symbol_current_context(address),
description: self.describe_address(address)?,
})
})
.collect()
}
pub fn search_details(
&self,
start: VirtAddr,
pattern: &[u8],
length: usize,
) -> Result<Vec<MemorySearchMatch>> {
let matches = self.search(start, pattern, length)?;
self.describe_search_matches(start, &matches)
}
pub fn walk_list(&self, head: VirtAddr, link_offset: u64) -> Result<Vec<u64>> {
const MAX: usize = 1000;
let mem = self.current_process()?.memory();
let mut cursor = ListCursor::new(head, MAX);
cursor.advance(Ok(mem.read::<VirtAddr>(head)?));
let mut out = Vec::new();
while let Some(current) = cursor.next() {
out.push(current.0.wrapping_sub(link_offset));
cursor.advance(
mem.read::<VirtAddr>(current)
.map_err(|error| error.to_string()),
);
}
Ok(out)
}
pub fn read_unicode_string(&self, addr: VirtAddr) -> Result<String> {
let proc = self.current_process()?;
match proc.types().struct_at("_UNICODE_STRING", addr) {
Ok(s) => s.read_unicode_string(),
Err(Error::ExpectedSymbols) => {
let dtb = self.kernel_dtb();
let ti = self
.symbols
.find_type_across_modules(dtb, "_UNICODE_STRING")
.ok_or(Error::ExpectedSymbols)?;
let mem = proc.memory();
let len_off = ti.field_offset("Length")?;
let buf_off = ti.field_offset("Buffer")?;
let length: u16 = mem.read(addr + len_off)?;
let buffer: VirtAddr = mem.read(addr + buf_off)?;
if length == 0 || buffer.is_zero() {
return Ok(String::new());
}
let mut buf = vec![0u8; length as usize];
mem.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))
}
Err(e) => Err(e),
}
}
pub fn read_c_string(&self, addr: VirtAddr, max_len: usize) -> Result<String> {
let mem = self.current_process()?.memory();
let mut bytes = Vec::new();
while bytes.len() < max_len {
let cur = addr + bytes.len() as u64;
let to_page_end = PAGE_SIZE - cur.page_offset() as usize;
let chunk = to_page_end.min(max_len - bytes.len());
let mut buf = vec![0u8; chunk];
match mem.read_bytes(cur, &mut buf) {
Ok(()) => {}
Err(_) if !bytes.is_empty() => break,
Err(e) => return Err(e),
}
if let Some(nul) = buf.iter().position(|&b| b == 0) {
bytes.extend_from_slice(&buf[..nul]);
return Ok(String::from_utf8_lossy(&bytes).into_owned());
}
bytes.extend_from_slice(&buf);
}
Ok(String::from_utf8_lossy(&bytes).into_owned())
}
pub fn attach(&mut self, pid: u64) -> Result<AttachReport> {
let processes = self.guest()?.enumerate_processes()?;
let process_info = processes
.iter()
.find(|p| p.pid == pid)
.ok_or(Error::ProcessNotFound(pid))?
.clone();
self.attach_process_info(process_info)
}
pub fn attach_process_info(&mut self, process_info: ProcessInfo) -> Result<AttachReport> {
let name = process_info.name.clone();
let guest = self.guest.as_ref().ok_or(Error::NtoskrnlNotFound)?;
let symbol_report =
guest.load_all_process_module_symbols(&self.phys, &self.symbols, &process_info);
let winobj = guest.winobj_from_process_info(&process_info)?;
self.current_process = Some(winobj);
self.current_process_info = Some(process_info);
self.selected_frame = None;
self.clear_context_dtb_override();
self.clear_current_windows_thread_context();
Ok(AttachReport {
name,
symbol_report: symbol_report?,
})
}
pub fn detach(&mut self) {
self.selected_frame = None;
self.clear_context_dtb_override();
self.clear_current_windows_thread_context();
self.current_process = None;
self.current_process_info = None;
}
pub fn set_context_dtb_override(&mut self, dtb: Dtb) {
self.context_dtb_override = Some(Self::normalize_cr3(dtb));
}
pub fn clear_context_dtb_override(&mut self) {
self.context_dtb_override = None;
}
pub fn normalize_cr3(cr3: u64) -> Dtb {
cr3 & Arch::Amd64.dtb_page_mask()
}
pub fn set_current_windows_thread_context(&mut self, thread: ThreadInfo) {
self.selected_frame = None;
self.windows_thread_selection = Some(thread);
}
pub fn set_parked_windows_thread(&mut self, thread: ThreadInfo) {
self.selected_frame = None;
self.windows_thread_selection = Some(thread);
self.registers = None;
}
pub fn clear_current_windows_thread_context(&mut self) {
self.windows_thread_selection = None;
}
pub fn thread_process_dtb(&self, thread: &ThreadInfo) -> Option<Dtb> {
if thread.pid != Some(0)
&& let Some(eprocess) = thread.eprocess
&& let Some(process) = self
.guest
.as_ref()
.and_then(|guest| guest.process_at(eprocess).ok())
&& process.dtb != 0
{
return Some(process.dtb);
}
let processes = self
.guest
.as_ref()
.and_then(|guest| guest.enumerate_processes().ok())
.unwrap_or_default();
select_thread_process_dtb(
thread,
self.current_process_info.as_ref(),
&processes,
self.kernel_dtb(),
)
}
pub fn process_for_cr3(&self, cr3_masked: u64) -> Option<ProcessInfo> {
let guest = self.guest.as_ref()?;
if let Some(eprocess) = self
.windows_thread_selection
.as_ref()
.and_then(|thread| thread.eprocess)
&& let Ok(process) = guest.process_at(eprocess)
&& (process.dtb & self.arch().dtb_page_mask()) == cr3_masked
{
return Some(process);
}
guest
.enumerate_processes()
.ok()?
.into_iter()
.find(|process| (process.dtb & self.arch().dtb_page_mask()) == cr3_masked)
}
pub fn current_thread_pseudo_register(&self, name: &str) -> Option<u64> {
self.current_thread_pseudo_register_slot(name).flatten()
}
fn current_thread_pseudo_register_slot(&self, name: &str) -> Option<Option<u64>> {
let thread = self.windows_thread_selection.as_ref()?;
thread.pseudo_register(name)
}
pub fn register_value(&self, name: &str) -> Option<u64> {
lookup_register(self.registers.as_ref()?, name)
}
pub fn builtin_variable_value(&self, name: &str) -> Option<u64> {
self.builtin_variable(name).flatten()
}
pub fn builtin_variable(&self, name: &str) -> Option<Option<u64>> {
let name = name.trim_start_matches('$').to_ascii_lowercase();
let thread = self.current_thread_pseudo_register_slot(&name);
if let Some(Some(value)) = thread {
return Some(Some(value));
}
let target = match name.as_str() {
"dtb" => Some(self.current_dtb()),
"ip" => self.register_value(self.instruction_pointer_register()),
"ra" => self.scope_return_address(),
"csp" => self.register_value(self.stack_pointer_register()),
"retreg" => self.register_value(self.return_value_register()),
"ptrsize" => Some(8),
"pagesize" => Some(
self.debugger_data
.as_ref()
.and_then(|data| data.mm_page_size())
.map(|page_size| page_size.value)
.filter(|page_size| *page_size != 0)
.unwrap_or(PAGE_SIZE as u64),
),
"tpid" => self.current_thread_pseudo_register("pid"),
"frame" => Some(
self.selected_frame
.as_ref()
.map(|frame| frame.index as u64)
.unwrap_or(0),
),
"scopeip" => self
.selected_frame
.as_ref()
.map(|frame| frame.ip)
.or_else(|| self.register_value(self.instruction_pointer_register())),
"exp" => self.results.first().copied(),
"exr_code" => self.last_exception_code.map(u64::from),
"bug_code" => self.bugcheck_data(0),
"bug_param1" => self.bugcheck_data(1),
"bug_param2" => self.bugcheck_data(2),
"bug_param3" => self.bugcheck_data(3),
"bug_param4" => self.bugcheck_data(4),
"ntbase" | "kernelbase" => self.guest.as_ref().map(|g| g.ntoskrnl.base_address.0),
"processbase" | "imagebase" => self.current_process.as_ref().map(|p| p.base_address.0),
"processdtb" => self.current_process_info.as_ref().map(|p| p.dtb),
"attachedeprocess" | "attachedprocess" => {
self.current_process_info.as_ref().map(|p| p.eprocess_va.0)
}
"attachedpid" => self.current_process_info.as_ref().map(|p| p.pid),
"eprocess" | "process" => self.current_process_info.as_ref().map(|p| p.eprocess_va.0),
"peb" => self.current_process_peb(),
"pid" => self.current_process_info.as_ref().map(|p| p.pid),
_ => {
return match Self::user_pseudo_register_slot(&name) {
Some(_) => Some(Some(0)),
None => thread.map(|_| None),
};
}
};
Some(target)
}
pub fn stack_pointer_register(&self) -> &'static str {
match self.arch() {
Arch::Amd64 => "rsp",
Arch::Arm64 => "sp",
}
}
fn scope_return_address(&self) -> Option<u64> {
let registers = self
.selected_frame
.as_ref()
.map(|frame| &frame.registers)
.or(self.registers.as_ref())?;
return_address_for_register_values(self, registers)
}
fn current_process_peb(&self) -> Option<u64> {
let eprocess_va = self.current_process_info.as_ref()?.eprocess_va;
let peb: VirtAddr = self
.guest()
.ok()?
.ntoskrnl
.types_in(self.kernel_dtb())
.struct_at("_EPROCESS", eprocess_va)
.ok()?
.read_field("Peb")
.ok()?;
(!peb.is_zero()).then_some(peb.0)
}
fn bugcheck_data(&self, index: u64) -> Option<u64> {
let address = self
.symbols
.find_symbol_across_modules(self.kernel_dtb(), "nt!KiBugCheckData")
.ok()
.flatten()?;
self.address_space(self.kernel_dtb())
.read::<u64>(address + index * 8)
.ok()
}
pub fn instruction_pointer_register(&self) -> &'static str {
match self.arch() {
Arch::Amd64 => "rip",
Arch::Arm64 => "pc",
}
}
fn return_value_register(&self) -> &'static str {
match self.arch() {
Arch::Amd64 => "rax",
Arch::Arm64 => "x0",
}
}
fn user_pseudo_register_slot(name: &str) -> Option<u8> {
let digits = name.strip_prefix('t')?;
if digits.is_empty() || (digits.len() > 1 && digits.starts_with('0')) {
return None;
}
digits.parse::<u8>().ok().filter(|slot| *slot <= 19)
}
pub fn builtin_variables(&self) -> Vec<BuiltinVar> {
let mut vars = vec![BuiltinVar {
name: "dtb",
value: self.current_dtb(),
source: "current address space",
}];
for (name, source) in [
("ptrsize", "target pointer size"),
("pagesize", "target page size"),
("frame", "selected frame index"),
("csp", "call stack pointer"),
("retreg", "return value register"),
("scopeip", "local context instruction pointer"),
("ip", "instruction pointer"),
("exp", "last expression result"),
("exr_code", "last exception code"),
("tpid", "current Windows PID"),
] {
if let Some(value) = self.builtin_variable_value(name) {
vars.push(BuiltinVar {
name,
value,
source,
});
}
}
if let Some(ref guest) = self.guest {
vars.push(BuiltinVar {
name: "ntbase",
value: guest.ntoskrnl.base_address.0,
source: "kernel base",
});
}
if let Some(process) = &self.current_process_info {
vars.extend([
BuiltinVar {
name: "processbase",
value: self
.current_process
.as_ref()
.map(|p| p.base_address.0)
.unwrap_or(0),
source: "attached process image base",
},
BuiltinVar {
name: "processdtb",
value: process.dtb,
source: "attached process DTB",
},
BuiltinVar {
name: "attachedeprocess",
value: process.eprocess_va.0,
source: "attached process EPROCESS",
},
BuiltinVar {
name: "attachedpid",
value: process.pid,
source: "attached process PID",
},
]);
if self.windows_thread_selection.is_none() {
vars.extend([
BuiltinVar {
name: "eprocess",
value: process.eprocess_va.0,
source: "attached process EPROCESS",
},
BuiltinVar {
name: "pid",
value: process.pid,
source: "attached process PID",
},
]);
}
}
if let Some(thread) = &self.windows_thread_selection {
let mut push = |name, value: Option<u64>, source| {
if let Some(value) = value {
vars.push(BuiltinVar {
name,
value,
source,
});
}
};
push("thread", Some(thread.ethread.0), "current Windows ETHREAD");
push("ethread", Some(thread.ethread.0), "current Windows ETHREAD");
push("kthread", Some(thread.kthread.0), "current Windows KTHREAD");
push("tid", thread.tid, "current Windows TID");
push("pid", thread.pid, "current Windows PID");
push(
"eprocess",
thread.eprocess.map(|addr| addr.0),
"current thread EPROCESS",
);
push(
"process",
thread.eprocess.map(|addr| addr.0),
"current thread EPROCESS",
);
push("teb", thread.teb.map(|addr| addr.0), "current thread TEB");
push(
"threadstart",
thread.start_address.map(|addr| addr.0),
"current thread start address",
);
push(
"win32start",
thread.win32_start_address.map(|addr| addr.0),
"current thread Win32 start address",
);
push(
"kernelstack",
thread.kernel_stack.map(|addr| addr.0),
"current thread kernel stack",
);
push(
"stackbase",
thread.stack_base.map(|addr| addr.0),
"current thread stack base",
);
push(
"stacklimit",
thread.stack_limit.map(|addr| addr.0),
"current thread stack limit",
);
push(
"trapframe",
thread.trap_frame.map(|addr| addr.0),
"current thread trap frame",
);
}
vars
}
pub fn set_results(&mut self, results: Vec<u64>, origin: impl Into<String>) {
self.results = results;
self.results_origin = Some(origin.into());
}
pub fn debugger_data(&self) -> Option<&DebuggerDataBlock> {
self.debugger_data.as_ref()
}
pub fn refresh_debugger_data(&mut self, transport_hint: Option<DebuggerDataCandidate>) {
let mut candidates = Vec::with_capacity(4);
if let Some(candidate) = transport_hint {
candidates.push(candidate);
}
if let Some(guest) = &self.guest {
for symbol in ["KdDebuggerDataBlock", "KdDebuggerDataListHead"] {
if let Ok(Some(address)) = self
.symbols
.find_symbol_across_modules(guest.ntoskrnl.dtb(), &format!("nt!{symbol}"))
{
candidates.push(DebuggerDataCandidate {
address,
source: MetadataSource::KernelSymbol,
});
}
}
}
if let Some(address) = self
.phys
.dmp_info()
.and_then(|info| info.debugger_data_block)
{
candidates.push(DebuggerDataCandidate {
address: VirtAddr(address),
source: MetadataSource::DumpHeader,
});
}
let expected_kernel_base = self.kernel_base();
let debugger_data = {
let memory = self.context_memory();
locate_debugger_data_block(&memory, candidates, expected_kernel_base)
};
self.debugger_data = debugger_data;
}
pub fn reload_guest_with_kernel_base_hint(
&mut self,
kernel_base_hint: Option<VirtAddr>,
) -> Result<ReloadReport> {
self.debugger_data = None;
let previous_base_address = self
.guest
.as_ref()
.map(|g| g.ntoskrnl.base_address)
.unwrap_or(VirtAddr(0));
let previous_dtb = self.guest.as_ref().map(|g| g.ntoskrnl.dtb());
let guest = Guest::new_with_kernel_base_hint(
self.phys.clone(),
self.symbols.clone(),
kernel_base_hint,
)?;
let new_dtb = guest.ntoskrnl.dtb();
if let Some(prev_dtb) = previous_dtb {
self.symbols.clear_modules_for_dtb(prev_dtb);
}
self.symbols.clear_modules_for_dtb(new_dtb);
let (symbol_report, symbol_error) =
match guest.load_all_kernel_module_symbols(&self.phys, &self.symbols) {
Ok(report) => (Some(report), None),
Err(e) => (None, Some(e.to_string())),
};
self.guest = Some(guest);
self.triage_fallback = None;
self.triage_modules_cache = None;
self.detach();
self.clear_context_dtb_override();
self.registers = None;
self.clear_current_windows_thread_context();
let startup = self.startup_message_data().ok();
Ok(ReloadReport {
previous_base_address,
startup,
symbol_report,
symbol_error,
})
}
pub fn current_kernel_mapping_is_valid(&self) -> bool {
if self.triage_fallback.is_some() {
return true;
}
if self.phys.dmp_info().is_some_and(|i| i.is_triage) {
return true;
}
let Some(ref guest) = self.guest else {
return false;
};
let memory = guest.ntoskrnl.memory();
let mut signature = [0u8; 2];
memory
.read_bytes(guest.ntoskrnl.base_address, &mut signature)
.is_ok_and(|()| signature == *b"MZ")
}
pub fn rediscovered_kernel_identity_changed(&self) -> Result<bool> {
let current_guest = self.guest()?;
let guest = Guest::new(self.phys.clone(), self.symbols.clone())?;
Ok(
guest.ntoskrnl.base_address != current_guest.ntoskrnl.base_address
|| guest.ntoskrnl.dtb() != current_guest.ntoskrnl.dtb(),
)
}
pub fn refresh_kernel_module_symbols(&self) -> Result<ModuleSymbolLoadReport> {
self.guest
.as_ref()
.ok_or(Error::NtoskrnlNotFound)?
.load_missing_kernel_module_symbols(&self.phys, &self.symbols)
}
pub fn reload_module_symbols(
&self,
module_name: Option<&str>,
) -> Result<ModuleSymbolLoadReport> {
let mut modules = self.modules()?;
if let Some(name) = module_name {
modules.retain(|module| {
module.short_name.eq_ignore_ascii_case(name)
|| module
.name
.rsplit(['\\', '/'])
.next()
.is_some_and(|image| image.eq_ignore_ascii_case(name))
});
if modules.is_empty() {
return Err(Error::DebugInfo(format!("module not found: {name}")));
}
}
let dtb = self
.current_process_info
.as_ref()
.map(|process| process.dtb)
.unwrap_or_else(|| self.kernel_dtb());
let bases = modules
.iter()
.map(|module| module.base_address)
.collect::<Vec<_>>();
self.symbols.invalidate_modules(dtb, &bases);
match self.guest.as_ref() {
Some(guest) => guest.load_symbols_for_modules(&self.phys, &self.symbols, modules, dtb),
None => Guest::load_module_symbols(
&self.phys,
&self.symbols,
modules,
dtb,
false,
self.arch(),
),
}
}
pub fn current_dtb(&self) -> Dtb {
match &self.current_process {
Some(p) => p.dtb(),
None => self
.context_dtb_override
.unwrap_or_else(|| self.kernel_dtb()),
}
}
pub fn context_memory(&self) -> AddressSpace<'_, PhysMem> {
self.address_space(self.current_dtb())
}
pub fn arch(&self) -> Arch {
self.guest
.as_ref()
.map(|g| g.ntoskrnl.arch())
.unwrap_or(Arch::Amd64)
}
pub fn address_space(&self, dtb: Dtb) -> AddressSpace<'_, PhysMem> {
match self.arch() {
Arch::Amd64 => AddressSpace::new(&self.phys, dtb),
Arch::Arm64 => AddressSpace::new_arm64(&self.phys, dtb, self.kernel_dtb()),
}
}
pub fn kernel_address_space(&self) -> AddressSpace<'_, PhysMem> {
self.address_space(self.kernel_dtb())
}
pub fn symbol_candidates(&self, name: &str) -> Vec<SymbolCandidate> {
self.symbols
.find_symbol_candidates(self.current_dtb(), name)
}
pub fn search_symbols(&self, query: &str, limit: usize) -> Vec<SymbolSearchMatch> {
let dtb = self.current_dtb();
let names: Vec<String> = match query.split_once('!') {
Some((module, query)) => self
.symbols
.search_symbols_in_module(dtb, module, query, limit)
.into_iter()
.map(|name| format!("{module}!{name}"))
.collect(),
None => self.current_symbol_index().search(query, limit),
};
names
.into_iter()
.map(|qualified| {
let locations: HashSet<(String, u64)> = self
.symbol_candidates(&qualified)
.into_iter()
.map(|candidate| (candidate.module, candidate.address.0))
.collect();
let (resolved_module, address) = if locations.len() == 1 {
let (module, address) = locations.into_iter().next().unwrap();
(Some(module), Some(VirtAddr(address)))
} else {
(None, None)
};
let name = qualified
.rsplit_once('!')
.map_or(qualified.as_str(), |(_, bare)| bare)
.to_string();
SymbolSearchMatch {
name,
address,
module: resolved_module,
}
})
.collect()
}
pub fn nearest_symbol_current_context(
&self,
address: VirtAddr,
) -> Option<(String, String, u32)> {
self.symbols
.find_closest_symbol_for_address(self.current_dtb(), address)
}
pub fn closest_symbol_current_context(&self, address: VirtAddr) -> Option<String> {
self.nearest_symbol_current_context(address)
.map(|(module, name, offset)| format_symbol_with_offset(&module, &name, offset))
}
pub fn source_location(&self, address: VirtAddr) -> Option<SourceLocation> {
self.symbols.source_location(self.current_dtb(), address)
}
pub fn source_line_extent(&self, address: VirtAddr) -> Option<SourceLineExtent> {
self.symbols.source_line_extent(self.current_dtb(), address)
}
pub fn post_prologue_address(&self, dtb: Dtb, address: VirtAddr) -> Option<VirtAddr> {
let end = self.symbols.source_line_extent(dtb, address)?.end?;
let function = self.symbols.find_closest_symbol_for_address(dtb, address)?;
let at_end = self.symbols.find_closest_symbol_for_address(dtb, end)?;
(function.0 == at_end.0
&& function.1 == at_end.1
&& self.symbols.source_line_extent(dtb, end).is_some())
.then_some(end)
}
pub fn source_addresses(&self, file: &str, line: u32) -> Vec<VirtAddr> {
self.symbols
.source_addresses(self.current_dtb(), file, line)
}
pub fn procedure_locals(&self, address: VirtAddr) -> Result<Option<Arc<Vec<ProcedureLocal>>>> {
self.symbols.procedure_locals(self.current_dtb(), address)
}
pub fn procedure_local_address(&self, local: &ProcedureLocal) -> Option<u64> {
let registers = self.registers.as_ref()?;
match &local.location {
LocalVariableLocation::Register { .. } | LocalVariableLocation::Unavailable { .. } => {
None
}
LocalVariableLocation::RegisterRelative { register, offset } => {
let base = lookup_register(registers, register)?;
Some(base.wrapping_add_signed(i64::from(*offset)))
}
LocalVariableLocation::FrameRelative { offset } => {
let frame_base = self
.selected_frame
.as_ref()
.and_then(|frame| frame.frame_base)
.or_else(|| frame_base_for_register_values(self, registers))
.or_else(|| lookup_register(registers, self.stack_pointer_register()))?;
Some(frame_base.wrapping_add_signed(i64::from(*offset)))
}
}
}
pub fn resolve_procedure_local_value(
&self,
address: VirtAddr,
local: &ProcedureLocal,
) -> Option<u64> {
if self
.register_value("rip")
.is_none_or(|rip| rip != address.0)
{
return None;
}
let size = usize::try_from(local.byte_size?).ok()?;
if size == 0 || size > 8 {
return None;
}
let registers = self.registers.as_ref()?;
match &local.location {
LocalVariableLocation::Register { register } => {
let value = lookup_register(registers, register)?;
Some(if size == 8 {
value
} else {
value & ((1u64 << (size * 8)) - 1)
})
}
LocalVariableLocation::RegisterRelative { .. }
| LocalVariableLocation::FrameRelative { .. } => {
let address = VirtAddr(self.procedure_local_address(local)?);
let mut bytes = [0u8; 8];
self.context_memory()
.read_bytes(address, &mut bytes[..size])
.ok()?;
Some(u64::from_le_bytes(bytes))
}
LocalVariableLocation::Unavailable { .. } => None,
}
}
fn kernel_struct(&self, name: &str, base: VirtAddr) -> Result<StructRef<'_>> {
self.guest()?.ntoskrnl.types().struct_at(name, base)
}
fn read_kernel_unicode_string(&self, addr: VirtAddr) -> Result<String> {
self.kernel_struct("_UNICODE_STRING", addr)?
.read_unicode_string()
}
fn object_name_layout(&self) -> Result<ObjectNameLayout> {
let dtb = self.guest()?.ntoskrnl.dtb();
let header_type = self
.symbols
.find_type_across_modules(dtb, "_OBJECT_HEADER")
.ok_or_else(|| Error::StructNotFound("_OBJECT_HEADER".to_string()))?;
let creator_info_size = self
.symbols
.find_type_across_modules(dtb, "_OBJECT_HEADER_CREATOR_INFO")
.map(|ty| ty.size as u64);
let name_info_type = self
.symbols
.find_type_across_modules(dtb, "_OBJECT_HEADER_NAME_INFO")
.ok_or_else(|| Error::StructNotFound("_OBJECT_HEADER_NAME_INFO".to_string()))?;
Ok(ObjectNameLayout {
body_offset: header_type.field_offset("Body")?,
info_mask_offset: header_type.field_offset("InfoMask")?,
creator_info_size,
name_info_size: name_info_type.size as u64,
name_offset: name_info_type.field_offset("Name")?,
})
}
fn read_kernel_object_name(
&self,
object: VirtAddr,
object_name: &ObjectNameLayout,
) -> Result<Option<String>> {
let memory = self.guest()?.ntoskrnl.memory();
let header = object - object_name.body_offset;
let info_mask: u8 = memory.read(header + object_name.info_mask_offset)?;
let Some(name_info) = object_name.name_info_address(header, info_mask)? else {
return Ok(None);
};
Ok(Some(self.read_kernel_unicode_string(
name_info + object_name.name_offset,
)?))
}
fn object_directory_layout(&self) -> Result<ObjectDirectoryLayout> {
let dtb = self.guest()?.ntoskrnl.dtb();
let dir_type = self
.symbols
.find_type_across_modules(dtb, "_OBJECT_DIRECTORY")
.ok_or_else(|| Error::StructNotFound("_OBJECT_DIRECTORY".to_string()))?;
let entry_type = self
.symbols
.find_type_across_modules(dtb, "_OBJECT_DIRECTORY_ENTRY")
.ok_or_else(|| Error::StructNotFound("_OBJECT_DIRECTORY_ENTRY".to_string()))?;
let buckets = dir_type
.fields
.get("HashBuckets")
.ok_or_else(|| Error::FieldNotFound("HashBuckets".to_string()))?;
Ok(ObjectDirectoryLayout {
buckets_offset: buckets.offset as u64,
bucket_count: (buckets.size / 8).max(1),
chain_offset: entry_type.field_offset("ChainLink")?,
object_offset: entry_type.field_offset("Object")?,
name_offset: entry_type.fields.get("Name").map(|f| f.offset as u64),
})
}
fn enumerate_object_directory(
&self,
directory: VirtAddr,
dir: &ObjectDirectoryLayout,
object_name: &ObjectNameLayout,
) -> Result<Vec<(String, VirtAddr)>> {
let memory = self.guest()?.ntoskrnl.memory();
let mut out = Vec::new();
for bucket in 0..dir.bucket_count {
let mut entry: VirtAddr = memory.read(directory + dir.buckets_offset + bucket * 8)?;
for _ in 0..4096 {
if entry.is_zero() {
break;
}
let object: VirtAddr = memory.read(entry + dir.object_offset)?;
if !object.is_zero() {
let name = match dir.name_offset {
Some(offset) => Some(self.read_kernel_unicode_string(entry + offset)?),
None => self.read_kernel_object_name(object, object_name)?,
};
if let Some(name) = name
&& !name.is_empty()
{
out.push((name, object));
}
}
entry = memory.read(entry + dir.chain_offset)?;
}
}
out.sort_by(|a, b| a.0.cmp(&b.0));
Ok(out)
}
pub fn matching_processes(&self, filter: Option<&str>) -> Result<Vec<ProcessInfo>> {
let procs = match self.guest().and_then(|g| g.enumerate_processes()) {
Ok(p) if !p.is_empty() => p,
Ok(_) => self.triage_process_list().unwrap_or_default(),
Err(Error::NtoskrnlNotFound) => self.triage_process_list()?,
Err(e) => return Err(e),
};
Ok(match filter {
None => procs,
Some(f) => procs
.into_iter()
.filter(|p| process_matches(p, f))
.collect(),
})
}
fn triage_process_list(&self) -> Result<Vec<ProcessInfo>> {
let info = self.phys.dmp_info().ok_or(Error::NtoskrnlNotFound)?;
let proc_snap = info
.triage_process_snapshot
.as_deref()
.ok_or(Error::NtoskrnlNotFound)?;
let dtb = self.kernel_dtb();
let eprocess_layout = self
.symbols
.find_type_across_modules(dtb, "_EPROCESS")
.ok_or(Error::ExpectedSymbols)?;
let pid = eprocess_layout
.field_offset("UniqueProcessId")
.ok()
.and_then(|off| {
let off = off as usize;
if off + 8 <= proc_snap.len() {
proc_snap[off..off + 8]
.try_into()
.ok()
.map(u64::from_le_bytes)
} else {
None
}
})
.unwrap_or(0);
let name = eprocess_layout
.field_offset("ImageFileName")
.ok()
.and_then(|off| {
let off = off as usize;
if off + 15 <= proc_snap.len() {
let buf = &proc_snap[off..off + 15];
let end = buf.iter().position(|&c| c == 0).unwrap_or(15);
let s = String::from_utf8_lossy(&buf[..end]).to_string();
if s.is_empty() { None } else { Some(s) }
} else {
None
}
})
.unwrap_or_else(|| "<unknown>".to_string());
Ok(vec![ProcessInfo {
pid,
name,
dtb: self.kernel_dtb(),
eprocess_va: VirtAddr(0),
}])
}
pub fn enumerate_driver_objects(&self) -> Result<Vec<DriverObjectInfo>> {
let guest = self.guest()?;
guest.memoized_drivers(|| self.walk_driver_objects(guest))
}
fn walk_driver_objects(&self, guest: &Guest) -> Result<Vec<DriverObjectInfo>> {
let memory = guest.ntoskrnl.memory();
let object_name = self.object_name_layout()?;
let dir = self.object_directory_layout()?;
let root_ptr = guest.ntoskrnl.symbol("ObpRootDirectoryObject")?.address();
let root: VirtAddr = memory.read(root_ptr)?;
let driver_dir = self
.enumerate_object_directory(root, &dir, &object_name)?
.into_iter()
.find(|(name, _)| name.eq_ignore_ascii_case("Driver"))
.map(|(_, object)| object)
.ok_or_else(|| Error::DebugInfo("\\Driver object directory not found".to_string()))?;
let mut drivers = Vec::new();
for (name, object) in self.enumerate_object_directory(driver_dir, &dir, &object_name)? {
let driver = self.kernel_struct("_DRIVER_OBJECT", object)?;
drivers.push(DriverObjectInfo {
name: format!("\\Driver\\{name}"),
object,
driver_start: driver.read_field("DriverStart")?,
driver_size: driver.read_field::<u32>("DriverSize")? as u64,
device_object: driver.read_field("DeviceObject")?,
driver_unload: driver.read_field("DriverUnload")?,
});
}
Ok(drivers)
}
pub fn inspect_irp(&self, address: VirtAddr) -> Result<IrpInfo> {
let irp = self.kernel_struct("_IRP", address)?;
let io_status = irp
.embedded("IoStatus")
.and_then(|s| s.read_field::<u32>("Status"))
.ok();
let current_location: u8 = irp.read_field("CurrentLocation")?;
let current_stack = self
.read_current_io_stack(address, current_location)
.ok()
.flatten();
Ok(IrpInfo {
address,
irp_type: irp.read_field("Type")?,
size: irp.read_field("Size")?,
stack_count: irp.read_field("StackCount")?,
current_location,
pending_returned: irp.read_field::<u8>("PendingReturned")? != 0,
requestor_mode: irp.read_field("RequestorMode")?,
io_status,
user_event: irp.read_field("UserEvent")?,
user_buffer: irp.read_field("UserBuffer")?,
mdl_address: irp.read_field("MdlAddress")?,
thread: irp_thread(&irp),
current_stack,
})
}
fn read_current_io_stack(
&self,
irp: VirtAddr,
current_location: u8,
) -> Result<Option<IoStackLocationInfo>> {
if current_location == 0 || current_location as u64 > 0x40 {
return Ok(None);
}
let types = self.guest()?.ntoskrnl.types();
let irp_size = types.layout("_IRP")?.size as u64;
let stack_size = types.layout("_IO_STACK_LOCATION")?.size as u64;
let addr = irp + irp_size + (current_location as u64 - 1) * stack_size;
let Ok(ios) = self.kernel_struct("_IO_STACK_LOCATION", addr) else {
return Ok(None);
};
Ok(Some(IoStackLocationInfo {
address: addr,
major_function: ios.read_field("MajorFunction")?,
minor_function: ios.read_field("MinorFunction")?,
device_object: ios.read_field("DeviceObject")?,
file_object: ios.read_field("FileObject")?,
completion_routine: ios.read_field("CompletionRoutine")?,
context: ios.read_field("Context")?,
}))
}
fn read_device_link(&self, device: VirtAddr) -> Result<DeviceLink> {
let d = self.kernel_struct("_DEVICE_OBJECT", device)?;
Ok(DeviceLink {
device,
device_type: d.read_field("DeviceType")?,
flags: d.read_field("Flags")?,
characteristics: d.read_field("Characteristics")?,
attached: d.read_field("AttachedDevice")?,
next: d.read_field("NextDevice")?,
})
}
pub fn inspect_driver_object(&self, addr: VirtAddr) -> Result<DriverObjectDetail> {
let guest = self.guest()?;
let mem = guest.ntoskrnl.memory();
let layout = guest.ntoskrnl.types().layout("_DRIVER_OBJECT")?;
let size_off = layout.field_offset("Size")?;
let mf_off = layout.field_offset("MajorFunction")?;
let name_off = layout.field_offset("DriverName")?;
let min_size = mf_off + 28 * 8;
let valid = |a: VirtAddr| -> bool {
let ty: u16 = match mem.read(a) {
Ok(v) => v,
Err(_) => return false,
};
let size: u16 = match mem.read(a + size_off) {
Ok(v) => v,
Err(_) => return false,
};
ty == 4 && size as u64 >= min_size
};
let object = if valid(addr) {
addr
} else {
let ptr: VirtAddr = mem.read(addr)?;
if !ptr.is_zero() && valid(ptr) {
ptr
} else {
return Err(Error::DebugInfo(format!(
"{:#x} is not a _DRIVER_OBJECT or a pointer to one",
addr.0
)));
}
};
let drv = self.kernel_struct("_DRIVER_OBJECT", object)?;
let name = self
.read_kernel_unicode_string(object + name_off)
.ok()
.filter(|s| !s.is_empty());
let mut device_chain = Vec::new();
let mut seen = Vec::new();
let mut cur: VirtAddr = drv.read_field("DeviceObject")?;
for _ in 0..128 {
if cur.is_zero() || seen.contains(&cur.0) {
break;
}
seen.push(cur.0);
let Ok(link) = self.read_device_link(cur) else {
break;
};
let next = link.next;
device_chain.push(link);
if next.is_zero() {
break;
}
cur = next;
}
let mut dispatch = Vec::with_capacity(28);
for i in 0..28u64 {
dispatch.push(
mem.read::<VirtAddr>(object + mf_off + i * 8)
.unwrap_or(VirtAddr(0)),
);
}
Ok(DriverObjectDetail {
object,
via_pointer: object != addr,
name,
driver_start: drv.read_field("DriverStart")?,
driver_size: drv.read_field::<u32>("DriverSize")? as u64,
driver_section: drv.read_field("DriverSection")?,
driver_unload: drv.read_field("DriverUnload")?,
device_chain,
dispatch,
})
}
pub fn inspect_device_object(&self, addr: VirtAddr) -> Result<DeviceObjectDetail> {
let guest = self.guest()?;
let mem = guest.ntoskrnl.memory();
let layout = guest.ntoskrnl.types().layout("_DEVICE_OBJECT")?;
let size_off = layout.field_offset("Size")?;
let min_size = layout.size as u64;
let valid = |a: VirtAddr| -> bool {
let ty: u16 = match mem.read(a) {
Ok(v) => v,
Err(_) => return false,
};
let size: u16 = match mem.read(a + size_off) {
Ok(v) => v,
Err(_) => return false,
};
ty == 3 && size as u64 >= min_size
};
let object = if valid(addr) {
addr
} else {
let ptr: VirtAddr = mem.read(addr)?;
if !ptr.is_zero() && valid(ptr) {
ptr
} else {
return Err(Error::DebugInfo(format!(
"{:#x} is not a _DEVICE_OBJECT or a pointer to one",
addr.0
)));
}
};
let dev = self.kernel_struct("_DEVICE_OBJECT", object)?;
let attached_device: VirtAddr = dev.read_field("AttachedDevice")?;
let mut attached_stack = Vec::new();
let mut seen = Vec::new();
let mut cur = attached_device;
for _ in 0..64 {
if cur.is_zero() || seen.contains(&cur.0) {
break;
}
seen.push(cur.0);
let Ok(d) = self.kernel_struct("_DEVICE_OBJECT", cur) else {
break;
};
let next: VirtAddr = d.read_field("AttachedDevice")?;
attached_stack.push(DeviceStackEntry {
device: cur,
driver_object: d.read_field("DriverObject")?,
device_type: d.read_field("DeviceType")?,
flags: d.read_field("Flags")?,
});
if next.is_zero() {
break;
}
cur = next;
}
Ok(DeviceObjectDetail {
object,
via_pointer: object != addr,
device_type: dev.read_field("DeviceType")?,
flags: dev.read_field("Flags")?,
characteristics: dev.read_field("Characteristics")?,
driver_object: dev.read_field("DriverObject")?,
attached_device,
next_device: dev.read_field("NextDevice")?,
current_irp: dev.read_field("CurrentIrp")?,
device_extension: dev.read_field("DeviceExtension")?,
attached_stack,
})
}
fn selected_process_info(&self) -> Result<ProcessInfo> {
if let Some(process) = self.current_process_info.as_ref() {
return Ok(process.clone());
}
let processes = self.matching_processes(None)?;
if let Some(thread) = self.windows_thread_selection.as_ref()
&& let Some(process) = processes
.iter()
.find(|process| thread_owner_matches(thread, process))
{
return Ok(process.clone());
}
let dtb = self.current_dtb();
processes
.into_iter()
.find(|process| process.dtb == dtb)
.ok_or_else(|| {
Error::DebugInfo(
"current process unavailable: select a process or halted Windows thread"
.to_string(),
)
})
}
fn read_layout_field<T>(&self, layout: &TypeInfo, base: VirtAddr, name: &str) -> Result<T>
where
T: Copy + zerocopy::FromZeros + zerocopy::FromBytes + zerocopy::IntoBytes,
{
self.context_memory()
.read(base + layout.field_offset(name)?)
}
fn read_luid(&self, layout: &TypeInfo, base: VirtAddr) -> Result<u64> {
let low: u32 = self.read_layout_field(layout, base, "LowPart")?;
let high: i32 = self.read_layout_field(layout, base, "HighPart")?;
Ok(((high as u32 as u64) << 32) | u64::from(low))
}
fn extract_layout_bits(&self, layout: &TypeInfo, base: VirtAddr, name: &str) -> Result<u64> {
let field = layout
.fields
.get(name)
.ok_or_else(|| Error::FieldNotFound(name.to_string()))?;
let raw: u64 = self.context_memory().read(base + field.offset as u64)?;
if let ParsedType::Bitfield { pos, len, .. } = &field.type_data {
let mask = if *len == 64 {
u64::MAX
} else {
(1u64 << *len) - 1
};
Ok((raw >> *pos) & mask)
} else {
Ok(raw)
}
}
fn handle_entry_address(
&self,
table_base: VirtAddr,
level: u8,
index: usize,
entry_size: usize,
) -> Result<VirtAddr> {
let memory = self.context_memory();
let leaf_entries = PAGE_SIZE / entry_size;
match level {
0 => Ok(table_base + (index * entry_size) as u64),
1 => {
let leaf: VirtAddr =
memory.read(table_base + ((index / leaf_entries) * 8) as u64)?;
if leaf.is_zero() {
return Err(Error::DebugInfo(format!(
"handle leaf {} is null",
index / leaf_entries
)));
}
Ok(leaf + ((index % leaf_entries) * entry_size) as u64)
}
2 => {
let middle_index = index / (leaf_entries * 512);
let leaf_index = (index / leaf_entries) % 512;
let middle: VirtAddr = memory.read(table_base + (middle_index * 8) as u64)?;
if middle.is_zero() {
return Err(Error::DebugInfo(format!(
"handle middle table {middle_index} is null"
)));
}
let leaf: VirtAddr = memory.read(middle + (leaf_index * 8) as u64)?;
if leaf.is_zero() {
return Err(Error::DebugInfo(format!(
"handle leaf {middle_index}:{leaf_index} is null"
)));
}
Ok(leaf + ((index % leaf_entries) * entry_size) as u64)
}
_ => Err(Error::DebugInfo(format!(
"unsupported HANDLE_TABLE level {level}"
))),
}
}
fn decode_handle_entry(
&self,
entry_layout: &TypeInfo,
entry: VirtAddr,
handle: u64,
) -> HandleEntryDetail {
let object = (|| -> Result<VirtAddr> {
if entry_layout.fields.contains_key("ObjectPointerBits") {
let bits = self.extract_layout_bits(entry_layout, entry, "ObjectPointerBits")?;
let mut pointer = bits << 4;
if pointer & (1 << 47) != 0 {
pointer |= 0xffff_0000_0000_0000;
}
return Ok(VirtAddr(pointer));
}
let raw = self.extract_layout_bits(entry_layout, entry, "Object")?;
Ok(VirtAddr(raw & !0xf))
})();
let granted_access = ["GrantedAccessBits", "GrantedAccess"]
.into_iter()
.find_map(|name| {
entry_layout
.fields
.contains_key(name)
.then(|| self.extract_layout_bits(entry_layout, entry, name))
})
.unwrap_or_else(|| Err(Error::FieldNotFound("GrantedAccessBits".to_string())))
.map(|value| value as u32);
let attributes = ["ObAttributes", "Attributes"]
.into_iter()
.find_map(|name| {
entry_layout
.fields
.contains_key(name)
.then(|| self.extract_layout_bits(entry_layout, entry, name))
})
.unwrap_or_else(|| Err(Error::FieldNotFound("ObAttributes".to_string())))
.map(|value| value as u32);
let header = object
.as_ref()
.map_err(|error| Error::DebugInfo(error.to_string()))
.and_then(|object| {
if object.is_zero() {
Err(Error::DebugInfo("handle entry is free".to_string()))
} else {
self.inspect_object_header(*object)
}
});
let type_name = match &header {
Ok(header) => DiagnosticValue::Available(header.type_name.clone()),
Err(error) => DiagnosticValue::Unavailable(error.to_string()),
};
let name = match &header {
Ok(header) => DiagnosticValue::Available(header.name.clone()),
Err(error) => DiagnosticValue::Unavailable(error.to_string()),
};
HandleEntryDetail {
handle,
entry,
object: DiagnosticValue::from_result(object),
type_name,
name,
granted_access: DiagnosticValue::from_result(granted_access),
attributes: DiagnosticValue::from_result(attributes),
}
}
fn handle_table_context(
&self,
) -> Result<(ProcessInfo, VirtAddr, VirtAddr, u8, usize, Arc<TypeInfo>)> {
let process = self.selected_process_info()?;
let types = self.guest()?.ntoskrnl.types_in(process.dtb);
let eprocess = types.struct_at("_EPROCESS", process.eprocess_va)?;
let table: VirtAddr = eprocess.read_field("ObjectTable")?;
if table.is_zero() {
return Err(Error::DebugInfo(
"_EPROCESS.ObjectTable is null".to_string(),
));
}
let table_layout = types.layout("_HANDLE_TABLE")?;
let table_code: u64 = self.read_layout_field(&table_layout, table, "TableCode")?;
let level = (table_code & 3) as u8;
if level > 2 {
return Err(Error::DebugInfo(format!(
"unsupported HANDLE_TABLE level {level}"
)));
}
let next_handle: u64 =
self.read_layout_field(&table_layout, table, "NextHandleNeedingPool")?;
let entry_layout = types.layout("_HANDLE_TABLE_ENTRY")?;
Ok((
process,
table,
VirtAddr(table_code & !3),
level,
(next_handle / 4) as usize,
entry_layout,
))
}
pub fn enumerate_handles(&self, limit: usize) -> Result<HandleTableSummary> {
let limit = limit.clamp(1, 4096);
let (process, table, table_base, level, advertised, entry_layout) =
self.handle_table_context()?;
let scanned = advertised.min(limit);
let mut entries = Vec::new();
let mut skipped_entries = 0usize;
for index in 0..scanned {
let entry = match self.handle_entry_address(table_base, level, index, entry_layout.size)
{
Ok(entry) => entry,
Err(_) => {
skipped_entries += 1;
continue;
}
};
let decoded = self.decode_handle_entry(&entry_layout, entry, (index as u64) * 4);
if !matches!(
decoded.object,
DiagnosticValue::Available(address) if address.is_zero()
) {
entries.push(decoded);
}
}
Ok(HandleTableSummary {
process,
table,
table_level: level,
advertised_handles: advertised,
scanned_handles: scanned,
skipped_entries,
truncated: advertised > scanned,
entries,
})
}
pub fn inspect_handle(&self, handle: u64) -> Result<HandleEntryDetail> {
if handle & 3 != 0 {
return Err(Error::DebugInfo(format!(
"handle {handle:#x} is not 4-byte aligned"
)));
}
let (_, _, table_base, level, advertised, entry_layout) = self.handle_table_context()?;
let index = (handle / 4) as usize;
if index >= advertised {
return Err(Error::DebugInfo(format!(
"handle {handle:#x} is beyond NextHandleNeedingPool ({:#x})",
advertised * 4
)));
}
let entry = self.handle_entry_address(table_base, level, index, entry_layout.size)?;
Ok(self.decode_handle_entry(&entry_layout, entry, handle))
}
fn read_sid(&self, address: VirtAddr) -> Result<String> {
if address.is_zero() {
return Err(Error::DebugInfo("SID pointer is null".to_string()));
}
let memory = self.context_memory();
let revision: u8 = memory.read(address)?;
let count: u8 = memory.read(address + 1u64)?;
if count > 15 {
return Err(Error::DebugInfo(format!(
"SID subauthority count {count} exceeds 15"
)));
}
let mut authority = [0u8; 6];
memory.read_bytes(address + 2u64, &mut authority)?;
let authority = authority
.into_iter()
.fold(0u64, |value, byte| (value << 8) | u64::from(byte));
let mut sid = format!("S-{revision}-{authority}");
for index in 0..count {
let sub: u32 = memory.read(address + 8u64 + u64::from(index) * 4)?;
sid.push_str(&format!("-{sub}"));
}
Ok(sid)
}
fn read_token_id_field(
&self,
token_layout: &TypeInfo,
luid_layout: &TypeInfo,
token: VirtAddr,
name: &str,
) -> Result<u64> {
self.read_luid(luid_layout, token + token_layout.field_offset(name)?)
}
pub fn inspect_process_token(&self) -> Result<TokenDetail> {
const MAX_TOKEN_ITEMS: usize = 256;
let process = self.selected_process_info()?;
let types = self.guest()?.ntoskrnl.types_in(process.dtb);
let eprocess_layout = types.layout("_EPROCESS")?;
let raw_token: u64 =
self.read_layout_field(&eprocess_layout, process.eprocess_va, "Token")?;
let token = VirtAddr(raw_token & !0xf);
if token.is_zero() {
return Err(Error::DebugInfo("_EPROCESS.Token is null".to_string()));
}
let token_layout = types.layout("_TOKEN")?;
let luid_layout = types.layout("_LUID")?;
let token_id = DiagnosticValue::from_result(self.read_token_id_field(
&token_layout,
&luid_layout,
token,
"TokenId",
));
let authentication_id = DiagnosticValue::from_result(self.read_token_id_field(
&token_layout,
&luid_layout,
token,
"AuthenticationId",
));
let token_type =
DiagnosticValue::from_result(self.read_layout_field(&token_layout, token, "TokenType"));
let impersonation_level = DiagnosticValue::from_result(self.read_layout_field(
&token_layout,
token,
"ImpersonationLevel",
));
let flags = DiagnosticValue::from_result(self.read_layout_field(
&token_layout,
token,
"TokenFlags",
));
let sid_items = (|| -> Result<Vec<SidAndAttributes>> {
let count: u32 = self.read_layout_field(&token_layout, token, "UserAndGroupCount")?;
if count as usize > MAX_TOKEN_ITEMS {
return Err(Error::DebugInfo(format!(
"_TOKEN.UserAndGroupCount {count} exceeds bound {MAX_TOKEN_ITEMS}"
)));
}
let array: VirtAddr = self.read_layout_field(&token_layout, token, "UserAndGroups")?;
if count != 0 && array.is_zero() {
return Err(Error::DebugInfo(
"_TOKEN.UserAndGroups is null with nonzero count".to_string(),
));
}
let item_layout = types.layout("_SID_AND_ATTRIBUTES")?;
let mut items = Vec::with_capacity(count as usize);
for index in 0..count as usize {
let base = array + (index * item_layout.size) as u64;
let sid: VirtAddr = self.read_layout_field(&item_layout, base, "Sid")?;
let attributes: u32 = self.read_layout_field(&item_layout, base, "Attributes")?;
items.push(SidAndAttributes {
sid: self.read_sid(sid)?,
attributes,
});
}
Ok(items)
})();
let (user, groups) = match sid_items {
Ok(items) => (
DiagnosticValue::Available(items.first().cloned()),
DiagnosticValue::Available(items.into_iter().skip(1).collect()),
),
Err(error) => (
DiagnosticValue::Unavailable(error.to_string()),
DiagnosticValue::Unavailable(error.to_string()),
),
};
let privileges = DiagnosticValue::from_result((|| -> Result<Vec<PrivilegeInfo>> {
let privileges = token + token_layout.field_offset("Privileges")?;
let privileges_layout = types.layout("_SEP_TOKEN_PRIVILEGES")?;
let present: u64 = self.read_layout_field(&privileges_layout, privileges, "Present")?;
let enabled: u64 = self.read_layout_field(&privileges_layout, privileges, "Enabled")?;
let enabled_by_default: u64 =
self.read_layout_field(&privileges_layout, privileges, "EnabledByDefault")?;
Ok(decode_token_privilege_bitmaps(
present,
enabled,
enabled_by_default,
))
})());
Ok(TokenDetail {
process,
token,
token_id,
authentication_id,
token_type,
impersonation_level,
flags,
user,
groups,
privileges,
})
}
pub fn inspect_file_object(&self, address: VirtAddr) -> Result<FileObjectDetail> {
let types = self.guest()?.ntoskrnl.types_in(self.current_dtb());
let layout = types.layout("_FILE_OBJECT")?;
let read_ptr =
|name| DiagnosticValue::from_result(self.read_layout_field(&layout, address, name));
let read_bool = |name| {
DiagnosticValue::from_result(
self.read_layout_field::<u8>(&layout, address, name)
.map(|value| value != 0),
)
};
let device_object: Result<VirtAddr> =
self.read_layout_field(&layout, address, "DeviceObject");
let device_type = match &device_object {
Ok(device) if !device.is_zero() => DiagnosticValue::from_result(
self.inspect_device_object(*device)
.map(|detail| detail.device_type),
),
Ok(_) => DiagnosticValue::Unavailable("_FILE_OBJECT.DeviceObject is null".to_string()),
Err(error) => DiagnosticValue::Unavailable(error.to_string()),
};
let device_name = match &device_object {
Ok(device) if !device.is_zero() => DiagnosticValue::from_result(
self.inspect_object_header(*device)
.map(|detail| detail.name),
),
Ok(_) => DiagnosticValue::Unavailable("_FILE_OBJECT.DeviceObject is null".to_string()),
Err(error) => DiagnosticValue::Unavailable(error.to_string()),
};
Ok(FileObjectDetail {
address,
file_type: DiagnosticValue::from_result(
self.read_layout_field(&layout, address, "Type"),
),
size: DiagnosticValue::from_result(self.read_layout_field(&layout, address, "Size")),
device_object: DiagnosticValue::from_result(device_object),
device_type,
device_name,
file_name: DiagnosticValue::from_result(
types
.struct_at("_FILE_OBJECT", address)?
.unicode_string("FileName"),
),
related_file_object: read_ptr("RelatedFileObject"),
flags: DiagnosticValue::from_result(self.read_layout_field(&layout, address, "Flags")),
current_byte_offset: DiagnosticValue::from_result(self.read_layout_field(
&layout,
address,
"CurrentByteOffset",
)),
fs_context: read_ptr("FsContext"),
fs_context2: read_ptr("FsContext2"),
section_object_pointer: read_ptr("SectionObjectPointer"),
private_cache_map: read_ptr("PrivateCacheMap"),
final_status: DiagnosticValue::from_result(self.read_layout_field(
&layout,
address,
"FinalStatus",
)),
lock_operation: read_bool("LockOperation"),
delete_pending: read_bool("DeletePending"),
read_access: read_bool("ReadAccess"),
write_access: read_bool("WriteAccess"),
delete_access: read_bool("DeleteAccess"),
shared_read: read_bool("SharedRead"),
shared_write: read_bool("SharedWrite"),
shared_delete: read_bool("SharedDelete"),
})
}
pub fn inspect_resource(&self, address: VirtAddr) -> Result<ResourceDetail> {
const MAX_RESOURCE_OWNERS: usize = 64;
let types = self.guest()?.ntoskrnl.types_in(self.current_dtb());
let layout = types.layout("_ERESOURCE")?;
let owner_layout = types.layout("_OWNER_ENTRY")?;
let owners = DiagnosticValue::from_result((|| -> Result<Vec<ResourceOwner>> {
let mut owners = Vec::new();
let owner_entry = address + layout.field_offset("OwnerEntry")?;
let thread: u64 = self.read_layout_field(&owner_layout, owner_entry, "OwnerThread")?;
let count: i32 = self.read_layout_field(&owner_layout, owner_entry, "OwnerCount")?;
if thread & !3 != 0 && count != 0 {
owners.push(ResourceOwner {
thread: VirtAddr(thread & !3),
count,
});
}
let table: VirtAddr = self.read_layout_field(&layout, address, "OwnerTable")?;
if table.is_zero() {
return Ok(owners);
}
let table_size: u32 = self.read_layout_field(&owner_layout, table, "TableSize")?;
if table_size as usize > MAX_RESOURCE_OWNERS {
return Err(Error::DebugInfo(format!(
"_OWNER_ENTRY.TableSize {table_size} exceeds bound {MAX_RESOURCE_OWNERS}"
)));
}
for index in 1..table_size as usize {
let entry = table + (index * owner_layout.size) as u64;
let thread: u64 = self.read_layout_field(&owner_layout, entry, "OwnerThread")?;
let count: i32 = self.read_layout_field(&owner_layout, entry, "OwnerCount")?;
if thread & !3 != 0 && count != 0 {
owners.push(ResourceOwner {
thread: VirtAddr(thread & !3),
count,
});
}
}
Ok(owners)
})());
Ok(ResourceDetail {
address,
active_count: DiagnosticValue::from_result(self.read_layout_field(
&layout,
address,
"ActiveCount",
)),
flags: DiagnosticValue::from_result(self.read_layout_field(&layout, address, "Flag")),
contention_count: DiagnosticValue::from_result(self.read_layout_field(
&layout,
address,
"ContentionCount",
)),
shared_waiters: DiagnosticValue::from_result(self.read_layout_field(
&layout,
address,
"NumberOfSharedWaiters",
)),
exclusive_waiters: DiagnosticValue::from_result(self.read_layout_field(
&layout,
address,
"NumberOfExclusiveWaiters",
)),
owners,
})
}
pub fn enumerate_resources(&self, limit: usize) -> Result<ResourceListSummary> {
let limit = limit.clamp(1, 1024);
let guest = self.guest()?;
let head = guest.ntoskrnl.symbol("ExpSystemResourcesList")?.address();
let layout = guest.ntoskrnl.types().layout("_ERESOURCE")?;
let link_offset = layout.field_offset("SystemResourcesList")?;
let memory = self.context_memory();
let (links, termination) =
bounded_list_walk(head, limit, |link| memory.read::<VirtAddr>(link));
let resources = links
.into_iter()
.map(|link| self.inspect_resource(link - link_offset))
.collect::<Result<Vec<_>>>()?;
Ok(ResourceListSummary {
head,
resources,
termination,
})
}
fn process_memory_counter(
&self,
eprocess_layout: &TypeInfo,
vm_layout: &TypeInfo,
eprocess: VirtAddr,
field: &str,
) -> DiagnosticValue<u64> {
DiagnosticValue::from_result((|| -> Result<u64> {
let vm = eprocess + eprocess_layout.field_offset("Vm")?;
if let Ok(value) = self.read_layout_field(vm_layout, vm, field) {
return Ok(value);
}
if let Ok(value) = self.read_layout_field(eprocess_layout, eprocess, field) {
return Ok(value);
}
let types = self.guest()?.ntoskrnl.types();
for container in ["Instance", "Shared"] {
let Some(container_field) = vm_layout.fields.get(container) else {
continue;
};
let (ParsedType::Struct(layout_name) | ParsedType::Union(layout_name)) =
&container_field.type_data
else {
continue;
};
let Ok(layout) = types.layout(layout_name) else {
continue;
};
if let Ok(value) =
self.read_layout_field(&layout, vm + u64::from(container_field.offset), field)
{
return Ok(value);
}
}
let page_field = match field {
"PagefileUsage" => Some("CommitCharge"),
"PeakPagefileUsage" => Some("CommitChargePeak"),
"PrivateUsage" => Some("NumberOfPrivatePages"),
_ => None,
};
if let Some(page_field) = page_field {
let pages: u64 = self.read_layout_field(eprocess_layout, eprocess, page_field)?;
return pages.checked_mul(PAGE_SIZE as u64).ok_or_else(|| {
Error::DebugInfo(format!("_EPROCESS.{page_field} overflows a byte count"))
});
}
Err(Error::FieldNotFound(field.to_string()))
})())
}
fn debugger_data_counter(
&self,
address: Option<MetadataValue<VirtAddr>>,
) -> Option<Result<MetadataValue<u64>>> {
address.map(|address| {
self.context_memory()
.read::<u64>(address.value)
.map(|value| MetadataValue {
value,
source: address.source,
})
})
}
fn global_memory_counter(
&self,
symbol_name: &str,
debugger_data_value: Option<Result<MetadataValue<u64>>>,
getter_name: Option<&str>,
) -> DiagnosticMetric<u64> {
let mut errors = Vec::new();
match self
.guest()
.and_then(|guest| guest.ntoskrnl.symbol(symbol_name))
.and_then(|symbol| symbol.read())
{
Ok(value) => {
return DiagnosticMetric::available(MetadataValue {
value,
source: MetadataSource::KernelSymbol,
});
}
Err(error) => errors.push(error.to_string()),
}
if let Some(value) = debugger_data_value {
match value {
Ok(value) if value.value != 0 => return DiagnosticMetric::available(value),
Ok(_) => {}
Err(error) => errors.push(error.to_string()),
}
}
if let Some(getter_name) = getter_name {
match (|| -> Result<MetadataValue<u64>> {
let guest = self.guest()?;
let getter = guest.ntoskrnl.symbol(getter_name)?.address();
let system_partition = guest.ntoskrnl.symbol("MiSystemPartition")?.address();
read_counter_from_getter(&self.context_memory(), getter, system_partition)
})() {
Ok(value) => return DiagnosticMetric::available(value),
Err(error) => errors.push(error.to_string()),
}
}
DiagnosticMetric::unavailable(errors)
}
pub fn memory_use_summary(&self, process_limit: usize) -> Result<SystemMemorySummary> {
let process_limit = process_limit.clamp(1, 256);
let all_processes = self.matching_processes(None)?;
let process_count = all_processes.len();
let guest = self.guest()?;
let types = guest.ntoskrnl.types();
let eprocess_layout = types.layout("_EPROCESS")?;
let vm_field = eprocess_layout
.fields
.get("Vm")
.ok_or_else(|| Error::FieldNotFound("Vm".to_string()))?;
let vm_name = match &vm_field.type_data {
ParsedType::Struct(name) | ParsedType::Union(name) => name,
_ => {
return Err(Error::FieldTypeMismatch(
"Vm".to_string(),
"embedded struct".to_string(),
));
}
};
let vm_layout = types.layout(vm_name)?;
let processes = all_processes
.into_iter()
.take(process_limit)
.map(|process| ProcessMemoryUsage {
virtual_size: self.process_memory_counter(
&eprocess_layout,
&vm_layout,
process.eprocess_va,
"VirtualSize",
),
peak_virtual_size: self.process_memory_counter(
&eprocess_layout,
&vm_layout,
process.eprocess_va,
"PeakVirtualSize",
),
working_set_size: self.process_memory_counter(
&eprocess_layout,
&vm_layout,
process.eprocess_va,
"WorkingSetSize",
),
peak_working_set_size: self.process_memory_counter(
&eprocess_layout,
&vm_layout,
process.eprocess_va,
"PeakWorkingSetSize",
),
pagefile_usage: self.process_memory_counter(
&eprocess_layout,
&vm_layout,
process.eprocess_va,
"PagefileUsage",
),
peak_pagefile_usage: self.process_memory_counter(
&eprocess_layout,
&vm_layout,
process.eprocess_va,
"PeakPagefileUsage",
),
private_usage: self.process_memory_counter(
&eprocess_layout,
&vm_layout,
process.eprocess_va,
"PrivateUsage",
),
process,
})
.collect();
let debugger_data = self.debugger_data();
Ok(SystemMemorySummary {
physical_pages: self.global_memory_counter(
"MmNumberOfPhysicalPages",
self.debugger_data_counter(
debugger_data.and_then(DebuggerDataBlock::mm_number_of_physical_pages_address),
),
Some("MmGetNumberOfPhysicalPages"),
),
available_pages: self.global_memory_counter(
"MmAvailablePages",
self.debugger_data_counter(
debugger_data.and_then(DebuggerDataBlock::mm_available_pages_address),
),
Some("MmGetAvailablePages"),
),
committed_pages: self.global_memory_counter(
"MmTotalCommittedPages",
self.debugger_data_counter(
debugger_data.and_then(DebuggerDataBlock::mm_total_committed_pages_address),
),
Some("MmGetTotalCommittedPages"),
),
commit_limit_pages: self.global_memory_counter(
"MmTotalCommitLimit",
self.debugger_data_counter(
debugger_data.and_then(DebuggerDataBlock::mm_total_commit_limit_address),
),
Some("MmGetTotalCommitLimit"),
),
paged_pool_pages: self.global_memory_counter("MmSizeOfPagedPoolInPages", None, None),
nonpaged_pool_bytes: self.global_memory_counter(
"MmSizeOfNonPagedPoolInBytes",
None,
None,
),
processes,
process_count,
truncated: process_count > process_limit,
})
}
pub fn inspect_object_header(&self, addr: VirtAddr) -> Result<ObjectHeaderDetail> {
let guest = self.guest()?;
let mem = guest.ntoskrnl.memory();
let layout = guest.ntoskrnl.types().layout("_OBJECT_HEADER")?;
let header_size = layout.size as u64;
let body_off = layout.field_offset("Body")?;
let type_index_off = layout.field_offset("TypeIndex")?;
let object_name = self.object_name_layout().ok();
let cookie = match guest.ntoskrnl.symbol("ObHeaderCookie") {
Ok(symbol) => symbol.read::<u8>()?,
Err(Error::SymbolNotFound(_)) => 0,
Err(error) => return Err(error),
};
let type_table = guest
.ntoskrnl
.symbol("ObTypeIndexTable")
.ok()
.map(|symbol| symbol.address());
let header_ok = |header: VirtAddr| -> bool {
header_size
.checked_sub(1)
.and_then(|last| header.0.checked_add(last))
.map(VirtAddr)
.is_some_and(|end| mem.read::<u8>(header).is_ok() && mem.read::<u8>(end).is_ok())
};
let candidate_type_object = |header: VirtAddr| -> Option<VirtAddr> {
let raw: u8 = mem.read(header + type_index_off).ok()?;
let index = u64::from(raw ^ ((header.0 >> 8) as u8) ^ cookie);
mem.read::<VirtAddr>(type_table? + index * 8)
.ok()
.filter(|object| looks_like_kernel_pointer(object.0))
};
let body_candidate = addr
.0
.checked_sub(body_off)
.map(VirtAddr)
.filter(|header| header_ok(*header));
let direct_candidate = header_ok(addr).then_some(addr);
let body_type = body_candidate.and_then(candidate_type_object);
let direct_type = direct_candidate.and_then(candidate_type_object);
let (header, body, mode) = select_object_header_candidate(
addr,
body_off,
body_candidate,
direct_candidate,
body_type.is_some(),
direct_type.is_some(),
)
.ok_or_else(|| {
Error::DebugInfo(format!("no plausible _OBJECT_HEADER for {:#x}", addr.0))
})?;
let h = self.kernel_struct("_OBJECT_HEADER", header)?;
let info_mask: Option<u8> = h.read_field("InfoMask").ok();
let type_index: Option<u64> = h
.read_field::<u8>("TypeIndex")
.ok()
.map(|raw| (raw ^ ((header.0 >> 8) as u8) ^ cookie) as u64);
let (type_object, type_name) = match (type_table, type_index) {
(Some(table), Some(index)) => {
let resolved = mem
.read::<VirtAddr>(table + index * 8)
.ok()
.filter(|object| looks_like_kernel_pointer(object.0));
let name = resolved.and_then(|t| {
let off = guest
.ntoskrnl
.types()
.layout("_OBJECT_TYPE")
.ok()?
.field_offset("Name")
.ok()?;
self.read_kernel_unicode_string(t + off)
.ok()
.filter(|s| !s.is_empty())
});
(resolved, name)
}
_ => (None, None),
};
let name_info = info_mask.and_then(|mask| {
object_name
.as_ref()
.and_then(|layout| layout.name_info_address(header, mask).ok().flatten())
});
let name = name_info.and_then(|info| {
object_name.as_ref().and_then(|layout| {
self.read_kernel_unicode_string(info + layout.name_offset)
.ok()
.filter(|name| !name.is_empty())
})
});
Ok(ObjectHeaderDetail {
input: addr,
mode,
header,
body,
pointer_count: h.read_field("PointerCount")?,
handle_count: h.read_field("HandleCount")?,
type_index,
type_object,
type_name,
info_mask,
name_info,
name,
})
}
pub fn describe_address(&self, address: VirtAddr) -> Result<AddressDescription> {
let dtb = self.current_dtb();
let is_kernel = address.0 >= 0xffff_0000_0000_0000;
let modules = if is_kernel {
self.kernel_modules()
} else {
self.modules()
};
if let Ok(mods) = modules
&& let Some(m) = mods.into_iter().find(|m| {
address.0 >= m.base_address.0 && address.0 < m.base_address.0 + m.size as u64
})
{
let memory = self.current_process()?.memory();
let section = section_name_at(&memory, m.base_address, address);
return Ok(AddressDescription {
address,
dtb,
kind: if is_kernel {
"kernel-module"
} else {
"user-image"
},
module: Some(AddressModule {
name: m.name,
base: m.base_address,
size: m.size,
offset: address.0 - m.base_address.0,
}),
section,
va_type: None,
region: None,
});
}
if is_kernel && let Some(va_type) = self.kernel_va_region(address) {
return Ok(AddressDescription {
address,
dtb,
kind: "kernel-region",
module: None,
section: None,
va_type: Some(va_type),
region: None,
});
}
if let Some(p) = self.current_process_info.as_ref()
&& let Ok(regions) = self.enumerate_vad_regions_for_process_info(p)
&& let Some(r) = regions
.into_iter()
.find(|r| address.0 >= r.start.0 && address.0 < r.end.0)
{
let kind = match r.private_memory {
Some(true) => "private",
_ => "mapped",
};
return Ok(AddressDescription {
address,
dtb,
kind,
module: None,
section: None,
va_type: None,
region: Some(r),
});
}
Ok(AddressDescription {
address,
dtb,
kind: "unknown",
module: None,
section: None,
va_type: None,
region: None,
})
}
fn kernel_va_region(&self, address: VirtAddr) -> Option<String> {
let ntos = &self.guest.as_ref()?.ntoskrnl;
let range_start: VirtAddr = ntos.symbol("MmSystemRangeStart").ok()?.read().ok()?;
if address.0 < range_start.0 {
return None;
}
let granularity = range_start.0.wrapping_neg() / 256;
if granularity == 0 {
return None;
}
let chunk = (address.0 - range_start.0) / granularity;
if chunk >= 256 {
return None;
}
let vs: VirtAddr = ntos.symbol("MiVisibleState").ok()?.read().ok()?;
let type_off = ntos
.types()
.layout("_MI_VISIBLE_STATE")
.ok()?
.field_offset("SystemVaType")
.ok()?;
let type_byte: u8 = ntos.memory().read(vs + type_off + chunk).ok()?;
let variants = self
.symbols
.find_enum_across_modules(ntos.dtb(), "_MI_SYSTEM_VA_TYPE")?;
let name = variants
.into_iter()
.find(|(_, v)| *v == type_byte as i64)
.map(|(n, _)| n)?;
Some(name.strip_prefix("MiVa").unwrap_or(&name).to_string())
}
pub fn enumerate_notify_callbacks(&self) -> Result<Vec<NotifyCallback>> {
const MAX_NOTIFY: u64 = 64;
let sets: [(&str, &str); 3] = [
("process", "PspCreateProcessNotifyRoutine"),
("thread", "PspCreateThreadNotifyRoutine"),
("image", "PspLoadImageNotifyRoutine"),
];
let guest = self.guest()?;
let mem = guest.ntoskrnl.memory();
let ex_callback_size = guest
.ntoskrnl
.types()
.layout("_EX_CALLBACK")
.ok()
.map(|l| l.size as u64)
.filter(|s| (8..=0x40).contains(s))
.unwrap_or(8);
let block_layout = guest
.ntoskrnl
.types()
.layout("_EX_CALLBACK_ROUTINE_BLOCK")
.ok();
let function_off = block_layout
.as_ref()
.and_then(|l| l.field_offset("Function").ok())
.unwrap_or(8);
let context_off = block_layout
.as_ref()
.and_then(|l| l.field_offset("Context").ok())
.unwrap_or(16);
let is_kernel = |a: VirtAddr| a.0 >= 0xffff_0000_0000_0000;
let mut out = Vec::new();
for (kind, symbol) in sets {
let Ok(sym) = guest.ntoskrnl.symbol(symbol) else {
continue;
};
let base = sym.address();
for i in 0..MAX_NOTIFY {
let entry = base + i * ex_callback_size;
let Ok(raw): Result<VirtAddr> = mem.read(entry) else {
continue;
};
if raw.is_zero() {
continue;
}
let block = VirtAddr(raw.0 & !0xf);
if block.is_zero() {
continue;
}
let mut function = mem
.read::<VirtAddr>(block + function_off)
.unwrap_or(VirtAddr(0));
let mut context = mem
.read::<VirtAddr>(block + context_off)
.unwrap_or(VirtAddr(0));
if !is_kernel(function) {
function = mem.read::<VirtAddr>(block + 8u64).unwrap_or(VirtAddr(0));
context = mem.read::<VirtAddr>(block + 16u64).unwrap_or(VirtAddr(0));
}
if !is_kernel(function) {
continue;
}
out.push(NotifyCallback {
kind,
index: i as usize,
function,
block,
raw,
context,
});
}
}
Ok(out)
}
fn dump_ssdt_table(&self, label: &str, base: VirtAddr, limit: u32, guest: &Guest) -> SsdtTable {
let mem = guest.ntoskrnl.memory();
let dtb = guest.ntoskrnl.dtb();
let mut entries = Vec::new();
let limit = limit.min(0x4000);
for i in 0..limit {
let Ok(raw) = mem.read::<u32>(base + (i as u64) * 4) else {
break;
};
let offset = (raw as i32 >> 4) as i64;
let target = VirtAddr((base.0 as i64 + offset) as u64);
let resolved = self.symbols.find_closest_symbol_for_address(dtb, target);
let (symbol, module) = match resolved {
Some((module, name, off)) => {
let sym = if off == 0 {
format!("{module}!{name}")
} else {
format!("{module}!{name}+{off:#x}")
};
(Some(sym), Some(module))
}
None => (None, None),
};
entries.push(SsdtEntry {
index: i,
target,
symbol,
module,
});
}
SsdtTable {
label: label.to_string(),
base,
limit,
entries,
}
}
pub fn dump_ssdt(&self) -> Result<Vec<SsdtTable>> {
let guest = self.guest()?;
let mem = guest.ntoskrnl.memory();
let base = guest.ntoskrnl.symbol("KiServiceTable")?.address();
let limit = guest.ntoskrnl.symbol("KiServiceLimit")?.read::<u32>()?;
let mut tables = vec![self.dump_ssdt_table("SSDT", base, limit, guest)];
if let Ok(sdt) = guest.ntoskrnl.symbol("KeServiceDescriptorTableShadow") {
let desc = guest.ntoskrnl.types().layout("_KSERVICE_TABLE_DESCRIPTOR");
let desc_size = desc.as_ref().ok().map(|l| l.size as u64).unwrap_or(0x20);
let base_off = desc
.as_ref()
.ok()
.and_then(|l| l.field_offset("Base").ok())
.unwrap_or(0);
let limit_off = desc
.as_ref()
.ok()
.and_then(|l| l.field_offset("Limit").ok())
.unwrap_or(0x10);
let win32k = sdt.address() + desc_size;
if let Ok(w_base) = mem.read::<VirtAddr>(win32k + base_off)
&& !w_base.is_zero()
{
let w_limit = mem.read::<u32>(win32k + limit_off).unwrap_or(0);
tables.push(self.dump_ssdt_table("shadow SSDT (win32k)", w_base, w_limit, guest));
}
}
Ok(tables)
}
fn plausible_irp(&self, irp: VirtAddr) -> Option<(u8, u8)> {
let guest = self.guest.as_ref()?;
let mem = guest.ntoskrnl.memory();
let layout = guest.ntoskrnl.types().layout("_IRP").ok()?;
let ty: u16 = mem.read(irp).ok()?;
if ty != 6 {
return None;
}
let size: u16 = mem.read(irp + layout.field_offset("Size").ok()?).ok()?;
if (size as u64) < layout.size as u64 || size > 0x1000 {
return None;
}
let sc = mem
.read::<u8>(irp + layout.field_offset("StackCount").ok()?)
.unwrap_or(0);
let cl = mem
.read::<u8>(irp + layout.field_offset("CurrentLocation").ok()?)
.unwrap_or(0);
Some((sc, cl))
}
pub fn discover_irps(&self, filter: Option<&str>) -> Result<Vec<IrpHit>> {
let guest = self.guest()?;
let mem = guest.ntoskrnl.memory();
let off = |ty: &str, field: &str| -> Option<u64> {
guest
.ntoskrnl
.types()
.layout(ty)
.ok()
.and_then(|l| l.field_offset(field).ok())
};
let read_ptr = |a: VirtAddr| mem.read::<VirtAddr>(a).ok();
let filter_l = filter.map(|f| f.to_ascii_lowercase());
let numeric_filter = filter.and_then(|f| f.parse::<u64>().ok());
let mut out = Vec::new();
let procs = guest.enumerate_processes()?;
let thread_head_off = off("_EPROCESS", "ThreadListHead");
let thread_link_off = off("_ETHREAD", "ThreadListEntry");
let irp_list_off = off("_ETHREAD", "IrpList");
let irp_link_off = off("_IRP", "ThreadListEntry");
let cid_off = off("_ETHREAD", "Cid");
let tcb_off = off("_ETHREAD", "Tcb").unwrap_or(0);
let unique_thread_off = off("_CLIENT_ID", "UniqueThread");
let state_off = off("_KTHREAD", "State");
let wait_off = off("_KTHREAD", "WaitReason");
for p in &procs {
let matched = match (&filter_l, numeric_filter) {
(None, _) => true,
(Some(_), Some(pid)) => p.pid == pid,
(Some(f), None) => p.name.to_ascii_lowercase().contains(f.as_str()),
};
if !matched {
continue;
}
let (Some(head_off), Some(link_off), Some(list_off), Some(rec_off)) =
(thread_head_off, thread_link_off, irp_list_off, irp_link_off)
else {
break;
};
let head = p.eprocess_va + head_off;
let mut seen_t = Vec::new();
let mut cur = read_ptr(head);
for _ in 0..4096 {
let Some(node) = cur else { break };
if node.is_zero() || node == head || seen_t.contains(&node.0) {
break;
}
seen_t.push(node.0);
let ethread = node - link_off;
let tid = cid_off
.zip(unique_thread_off)
.and_then(|(c, u)| mem.read::<u64>(ethread + c + u).ok());
let state = state_off.and_then(|o| mem.read::<u8>(ethread + tcb_off + o).ok());
let wait = wait_off.and_then(|o| mem.read::<u8>(ethread + tcb_off + o).ok());
let irp_head = ethread + list_off;
let mut seen_i = Vec::new();
let mut icur = read_ptr(irp_head);
for _ in 0..256 {
let Some(inode) = icur else { break };
if inode.is_zero() || inode == irp_head || seen_i.contains(&inode.0) {
break;
}
seen_i.push(inode.0);
let irp = inode - rec_off;
if let Some((sc, cl)) = self.plausible_irp(irp) {
out.push(IrpHit {
irp,
source: "thread",
stack_count: sc,
current_location: cl,
pid: Some(p.pid),
tid,
ethread: Some(ethread),
state,
wait_reason: wait,
driver: None,
device: None,
});
}
icur = read_ptr(inode);
}
cur = read_ptr(node);
}
}
if numeric_filter.is_none() {
let current_irp_off = off("_DEVICE_OBJECT", "CurrentIrp");
let next_off = off("_DEVICE_OBJECT", "NextDevice");
if let (Some(cur_off), Some(next_off)) = (current_irp_off, next_off) {
for driver in self.enumerate_driver_objects()? {
if let Some(f) = &filter_l
&& !driver.name.to_ascii_lowercase().contains(f.as_str())
{
continue;
}
let mut seen = Vec::new();
let mut cur = Some(driver.device_object);
for _ in 0..256 {
let Some(dev) = cur else { break };
if dev.is_zero() || seen.contains(&dev.0) {
break;
}
seen.push(dev.0);
let current_irp = read_ptr(dev + cur_off).unwrap_or(VirtAddr(0));
if !current_irp.is_zero()
&& let Some((sc, cl)) = self.plausible_irp(current_irp)
{
out.push(IrpHit {
irp: current_irp,
source: "device",
stack_count: sc,
current_location: cl,
pid: None,
tid: None,
ethread: None,
state: None,
wait_reason: None,
driver: Some(driver.name.clone()),
device: Some(dev),
});
}
cur = read_ptr(dev + next_off);
}
}
}
}
Ok(out)
}
pub fn enumerate_threads_for_process_info(
&self,
process: &ProcessInfo,
) -> Result<Vec<ThreadInfo>> {
let guest = self.guest()?;
let memory = guest.ntoskrnl.memory();
let eprocess = guest
.ntoskrnl
.types_in(process.dtb)
.struct_at("_EPROCESS", process.eprocess_va)?;
let eprocess_layout = guest.ntoskrnl.types().layout("_EPROCESS")?;
let thread_list_head_offset = eprocess_layout.field_offset("ThreadListHead")?;
let ethread_layout = guest.ntoskrnl.types().layout("_ETHREAD")?;
let thread_list_entry_offset = ethread_layout.field_offset("ThreadListEntry")?;
let head = eprocess.addr() + thread_list_head_offset;
let mut threads = Vec::new();
let mut visited = HashSet::new();
let mut current: VirtAddr = memory.read(head)?;
for _ in 0..16384 {
if current.is_zero() || current == head || !visited.insert(current.0) {
break;
}
let ethread = current - thread_list_entry_offset;
threads.push(self.thread_info_from_ethread_with_hint(ethread, Some(process))?);
current = memory.read(current)?;
}
Ok(threads)
}
pub fn enumerate_threads(&self) -> Result<Vec<ThreadInfo>> {
const MAX_ENUMERATED_THREADS: usize = 65_536;
let processes = self.guest()?.enumerate_processes()?;
let mut threads = Vec::new();
for process in &processes {
if threads.len() >= MAX_ENUMERATED_THREADS {
break;
}
let Ok(process_threads) = self.enumerate_threads_for_process_info(process) else {
continue;
};
threads.extend(
process_threads
.into_iter()
.take(MAX_ENUMERATED_THREADS.saturating_sub(threads.len())),
);
}
Ok(threads)
}
pub fn thread_info_from_ethread(&self, ethread: VirtAddr) -> Result<ThreadInfo> {
self.thread_info_from_ethread_with_hint(ethread, None)
}
fn thread_info_from_ethread_with_hint(
&self,
ethread: VirtAddr,
process_hint: Option<&ProcessInfo>,
) -> Result<ThreadInfo> {
let guest = self.guest()?;
let memory = guest.ntoskrnl.memory();
let types = guest.ntoskrnl.types();
let ethread_layout = types.layout("_ETHREAD")?;
let kthread_layout = types.layout("_KTHREAD")?;
let tcb_offset = ethread_layout.field_offset("Tcb").unwrap_or(0);
let kthread = ethread + tcb_offset;
let cid_base = ethread_layout
.field_offset("Cid")
.ok()
.map(|offset| ethread + offset);
let client_id_layout = types.layout("_CLIENT_ID").ok();
let tid = cid_base.and_then(|base| {
client_id_layout
.as_ref()
.and_then(|layout| layout.field_offset("UniqueThread").ok())
.and_then(|offset| memory.read::<u64>(base + offset).ok())
});
let pid = cid_base.and_then(|base| {
client_id_layout
.as_ref()
.and_then(|layout| layout.field_offset("UniqueProcess").ok())
.and_then(|offset| memory.read::<u64>(base + offset).ok())
});
let read_ethread_ptr = |field: &str| -> Option<VirtAddr> {
ethread_layout
.field_offset(field)
.ok()
.and_then(|offset| memory.read::<VirtAddr>(ethread + offset).ok())
.filter(|addr| !addr.is_zero())
};
let read_kthread_ptr = |field: &str| -> Option<VirtAddr> {
kthread_layout
.field_offset(field)
.ok()
.and_then(|offset| memory.read::<VirtAddr>(kthread + offset).ok())
.filter(|addr| !addr.is_zero())
};
let read_kthread_u8 = |field: &str| -> Option<u8> {
kthread_layout
.field_offset(field)
.ok()
.and_then(|offset| memory.read::<u8>(kthread + offset).ok())
};
let eprocess = read_kthread_ptr("Process")
.or_else(|| read_ethread_ptr("ThreadsProcess"))
.or_else(|| {
ethread_layout
.field_offset("ProcessFastRef")
.ok()
.and_then(|offset| memory.read::<u64>(ethread + offset).ok())
.map(|raw| VirtAddr(raw & !0xf))
.filter(|addr| !addr.is_zero())
});
let owner: Option<ProcessInfo> = match (process_hint, eprocess) {
(Some(_), _) | (None, Some(_)) => None,
(None, None) => guest.enumerate_processes().ok().and_then(|processes| {
processes
.into_iter()
.find(|process| pid.is_some_and(|pid| process.pid == pid))
}),
};
let owner = process_hint.or(owner.as_ref());
let process_name = owner
.map(|process| process.name.clone())
.or_else(|| eprocess.and_then(|eprocess| guest.process_name_at(eprocess)))
.or_else(|| (pid == Some(0)).then(|| "Idle".to_string()));
Ok(ThreadInfo {
ethread,
kthread,
tid,
pid: pid.or_else(|| owner.map(|process| process.pid)),
process_name,
eprocess: eprocess.or_else(|| owner.map(|process| process.eprocess_va)),
state: read_kthread_u8("State"),
wait_reason: read_kthread_u8("WaitReason"),
priority: read_kthread_u8("Priority"),
base_priority: read_kthread_u8("BasePriority"),
wait_irql: read_kthread_u8("WaitIrql"),
kernel_stack_resident: read_kthread_u8("KernelStackResident").map(|value| value != 0),
start_address: read_ethread_ptr("StartAddress"),
win32_start_address: read_ethread_ptr("Win32StartAddress"),
teb: read_ethread_ptr("Teb"),
kernel_stack: read_kthread_ptr("KernelStack"),
stack_base: read_kthread_ptr("StackBase"),
stack_limit: read_kthread_ptr("StackLimit"),
trap_frame: read_kthread_ptr("TrapFrame"),
pending_irps: self.thread_pending_irps(&memory, ðread_layout, ethread),
})
}
fn thread_pending_irps(
&self,
memory: &impl MemoryOps<VirtAddr>,
ethread_layout: &TypeInfo,
ethread: VirtAddr,
) -> Option<Vec<VirtAddr>> {
let irp_list_offset = ethread_layout.field_offset("IrpList").ok()?;
let irp_layout = self.guest.as_ref()?.ntoskrnl.types().layout("_IRP").ok()?;
let thread_list_entry_offset = irp_layout.field_offset("ThreadListEntry").ok()?;
let head = ethread + irp_list_offset;
let mut current: VirtAddr = memory.read(head).ok()?;
let mut irps = Vec::new();
let mut visited = HashSet::new();
for _ in 0..256 {
if current.is_zero() || current == head || !visited.insert(current.0) {
break;
}
irps.push(current - thread_list_entry_offset);
current = match memory.read(current) {
Ok(next) => next,
Err(_) => break,
};
}
Some(irps)
}
pub fn current_windows_thread_for_processor(&self, processor: u16) -> Result<ThreadInfo> {
let guest = self.guest()?;
let memory = guest.ntoskrnl.memory();
let prcb_current_thread_offset = guest
.ntoskrnl
.types()
.layout("_KPRCB")?
.field_offset("CurrentThread")?;
let ethread_tcb_offset = guest
.ntoskrnl
.types()
.layout("_ETHREAD")?
.field_offset("Tcb")
.unwrap_or(0);
let processor_block = guest.ntoskrnl.symbol("KiProcessorBlock")?.address();
let prcb: VirtAddr = memory.read(processor_block + (processor as u64) * 8)?;
if prcb.is_zero() {
return Err(Error::DebugInfo(format!(
"KiProcessorBlock[{}] is null",
processor
)));
}
let kthread: VirtAddr = memory.read(prcb + prcb_current_thread_offset)?;
if kthread.is_zero() {
return Err(Error::DebugInfo(format!(
"KPRCB.CurrentThread for processor {} is null",
processor
)));
}
self.thread_info_from_ethread(kthread - ethread_tcb_offset)
}
pub fn enumerate_vad_regions_for_process_info(
&self,
process: &ProcessInfo,
) -> Result<Vec<MemoryRegionInfo>> {
let guest = self.guest()?;
let memory = self.address_space(process.dtb);
let types = guest.ntoskrnl.types_in(process.dtb);
let eprocess_layout = guest.ntoskrnl.types().layout("_EPROCESS")?;
let vad_root_base = process.eprocess_va + eprocess_layout.field_offset("VadRoot")?;
let root = self.read_vad_root(process.dtb, vad_root_base)?;
if root.is_zero() {
return Ok(Vec::new());
}
let vad_layout = types
.layout("_MMVAD_SHORT")
.or_else(|_| types.layout("_MMVAD"))?;
let vad_node_offset = vad_layout.field_offset("VadNode").unwrap_or(0);
let node_layout = types.layout("_RTL_BALANCED_NODE")?;
let left_offset = node_layout.field_offset("Left").unwrap_or(0);
let right_offset = node_layout.field_offset("Right").unwrap_or(8);
let flags_layout = types.layout("_MMVAD_FLAGS").ok();
let modules = guest.process_modules(process).unwrap_or_default();
let mut regions = Vec::new();
let mut stack = vec![(root, 0usize)];
let mut visited = HashSet::new();
while let Some((node, level)) = stack.pop() {
if node.is_zero() || !visited.insert(node.0) || visited.len() > 65536 {
continue;
}
let left = Self::canonical_vad_link(memory.read::<VirtAddr>(node + left_offset)?);
let right = Self::canonical_vad_link(memory.read::<VirtAddr>(node + right_offset)?);
if !right.is_zero() {
stack.push((right, level.saturating_add(1)));
}
if !left.is_zero() {
stack.push((left, level.saturating_add(1)));
}
let vad = node - vad_node_offset;
if let Some(region) = self.read_vad_region(
&memory,
&vad_layout,
flags_layout.as_deref(),
node,
level,
vad,
&modules,
) {
regions.push(region);
}
}
regions.sort_by_key(|region| region.start.0);
Ok(regions)
}
fn read_vad_root(&self, dtb: Dtb, vad_root_base: VirtAddr) -> Result<VirtAddr> {
let memory = self.address_space(dtb);
let types = self.guest()?.ntoskrnl.types_in(dtb);
if let Ok(tree_layout) = types.layout("_RTL_AVL_TREE")
&& let Ok(root_offset) = tree_layout.field_offset("Root")
{
let root: VirtAddr = memory.read(vad_root_base + root_offset)?;
return Ok(Self::canonical_vad_link(root));
}
let root: VirtAddr = memory.read(vad_root_base)?;
Ok(Self::canonical_vad_link(root))
}
fn canonical_vad_link(link: VirtAddr) -> VirtAddr {
VirtAddr(link.0 & !0xf)
}
fn read_integer_field(
memory: &impl MemoryOps<VirtAddr>,
layout: &TypeInfo,
base: VirtAddr,
field: &str,
) -> Option<u64> {
let info = layout.fields.get(field)?;
let address = base + info.offset as u64;
match info.size {
1 => memory.read::<u8>(address).ok().map(u64::from),
2 => memory.read::<u16>(address).ok().map(u64::from),
4 => memory.read::<u32>(address).ok().map(u64::from),
8 => memory.read::<u64>(address).ok(),
_ => None,
}
}
fn bitfield_value(layout: &TypeInfo, field: &str, raw: u64) -> Option<u64> {
let info = layout.fields.get(field)?;
let ParsedType::Bitfield { pos, len, .. } = info.type_data else {
return None;
};
let mask = if len >= 64 {
u64::MAX
} else {
(1u64 << len) - 1
};
Some((raw >> pos) & mask)
}
fn vad_flags_base_offset(vad_layout: &TypeInfo) -> Option<u64> {
vad_layout
.field_offset("u")
.or_else(|_| vad_layout.field_offset("u1"))
.or_else(|_| vad_layout.field_offset("VadFlags"))
.ok()
}
fn read_vad_region(
&self,
memory: &impl MemoryOps<VirtAddr>,
vad_layout: &TypeInfo,
flags_layout: Option<&TypeInfo>,
node_address: VirtAddr,
level: usize,
vad: VirtAddr,
modules: &[ModuleInfo],
) -> Option<MemoryRegionInfo> {
let start_low = Self::read_integer_field(memory, vad_layout, vad, "StartingVpn")?;
let end_low = Self::read_integer_field(memory, vad_layout, vad, "EndingVpn")?;
let start_high =
Self::read_integer_field(memory, vad_layout, vad, "StartingVpnHigh").unwrap_or(0);
let end_high =
Self::read_integer_field(memory, vad_layout, vad, "EndingVpnHigh").unwrap_or(0);
let start_vpn = start_low | (start_high << 32);
let end_vpn = end_low | (end_high << 32);
let start = VirtAddr(start_vpn.checked_shl(12)?);
let end = VirtAddr(end_vpn.checked_add(1)?.checked_shl(12)?);
let flags = Self::vad_flags_base_offset(vad_layout)
.and_then(|offset| memory.read::<u32>(vad + offset).ok())
.map(u64::from);
let protection = flags
.zip(flags_layout)
.and_then(|(raw, layout)| Self::bitfield_value(layout, "Protection", raw));
let vad_type = flags
.zip(flags_layout)
.and_then(|(raw, layout)| Self::bitfield_value(layout, "VadType", raw));
let private_memory = flags.zip(flags_layout).and_then(|(raw, layout)| {
Self::bitfield_value(layout, "PrivateMemory", raw).map(|v| v != 0)
});
let commit_charge = flags
.zip(flags_layout)
.and_then(|(raw, layout)| Self::bitfield_value(layout, "CommitCharge", raw));
let details = modules
.iter()
.find(|module| {
module.base_address.0 >= start.0 && module.base_address.0 < end.0
|| start.0 >= module.base_address.0 && start.0 < module.end_address().0
})
.map(|module| module.name.clone());
Some(MemoryRegionInfo {
node_address,
level,
start,
end,
protection,
vad_type,
private_memory,
commit_charge,
details,
})
}
pub fn current_symbol_index(&self) -> SymbolIndex {
self.symbols.merged_symbol_index(Some(self.current_dtb()))
}
pub fn current_types_index(&self) -> SymbolIndex {
self.symbols.merged_types_index(Some(self.current_dtb()))
}
pub fn current_enums_index(&self) -> SymbolIndex {
self.symbols.merged_enum_index(Some(self.current_dtb()))
}
pub fn startup_message_data(&mut self) -> Result<StartupMessage> {
let degraded = self.phys.dmp_info().is_some();
let guest = self.guest()?;
let build_number: u16 = match guest
.ntoskrnl
.symbol("NtBuildNumber")
.and_then(|s| s.read())
{
Ok(v) => v,
Err(_) if degraded => 0,
Err(e) => return Err(e),
};
let base_address = guest.ntoskrnl.base_address;
let loaded_module_list = match guest
.ntoskrnl
.symbol("PsLoadedModuleList")
.and_then(|s| s.read())
{
Ok(v) => v,
Err(_) if degraded => VirtAddr(0),
Err(e) => return Err(e),
};
Ok(StartupMessage {
build_number: Value(build_number),
base_address,
loaded_module_list,
})
}
pub fn pte_traverse(&self, address: VirtAddr) -> Result<PteWalk> {
let memory = self.current_process()?.memory();
let dtb = self.current_dtb();
let pte_base: VirtAddr = self.guest()?.ntoskrnl.symbol("MmPteBase")?.read()?;
let pde_base = pte_base + (pte_base.0 >> 9 & 0x7FFFFFFFFF);
let ppe_base = pde_base + (pde_base.0 >> 9 & 0x3FFFFFFF);
let pxe_base = ppe_base + (ppe_base.0 >> 9 & 0x1FFFFF);
let pxe_address = VirtAddr(pxe_base.0 + (((address.0 >> 39) & 0x1FF) << 3));
let ppe_address = VirtAddr((((address.0 & 0xFFFFFFFFFFFF) >> 30) << 3) + ppe_base.0);
let pxe_value: PageTableEntry = memory.read(pxe_address)?;
let ppe_value: PageTableEntry = memory.read(ppe_address)?;
let pxe = PteLevel {
name: "PXE".into(),
address: pxe_address,
value: pxe_value,
};
let ppe = PteLevel {
name: "PPE".into(),
address: ppe_address,
value: ppe_value,
};
if ppe_value.is_large_page() {
return Ok(PteWalk {
address,
dtb,
pxe,
ppe,
pde: None,
pte: None,
});
}
let pde_address = VirtAddr((((address.0 & 0xFFFFFFFFFFFF) >> 21) << 3) + pde_base.0);
let pde_value: PageTableEntry = memory.read(pde_address)?;
let pde = PteLevel {
name: "PDE".into(),
address: pde_address,
value: pde_value,
};
if pde_value.is_large_page() {
return Ok(PteWalk {
address,
dtb,
pxe,
ppe,
pde: Some(pde),
pte: None,
});
}
let pte_address = VirtAddr(((address.0 & 0xFFFFFFFFFFFF) >> 12) << 3) + pte_base.0;
let pte_value: PageTableEntry = memory.read(pte_address)?;
let pte = PteLevel {
name: "PTE".into(),
address: pte_address,
value: pte_value,
};
Ok(PteWalk {
address,
dtb,
pxe,
ppe,
pde: Some(pde),
pte: Some(pte),
})
}
}
impl fmt::Display for PteLevel {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let flags = format!("pfn {:<5x} {:>11}", self.value.pfn(), self.value.flags());
write!(
f,
"{} at {:X}\ncontains {:016X}\n{}",
self.name,
self.address,
Value(self.value.0),
flags
)
}
}
#[cfg(test)]
mod tests {
use super::{
ListTermination, ObjectNameLayout, ThreadInfo, bounded_list_walk,
decode_token_privilege_bitmaps, select_object_header_candidate, select_thread_process_dtb,
thread_owner_matches,
};
use crate::error::Error;
use crate::guest::ProcessInfo;
use crate::session::{Session, session_over_memory};
use crate::types::VirtAddr;
fn target_with_lines(symbols: &[(&str, u32)], records: &[(u32, Option<u32>, u32)]) -> Session {
let session = session_over_memory(0x1000, &[0u8; 0x80]);
let dtb = session.target.current_dtb();
session.target.symbols.set_kernel(Some(1), dtb);
session
.target
.symbols
.inject_module_for_test(1, Vec::new(), symbols);
session.target.symbols.inject_source_lines_for_test(
1,
dtb,
VirtAddr(0x1000),
0x1000,
"driver.c",
records,
);
session
}
#[test]
fn windbg_pseudo_register_slots_and_exception_code() {
let mut session = session_over_memory(0x1000, &[0u8; 0x80]);
let target = &session.target;
assert_eq!(target.builtin_variable_value("t0"), Some(0));
assert_eq!(target.builtin_variable_value("$t19"), Some(0));
assert_eq!(target.builtin_variable_value("t20"), None);
assert_eq!(target.builtin_variable_value("t007"), None);
assert_eq!(target.builtin_variable_value("ta"), None);
assert_eq!(target.builtin_variable_value("exr_code"), None);
session.target.last_exception_code = Some(0x8000_0003);
assert_eq!(
session.target.builtin_variable_value("exr_code"),
Some(0x8000_0003)
);
}
#[test]
fn post_prologue_address_lands_on_the_first_statement() {
let session = target_with_lines(
&[("DriverEntry", 0)],
&[(0, Some(0x0e), 123), (0x0e, Some(0x1b), 132)],
);
assert_eq!(
session
.target
.post_prologue_address(session.target.current_dtb(), VirtAddr(0x1000)),
Some(VirtAddr(0x100e))
);
}
#[test]
fn post_prologue_address_refuses_to_leave_the_function() {
let session = target_with_lines(
&[("DriverEntry", 0), ("Unload", 0x10)],
&[(0, Some(0x10), 123), (0x10, Some(0x20), 200)],
);
assert_eq!(
session
.target
.post_prologue_address(session.target.current_dtb(), VirtAddr(0x1000)),
None
);
let single = target_with_lines(&[("DriverEntry", 0)], &[(0, None, 123)]);
assert_eq!(
single
.target
.post_prologue_address(single.target.current_dtb(), VirtAddr(0x1000)),
None
);
}
fn sample_thread() -> ThreadInfo {
ThreadInfo {
ethread: VirtAddr(0xffff_8000_0000_1000),
kthread: VirtAddr(0xffff_8000_0000_1100),
tid: Some(0x44),
pid: Some(0x22),
process_name: Some("sample.exe".to_string()),
eprocess: Some(VirtAddr(0xffff_8000_0000_2000)),
state: Some(5),
wait_reason: Some(6),
priority: Some(13),
base_priority: Some(8),
wait_irql: Some(2),
kernel_stack_resident: Some(true),
start_address: Some(VirtAddr(0x7ff7_0000_1234)),
win32_start_address: Some(VirtAddr(0x7ff7_0000_5678)),
teb: Some(VirtAddr(0x0000_0000_0050_0000)),
kernel_stack: Some(VirtAddr(0xffff_f000_0000_8000)),
stack_base: Some(VirtAddr(0xffff_f000_0000_a000)),
stack_limit: Some(VirtAddr(0xffff_f000_0000_6000)),
trap_frame: Some(VirtAddr(0xffff_f000_0000_7000)),
pending_irps: Some(vec![VirtAddr(0xffff_8000_0000_3000)]),
}
}
#[test]
fn thread_pseudo_registers_are_case_insensitive_and_optional() {
let mut thread = sample_thread();
assert_eq!(
thread.pseudo_register_value("TrapFrame"),
thread.trap_frame.map(|addr| addr.0)
);
thread.teb = None;
assert_eq!(thread.pseudo_register_value("TEB"), None);
assert_eq!(thread.pseudo_register_value("unknown"), None);
}
#[test]
fn cr3_normalization_strips_pcid_and_reserved_bits() {
assert_eq!(
super::Target::normalize_cr3(0xffff_8123_4567_8abc),
0x000f_8123_4567_8000
);
}
#[test]
fn owning_process_selection_prefers_eprocess_identity() {
let thread = sample_thread();
let same_pid_wrong_process = ProcessInfo {
pid: thread.pid.unwrap(),
name: "reused.exe".into(),
dtb: 0x1111_0000,
eprocess_va: VirtAddr(0xffff_8000_0000_9999),
};
let owner = ProcessInfo {
pid: 0x99,
name: "sample.exe".into(),
dtb: 0x2222_0000,
eprocess_va: thread.eprocess.unwrap(),
};
assert!(!thread_owner_matches(&thread, &same_pid_wrong_process));
assert!(thread_owner_matches(&thread, &owner));
assert_eq!(
select_thread_process_dtb(
&thread,
Some(&same_pid_wrong_process),
&[same_pid_wrong_process.clone(), owner.clone()],
0x3333_0000,
),
Some(0x2222_0000)
);
}
#[test]
fn diagnostic_list_walk_honors_bound() {
let (links, termination) =
bounded_list_walk(VirtAddr(0), 2, |address| Ok(VirtAddr(address.0 + 1)));
assert_eq!(links, vec![VirtAddr(1), VirtAddr(2)]);
assert_eq!(termination, ListTermination::Bound);
}
#[test]
fn diagnostic_list_walk_flags_non_head_cycle() {
let (links, termination) = bounded_list_walk(VirtAddr(0), 8, |address| {
Ok(match address.0 {
0 => VirtAddr(1),
1 => VirtAddr(2),
_ => VirtAddr(1),
})
});
assert_eq!(links, vec![VirtAddr(1), VirtAddr(2)]);
assert_eq!(termination, ListTermination::Cycle(VirtAddr(1)));
}
#[test]
fn diagnostic_list_walk_preserves_corrupt_read_error() {
let (links, termination) = bounded_list_walk(VirtAddr(0), 8, |address| match address.0 {
0 => Ok(VirtAddr(1)),
1 => Ok(VirtAddr(2)),
_ => Err(Error::DebugInfo("synthetic bad flink".into())),
});
assert_eq!(links, vec![VirtAddr(1), VirtAddr(2)]);
assert_eq!(
termination,
ListTermination::Corrupt("synthetic bad flink".into())
);
}
#[test]
fn token_privilege_bitmaps_preserve_ids_and_attributes() {
let privileges =
decode_token_privilege_bitmaps((1 << 2) | (1 << 20), 1 << 20, (1 << 2) | (1 << 20));
assert_eq!(
privileges,
vec![
super::PrivilegeInfo {
luid: 2,
attributes: super::SE_PRIVILEGE_ENABLED_BY_DEFAULT,
},
super::PrivilegeInfo {
luid: 20,
attributes: super::SE_PRIVILEGE_ENABLED_BY_DEFAULT
| super::SE_PRIVILEGE_ENABLED,
},
]
);
}
#[test]
fn direct_object_header_wins_when_only_it_has_a_valid_type() {
let input = VirtAddr(0x1000);
assert_eq!(
select_object_header_candidate(
input,
0x30,
Some(VirtAddr(0x0fd0)),
Some(input),
false,
true,
),
Some((input, VirtAddr(0x1030), "header"))
);
}
#[test]
fn ambiguous_readable_object_headers_are_rejected() {
assert_eq!(
select_object_header_candidate(
VirtAddr(0x1000),
0x30,
Some(VirtAddr(0x0fd0)),
Some(VirtAddr(0x1000)),
false,
false,
),
None
);
}
#[test]
fn object_name_info_address_accounts_for_creator_info_only() {
let layout = ObjectNameLayout {
body_offset: 0x30,
info_mask_offset: 0x1a,
creator_info_size: Some(0x20),
name_info_size: 0x20,
name_offset: 0x08,
};
let header = VirtAddr(0x1000);
assert_eq!(layout.name_info_address(header, 0x00).unwrap(), None);
assert_eq!(
layout.name_info_address(header, 0x02).unwrap(),
Some(VirtAddr(0x0fe0))
);
assert_eq!(
layout.name_info_address(header, 0x03).unwrap(),
Some(VirtAddr(0x0fc0))
);
assert_eq!(
layout.name_info_address(header, 0x7e).unwrap(),
Some(VirtAddr(0x0fe0))
);
}
#[test]
fn object_name_info_address_requires_present_creator_layout() {
let layout = ObjectNameLayout {
body_offset: 0x30,
info_mask_offset: 0x1a,
creator_info_size: None,
name_info_size: 0x20,
name_offset: 0x08,
};
let error = layout
.name_info_address(VirtAddr(0x1000), 0x03)
.unwrap_err();
assert!(matches!(
&error,
Error::StructNotFound(name)
if name == "_OBJECT_HEADER_CREATOR_INFO"
));
}
}