Skip to main content

windows_bridge/monitor/
hooks.rs

1#![expect(non_snake_case)]
2
3use std::{
4    fmt::Debug,
5    hash::{Hash, Hasher},
6};
7
8use vmi::{
9    Registers as _, Va, VmiContext, VmiError, VmiEventResponse, VmiVa as _,
10    arch::amd64::Amd64,
11    driver::VmiFullDriver,
12    os::{
13        ProcessObject, ThreadObject, VmiOsProcess as _, VmiOsThread as _,
14        windows::{WindowsFileObject, WindowsOs, WindowsOsExt as _},
15    },
16    trace::Hex,
17};
18
19use super::{MonitorState, Process, Thread};
20use crate::file_transfer::FileTransfer;
21
22/// Kernel breakpoint callback.
23type MonitorCallback<Driver> = fn(
24    &VmiContext<WindowsOs<Driver>>,
25    &mut MonitorState<Driver>,
26) -> Result<VmiEventResponse<Amd64>, VmiError>;
27
28/// Named kernel breakpoint handler.
29///
30/// Each name must identify exactly one callback. Tags compare and hash by name.
31pub struct MonitorHook<Driver>
32where
33    Driver: VmiFullDriver<Architecture = Amd64>,
34{
35    /// Symbol name used for diagnostics and tag identity.
36    pub name: &'static str,
37
38    /// Handler invoked when the breakpoint fires.
39    pub callback: MonitorCallback<Driver>,
40}
41
42impl<Driver> Debug for MonitorHook<Driver>
43where
44    Driver: VmiFullDriver<Architecture = Amd64>,
45{
46    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
47        f.write_str(self.name)
48    }
49}
50
51impl<Driver> Clone for MonitorHook<Driver>
52where
53    Driver: VmiFullDriver<Architecture = Amd64>,
54{
55    fn clone(&self) -> Self {
56        *self
57    }
58}
59
60impl<Driver> Copy for MonitorHook<Driver> where Driver: VmiFullDriver<Architecture = Amd64> {}
61
62impl<Driver> PartialEq for MonitorHook<Driver>
63where
64    Driver: VmiFullDriver<Architecture = Amd64>,
65{
66    fn eq(&self, other: &Self) -> bool {
67        self.name.eq(other.name)
68    }
69}
70
71impl<Driver> Eq for MonitorHook<Driver> where Driver: VmiFullDriver<Architecture = Amd64> {}
72
73impl<Driver> Hash for MonitorHook<Driver>
74where
75    Driver: VmiFullDriver<Architecture = Amd64>,
76{
77    fn hash<H: Hasher>(&self, state: &mut H) {
78        self.name.hash(state);
79    }
80}
81
82/// Handles a `PspInsertProcess` breakpoint.
83///
84/// Checks whether the new process was created by the thread we expected. If so,
85/// selects it as the program to watch. Records the process and its parent ID.
86#[tracing::instrument(skip_all)]
87pub fn PspInsertProcess<Driver>(
88    vmi: &VmiContext<WindowsOs<Driver>>,
89    state: &mut MonitorState<Driver>,
90) -> Result<VmiEventResponse<Amd64>, VmiError>
91where
92    Driver: VmiFullDriver<Architecture = Amd64>,
93{
94    //
95    // NTSTATUS
96    // PspInsertProcess (
97    //     _In_ PEPROCESS NewProcess,
98    //     _In_ PEPROCESS Parent,
99    //     _In_ ULONG DesiredAccess,
100    //     _In_ ULONG CreateFlags,
101    //     ...
102    //     );
103    //
104
105    let NewProcess = ProcessObject(Va(vmi.os().function_argument(0)?));
106    let Parent = ProcessObject(Va(vmi.os().function_argument(1)?));
107
108    tracing::trace!(%NewProcess, %Parent);
109
110    let new_process = vmi.os().process(NewProcess)?;
111    let parent = vmi.os().process(Parent)?;
112
113    let process = Process::new(new_process.id()?, parent.id()?, new_process.name()?);
114
115    tracing::debug!(
116        name = process.name,
117        pid = %process.pid,
118        ppid = %process.ppid,
119        process = %NewProcess,
120        parent = %Parent,
121    );
122
123    // Check if the current thread is the creator thread.
124    if state.target_process.is_none()
125        && let Some(creator_thread) = state.creator_thread
126        && vmi.os().current_thread()?.object()? == creator_thread
127    {
128        state.target_process = Some(NewProcess);
129        state.creator_thread = None;
130
131        tracing::info!(
132            name = process.name,
133            pid = %process.pid,
134            process = %NewProcess,
135            "deployed process started"
136        );
137    }
138
139    state.tracker.insert_process(NewProcess, process);
140
141    Ok(VmiEventResponse::fast_singlestep(vmi.default_view()))
142}
143
144/// Handles an `MmCleanProcessAddressSpace` breakpoint.
145///
146/// Retires the process while retaining its context for later events. If it is our
147/// program, saves its process ID as the result and marks the run as finished.
148#[tracing::instrument(skip_all)]
149pub fn MmCleanProcessAddressSpace<Driver>(
150    vmi: &VmiContext<WindowsOs<Driver>>,
151    state: &mut MonitorState<Driver>,
152) -> Result<VmiEventResponse<Amd64>, VmiError>
153where
154    Driver: VmiFullDriver<Architecture = Amd64>,
155{
156    //
157    // VOID
158    // MmCleanProcessAddressSpace (
159    //     _In_ PEPROCESS Process
160    //     );
161    //
162
163    let Process = ProcessObject(Va(vmi.os().function_argument(0)?));
164
165    tracing::trace!(%Process);
166
167    let process = match state.tracker.retire_process(Process) {
168        Some(process) => process,
169        None => return Ok(VmiEventResponse::fast_singlestep(vmi.default_view())),
170    };
171
172    tracing::debug!(
173        name = process.name,
174        pid = %process.pid,
175        process = %Process,
176    );
177
178    if state.target_process == Some(Process) {
179        state.output = Some(Ok(Some(process.pid)));
180
181        tracing::info!(
182            name = process.name,
183            pid = %process.pid,
184            process = %Process,
185            "deployed process terminated"
186        );
187    }
188
189    Ok(VmiEventResponse::fast_singlestep(vmi.default_view()))
190}
191
192/// Handles a `PspInsertThread` breakpoint.
193///
194/// Replaces the new thread's context and preserves existing process context.
195/// Records its process if that process is not yet known to the monitor.
196#[tracing::instrument(skip_all)]
197pub fn PspInsertThread<Driver>(
198    vmi: &VmiContext<WindowsOs<Driver>>,
199    state: &mut MonitorState<Driver>,
200) -> Result<VmiEventResponse<Amd64>, VmiError>
201where
202    Driver: VmiFullDriver<Architecture = Amd64>,
203{
204    //
205    // VOID
206    // PspInsertThread (
207    //     _In_ PETHREAD Thread,
208    //     _In_ PEPROCESS Process,
209    //     ...
210    //     );
211    //
212
213    let Thread = ThreadObject(Va(vmi.os().function_argument(0)?));
214    let Process = ProcessObject(Va(vmi.os().function_argument(1)?));
215
216    tracing::trace!(%Thread, %Process);
217
218    let os_thread = vmi.os().thread(Thread)?;
219    let os_process = vmi.os().process(Process)?;
220
221    let tid = os_thread.id()?;
222    let pid = os_process.id()?;
223
224    let thread_object = Thread;
225    let process_object = Process;
226
227    state.tracker.try_insert_thread(
228        process_object,
229        thread_object,
230        || {
231            Ok::<_, VmiError>(Process::new(
232                pid,
233                os_process.parent_id()?,
234                os_process.name()?,
235            ))
236        },
237        Thread::new(tid),
238    )?;
239
240    tracing::debug!(
241        %pid,
242        %tid,
243        process = %process_object,
244        thread = %thread_object,
245    );
246
247    Ok(VmiEventResponse::fast_singlestep(vmi.default_view()))
248}
249
250/// Handles a `KeTerminateThread` breakpoint.
251///
252/// Clears the expected creator reference when that thread terminates. Retires
253/// the thread's record while retaining its context for later events.
254#[tracing::instrument(skip_all)]
255pub fn KeTerminateThread<Driver>(
256    vmi: &VmiContext<WindowsOs<Driver>>,
257    state: &mut MonitorState<Driver>,
258) -> Result<VmiEventResponse<Amd64>, VmiError>
259where
260    Driver: VmiFullDriver<Architecture = Amd64>,
261{
262    //
263    // VOID
264    // KeTerminateThread (
265    //     _In_ PKTHREAD Thread
266    //     );
267    //
268
269    let Thread = ThreadObject(Va(vmi.os().function_argument(0)?));
270
271    // Clear `creator_thread` so a reused thread object cannot be mistaken for
272    // the expected creating thread. Do this before tracker retirement, since
273    // the deploy thread may have been created before tracking began.
274    if state.creator_thread == Some(Thread) {
275        state.creator_thread = None;
276    }
277
278    tracing::trace!(%Thread);
279
280    let os_thread = vmi.os().thread(Thread)?;
281    let os_process = os_thread.process()?;
282
283    let tid = os_thread.id()?;
284    let pid = os_process.id()?;
285
286    let thread_object = Thread;
287    let process_object = os_process.object()?;
288
289    let thread = match state.tracker.retire_thread(process_object, thread_object) {
290        Some(thread) => thread,
291        None => return Ok(VmiEventResponse::fast_singlestep(vmi.default_view())),
292    };
293
294    tracing::debug!(
295        %pid,
296        %tid,
297        process = %process_object,
298        thread = %thread_object,
299        transfer_active = thread.file_transfer.is_some(),
300    );
301
302    Ok(VmiEventResponse::fast_singlestep(vmi.default_view()))
303}
304
305/// Handles an `NtWriteFile` breakpoint.
306///
307/// Keeps a list of files that our program tries to write. The `NtClose` handler
308/// uses this list to decide which files to copy out of the Windows VM.
309#[tracing::instrument(skip_all)]
310pub fn NtWriteFile<Driver>(
311    vmi: &VmiContext<WindowsOs<Driver>>,
312    state: &mut MonitorState<Driver>,
313) -> Result<VmiEventResponse<Amd64>, VmiError>
314where
315    Driver: VmiFullDriver<Architecture = Amd64>,
316{
317    //
318    // NTSTATUS
319    // NTAPI
320    // NtWriteFile(
321    //     _In_ HANDLE FileHandle,
322    //     _In_opt_ HANDLE Event,
323    //     _In_opt_ PIO_APC_ROUTINE ApcRoutine,
324    //     _In_opt_ PVOID ApcContext,
325    //     _Out_ PIO_STATUS_BLOCK IoStatusBlock,
326    //     _In_reads_bytes_(Length) PVOID Buffer,
327    //     _In_ ULONG Length,
328    //     _In_opt_ PLARGE_INTEGER ByteOffset,
329    //     _In_opt_ PULONG Key
330    //     );
331    //
332
333    let current_process = vmi.os().current_process()?;
334    let process_object = current_process.object()?;
335
336    if state.target_process != Some(process_object) {
337        return Ok(VmiEventResponse::fast_singlestep(vmi.default_view()));
338    }
339
340    let FileHandle = vmi.os().function_argument(0)?;
341
342    tracing::trace!(FileHandle = %Hex(FileHandle));
343
344    if vmi.os().is_kernel_handle(FileHandle)? {
345        tracing::debug!(handle = %Hex(FileHandle), "kernel handle, skipping");
346        return Ok(VmiEventResponse::fast_singlestep(vmi.default_view()));
347    }
348
349    let process = match state.tracker.get_process_mut(process_object) {
350        Some(process) => process,
351        None => {
352            tracing::warn!("target process not tracked");
353            return Ok(VmiEventResponse::fast_singlestep(vmi.default_view()));
354        }
355    };
356
357    let file_object = match current_process.lookup_object::<WindowsFileObject<_>>(FileHandle)? {
358        Some(file_object) => file_object,
359        None => {
360            tracing::warn!(handle = %Hex(FileHandle), "cannot resolve file handle");
361            return Ok(VmiEventResponse::fast_singlestep(vmi.default_view()));
362        }
363    };
364
365    let path = file_object.full_path()?;
366    let transfer = FileTransfer::new(FileHandle, file_object.va(), path.clone());
367
368    if process.record_file_transfer(transfer) {
369        tracing::info!(handle = %Hex(FileHandle), path, "recorded file transfer");
370    }
371
372    Ok(VmiEventResponse::fast_singlestep(vmi.default_view()))
373}
374
375/// Handles an `NtClose` breakpoint.
376///
377/// Starts copying a file when our program closes a handle recorded by the
378/// `NtWriteFile` handler. Later calls on the same thread run the next steps of
379/// the copy.
380#[tracing::instrument(skip_all)]
381pub fn NtClose<Driver>(
382    vmi: &VmiContext<WindowsOs<Driver>>,
383    state: &mut MonitorState<Driver>,
384) -> Result<VmiEventResponse<Amd64>, VmiError>
385where
386    Driver: VmiFullDriver<Architecture = Amd64>,
387{
388    //
389    // NTSTATUS
390    // NTAPI
391    // NtClose (
392    //     _In_ _Post_ptr_invalid_ HANDLE Handle
393    //     );
394    //
395
396    let current_thread = vmi.os().current_thread()?;
397    let thread_object = current_thread.object()?;
398
399    if let Some(thread) = state.tracker.get_thread(thread_object)
400        && thread.file_transfer.is_some()
401    {
402        return advance_file_transfer(vmi, state, thread_object);
403    }
404
405    // (equivalent to `vmi.os().current_process()?`)
406    let current_process = current_thread.current_process()?;
407    let process_object = current_process.object()?;
408
409    if state.target_process != Some(process_object) {
410        return Ok(VmiEventResponse::fast_singlestep(vmi.default_view()));
411    }
412
413    if !state.tracker.contains_thread(thread_object) {
414        tracing::warn!(%thread_object, "close on untracked target thread");
415        return Ok(VmiEventResponse::fast_singlestep(vmi.default_view()));
416    }
417
418    let Handle = vmi.os().function_argument(0)?;
419
420    tracing::trace!(Handle = %Hex(Handle));
421
422    if vmi.os().is_kernel_handle(Handle)? {
423        return Ok(VmiEventResponse::fast_singlestep(vmi.default_view()));
424    }
425
426    // REVIEW: if-let-chain?
427    let mut transfer = match state
428        .tracker
429        .get_process_mut(process_object)
430        .and_then(|process| process.take_file_transfer(Handle))
431    {
432        Some(transfer) => transfer,
433        None => return Ok(VmiEventResponse::fast_singlestep(vmi.default_view())),
434    };
435
436    if let Some(file_object) = current_process.lookup_object::<WindowsFileObject<_>>(Handle)?
437        && file_object.va() != transfer.file_object()
438    {
439        tracing::warn!(
440            handle = %Hex(Handle),
441            path = transfer.path(),
442            "discarding stale file-transfer handle"
443        );
444        return Ok(VmiEventResponse::fast_singlestep(vmi.default_view()));
445    }
446
447    transfer.start();
448
449    if let Some(thread) = state.tracker.get_thread_mut(thread_object) {
450        thread.file_transfer = Some(transfer);
451    }
452
453    advance_file_transfer(vmi, state, thread_object)
454}
455
456/// Advances the `file-transfer` recipe on the current thread.
457fn advance_file_transfer<Driver>(
458    vmi: &VmiContext<WindowsOs<Driver>>,
459    state: &mut MonitorState<Driver>,
460    thread_object: ThreadObject,
461) -> Result<VmiEventResponse<Amd64>, VmiError>
462where
463    Driver: VmiFullDriver<Architecture = Amd64>,
464{
465    let thread = match state.tracker.get_thread_mut(thread_object) {
466        Some(thread) => thread,
467        None => return Ok(VmiEventResponse::fast_singlestep(vmi.default_view())),
468    };
469
470    let file_transfer = match thread.file_transfer.as_mut() {
471        Some(file_transfer) => file_transfer,
472        None => return Ok(VmiEventResponse::fast_singlestep(vmi.default_view())),
473    };
474
475    let registers = match file_transfer.execute(vmi)? {
476        Some(registers) => registers,
477        None => return Ok(VmiEventResponse::fast_singlestep(vmi.default_view())),
478    };
479
480    if !file_transfer.done() {
481        return Ok(VmiEventResponse::default().with_registers(registers.gp_registers()));
482    }
483
484    let file_transfer = match thread.file_transfer.take() {
485        Some(file_transfer) => file_transfer,
486        None => return Ok(VmiEventResponse::fast_singlestep(vmi.default_view())),
487    };
488
489    tracing::info!(
490        path = file_transfer.path(),
491        "file-transfer injection completed"
492    );
493
494    Ok(VmiEventResponse::fast_singlestep(vmi.default_view())
495        .with_registers(registers.gp_registers()))
496}