Skip to main content

windows_bridge/monitor/
mod.rs

1mod hooks;
2
3use std::{
4    collections::HashMap,
5    path::PathBuf,
6    sync::{
7        Arc,
8        atomic::{AtomicBool, Ordering},
9    },
10};
11
12use isr::{Profile, macros::symbols};
13use vmi::{
14    MemoryAccess, Registers as _, View, VmiContext, VmiError, VmiEventResponse, VmiHandler,
15    VmiSession,
16    arch::amd64::{Amd64, EventMonitor, EventReason, ExceptionVector, Interrupt},
17    driver::VmiFullDriver,
18    os::{
19        ProcessId, ProcessObject, ThreadId, ThreadObject, VmiOsProcess as _, VmiOsThread as _,
20        windows::WindowsOs,
21    },
22    utils::{
23        bpm::{Breakpoint, BreakpointController, BreakpointManager},
24        bridge::Bridge,
25        ptm::PageTableMonitor,
26        tracker::Tracker,
27    },
28};
29
30use self::hooks::MonitorHook;
31use crate::{
32    bridge::BridgeOutput,
33    deploy::{DeployBridge, DeployPolicy, DeployStatus, ExecuteResponse},
34    file_transfer::{FileTransfer, FileTransferBridge},
35};
36
37/// Process metadata and pending file transfers.
38struct Process<Driver>
39where
40    Driver: VmiFullDriver<Architecture = Amd64>,
41{
42    /// Process ID.
43    pid: ProcessId,
44
45    /// Parent process ID.
46    ppid: ProcessId,
47
48    /// Process name.
49    name: String,
50
51    /// Pending file transfers indexed by handles in this process.
52    file_transfers: HashMap<u64, FileTransfer<Driver>>,
53}
54
55impl<Driver> Process<Driver>
56where
57    Driver: VmiFullDriver<Architecture = Amd64>,
58{
59    /// Creates process metadata.
60    fn new(pid: ProcessId, ppid: ProcessId, name: String) -> Self {
61        Self {
62            pid,
63            ppid,
64            name,
65            file_transfers: HashMap::new(),
66        }
67    }
68
69    /// Records a file transfer indexed by its handle.
70    ///
71    /// Returns `true` if the handle was not already present.
72    fn record_file_transfer(&mut self, transfer: FileTransfer<Driver>) -> bool {
73        self.file_transfers
74            .insert(transfer.handle(), transfer)
75            .is_none()
76    }
77
78    /// Removes and returns the file transfer for a handle.
79    fn take_file_transfer(&mut self, handle: u64) -> Option<FileTransfer<Driver>> {
80        self.file_transfers.remove(&handle)
81    }
82}
83
84/// Thread metadata and the synchronous transfer currently using its stack.
85struct Thread<Driver>
86where
87    Driver: VmiFullDriver<Architecture = Amd64>,
88{
89    /// Thread ID.
90    #[expect(unused)]
91    tid: ThreadId,
92
93    /// Active file transfer using this thread's stack.
94    file_transfer: Option<FileTransfer<Driver>>,
95}
96
97impl<Driver> Thread<Driver>
98where
99    Driver: VmiFullDriver<Architecture = Amd64>,
100{
101    /// Creates thread metadata.
102    fn new(tid: ThreadId) -> Self {
103        Self {
104            tid,
105            file_transfer: None,
106        }
107    }
108}
109
110symbols! {
111    #[derive(Debug)]
112    struct Symbols {
113        PspInsertProcess: u64,
114        PspInsertThread: u64,
115        KeTerminateThread: u64,
116        MmCleanProcessAddressSpace: u64,
117        NtWriteFile: u64,
118        NtClose: u64,
119    }
120}
121
122/// State passed through kernel breakpoint dispatch.
123struct MonitorState<Driver>
124where
125    Driver: VmiFullDriver<Architecture = Amd64>,
126{
127    /// Tracks process and thread metadata.
128    tracker: Tracker<Process<Driver>, Thread<Driver>>,
129
130    /// Thread performing the deployed process creation.
131    creator_thread: Option<ThreadObject>,
132
133    /// Process object of the deployed process, once discovered.
134    target_process: Option<ProcessObject>,
135
136    /// Result produced when the deployed process terminates.
137    output: Option<<Monitor<Driver> as VmiHandler<WindowsOs<Driver>>>::Output>,
138}
139
140/// Monitor for a deployed process.
141///
142/// Setup and event errors are returned or treated as fatal. The monitor does
143/// not attempt recovery.
144pub struct Monitor<Driver>
145where
146    Driver: VmiFullDriver<Architecture = Amd64>,
147{
148    terminate_flag: Arc<AtomicBool>,
149    view: View,
150    bpm: BreakpointManager<BreakpointController<Driver>, (), MonitorHook<Driver>>,
151    ptm: PageTableMonitor<Driver, MonitorHook<Driver>>,
152    bridge: Bridge<WindowsOs<Driver>, (DeployBridge, FileTransferBridge)>,
153    state: MonitorState<Driver>,
154}
155
156impl<Driver> Monitor<Driver>
157where
158    Driver: VmiFullDriver<Architecture = Amd64>,
159{
160    /// Creates the monitor and installs all kernel breakpoints.
161    pub fn new(
162        session: &VmiSession<WindowsOs<Driver>>,
163        profile: &Profile,
164        terminate_flag: Arc<AtomicBool>,
165        output_directory: PathBuf,
166    ) -> Result<Self, VmiError> {
167        let paused = session.pause_guard()?;
168        let vmi = paused.state();
169
170        let kernel_image_base = vmi.os().kernel_image_base()?;
171        let root = vmi.os().system_process()?.translation_root()?;
172        let symbols = Symbols::new(profile)?;
173
174        vmi.monitor_enable(EventMonitor::Interrupt(ExceptionVector::Breakpoint))?;
175        vmi.monitor_enable(EventMonitor::Singlestep)?;
176        vmi.monitor_enable(EventMonitor::Hypercall {
177            allow_userspace: true,
178        })?;
179
180        let view = vmi.create_view(MemoryAccess::RWX)?;
181        vmi.switch_to_view(view)?;
182
183        let mut bpm = BreakpointManager::new();
184        let mut ptm = PageTableMonitor::new();
185
186        macro_rules! install {
187            ($($name:ident),+ $(,)?) => {
188                $(
189                    let va = kernel_image_base + symbols.$name;
190                    let context = (va, root);
191                    let hook = MonitorHook {
192                        name: stringify!($name),
193                        callback: self::hooks::$name::<Driver>,
194                    };
195                    let breakpoint = Breakpoint::new(context, view).global().with_tag(hook);
196                    bpm.insert(&vmi, breakpoint)?;
197                    ptm.monitor(&vmi, context, view, hook)?;
198                    tracing::debug!(
199                        ?hook,
200                        %va,
201                        "installed monitor hook"
202                    );
203                )+
204            };
205        }
206
207        install!(
208            PspInsertProcess,
209            PspInsertThread,
210            KeTerminateThread,
211            MmCleanProcessAddressSpace,
212            NtWriteFile,
213            NtClose,
214        );
215
216        Ok(Self {
217            terminate_flag,
218            view,
219            bpm,
220            ptm,
221            bridge: Bridge::new((
222                DeployBridge::new(
223                    DeployPolicy::default().execute_response(ExecuteResponse::ContinueAndNotify),
224                ),
225                FileTransferBridge::new(output_directory),
226            )),
227            state: MonitorState {
228                tracker: Tracker::default(),
229                creator_thread: None,
230                target_process: None,
231                output: None,
232            },
233        })
234    }
235
236    #[tracing::instrument(skip_all)]
237    fn memory_access(
238        &mut self,
239        vmi: &VmiContext<WindowsOs<Driver>>,
240    ) -> Result<VmiEventResponse<Amd64>, VmiError> {
241        let memory_access = vmi.event().reason().as_memory_access();
242
243        if memory_access.access.contains(MemoryAccess::W) {
244            self.ptm
245                .mark_dirty_entry(memory_access.pa, self.view, vmi.event().vcpu_id());
246
247            Ok(VmiEventResponse::singlestep().with_view(vmi.default_view()))
248        }
249        else if memory_access.access.contains(MemoryAccess::R) {
250            Ok(VmiEventResponse::fast_singlestep(vmi.default_view()))
251        }
252        else {
253            panic!("unhandled memory access: {memory_access:?}");
254        }
255    }
256
257    #[tracing::instrument(skip_all)]
258    fn interrupt(
259        &mut self,
260        vmi: &VmiContext<WindowsOs<Driver>>,
261    ) -> Result<VmiEventResponse<Amd64>, VmiError> {
262        let hook = match self.bpm.get_by_event(vmi.event(), ()) {
263            Some(breakpoint) => breakpoint.tag(),
264            None => {
265                if BreakpointController::is_breakpoint(vmi, vmi.event())? {
266                    tracing::warn!("unknown breakpoint, reinjecting");
267                    return Ok(VmiEventResponse::reinject_interrupt());
268                }
269
270                tracing::warn!("ignoring stale breakpoint event");
271                return Ok(VmiEventResponse::fast_singlestep(vmi.default_view()));
272            }
273        };
274
275        (hook.callback)(vmi, &mut self.state)
276    }
277
278    #[tracing::instrument(skip_all)]
279    fn singlestep(
280        &mut self,
281        vmi: &VmiContext<WindowsOs<Driver>>,
282    ) -> Result<VmiEventResponse<Amd64>, VmiError> {
283        let events = self.ptm.process_dirty_entries(vmi, vmi.event().vcpu_id())?;
284        self.bpm.handle_ptm_events(vmi, events)?;
285
286        Ok(VmiEventResponse::default().with_view(self.view))
287    }
288
289    /// Dispatches `deploy` and `file-transfer` hypercalls through the composed
290    /// bridge.
291    #[tracing::instrument(skip_all)]
292    fn hypercall(
293        &mut self,
294        vmi: &VmiContext<WindowsOs<Driver>>,
295    ) -> Result<VmiEventResponse<Amd64>, VmiError> {
296        let hypercall = vmi.event().reason().as_hypercall();
297
298        let mut registers = vmi.registers().gp_registers();
299        registers.rip += hypercall.instruction_length as u64;
300
301        if let Some(result) = self.bridge.dispatch(vmi) {
302            match result {
303                Ok(response) => {
304                    response.write_to(&mut registers);
305
306                    if let Some(output) = response.into_output() {
307                        self.process_bridge(vmi, output)?;
308                    }
309                }
310                Err(packet) => tracing::error!(
311                    request = packet.request(),
312                    method = packet.method(),
313                    "empty bridge response"
314                ),
315            }
316        }
317
318        Ok(VmiEventResponse::default().with_registers(registers))
319    }
320
321    /// Processes host-side bridge outputs before the guest resumes.
322    fn process_bridge(
323        &mut self,
324        vmi: &VmiContext<WindowsOs<Driver>>,
325        output: BridgeOutput,
326    ) -> Result<(), VmiError> {
327        match output {
328            BridgeOutput::DeployExecuting => {
329                if self.state.creator_thread.is_none() && self.state.target_process.is_none() {
330                    let current_thread = vmi.os().current_thread()?;
331                    self.state.creator_thread = Some(current_thread.object()?);
332                }
333                else {
334                    tracing::warn!(
335                        creator_thread = ?self.state.creator_thread,
336                        target_process = ?self.state.target_process,
337                        "ignoring repeated deploy execute notification"
338                    );
339                }
340            }
341            BridgeOutput::DeployWaiting | BridgeOutput::DeployFinished(_) => {}
342        }
343
344        Ok(())
345    }
346
347    #[tracing::instrument(
348        name = "monitor",
349        skip_all,
350        fields(
351            pid = vmi::trace::current_process_id(vmi),
352            tid = vmi::trace::current_thread_id(vmi),
353        )
354    )]
355    fn dispatch(
356        &mut self,
357        vmi: &VmiContext<WindowsOs<Driver>>,
358    ) -> Result<VmiEventResponse<Amd64>, VmiError> {
359        let result = match vmi.event().reason() {
360            EventReason::MemoryAccess(_) => self.memory_access(vmi),
361            EventReason::Interrupt(_) => self.interrupt(vmi),
362            EventReason::Singlestep(_) => self.singlestep(vmi),
363            EventReason::Hypercall(_) => self.hypercall(vmi),
364            reason => panic!("unhandled deploy monitor event: {reason:?}"),
365        };
366
367        if let Err(VmiError::Translation(page_fault)) = result {
368            tracing::warn!(?page_fault, "page fault, injecting");
369
370            vmi.inject_interrupt(
371                vmi.event().vcpu_id(),
372                Interrupt::page_fault(page_fault.va, 0),
373            )?;
374
375            return Ok(VmiEventResponse::default());
376        }
377
378        result
379    }
380}
381
382impl<Driver> VmiHandler<WindowsOs<Driver>> for Monitor<Driver>
383where
384    Driver: VmiFullDriver<Architecture = Amd64>,
385{
386    type Output = Result<Option<ProcessId>, DeployStatus>;
387
388    fn handle_event(&mut self, vmi: VmiContext<WindowsOs<Driver>>) -> VmiEventResponse<Amd64> {
389        vmi.flush_v2p_cache();
390
391        match self.dispatch(&vmi) {
392            Ok(response) => response,
393            Err(err) => panic!("deploy monitor dispatch failed: {err:?}"),
394        }
395    }
396
397    fn cleanup(&mut self, vmi: &VmiSession<WindowsOs<Driver>>) {
398        if let Err(err) = vmi.switch_to_view(vmi.default_view()) {
399            tracing::error!(%err, "failed to switch to the default view");
400        }
401
402        if let Err(err) = vmi.monitor_disable(EventMonitor::Singlestep) {
403            tracing::error!(%err, "failed to disable singlestep monitoring");
404        }
405
406        if let Err(err) = vmi.monitor_disable(EventMonitor::Interrupt(ExceptionVector::Breakpoint))
407        {
408            tracing::error!(%err, "failed to disable breakpoint monitoring");
409        }
410
411        if let Err(err) = vmi.monitor_disable(EventMonitor::Hypercall {
412            allow_userspace: true,
413        }) {
414            tracing::error!(%err, "failed to disable hypercall monitoring");
415        }
416
417        match self.bpm.remove_by_view(vmi, self.view) {
418            Ok(true) => {}
419            Ok(false) => tracing::warn!("no deploy monitor breakpoints to remove"),
420            Err(err) => tracing::error!(%err, "failed to remove deploy monitor breakpoints"),
421        }
422        self.ptm.unmonitor_all(vmi);
423
424        if let Err(err) = vmi.destroy_view(self.view) {
425            tracing::error!(%err, "failed to destroy deploy monitor view");
426        }
427    }
428
429    fn poll(&mut self) -> Option<Self::Output> {
430        self.state.output.or_else(|| {
431            self.terminate_flag
432                .load(Ordering::Relaxed)
433                .then_some(Ok(None))
434        })
435    }
436}