mirror of
https://github.com/microsoft/onefuzz.git
synced 2025-06-13 02:28:10 +00:00
Move Win32 debugger Target to own module (#151)
This commit is contained in:
@ -8,9 +8,8 @@
|
|||||||
#![allow(clippy::redundant_closure)]
|
#![allow(clippy::redundant_closure)]
|
||||||
#![allow(clippy::redundant_clone)]
|
#![allow(clippy::redundant_clone)]
|
||||||
use std::{
|
use std::{
|
||||||
collections::{hash_map, HashMap},
|
collections::HashMap,
|
||||||
ffi::OsString,
|
ffi::OsString,
|
||||||
fs,
|
|
||||||
mem::MaybeUninit,
|
mem::MaybeUninit,
|
||||||
os::windows::process::CommandExt,
|
os::windows::process::CommandExt,
|
||||||
path::{Path, PathBuf},
|
path::{Path, PathBuf},
|
||||||
@ -19,32 +18,29 @@ use std::{
|
|||||||
|
|
||||||
use anyhow::{Context, Result};
|
use anyhow::{Context, Result};
|
||||||
use log::{error, trace};
|
use log::{error, trace};
|
||||||
use win_util::{check_winapi, file, last_os_error, process};
|
use win_util::{check_winapi, last_os_error, process};
|
||||||
use winapi::{
|
use winapi::{
|
||||||
shared::{
|
shared::{
|
||||||
minwindef::{DWORD, FALSE, LPCVOID, LPVOID, TRUE},
|
minwindef::{DWORD, FALSE, LPCVOID, TRUE},
|
||||||
winerror::ERROR_SEM_TIMEOUT,
|
winerror::ERROR_SEM_TIMEOUT,
|
||||||
},
|
},
|
||||||
um::{
|
um::{
|
||||||
dbghelp::ADDRESS64,
|
dbghelp::ADDRESS64,
|
||||||
debugapi::{ContinueDebugEvent, WaitForDebugEvent},
|
debugapi::{ContinueDebugEvent, WaitForDebugEvent},
|
||||||
errhandlingapi::GetLastError,
|
errhandlingapi::GetLastError,
|
||||||
handleapi::CloseHandle,
|
|
||||||
minwinbase::{
|
minwinbase::{
|
||||||
CREATE_PROCESS_DEBUG_INFO, CREATE_THREAD_DEBUG_INFO, EXCEPTION_BREAKPOINT,
|
CREATE_PROCESS_DEBUG_INFO, CREATE_THREAD_DEBUG_INFO, EXCEPTION_BREAKPOINT,
|
||||||
EXCEPTION_DEBUG_INFO, EXCEPTION_SINGLE_STEP, EXIT_PROCESS_DEBUG_INFO,
|
EXCEPTION_DEBUG_INFO, EXCEPTION_SINGLE_STEP, EXIT_PROCESS_DEBUG_INFO,
|
||||||
EXIT_THREAD_DEBUG_INFO, LOAD_DLL_DEBUG_INFO, RIP_INFO, UNLOAD_DLL_DEBUG_INFO,
|
EXIT_THREAD_DEBUG_INFO, LOAD_DLL_DEBUG_INFO, RIP_INFO, UNLOAD_DLL_DEBUG_INFO,
|
||||||
},
|
},
|
||||||
winbase::{DebugSetProcessKillOnExit, DEBUG_ONLY_THIS_PROCESS, INFINITE},
|
winbase::{DebugSetProcessKillOnExit, DEBUG_ONLY_THIS_PROCESS, INFINITE},
|
||||||
winnt::{
|
winnt::{DBG_CONTINUE, DBG_EXCEPTION_NOT_HANDLED, HANDLE},
|
||||||
DBG_CONTINUE, DBG_EXCEPTION_NOT_HANDLED, HANDLE, IMAGE_FILE_MACHINE_AMD64,
|
|
||||||
IMAGE_FILE_MACHINE_I386,
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
use crate::target::Target;
|
||||||
use crate::{
|
use crate::{
|
||||||
dbghelp::{self, FrameContext, SymInfo},
|
dbghelp::{self, SymInfo},
|
||||||
debug_event::{DebugEvent, DebugEventInfo},
|
debug_event::{DebugEvent, DebugEventInfo},
|
||||||
stack,
|
stack,
|
||||||
};
|
};
|
||||||
@ -57,7 +53,7 @@ const STATUS_WX86_BREAKPOINT: u32 = ::winapi::shared::ntstatus::STATUS_WX86_BREA
|
|||||||
pub struct BreakpointId(pub u32);
|
pub struct BreakpointId(pub u32);
|
||||||
|
|
||||||
#[derive(Copy, Clone)]
|
#[derive(Copy, Clone)]
|
||||||
enum StepState {
|
pub(crate) enum StepState {
|
||||||
Breakpoint { pc: u64 },
|
Breakpoint { pc: u64 },
|
||||||
SingleStep,
|
SingleStep,
|
||||||
}
|
}
|
||||||
@ -69,12 +65,26 @@ pub enum BreakpointType {
|
|||||||
StepOut { rsp: u64 },
|
StepOut { rsp: u64 },
|
||||||
}
|
}
|
||||||
|
|
||||||
struct ModuleBreakpoint {
|
pub(crate) struct ModuleBreakpoint {
|
||||||
rva: u64,
|
rva: u64,
|
||||||
kind: BreakpointType,
|
kind: BreakpointType,
|
||||||
id: BreakpointId,
|
id: BreakpointId,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl ModuleBreakpoint {
|
||||||
|
pub fn rva(&self) -> u64 {
|
||||||
|
self.rva
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn kind(&self) -> BreakpointType {
|
||||||
|
self.kind
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn id(&self) -> BreakpointId {
|
||||||
|
self.id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[allow(unused)]
|
#[allow(unused)]
|
||||||
struct UnresolvedBreakpoint {
|
struct UnresolvedBreakpoint {
|
||||||
sym: String,
|
sym: String,
|
||||||
@ -85,7 +95,7 @@ struct UnresolvedBreakpoint {
|
|||||||
/// A breakpoint for a specific target. We can say it is bound because we know exactly
|
/// A breakpoint for a specific target. We can say it is bound because we know exactly
|
||||||
/// where to set it, but it might disabled.
|
/// where to set it, but it might disabled.
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
struct Breakpoint {
|
pub struct Breakpoint {
|
||||||
/// The address of the breakpoint.
|
/// The address of the breakpoint.
|
||||||
ip: u64,
|
ip: u64,
|
||||||
|
|
||||||
@ -102,6 +112,70 @@ struct Breakpoint {
|
|||||||
id: BreakpointId,
|
id: BreakpointId,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Breakpoint {
|
||||||
|
pub fn new(
|
||||||
|
ip: u64,
|
||||||
|
kind: BreakpointType,
|
||||||
|
enabled: bool,
|
||||||
|
original_byte: Option<u8>,
|
||||||
|
hit_count: usize,
|
||||||
|
id: BreakpointId,
|
||||||
|
) -> Self {
|
||||||
|
Breakpoint {
|
||||||
|
ip,
|
||||||
|
kind,
|
||||||
|
enabled,
|
||||||
|
original_byte,
|
||||||
|
hit_count,
|
||||||
|
id,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn ip(&self) -> u64 {
|
||||||
|
self.ip
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn kind(&self) -> BreakpointType {
|
||||||
|
self.kind
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn set_kind(&mut self, kind: BreakpointType) {
|
||||||
|
self.kind = kind;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn enabled(&self) -> bool {
|
||||||
|
self.enabled
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_enabled(&mut self, enabled: bool) {
|
||||||
|
self.enabled = enabled;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn original_byte(&self) -> Option<u8> {
|
||||||
|
self.original_byte
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn set_original_byte(&mut self, original_byte: Option<u8>) {
|
||||||
|
self.original_byte = original_byte;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn hit_count(&self) -> usize {
|
||||||
|
self.hit_count
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn increment_hit_count(&mut self) {
|
||||||
|
self.hit_count += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn id(&self) -> BreakpointId {
|
||||||
|
self.id
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn set_id(&mut self, id: BreakpointId) {
|
||||||
|
self.id = id;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub struct StackFrame {
|
pub struct StackFrame {
|
||||||
return_address: u64,
|
return_address: u64,
|
||||||
stack_pointer: u64,
|
stack_pointer: u64,
|
||||||
@ -124,458 +198,6 @@ impl StackFrame {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
struct Module {
|
|
||||||
path: PathBuf,
|
|
||||||
file_handle: HANDLE,
|
|
||||||
base_address: u64,
|
|
||||||
image_size: u32,
|
|
||||||
machine: Machine,
|
|
||||||
|
|
||||||
// Track if we need to call SymLoadModule for the dll.
|
|
||||||
sym_module_loaded: bool,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Module {
|
|
||||||
fn new(module_handle: HANDLE, base_address: u64) -> Result<Self> {
|
|
||||||
let path = file::get_path_from_handle(module_handle).unwrap_or_else(|e| {
|
|
||||||
error!("Error getting path from file handle: {}", e);
|
|
||||||
"???".into()
|
|
||||||
});
|
|
||||||
|
|
||||||
let image_details = get_image_details(&path)?;
|
|
||||||
|
|
||||||
Ok(Self {
|
|
||||||
path,
|
|
||||||
file_handle: module_handle,
|
|
||||||
base_address,
|
|
||||||
image_size: image_details.image_size,
|
|
||||||
machine: image_details.machine,
|
|
||||||
sym_module_loaded: false,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
fn sym_load_module(&mut self, process_handle: HANDLE) -> Result<()> {
|
|
||||||
if !self.sym_module_loaded {
|
|
||||||
let dbghelp = dbghelp::lock()?;
|
|
||||||
|
|
||||||
dbghelp.sym_load_module(
|
|
||||||
process_handle,
|
|
||||||
self.file_handle,
|
|
||||||
&self.path,
|
|
||||||
self.base_address,
|
|
||||||
self.image_size,
|
|
||||||
)?;
|
|
||||||
|
|
||||||
self.sym_module_loaded = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn name(&self) -> &Path {
|
|
||||||
// Unwrap guaranteed by construction, we always have a filename.
|
|
||||||
self.path.file_stem().unwrap().as_ref()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Drop for Module {
|
|
||||||
fn drop(&mut self) {
|
|
||||||
unsafe { CloseHandle(self.file_handle) };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct Target {
|
|
||||||
process_id: DWORD,
|
|
||||||
process_handle: HANDLE,
|
|
||||||
current_thread_handle: HANDLE,
|
|
||||||
saw_initial_bp: bool,
|
|
||||||
saw_initial_wow64_bp: bool,
|
|
||||||
|
|
||||||
// Track if we need to call SymInitialize for the process and if we need to notify
|
|
||||||
// dbghelp about loaded/unloaded dlls.
|
|
||||||
sym_initialized: bool,
|
|
||||||
exited: bool,
|
|
||||||
|
|
||||||
thread_handles: fnv::FnvHashMap<DWORD, HANDLE>,
|
|
||||||
|
|
||||||
// We cache the current thread context for possible repeated queries and modifications.
|
|
||||||
// We want to call GetThreadContext once, then call SetThreadContext (if necessary) before
|
|
||||||
// resuming. Calling Get/Set/Get/Set doesn't seem to work because the second Get doesn't
|
|
||||||
// see any the changes made in the Set call.
|
|
||||||
current_context: Option<FrameContext>,
|
|
||||||
|
|
||||||
// True if we need to set the thread context before resuming.
|
|
||||||
context_is_modified: bool,
|
|
||||||
|
|
||||||
// Key is base address (which also happens to be the HANDLE).
|
|
||||||
modules: fnv::FnvHashMap<u64, Module>,
|
|
||||||
|
|
||||||
breakpoints: fnv::FnvHashMap<u64, Breakpoint>,
|
|
||||||
|
|
||||||
// Map of thread to stepping state (e.g. breakpoint address to restore breakpoints)
|
|
||||||
single_step: fnv::FnvHashMap<HANDLE, StepState>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Target {
|
|
||||||
fn new(
|
|
||||||
process_id: DWORD,
|
|
||||||
thread_id: DWORD,
|
|
||||||
process_handle: HANDLE,
|
|
||||||
thread_handle: HANDLE,
|
|
||||||
) -> Self {
|
|
||||||
let mut thread_handles = fnv::FnvHashMap::default();
|
|
||||||
thread_handles.insert(thread_id, thread_handle);
|
|
||||||
|
|
||||||
Self {
|
|
||||||
process_id,
|
|
||||||
current_thread_handle: thread_handle,
|
|
||||||
process_handle,
|
|
||||||
saw_initial_bp: false,
|
|
||||||
saw_initial_wow64_bp: false,
|
|
||||||
sym_initialized: false,
|
|
||||||
exited: false,
|
|
||||||
thread_handles,
|
|
||||||
current_context: None,
|
|
||||||
context_is_modified: false,
|
|
||||||
modules: fnv::FnvHashMap::default(),
|
|
||||||
breakpoints: fnv::FnvHashMap::default(),
|
|
||||||
single_step: fnv::FnvHashMap::default(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn modules(&self) -> hash_map::Iter<u64, Module> {
|
|
||||||
self.modules.iter()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn initial_bp(&mut self, load_symbols: bool) -> Result<()> {
|
|
||||||
self.saw_initial_bp = true;
|
|
||||||
|
|
||||||
if load_symbols || !self.breakpoints.is_empty() {
|
|
||||||
self.sym_initialize()?;
|
|
||||||
|
|
||||||
for (_, module) in self.modules.iter_mut() {
|
|
||||||
if let Err(e) = module.sym_load_module(self.process_handle) {
|
|
||||||
error!("Error loading symbols: {}", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn sym_initialize(&mut self) -> Result<()> {
|
|
||||||
if !self.sym_initialized {
|
|
||||||
let dbghelp = dbghelp::lock()?;
|
|
||||||
if let Err(e) = dbghelp.sym_initialize(self.process_handle) {
|
|
||||||
error!("Error in SymInitializeW: {}", e);
|
|
||||||
|
|
||||||
if let Err(e) = dbghelp.sym_cleanup(self.process_handle) {
|
|
||||||
error!("Error in SymCleanup: {}", e);
|
|
||||||
}
|
|
||||||
|
|
||||||
return Err(e);
|
|
||||||
}
|
|
||||||
|
|
||||||
for (_, module) in self.modules.iter_mut() {
|
|
||||||
if let Err(e) = module.sym_load_module(self.process_handle) {
|
|
||||||
error!(
|
|
||||||
"Error loading symbols for module {}: {}",
|
|
||||||
module.path.display(),
|
|
||||||
e
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
self.sym_initialized = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Register the module loaded at `base_address`, returning the module name.
|
|
||||||
fn load_module(&mut self, module_handle: HANDLE, base_address: u64) -> Result<Option<PathBuf>> {
|
|
||||||
let mut module = Module::new(module_handle, base_address)?;
|
|
||||||
|
|
||||||
trace!(
|
|
||||||
"Loading module {} at {:x}",
|
|
||||||
module.name().display(),
|
|
||||||
base_address
|
|
||||||
);
|
|
||||||
|
|
||||||
if module.machine == Machine::X64 && process::is_wow64_process(self.process_handle) {
|
|
||||||
// We ignore native dlls in wow64 processes.
|
|
||||||
return Ok(None);
|
|
||||||
}
|
|
||||||
|
|
||||||
let module_name = module.name().to_owned();
|
|
||||||
if self.sym_initialized {
|
|
||||||
if let Err(e) = module.sym_load_module(self.process_handle) {
|
|
||||||
error!("Error loading symbols: {}", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let base_address = module.base_address;
|
|
||||||
if let Some(old_value) = self.modules.insert(base_address, module) {
|
|
||||||
error!(
|
|
||||||
"Existing module {} replace at base_address {}",
|
|
||||||
old_value.path.display(),
|
|
||||||
base_address
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(Some(module_name))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn unload_module(&mut self, base_address: u64) {
|
|
||||||
// Drop the module and remove any breakpoints.
|
|
||||||
if let Some(module) = self.modules.remove(&base_address) {
|
|
||||||
let image_size = module.image_size as u64;
|
|
||||||
self.breakpoints
|
|
||||||
.retain(|&ip, _| ip < base_address || ip >= base_address + image_size);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn apply_absolute_breakpoint(
|
|
||||||
&mut self,
|
|
||||||
address: u64,
|
|
||||||
kind: BreakpointType,
|
|
||||||
id: BreakpointId,
|
|
||||||
) -> Result<()> {
|
|
||||||
let original_byte: u8 = process::read_memory(self.process_handle, address as LPVOID)?;
|
|
||||||
|
|
||||||
self.breakpoints
|
|
||||||
.entry(address)
|
|
||||||
.and_modify(|e| {
|
|
||||||
e.kind = kind;
|
|
||||||
e.enabled = true;
|
|
||||||
e.original_byte = Some(original_byte);
|
|
||||||
e.id = id;
|
|
||||||
})
|
|
||||||
.or_insert(Breakpoint {
|
|
||||||
ip: address,
|
|
||||||
kind,
|
|
||||||
enabled: true,
|
|
||||||
original_byte: Some(original_byte),
|
|
||||||
hit_count: 0,
|
|
||||||
id,
|
|
||||||
});
|
|
||||||
|
|
||||||
write_instruction_byte(self.process_handle, address, 0xcc)?;
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn apply_module_breakpoints(
|
|
||||||
&mut self,
|
|
||||||
base_address: u64,
|
|
||||||
breakpoints: &[ModuleBreakpoint],
|
|
||||||
) -> Result<()> {
|
|
||||||
if breakpoints.is_empty() {
|
|
||||||
return Ok(());
|
|
||||||
}
|
|
||||||
|
|
||||||
// We want to set every breakpoint for the module at once. We'll read the just the
|
|
||||||
// memory we need to do that, so find the min and max rva to compute how much memory
|
|
||||||
// to read and update in the remote process.
|
|
||||||
let (min, max) = breakpoints
|
|
||||||
.iter()
|
|
||||||
.fold((u64::max_value(), u64::min_value()), |acc, bp| {
|
|
||||||
(acc.0.min(bp.rva), acc.1.max(bp.rva))
|
|
||||||
});
|
|
||||||
|
|
||||||
// Add 1 to include the final byte.
|
|
||||||
let region_size = (max - min)
|
|
||||||
.checked_add(1)
|
|
||||||
.ok_or_else(|| anyhow::anyhow!("overflow in region size trying to set breakpoints"))?
|
|
||||||
as usize;
|
|
||||||
let remote_address = base_address.checked_add(min).ok_or_else(|| {
|
|
||||||
anyhow::anyhow!("overflow in remote address calculation trying to set breakpoints")
|
|
||||||
})? as LPVOID;
|
|
||||||
|
|
||||||
let mut buffer: Vec<u8> = Vec::with_capacity(region_size);
|
|
||||||
unsafe {
|
|
||||||
buffer.set_len(region_size);
|
|
||||||
}
|
|
||||||
process::read_memory_array(self.process_handle, remote_address, &mut buffer[..])?;
|
|
||||||
|
|
||||||
for mbp in breakpoints {
|
|
||||||
let ip = base_address + mbp.rva;
|
|
||||||
let offset = (mbp.rva - min) as usize;
|
|
||||||
|
|
||||||
trace!("Setting breakpoint at {:x}", ip);
|
|
||||||
|
|
||||||
let bp = Breakpoint {
|
|
||||||
ip,
|
|
||||||
kind: mbp.kind,
|
|
||||||
enabled: true,
|
|
||||||
original_byte: Some(buffer[offset]),
|
|
||||||
hit_count: 0,
|
|
||||||
id: mbp.id,
|
|
||||||
};
|
|
||||||
|
|
||||||
buffer[offset] = 0xcc;
|
|
||||||
|
|
||||||
self.breakpoints.insert(ip, bp);
|
|
||||||
}
|
|
||||||
|
|
||||||
process::write_memory_slice(self.process_handle, remote_address, &buffer[..])?;
|
|
||||||
process::flush_instruction_cache(self.process_handle, remote_address, region_size)?;
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn prepare_to_resume(&mut self) -> Result<()> {
|
|
||||||
if let Some(context) = self.current_context.take() {
|
|
||||||
if self.context_is_modified {
|
|
||||||
context.set_thread_context(self.current_thread_handle)?;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
self.context_is_modified = false;
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn ensure_current_context(&mut self) -> Result<()> {
|
|
||||||
if self.current_context.is_none() {
|
|
||||||
self.current_context = Some(dbghelp::get_thread_frame(
|
|
||||||
self.process_handle,
|
|
||||||
self.current_thread_handle,
|
|
||||||
)?);
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn get_current_context(&mut self) -> Result<&FrameContext> {
|
|
||||||
self.ensure_current_context()?;
|
|
||||||
Ok(self.current_context.as_ref().unwrap())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn get_current_context_mut(&mut self) -> Result<&mut FrameContext> {
|
|
||||||
self.ensure_current_context()?;
|
|
||||||
|
|
||||||
// Assume the caller will modify the context. When it is modified,
|
|
||||||
// we must set it before resuming the target.
|
|
||||||
self.context_is_modified = true;
|
|
||||||
Ok(self.current_context.as_mut().unwrap())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn read_register_u64(&mut self, reg: iced_x86::Register) -> Result<u64> {
|
|
||||||
let current_context = self.get_current_context()?;
|
|
||||||
Ok(current_context.get_register_u64(reg))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn read_flags_register(&mut self) -> Result<u32> {
|
|
||||||
let current_context = self.get_current_context()?;
|
|
||||||
Ok(current_context.get_flags())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Handle a breakpoint that we set (as opposed to a breakpoint in user code, e.g.
|
|
||||||
/// assertion.)
|
|
||||||
///
|
|
||||||
/// Return the breakpoint id if it should be reported to the client.
|
|
||||||
fn handle_breakpoint(&mut self, pc: u64) -> Result<Option<BreakpointId>> {
|
|
||||||
enum HandleBreakpoint {
|
|
||||||
User(BreakpointId, bool),
|
|
||||||
StepOut(u64),
|
|
||||||
}
|
|
||||||
|
|
||||||
let handle_breakpoint = {
|
|
||||||
let bp = self.breakpoints.get_mut(&pc).unwrap();
|
|
||||||
|
|
||||||
bp.hit_count += 1;
|
|
||||||
|
|
||||||
write_instruction_byte(self.process_handle, bp.ip, bp.original_byte.unwrap())?;
|
|
||||||
|
|
||||||
match bp.kind {
|
|
||||||
BreakpointType::OneTime => {
|
|
||||||
bp.enabled = false;
|
|
||||||
bp.original_byte = None;
|
|
||||||
|
|
||||||
// We are clearing the breakpoint after hitting it, so we do not need
|
|
||||||
// to single step.
|
|
||||||
HandleBreakpoint::User(bp.id, false)
|
|
||||||
}
|
|
||||||
|
|
||||||
BreakpointType::Counter => {
|
|
||||||
// Single step so we can restore the breakpoint after stepping.
|
|
||||||
HandleBreakpoint::User(bp.id, true)
|
|
||||||
}
|
|
||||||
|
|
||||||
BreakpointType::StepOut { rsp } => HandleBreakpoint::StepOut(rsp),
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
let context = self.get_current_context_mut()?;
|
|
||||||
context.set_program_counter(pc);
|
|
||||||
|
|
||||||
// We need to single step if we need to restore the breakpoint.
|
|
||||||
let single_step = match handle_breakpoint {
|
|
||||||
HandleBreakpoint::User(_, single_step) => single_step,
|
|
||||||
|
|
||||||
// Single step only when in a recursive call, which is inferred when the current
|
|
||||||
// stack pointer (from context) is less then at the target to step out from (rsp).
|
|
||||||
// Note this only works if the stack grows down.
|
|
||||||
HandleBreakpoint::StepOut(rsp) => rsp > context.stack_pointer(),
|
|
||||||
};
|
|
||||||
|
|
||||||
if single_step {
|
|
||||||
context.set_single_step(true);
|
|
||||||
self.single_step
|
|
||||||
.insert(self.current_thread_handle, StepState::Breakpoint { pc });
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(match handle_breakpoint {
|
|
||||||
HandleBreakpoint::User(id, _) => Some(id),
|
|
||||||
HandleBreakpoint::StepOut(_) => None,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
fn handle_single_step(&mut self, step_state: StepState) -> Result<()> {
|
|
||||||
match step_state {
|
|
||||||
StepState::Breakpoint { pc } => {
|
|
||||||
write_instruction_byte(self.process_handle, pc, 0xcc)?;
|
|
||||||
}
|
|
||||||
_ => {}
|
|
||||||
}
|
|
||||||
|
|
||||||
self.single_step.remove(&self.current_thread_handle);
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn prepare_to_step(&mut self) -> Result<bool> {
|
|
||||||
// Don't change the reason we're single stepping on this thread if
|
|
||||||
// we previously set the reason (e.g. so we would restore a breakpoint).
|
|
||||||
self.single_step
|
|
||||||
.entry(self.current_thread_handle)
|
|
||||||
.or_insert(StepState::SingleStep);
|
|
||||||
|
|
||||||
let context = self.get_current_context_mut()?;
|
|
||||||
context.set_single_step(true);
|
|
||||||
|
|
||||||
Ok(true)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn set_exited(&mut self) -> Result<()> {
|
|
||||||
self.exited = true;
|
|
||||||
|
|
||||||
if self.sym_initialized {
|
|
||||||
let dbghelp = dbghelp::lock()?;
|
|
||||||
dbghelp.sym_cleanup(self.process_handle)?;
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn write_instruction_byte(process_handle: HANDLE, ip: u64, b: u8) -> Result<()> {
|
|
||||||
let orig_byte = [b; 1];
|
|
||||||
let remote_address = ip as LPVOID;
|
|
||||||
process::write_memory_slice(process_handle, remote_address, &orig_byte)?;
|
|
||||||
process::flush_instruction_cache(process_handle, remote_address, orig_byte.len())?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
#[rustfmt::skip]
|
#[rustfmt::skip]
|
||||||
#[allow(clippy::trivially_copy_pass_by_ref)]
|
#[allow(clippy::trivially_copy_pass_by_ref)]
|
||||||
pub trait DebugEventHandler {
|
pub trait DebugEventHandler {
|
||||||
@ -782,7 +404,7 @@ impl Debugger {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn run(&mut self, callbacks: &mut impl DebugEventHandler) -> Result<()> {
|
pub fn run(&mut self, callbacks: &mut impl DebugEventHandler) -> Result<()> {
|
||||||
while !self.target.exited {
|
while !self.target.exited() {
|
||||||
// Poll between every event so a client can add generic logic instead needing to
|
// Poll between every event so a client can add generic logic instead needing to
|
||||||
// handle every possible event.
|
// handle every possible event.
|
||||||
callbacks.on_poll(self);
|
callbacks.on_poll(self);
|
||||||
@ -798,9 +420,9 @@ impl Debugger {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn quit_debugging(&self) {
|
pub fn quit_debugging(&self) {
|
||||||
if !self.target.exited {
|
if !self.target.exited() {
|
||||||
trace!("timeout - terminating pid: {}", self.target.process_id);
|
trace!("timeout - terminating pid: {}", self.target.process_id());
|
||||||
process::terminate(self.target.process_handle);
|
process::terminate(self.target.process_handle());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -808,13 +430,9 @@ impl Debugger {
|
|||||||
let mut continue_status = DBG_CONTINUE;
|
let mut continue_status = DBG_CONTINUE;
|
||||||
|
|
||||||
if let DebugEventInfo::CreateThread(info) = de.info() {
|
if let DebugEventInfo::CreateThread(info) = de.info() {
|
||||||
self.target.current_thread_handle = info.hThread;
|
self.target.create_new_thread(info.hThread, de.thread_id());
|
||||||
self.target
|
|
||||||
.thread_handles
|
|
||||||
.insert(de.thread_id(), info.hThread);
|
|
||||||
} else {
|
} else {
|
||||||
self.target.current_thread_handle =
|
self.target.set_current_thread(de.thread_id());
|
||||||
*self.target.thread_handles.get(&de.thread_id()).unwrap();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
match de.info() {
|
match de.info() {
|
||||||
@ -832,7 +450,7 @@ impl Debugger {
|
|||||||
// breakpoint notification from the OS. Otherwise we may set
|
// breakpoint notification from the OS. Otherwise we may set
|
||||||
// breakpoints in startup code before the debugger is properly
|
// breakpoints in startup code before the debugger is properly
|
||||||
// initialized.
|
// initialized.
|
||||||
if self.target.saw_initial_bp {
|
if self.target.saw_initial_bp() {
|
||||||
self.apply_module_breakpoints(module_name, base_address)
|
self.apply_module_breakpoints(module_name, base_address)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -874,7 +492,7 @@ impl Debugger {
|
|||||||
|
|
||||||
DebugEventInfo::ExitThread(info) => {
|
DebugEventInfo::ExitThread(info) => {
|
||||||
callbacks.on_exit_thread(self, *info);
|
callbacks.on_exit_thread(self, *info);
|
||||||
self.target.thread_handles.remove(&de.thread_id());
|
self.target.exit_thread(de.thread_id());
|
||||||
}
|
}
|
||||||
|
|
||||||
DebugEventInfo::OutputDebugString(info) => {
|
DebugEventInfo::OutputDebugString(info) => {
|
||||||
@ -882,7 +500,7 @@ impl Debugger {
|
|||||||
let length = info.nDebugStringLength.saturating_sub(1) as usize;
|
let length = info.nDebugStringLength.saturating_sub(1) as usize;
|
||||||
if info.fUnicode != 0 {
|
if info.fUnicode != 0 {
|
||||||
if let Ok(message) = process::read_wide_string(
|
if let Ok(message) = process::read_wide_string(
|
||||||
self.target.process_handle,
|
self.target.process_handle(),
|
||||||
info.lpDebugStringData as LPCVOID,
|
info.lpDebugStringData as LPCVOID,
|
||||||
length,
|
length,
|
||||||
) {
|
) {
|
||||||
@ -890,7 +508,7 @@ impl Debugger {
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if let Ok(message) = process::read_narrow_string(
|
if let Ok(message) = process::read_narrow_string(
|
||||||
self.target.process_handle,
|
self.target.process_handle(),
|
||||||
info.lpDebugStringData as LPCVOID,
|
info.lpDebugStringData as LPCVOID,
|
||||||
length,
|
length,
|
||||||
) {
|
) {
|
||||||
@ -921,7 +539,7 @@ impl Debugger {
|
|||||||
) {
|
) {
|
||||||
Some(DebuggerNotification::InitialBreak) => {
|
Some(DebuggerNotification::InitialBreak) => {
|
||||||
let modules = {
|
let modules = {
|
||||||
self.target.saw_initial_bp = true;
|
self.target.set_saw_initial_bp();
|
||||||
let load_symbols = !self.symbolic_breakpoints.is_empty();
|
let load_symbols = !self.symbolic_breakpoints.is_empty();
|
||||||
self.target.initial_bp(load_symbols)?;
|
self.target.initial_bp(load_symbols)?;
|
||||||
self.target
|
self.target
|
||||||
@ -935,7 +553,7 @@ impl Debugger {
|
|||||||
Ok(DBG_CONTINUE)
|
Ok(DBG_CONTINUE)
|
||||||
}
|
}
|
||||||
Some(DebuggerNotification::InitialWow64Break) => {
|
Some(DebuggerNotification::InitialWow64Break) => {
|
||||||
self.target.saw_initial_wow64_bp = true;
|
self.target.set_saw_initial_wow64_bp();
|
||||||
Ok(DBG_CONTINUE)
|
Ok(DBG_CONTINUE)
|
||||||
}
|
}
|
||||||
Some(DebuggerNotification::Clr) => Ok(DBG_CONTINUE),
|
Some(DebuggerNotification::Clr) => Ok(DBG_CONTINUE),
|
||||||
@ -950,7 +568,7 @@ impl Debugger {
|
|||||||
Ok(DBG_CONTINUE)
|
Ok(DBG_CONTINUE)
|
||||||
}
|
}
|
||||||
None => {
|
None => {
|
||||||
let process_handle = self.target.process_handle;
|
let process_handle = self.target.process_handle();
|
||||||
Ok(callbacks.on_exception(self, info, process_handle))
|
Ok(callbacks.on_exception(self, info, process_handle))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -972,7 +590,7 @@ impl Debugger {
|
|||||||
Ok(dbghelp) => {
|
Ok(dbghelp) => {
|
||||||
for bp in unresolved_breakpoints {
|
for bp in unresolved_breakpoints {
|
||||||
match dbghelp.sym_from_name(
|
match dbghelp.sym_from_name(
|
||||||
self.target.process_handle,
|
self.target.process_handle(),
|
||||||
module_name.as_ref(),
|
module_name.as_ref(),
|
||||||
&bp.sym,
|
&bp.sym,
|
||||||
) {
|
) {
|
||||||
@ -1014,19 +632,19 @@ impl Debugger {
|
|||||||
// be too aggressive in dealing with failures.
|
// be too aggressive in dealing with failures.
|
||||||
let resolve_symbols = self.target.sym_initialize().is_ok();
|
let resolve_symbols = self.target.sym_initialize().is_ok();
|
||||||
return stack::get_stack(
|
return stack::get_stack(
|
||||||
self.target.process_handle,
|
self.target.process_handle(),
|
||||||
self.target.current_thread_handle,
|
self.target.current_thread_handle(),
|
||||||
resolve_symbols,
|
resolve_symbols,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_symbol(&self, pc: u64) -> Result<SymInfo> {
|
pub fn get_symbol(&self, pc: u64) -> Result<SymInfo> {
|
||||||
let dbghelp = dbghelp::lock()?;
|
let dbghelp = dbghelp::lock()?;
|
||||||
dbghelp.sym_from_inline_context(self.target.process_handle, pc, 0)
|
dbghelp.sym_from_inline_context(self.target.process_handle(), pc, 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_current_thread_id(&self) -> u64 {
|
pub fn get_current_thread_id(&self) -> u64 {
|
||||||
self.target.current_thread_handle as u64
|
self.target.current_thread_handle() as u64
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn read_register_u64(&mut self, reg: iced_x86::Register) -> Result<u64> {
|
pub fn read_register_u64(&mut self, reg: iced_x86::Register) -> Result<u64> {
|
||||||
@ -1042,7 +660,7 @@ impl Debugger {
|
|||||||
remote_address: LPCVOID,
|
remote_address: LPCVOID,
|
||||||
buf: &mut [T],
|
buf: &mut [T],
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
process::read_memory_array(self.target.process_handle, remote_address, buf)?;
|
process::read_memory_array(self.target.process_handle(), remote_address, buf)?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1052,8 +670,8 @@ impl Debugger {
|
|||||||
let mut return_address = ADDRESS64::default();
|
let mut return_address = ADDRESS64::default();
|
||||||
let mut stack_pointer = ADDRESS64::default();
|
let mut stack_pointer = ADDRESS64::default();
|
||||||
dbghlp.stackwalk_ex(
|
dbghlp.stackwalk_ex(
|
||||||
self.target.process_handle,
|
self.target.process_handle(),
|
||||||
self.target.current_thread_handle,
|
self.target.current_thread_handle(),
|
||||||
|_frame_context, frame| {
|
|_frame_context, frame| {
|
||||||
return_address = frame.AddrReturn;
|
return_address = frame.AddrReturn;
|
||||||
stack_pointer = frame.AddrStack;
|
stack_pointer = frame.AddrStack;
|
||||||
@ -1116,8 +734,8 @@ fn is_debugger_notification(
|
|||||||
|
|
||||||
// The first EXCEPTION_BREAKPOINT is sent to debuggers like us.
|
// The first EXCEPTION_BREAKPOINT is sent to debuggers like us.
|
||||||
EXCEPTION_BREAKPOINT => {
|
EXCEPTION_BREAKPOINT => {
|
||||||
if target.saw_initial_bp {
|
if target.saw_initial_bp() {
|
||||||
if target.breakpoints.contains_key(&exception_address) {
|
if target.breakpoint_set_at_addr(exception_address) {
|
||||||
Some(DebuggerNotification::Breakpoint(exception_address))
|
Some(DebuggerNotification::Breakpoint(exception_address))
|
||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
@ -1130,7 +748,7 @@ fn is_debugger_notification(
|
|||||||
// We may see a second breakpoint (STATUS_WX86_BREAKPOINT) when debugging a
|
// We may see a second breakpoint (STATUS_WX86_BREAKPOINT) when debugging a
|
||||||
// WoW64 process, this is also a debugger notification, not a real breakpoint.
|
// WoW64 process, this is also a debugger notification, not a real breakpoint.
|
||||||
STATUS_WX86_BREAKPOINT => {
|
STATUS_WX86_BREAKPOINT => {
|
||||||
if target.saw_initial_wow64_bp {
|
if target.saw_initial_wow64_bp() {
|
||||||
None
|
None
|
||||||
} else {
|
} else {
|
||||||
Some(DebuggerNotification::InitialWow64Break)
|
Some(DebuggerNotification::InitialWow64Break)
|
||||||
@ -1138,7 +756,7 @@ fn is_debugger_notification(
|
|||||||
}
|
}
|
||||||
|
|
||||||
EXCEPTION_SINGLE_STEP => {
|
EXCEPTION_SINGLE_STEP => {
|
||||||
if let Some(&step_state) = target.single_step.get(&target.current_thread_handle) {
|
if let Some(step_state) = target.single_step(target.current_thread_handle()) {
|
||||||
Some(DebuggerNotification::SingleStep(step_state))
|
Some(DebuggerNotification::SingleStep(step_state))
|
||||||
} else {
|
} else {
|
||||||
// Unexpected single step - could be a logic bug in the debugger or less
|
// Unexpected single step - could be a logic bug in the debugger or less
|
||||||
@ -1152,37 +770,3 @@ fn is_debugger_notification(
|
|||||||
_ => None,
|
_ => None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Copy, Clone, Debug, PartialEq)]
|
|
||||||
enum Machine {
|
|
||||||
Unknown,
|
|
||||||
X64,
|
|
||||||
X86,
|
|
||||||
}
|
|
||||||
|
|
||||||
struct ImageDetails {
|
|
||||||
image_size: u32,
|
|
||||||
machine: Machine,
|
|
||||||
}
|
|
||||||
|
|
||||||
fn get_image_details(path: &Path) -> Result<ImageDetails> {
|
|
||||||
let file = fs::File::open(path)?;
|
|
||||||
let map = unsafe { memmap::Mmap::map(&file)? };
|
|
||||||
|
|
||||||
let header = goblin::pe::header::Header::parse(&map)?;
|
|
||||||
let image_size = header
|
|
||||||
.optional_header
|
|
||||||
.map(|h| h.windows_fields.size_of_image)
|
|
||||||
.ok_or_else(|| anyhow::anyhow!("Missing optional header in PE image"))?;
|
|
||||||
|
|
||||||
let machine = match header.coff_header.machine {
|
|
||||||
IMAGE_FILE_MACHINE_AMD64 => Machine::X64,
|
|
||||||
IMAGE_FILE_MACHINE_I386 => Machine::X86,
|
|
||||||
_ => Machine::Unknown,
|
|
||||||
};
|
|
||||||
|
|
||||||
Ok(ImageDetails {
|
|
||||||
image_size,
|
|
||||||
machine,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
@ -7,3 +7,4 @@ pub mod dbghelp;
|
|||||||
pub mod debug_event;
|
pub mod debug_event;
|
||||||
pub mod debugger;
|
pub mod debugger;
|
||||||
pub mod stack;
|
pub mod stack;
|
||||||
|
mod target;
|
||||||
|
567
src/agent/debugger/src/target.rs
Normal file
567
src/agent/debugger/src/target.rs
Normal file
@ -0,0 +1,567 @@
|
|||||||
|
// Copyright (c) Microsoft Corporation.
|
||||||
|
// Licensed under the MIT License.
|
||||||
|
|
||||||
|
use std::{
|
||||||
|
collections::hash_map,
|
||||||
|
fs,
|
||||||
|
path::{Path, PathBuf},
|
||||||
|
};
|
||||||
|
|
||||||
|
use anyhow::Result;
|
||||||
|
use log::{error, trace};
|
||||||
|
use win_util::{file, process};
|
||||||
|
use winapi::{
|
||||||
|
shared::minwindef::{DWORD, LPVOID},
|
||||||
|
um::{
|
||||||
|
handleapi::CloseHandle,
|
||||||
|
winnt::{HANDLE, IMAGE_FILE_MACHINE_AMD64, IMAGE_FILE_MACHINE_I386},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
dbghelp::{self, FrameContext},
|
||||||
|
debugger::{Breakpoint, BreakpointId, BreakpointType, ModuleBreakpoint, StepState},
|
||||||
|
};
|
||||||
|
|
||||||
|
pub struct Module {
|
||||||
|
path: PathBuf,
|
||||||
|
file_handle: HANDLE,
|
||||||
|
base_address: u64,
|
||||||
|
image_size: u32,
|
||||||
|
machine: Machine,
|
||||||
|
|
||||||
|
// Track if we need to call SymLoadModule for the dll.
|
||||||
|
sym_module_loaded: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Module {
|
||||||
|
fn new(module_handle: HANDLE, base_address: u64) -> Result<Self> {
|
||||||
|
let path = file::get_path_from_handle(module_handle).unwrap_or_else(|e| {
|
||||||
|
error!("Error getting path from file handle: {}", e);
|
||||||
|
"???".into()
|
||||||
|
});
|
||||||
|
|
||||||
|
let image_details = get_image_details(&path)?;
|
||||||
|
|
||||||
|
Ok(Self {
|
||||||
|
path,
|
||||||
|
file_handle: module_handle,
|
||||||
|
base_address,
|
||||||
|
image_size: image_details.image_size,
|
||||||
|
machine: image_details.machine,
|
||||||
|
sym_module_loaded: false,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn sym_load_module(&mut self, process_handle: HANDLE) -> Result<()> {
|
||||||
|
if !self.sym_module_loaded {
|
||||||
|
let dbghelp = dbghelp::lock()?;
|
||||||
|
|
||||||
|
dbghelp.sym_load_module(
|
||||||
|
process_handle,
|
||||||
|
self.file_handle,
|
||||||
|
&self.path,
|
||||||
|
self.base_address,
|
||||||
|
self.image_size,
|
||||||
|
)?;
|
||||||
|
|
||||||
|
self.sym_module_loaded = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn name(&self) -> &Path {
|
||||||
|
// Unwrap guaranteed by construction, we always have a filename.
|
||||||
|
self.path.file_stem().unwrap().as_ref()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Drop for Module {
|
||||||
|
fn drop(&mut self) {
|
||||||
|
unsafe { CloseHandle(self.file_handle) };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct Target {
|
||||||
|
process_id: DWORD,
|
||||||
|
process_handle: HANDLE,
|
||||||
|
current_thread_handle: HANDLE,
|
||||||
|
saw_initial_bp: bool,
|
||||||
|
saw_initial_wow64_bp: bool,
|
||||||
|
|
||||||
|
// Track if we need to call SymInitialize for the process and if we need to notify
|
||||||
|
// dbghelp about loaded/unloaded dlls.
|
||||||
|
sym_initialized: bool,
|
||||||
|
exited: bool,
|
||||||
|
|
||||||
|
thread_handles: fnv::FnvHashMap<DWORD, HANDLE>,
|
||||||
|
|
||||||
|
// We cache the current thread context for possible repeated queries and modifications.
|
||||||
|
// We want to call GetThreadContext once, then call SetThreadContext (if necessary) before
|
||||||
|
// resuming. Calling Get/Set/Get/Set doesn't seem to work because the second Get doesn't
|
||||||
|
// see any the changes made in the Set call.
|
||||||
|
current_context: Option<FrameContext>,
|
||||||
|
|
||||||
|
// True if we need to set the thread context before resuming.
|
||||||
|
context_is_modified: bool,
|
||||||
|
|
||||||
|
// Key is base address (which also happens to be the HANDLE).
|
||||||
|
modules: fnv::FnvHashMap<u64, Module>,
|
||||||
|
|
||||||
|
breakpoints: fnv::FnvHashMap<u64, Breakpoint>,
|
||||||
|
|
||||||
|
// Map of thread to stepping state (e.g. breakpoint address to restore breakpoints)
|
||||||
|
single_step: fnv::FnvHashMap<HANDLE, StepState>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Target {
|
||||||
|
pub fn new(
|
||||||
|
process_id: DWORD,
|
||||||
|
thread_id: DWORD,
|
||||||
|
process_handle: HANDLE,
|
||||||
|
thread_handle: HANDLE,
|
||||||
|
) -> Self {
|
||||||
|
let mut thread_handles = fnv::FnvHashMap::default();
|
||||||
|
thread_handles.insert(thread_id, thread_handle);
|
||||||
|
|
||||||
|
Self {
|
||||||
|
process_id,
|
||||||
|
current_thread_handle: thread_handle,
|
||||||
|
process_handle,
|
||||||
|
saw_initial_bp: false,
|
||||||
|
saw_initial_wow64_bp: false,
|
||||||
|
sym_initialized: false,
|
||||||
|
exited: false,
|
||||||
|
thread_handles,
|
||||||
|
current_context: None,
|
||||||
|
context_is_modified: false,
|
||||||
|
modules: fnv::FnvHashMap::default(),
|
||||||
|
breakpoints: fnv::FnvHashMap::default(),
|
||||||
|
single_step: fnv::FnvHashMap::default(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn current_thread_handle(&self) -> HANDLE {
|
||||||
|
self.current_thread_handle
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn create_new_thread(&mut self, thread_handle: HANDLE, thread_id: DWORD) {
|
||||||
|
self.current_thread_handle = thread_handle;
|
||||||
|
self.thread_handles.insert(thread_id, thread_handle);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_current_thread(&mut self, thread_id: DWORD) {
|
||||||
|
self.current_thread_handle = *self.thread_handles.get(&thread_id).unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn exit_thread(&mut self, thread_id: DWORD) {
|
||||||
|
self.thread_handles.remove(&thread_id);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn process_handle(&self) -> HANDLE {
|
||||||
|
self.process_handle
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn process_id(&self) -> DWORD {
|
||||||
|
self.process_id
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn saw_initial_wow64_bp(&self) -> bool {
|
||||||
|
self.saw_initial_wow64_bp
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_saw_initial_wow64_bp(&mut self) {
|
||||||
|
self.saw_initial_wow64_bp = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn saw_initial_bp(&self) -> bool {
|
||||||
|
self.saw_initial_bp
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_saw_initial_bp(&mut self) {
|
||||||
|
self.saw_initial_bp = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn exited(&self) -> bool {
|
||||||
|
self.exited
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn modules(&self) -> hash_map::Iter<u64, Module> {
|
||||||
|
self.modules.iter()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn initial_bp(&mut self, load_symbols: bool) -> Result<()> {
|
||||||
|
self.saw_initial_bp = true;
|
||||||
|
|
||||||
|
if load_symbols || !self.breakpoints.is_empty() {
|
||||||
|
self.sym_initialize()?;
|
||||||
|
|
||||||
|
for (_, module) in self.modules.iter_mut() {
|
||||||
|
if let Err(e) = module.sym_load_module(self.process_handle) {
|
||||||
|
error!("Error loading symbols: {}", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn breakpoint_set_at_addr(&self, address: u64) -> bool {
|
||||||
|
self.breakpoints.contains_key(&address)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn single_step(&self, thread_handle: HANDLE) -> Option<StepState> {
|
||||||
|
self.single_step.get(&thread_handle).cloned()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn sym_initialize(&mut self) -> Result<()> {
|
||||||
|
if !self.sym_initialized {
|
||||||
|
let dbghelp = dbghelp::lock()?;
|
||||||
|
if let Err(e) = dbghelp.sym_initialize(self.process_handle) {
|
||||||
|
error!("Error in SymInitializeW: {}", e);
|
||||||
|
|
||||||
|
if let Err(e) = dbghelp.sym_cleanup(self.process_handle) {
|
||||||
|
error!("Error in SymCleanup: {}", e);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Err(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (_, module) in self.modules.iter_mut() {
|
||||||
|
if let Err(e) = module.sym_load_module(self.process_handle) {
|
||||||
|
error!(
|
||||||
|
"Error loading symbols for module {}: {}",
|
||||||
|
module.path.display(),
|
||||||
|
e
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
self.sym_initialized = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Register the module loaded at `base_address`, returning the module name.
|
||||||
|
pub fn load_module(
|
||||||
|
&mut self,
|
||||||
|
module_handle: HANDLE,
|
||||||
|
base_address: u64,
|
||||||
|
) -> Result<Option<PathBuf>> {
|
||||||
|
let mut module = Module::new(module_handle, base_address)?;
|
||||||
|
|
||||||
|
trace!(
|
||||||
|
"Loading module {} at {:x}",
|
||||||
|
module.name().display(),
|
||||||
|
base_address
|
||||||
|
);
|
||||||
|
|
||||||
|
if module.machine == Machine::X64 && process::is_wow64_process(self.process_handle) {
|
||||||
|
// We ignore native dlls in wow64 processes.
|
||||||
|
return Ok(None);
|
||||||
|
}
|
||||||
|
|
||||||
|
let module_name = module.name().to_owned();
|
||||||
|
if self.sym_initialized {
|
||||||
|
if let Err(e) = module.sym_load_module(self.process_handle) {
|
||||||
|
error!("Error loading symbols: {}", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let base_address = module.base_address;
|
||||||
|
if let Some(old_value) = self.modules.insert(base_address, module) {
|
||||||
|
error!(
|
||||||
|
"Existing module {} replace at base_address {}",
|
||||||
|
old_value.path.display(),
|
||||||
|
base_address
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(Some(module_name))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn unload_module(&mut self, base_address: u64) {
|
||||||
|
// Drop the module and remove any breakpoints.
|
||||||
|
if let Some(module) = self.modules.remove(&base_address) {
|
||||||
|
let image_size = module.image_size as u64;
|
||||||
|
self.breakpoints
|
||||||
|
.retain(|&ip, _| ip < base_address || ip >= base_address + image_size);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn apply_absolute_breakpoint(
|
||||||
|
&mut self,
|
||||||
|
address: u64,
|
||||||
|
kind: BreakpointType,
|
||||||
|
id: BreakpointId,
|
||||||
|
) -> Result<()> {
|
||||||
|
let original_byte: u8 = process::read_memory(self.process_handle, address as LPVOID)?;
|
||||||
|
|
||||||
|
self.breakpoints
|
||||||
|
.entry(address)
|
||||||
|
.and_modify(|bp| {
|
||||||
|
bp.set_kind(kind);
|
||||||
|
bp.set_enabled(true);
|
||||||
|
bp.set_original_byte(Some(original_byte));
|
||||||
|
bp.set_id(id);
|
||||||
|
})
|
||||||
|
.or_insert(Breakpoint::new(
|
||||||
|
address,
|
||||||
|
kind,
|
||||||
|
/*enabled*/ true,
|
||||||
|
/*original_byte*/ Some(original_byte),
|
||||||
|
/*hit_count*/ 0,
|
||||||
|
id,
|
||||||
|
));
|
||||||
|
|
||||||
|
write_instruction_byte(self.process_handle, address, 0xcc)?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn apply_module_breakpoints(
|
||||||
|
&mut self,
|
||||||
|
base_address: u64,
|
||||||
|
breakpoints: &[ModuleBreakpoint],
|
||||||
|
) -> Result<()> {
|
||||||
|
if breakpoints.is_empty() {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
// We want to set every breakpoint for the module at once. We'll read the just the
|
||||||
|
// memory we need to do that, so find the min and max rva to compute how much memory
|
||||||
|
// to read and update in the remote process.
|
||||||
|
let (min, max) = breakpoints
|
||||||
|
.iter()
|
||||||
|
.fold((u64::max_value(), u64::min_value()), |acc, bp| {
|
||||||
|
(acc.0.min(bp.rva()), acc.1.max(bp.rva()))
|
||||||
|
});
|
||||||
|
|
||||||
|
// Add 1 to include the final byte.
|
||||||
|
let region_size = (max - min)
|
||||||
|
.checked_add(1)
|
||||||
|
.ok_or_else(|| anyhow::anyhow!("overflow in region size trying to set breakpoints"))?
|
||||||
|
as usize;
|
||||||
|
let remote_address = base_address.checked_add(min).ok_or_else(|| {
|
||||||
|
anyhow::anyhow!("overflow in remote address calculation trying to set breakpoints")
|
||||||
|
})? as LPVOID;
|
||||||
|
|
||||||
|
let mut buffer: Vec<u8> = Vec::with_capacity(region_size);
|
||||||
|
unsafe {
|
||||||
|
buffer.set_len(region_size);
|
||||||
|
}
|
||||||
|
process::read_memory_array(self.process_handle, remote_address, &mut buffer[..])?;
|
||||||
|
|
||||||
|
for mbp in breakpoints {
|
||||||
|
let ip = base_address + mbp.rva();
|
||||||
|
let offset = (mbp.rva() - min) as usize;
|
||||||
|
|
||||||
|
trace!("Setting breakpoint at {:x}", ip);
|
||||||
|
|
||||||
|
let bp = Breakpoint::new(
|
||||||
|
ip,
|
||||||
|
mbp.kind(),
|
||||||
|
/*enabled*/ true,
|
||||||
|
Some(buffer[offset]),
|
||||||
|
/*hit_count*/ 0,
|
||||||
|
mbp.id(),
|
||||||
|
);
|
||||||
|
|
||||||
|
buffer[offset] = 0xcc;
|
||||||
|
|
||||||
|
self.breakpoints.insert(ip, bp);
|
||||||
|
}
|
||||||
|
|
||||||
|
process::write_memory_slice(self.process_handle, remote_address, &buffer[..])?;
|
||||||
|
process::flush_instruction_cache(self.process_handle, remote_address, region_size)?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn prepare_to_resume(&mut self) -> Result<()> {
|
||||||
|
if let Some(context) = self.current_context.take() {
|
||||||
|
if self.context_is_modified {
|
||||||
|
context.set_thread_context(self.current_thread_handle)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
self.context_is_modified = false;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn ensure_current_context(&mut self) -> Result<()> {
|
||||||
|
if self.current_context.is_none() {
|
||||||
|
self.current_context = Some(dbghelp::get_thread_frame(
|
||||||
|
self.process_handle,
|
||||||
|
self.current_thread_handle,
|
||||||
|
)?);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_current_context(&mut self) -> Result<&FrameContext> {
|
||||||
|
self.ensure_current_context()?;
|
||||||
|
Ok(self.current_context.as_ref().unwrap())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_current_context_mut(&mut self) -> Result<&mut FrameContext> {
|
||||||
|
self.ensure_current_context()?;
|
||||||
|
|
||||||
|
// Assume the caller will modify the context. When it is modified,
|
||||||
|
// we must set it before resuming the target.
|
||||||
|
self.context_is_modified = true;
|
||||||
|
Ok(self.current_context.as_mut().unwrap())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn read_register_u64(&mut self, reg: iced_x86::Register) -> Result<u64> {
|
||||||
|
let current_context = self.get_current_context()?;
|
||||||
|
Ok(current_context.get_register_u64(reg))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn read_flags_register(&mut self) -> Result<u32> {
|
||||||
|
let current_context = self.get_current_context()?;
|
||||||
|
Ok(current_context.get_flags())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Handle a breakpoint that we set (as opposed to a breakpoint in user code, e.g.
|
||||||
|
/// assertion.)
|
||||||
|
///
|
||||||
|
/// Return the breakpoint id if it should be reported to the client.
|
||||||
|
pub fn handle_breakpoint(&mut self, pc: u64) -> Result<Option<BreakpointId>> {
|
||||||
|
enum HandleBreakpoint {
|
||||||
|
User(BreakpointId, bool),
|
||||||
|
StepOut(u64),
|
||||||
|
}
|
||||||
|
|
||||||
|
let handle_breakpoint = {
|
||||||
|
let bp = self.breakpoints.get_mut(&pc).unwrap();
|
||||||
|
|
||||||
|
bp.increment_hit_count();
|
||||||
|
|
||||||
|
write_instruction_byte(self.process_handle, bp.ip(), bp.original_byte().unwrap())?;
|
||||||
|
|
||||||
|
match bp.kind() {
|
||||||
|
BreakpointType::OneTime => {
|
||||||
|
bp.set_enabled(false);
|
||||||
|
bp.set_original_byte(None);
|
||||||
|
|
||||||
|
// We are clearing the breakpoint after hitting it, so we do not need
|
||||||
|
// to single step.
|
||||||
|
HandleBreakpoint::User(bp.id(), false)
|
||||||
|
}
|
||||||
|
|
||||||
|
BreakpointType::Counter => {
|
||||||
|
// Single step so we can restore the breakpoint after stepping.
|
||||||
|
HandleBreakpoint::User(bp.id(), true)
|
||||||
|
}
|
||||||
|
|
||||||
|
BreakpointType::StepOut { rsp } => HandleBreakpoint::StepOut(rsp),
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let context = self.get_current_context_mut()?;
|
||||||
|
context.set_program_counter(pc);
|
||||||
|
|
||||||
|
// We need to single step if we need to restore the breakpoint.
|
||||||
|
let single_step = match handle_breakpoint {
|
||||||
|
HandleBreakpoint::User(_, single_step) => single_step,
|
||||||
|
|
||||||
|
// Single step only when in a recursive call, which is inferred when the current
|
||||||
|
// stack pointer (from context) is less then at the target to step out from (rsp).
|
||||||
|
// Note this only works if the stack grows down.
|
||||||
|
HandleBreakpoint::StepOut(rsp) => rsp > context.stack_pointer(),
|
||||||
|
};
|
||||||
|
|
||||||
|
if single_step {
|
||||||
|
context.set_single_step(true);
|
||||||
|
self.single_step
|
||||||
|
.insert(self.current_thread_handle, StepState::Breakpoint { pc });
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(match handle_breakpoint {
|
||||||
|
HandleBreakpoint::User(id, _) => Some(id),
|
||||||
|
HandleBreakpoint::StepOut(_) => None,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn handle_single_step(&mut self, step_state: StepState) -> Result<()> {
|
||||||
|
match step_state {
|
||||||
|
StepState::Breakpoint { pc } => {
|
||||||
|
write_instruction_byte(self.process_handle, pc, 0xcc)?;
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
|
||||||
|
self.single_step.remove(&self.current_thread_handle);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn prepare_to_step(&mut self) -> Result<bool> {
|
||||||
|
// Don't change the reason we're single stepping on this thread if
|
||||||
|
// we previously set the reason (e.g. so we would restore a breakpoint).
|
||||||
|
self.single_step
|
||||||
|
.entry(self.current_thread_handle)
|
||||||
|
.or_insert(StepState::SingleStep);
|
||||||
|
|
||||||
|
let context = self.get_current_context_mut()?;
|
||||||
|
context.set_single_step(true);
|
||||||
|
|
||||||
|
Ok(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_exited(&mut self) -> Result<()> {
|
||||||
|
self.exited = true;
|
||||||
|
|
||||||
|
if self.sym_initialized {
|
||||||
|
let dbghelp = dbghelp::lock()?;
|
||||||
|
dbghelp.sym_cleanup(self.process_handle)?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Copy, Clone, Debug, PartialEq)]
|
||||||
|
enum Machine {
|
||||||
|
Unknown,
|
||||||
|
X64,
|
||||||
|
X86,
|
||||||
|
}
|
||||||
|
|
||||||
|
struct ImageDetails {
|
||||||
|
image_size: u32,
|
||||||
|
machine: Machine,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_image_details(path: &Path) -> Result<ImageDetails> {
|
||||||
|
let file = fs::File::open(path)?;
|
||||||
|
let map = unsafe { memmap::Mmap::map(&file)? };
|
||||||
|
|
||||||
|
let header = goblin::pe::header::Header::parse(&map)?;
|
||||||
|
let image_size = header
|
||||||
|
.optional_header
|
||||||
|
.map(|h| h.windows_fields.size_of_image)
|
||||||
|
.ok_or_else(|| anyhow::anyhow!("Missing optional header in PE image"))?;
|
||||||
|
|
||||||
|
let machine = match header.coff_header.machine {
|
||||||
|
IMAGE_FILE_MACHINE_AMD64 => Machine::X64,
|
||||||
|
IMAGE_FILE_MACHINE_I386 => Machine::X86,
|
||||||
|
_ => Machine::Unknown,
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(ImageDetails {
|
||||||
|
image_size,
|
||||||
|
machine,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn write_instruction_byte(process_handle: HANDLE, ip: u64, b: u8) -> Result<()> {
|
||||||
|
let orig_byte = [b; 1];
|
||||||
|
let remote_address = ip as LPVOID;
|
||||||
|
process::write_memory_slice(process_handle, remote_address, &orig_byte)?;
|
||||||
|
process::flush_instruction_cache(process_handle, remote_address, orig_byte.len())?;
|
||||||
|
Ok(())
|
||||||
|
}
|
Reference in New Issue
Block a user