windows_breakpoint_manager/
windows-breakpoint-manager.rs1use std::sync::{
2 Arc,
3 atomic::{AtomicBool, Ordering},
4};
5
6use isr::{
7 Profile,
8 cache::{IsrCache, JsonCodec},
9 macros::symbols,
10};
11use vmi::{
12 Hex, MemoryAccess, Va, VcpuId, View, VmiContext, VmiCore, VmiDriver, VmiError,
13 VmiEventResponse, VmiHandler, VmiSession,
14 arch::amd64::{Amd64, EventMonitor, EventReason, ExceptionVector, Interrupt},
15 driver::xen::VmiXenDriver,
16 os::{
17 ProcessObject, VmiOsProcess as _,
18 windows::{WindowsOs, WindowsOsExt as _},
19 },
20 utils::{
21 bpm::{Breakpoint, BreakpointController, BreakpointManager},
22 ptm::{PageTableMonitor, PageTableMonitorEvent},
23 },
24};
25use xen::XenStore;
26
27symbols! {
28 #[derive(Debug)]
29 pub struct Symbols {
30 NtCreateFile: u64,
31 NtWriteFile: u64,
32
33 PspInsertProcess: u64,
34 MmCleanProcessAddressSpace: u64,
35
36 }
46}
47
48pub struct Monitor<Driver>
49where
50 Driver: VmiDriver<Architecture = Amd64>,
51{
52 terminate_flag: Arc<AtomicBool>,
53 view: View,
54 bpm: BreakpointManager<BreakpointController<Driver>>,
55 ptm: PageTableMonitor<Driver>,
56}
57
58#[expect(non_snake_case)]
59impl<Driver> Monitor<Driver>
60where
61 Driver: VmiDriver<Architecture = Amd64>,
62{
63 pub fn new(
64 session: &VmiSession<Driver, WindowsOs<Driver>>,
65 profile: &Profile,
66 terminate_flag: Arc<AtomicBool>,
67 ) -> Result<Self, VmiError> {
68 let registers = session.registers(VcpuId(0))?;
85 let vmi = session.with_registers(®isters);
86
87 let kernel_image_base = vmi.os().kernel_image_base()?;
88 tracing::info!(%kernel_image_base);
89
90 let system_process = vmi.os().system_process()?;
97 tracing::info!(system_process = %system_process.object()?);
98
99 let root = system_process.translation_root()?;
105 tracing::info!(%root);
106
107 let symbols = Symbols::new(profile)?;
109
110 vmi.monitor_enable(EventMonitor::Interrupt(ExceptionVector::Breakpoint))?;
116 vmi.monitor_enable(EventMonitor::Singlestep)?;
117
118 let view = vmi.create_view(MemoryAccess::RWX)?;
121 vmi.switch_to_view(view)?;
122
123 let mut bpm = BreakpointManager::new();
154
155 let mut ptm = PageTableMonitor::new();
179
180 let _pause_guard = vmi.pause_guard()?;
184
185 let va_NtCreateFile = kernel_image_base + symbols.NtCreateFile;
187 let cx_NtCreateFile = (va_NtCreateFile, root);
188 let bp_NtCreateFile = Breakpoint::new(cx_NtCreateFile, view)
189 .global()
190 .with_tag("NtCreateFile");
191 bpm.insert(&vmi, bp_NtCreateFile)?;
192 ptm.monitor(&vmi, cx_NtCreateFile, view, "NtCreateFile")?;
193 tracing::info!(%va_NtCreateFile);
194
195 let va_NtWriteFile = kernel_image_base + symbols.NtWriteFile;
197 let cx_NtWriteFile = (va_NtWriteFile, root);
198 let bp_NtWriteFile = Breakpoint::new(cx_NtWriteFile, view)
199 .global()
200 .with_tag("NtWriteFile");
201 bpm.insert(&vmi, bp_NtWriteFile)?;
202 ptm.monitor(&vmi, cx_NtWriteFile, view, "NtWriteFile")?;
203 tracing::info!(%va_NtWriteFile);
204
205 let va_PspInsertProcess = kernel_image_base + symbols.PspInsertProcess;
207 let cx_PspInsertProcess = (va_PspInsertProcess, root);
208 let bp_PspInsertProcess = Breakpoint::new(cx_PspInsertProcess, view)
209 .global()
210 .with_tag("PspInsertProcess");
211 bpm.insert(&vmi, bp_PspInsertProcess)?;
212 ptm.monitor(&vmi, cx_PspInsertProcess, view, "PspInsertProcess")?;
213
214 let va_MmCleanProcessAddressSpace = kernel_image_base + symbols.MmCleanProcessAddressSpace;
216 let cx_MmCleanProcessAddressSpace = (va_MmCleanProcessAddressSpace, root);
217 let bp_MmCleanProcessAddressSpace = Breakpoint::new(cx_MmCleanProcessAddressSpace, view)
218 .global()
219 .with_tag("MmCleanProcessAddressSpace");
220 bpm.insert(&vmi, bp_MmCleanProcessAddressSpace)?;
221 ptm.monitor(
222 &vmi,
223 cx_MmCleanProcessAddressSpace,
224 view,
225 "MmCleanProcessAddressSpace",
226 )?;
227
228 Ok(Self {
229 terminate_flag,
230 view,
231 bpm,
232 ptm,
233 })
234 }
235
236 #[tracing::instrument(skip_all)]
237 fn memory_access(
238 &mut self,
239 vmi: &VmiContext<'_, Driver, WindowsOs<Driver>>,
240 ) -> Result<VmiEventResponse<Amd64>, VmiError> {
241 let memory_access = vmi.event().reason().as_memory_access();
242
243 tracing::trace!(
244 pa = %memory_access.pa,
245 va = %memory_access.va,
246 access = %memory_access.access,
247 );
248
249 if memory_access.access.contains(MemoryAccess::W) {
250 self.ptm
256 .mark_dirty_entry(memory_access.pa, self.view, vmi.event().vcpu_id());
257
258 Ok(VmiEventResponse::toggle_singlestep().and_set_view(vmi.default_view()))
259 }
260 else if memory_access.access.contains(MemoryAccess::R) {
261 Ok(VmiEventResponse::toggle_fast_singlestep().and_set_view(vmi.default_view()))
266 }
267 else {
268 panic!("Unhandled memory access: {memory_access:?}");
269 }
270 }
271
272 #[tracing::instrument(skip_all, fields(pid, process))]
273 fn interrupt(
274 &mut self,
275 vmi: &VmiContext<'_, Driver, WindowsOs<Driver>>,
276 ) -> Result<VmiEventResponse<Amd64>, VmiError> {
277 let tag = match self.bpm.get_by_event(vmi.event(), ()) {
278 Some(breakpoints) => {
279 let first_breakpoint = breakpoints.into_iter().next().expect("breakpoint");
282 first_breakpoint.tag()
283 }
284 None => {
285 if BreakpointController::is_breakpoint(vmi, vmi.event())? {
286 tracing::warn!("Unknown breakpoint, reinjecting");
288 return Ok(VmiEventResponse::reinject_interrupt());
289 }
290 else {
291 tracing::warn!("Ignoring old breakpoint event");
296 return Ok(
297 VmiEventResponse::toggle_fast_singlestep().and_set_view(vmi.default_view())
298 );
299 }
300 }
301 };
302
303 let process = vmi.os().current_process()?;
304 let process_id = process.id()?;
305 let process_name = process.name()?;
306 tracing::Span::current()
307 .record("pid", process_id.0)
308 .record("process", process_name);
309
310 match tag {
311 "NtCreateFile" => self.NtCreateFile(vmi)?,
312 "NtWriteFile" => self.NtWriteFile(vmi)?,
313 "PspInsertProcess" => self.PspInsertProcess(vmi)?,
314 "MmCleanProcessAddressSpace" => self.MmCleanProcessAddressSpace(vmi)?,
315 _ => panic!("Unhandled tag: {tag}"),
316 }
317
318 Ok(VmiEventResponse::toggle_fast_singlestep().and_set_view(vmi.default_view()))
319 }
320
321 #[tracing::instrument(skip_all)]
322 fn singlestep(
323 &mut self,
324 vmi: &VmiContext<'_, Driver, WindowsOs<Driver>>,
325 ) -> Result<VmiEventResponse<Amd64>, VmiError> {
326 let ptm_events = self.ptm.process_dirty_entries(vmi, vmi.event().vcpu_id())?;
329
330 for event in &ptm_events {
331 match &event {
333 PageTableMonitorEvent::PageIn(update) => tracing::debug!(?update, "page-in"),
334 PageTableMonitorEvent::PageOut(update) => tracing::debug!(?update, "page-out"),
335 }
336
337 self.bpm.handle_ptm_event(vmi, event)?;
339 }
340
341 Ok(VmiEventResponse::toggle_singlestep().and_set_view(self.view))
343 }
344
345 #[tracing::instrument(skip_all)]
346 fn NtCreateFile(
347 &mut self,
348 vmi: &VmiContext<'_, Driver, WindowsOs<Driver>>,
349 ) -> Result<(), VmiError> {
350 let ObjectAttributes = Va(vmi.os().function_argument(2)?);
368
369 let object_attributes = vmi.os().object_attributes(ObjectAttributes)?;
370 let object_name = match object_attributes.object_name()? {
371 Some(object_name) => object_name,
372 None => {
373 tracing::warn!(%ObjectAttributes, "No object name found");
374 return Ok(());
375 }
376 };
377
378 tracing::info!(%object_name);
379
380 Ok(())
381 }
382
383 #[tracing::instrument(skip_all)]
384 fn NtWriteFile(
385 &mut self,
386 vmi: &VmiContext<'_, Driver, WindowsOs<Driver>>,
387 ) -> Result<(), VmiError> {
388 let FileHandle = vmi.os().function_argument(0)?;
404
405 let handle_table = match vmi.os().current_process()?.handle_table()? {
406 Some(handle_table) => handle_table,
407 None => {
408 tracing::warn!("No handle table found");
409 return Ok(());
410 }
411 };
412
413 let handle_table_entry = match handle_table.lookup(FileHandle)? {
414 Some(handle_table_entry) => handle_table_entry,
415 None => {
416 tracing::warn!(FileHandle = %Hex(FileHandle), "No handle table entry found");
417 return Ok(());
418 }
419 };
420
421 let object = match handle_table_entry.object()? {
422 Some(object) => object,
423 None => {
424 tracing::warn!(FileHandle = %Hex(FileHandle), "No object found");
425 return Ok(());
426 }
427 };
428
429 let file_object = match object.as_file()? {
430 Some(file_object) => file_object,
431 None => {
432 tracing::warn!(FileHandle = %Hex(FileHandle), "Not a file object");
433 return Ok(());
434 }
435 };
436
437 let path = file_object.full_path()?;
438 tracing::info!(%path);
439
440 Ok(())
441 }
442
443 #[tracing::instrument(skip_all)]
444 fn PspInsertProcess(
445 &mut self,
446 vmi: &VmiContext<'_, Driver, WindowsOs<Driver>>,
447 ) -> Result<(), VmiError> {
448 let NewProcess = vmi.os().function_argument(0)?;
460 let Parent = vmi.os().function_argument(1)?;
461
462 let process = vmi.os().process(ProcessObject(Va(NewProcess)))?;
463 let process_id = process.id()?;
464
465 let parent_process = vmi.os().process(ProcessObject(Va(Parent)))?;
466 let parent_process_id = parent_process.id()?;
467
468 debug_assert_eq!(parent_process_id, process.parent_id()?);
474
475 let name = process.name()?;
476 let image_base = process.image_base()?;
477 let peb = process.peb()?;
478
479 tracing::info!(
480 %process_id,
481 name,
482 %image_base,
483 ?peb,
484 );
485
486 Ok(())
487 }
488
489 #[tracing::instrument(skip_all)]
490 fn MmCleanProcessAddressSpace(
491 &mut self,
492 vmi: &VmiContext<'_, Driver, WindowsOs<Driver>>,
493 ) -> Result<(), VmiError> {
494 let Process = vmi.os().function_argument(0)?;
502
503 let process = vmi.os().process(ProcessObject(Va(Process)))?;
504 let process_id = process.id()?;
505
506 let name = process.name()?;
507 let image_base = process.image_base()?;
508
509 tracing::info!(%process_id, name, %image_base);
510
511 Ok(())
512 }
513
514 fn dispatch(
515 &mut self,
516 vmi: &VmiContext<'_, Driver, WindowsOs<Driver>>,
517 ) -> Result<VmiEventResponse<Amd64>, VmiError> {
518 let event = vmi.event();
519 let result = match event.reason() {
520 EventReason::MemoryAccess(_) => self.memory_access(vmi),
521 EventReason::Interrupt(_) => self.interrupt(vmi),
522 EventReason::Singlestep(_) => self.singlestep(vmi),
523 _ => panic!("Unhandled event: {:?}", event.reason()),
524 };
525
526 if let Err(VmiError::Translation(pfs)) = result {
533 tracing::warn!(?pfs, "Page fault, injecting");
534 vmi.inject_interrupt(event.vcpu_id(), Interrupt::page_fault(pfs[0].va, 0))?;
535 return Ok(VmiEventResponse::default());
536 }
537
538 result
539 }
540}
541
542impl<Driver> VmiHandler<Driver, WindowsOs<Driver>> for Monitor<Driver>
543where
544 Driver: VmiDriver<Architecture = Amd64>,
545{
546 type Output = ();
547
548 fn handle_event(
549 &mut self,
550 vmi: VmiContext<'_, Driver, WindowsOs<Driver>>,
551 ) -> VmiEventResponse<Amd64> {
552 vmi.flush_v2p_cache();
554
555 self.dispatch(&vmi).expect("dispatch")
556 }
557
558 fn check_completion(&self) -> Option<Self::Output> {
559 self.terminate_flag.load(Ordering::Relaxed).then_some(())
560 }
561}
562
563fn main() -> Result<(), Box<dyn std::error::Error>> {
564 tracing_subscriber::fmt()
565 .with_max_level(tracing::Level::DEBUG)
566 .init();
567
568 let domain_id = 'x: {
569 for name in &["win7", "win10", "win11", "ubuntu22"] {
570 if let Some(domain_id) = XenStore::new()?.domain_id_from_name(name)? {
571 break 'x domain_id;
572 }
573 }
574
575 panic!("Domain not found");
576 };
577
578 tracing::debug!(?domain_id);
579
580 let driver = VmiXenDriver::<Amd64>::new(domain_id)?;
582 let core = VmiCore::new(driver)?;
583
584 let kernel_info = {
587 let _pause_guard = core.pause_guard()?;
588 let regs = core.registers(0.into())?;
589
590 WindowsOs::find_kernel(&core, ®s)?.expect("kernel information")
591 };
592
593 let isr = IsrCache::<JsonCodec>::new("cache")?;
596 let entry = isr.entry_from_codeview(kernel_info.codeview)?;
597 let profile = entry.profile()?;
598
599 tracing::info!("Creating VMI session");
601 let terminate_flag = Arc::new(AtomicBool::new(false));
602 signal_hook::flag::register(signal_hook::consts::SIGHUP, terminate_flag.clone())?;
603 signal_hook::flag::register(signal_hook::consts::SIGINT, terminate_flag.clone())?;
604 signal_hook::flag::register(signal_hook::consts::SIGALRM, terminate_flag.clone())?;
605 signal_hook::flag::register(signal_hook::consts::SIGTERM, terminate_flag.clone())?;
606
607 let os = WindowsOs::<VmiXenDriver<Amd64>>::new(&profile)?;
608 let session = VmiSession::new(&core, &os);
609
610 session.handle(|session| Monitor::new(session, &profile, terminate_flag))?;
611
612 Ok(())
613}