Skip to main content

windows_reactor/
main.rs

1//! This example demonstrates how to use the `vmi_utils::reactor` module to
2//! monitor certain events in a Windows guest VM.
3//!
4//! In particular, we monitor these functions:
5//! - `nt!NtWriteFile`: to log the full path of files being written to.
6//! - `netio.sys!KfdClassify` and `netio.sys!KfdIsLayerEmpty`: to log network
7//!   connections and their originating process.
8//! - `ncrypt.dll!SslGenerateSessionKeys`: to log SSL/TLS session keys, to
9//!   eventually decrypt the network traffic.
10//!
11//! # Usage
12//!
13//! ```text
14//! VMI_XEN_DOMAIN_NAME=<domain name> cargo run --features="arch-amd64 driver-xen os-windows utils" --example windows-reactor
15//! ```
16//!
17//! or
18//!
19//! ```text
20//! VMI_XEN_DOMAIN_ID=<domain id> cargo run --features="arch-amd64 driver-xen os-windows utils" --example windows-reactor
21//! ```
22//!
23//! # Possible log output
24//!
25//! ```text
26//!  INFO resolving domain ID domain_name=win10-22h2
27//!  INFO setting up VMI domain_id=417
28//! DEBUG found kernel image base_address=0xfffff80106e00000
29//!  INFO loading kernel profile codeview=CodeView { name: "ntkrnlmp.pdb", guid: "68a17faf3012b7846079aeecdbe0a583", age: 1 }
30//! DEBUG found cached PE image path=cache/windows/ntkrnlmp.pdb/68a17faf3012b7846079aeecdbe0a5831/ntkrnlmp.pdb
31//!  INFO creating VMI session
32//! DEBUG resolver:user{module="ncrypt.dll"}:via_vad{process="lsass.exe"}: region found, reading image
33//! DEBUG resolver:user{module="ncrypt.dll"}: module resolved via VAD process="lsass.exe" codeview=CodeView { name: "ncrypt.pdb", guid: "bdff5e7b85792ad5e6cac5e8b4a2addc", age: 1 }
34//! DEBUG resolve_event{module=kernel name=NtWriteFile}: symbol found offset=0x00000000005ed180
35//! DEBUG resolve_event{module=NetioSys name=KfdClassify}: symbol found offset=0x0000000000007e10
36//! DEBUG resolve_event{module=NetioSys name=KfdIsLayerEmpty}: symbol found offset=0x0000000000005520
37//! DEBUG resolve_event{module=NcryptDll name=SslGenerateSessionKeys}: symbol found offset=0x00000000000012a0
38//! DEBUG active breakpoint inserted active=1 gfn=0x00000000000031ed ctx=0xfffff801073ed180 @ 0x00000000001aa000 view=1 global=true key=() tag=NtWriteFile
39//! DEBUG created shadow page address=0x00000000031ed180 original_gfn=0x00000000000031ed shadow_gfn=0x00000000001100fc view=1
40//! DEBUG active breakpoint inserted active=2 gfn=0x0000000000005a37 ctx=0xfffff8010bc37e10 @ 0x00000000001aa000 view=1 global=true key=() tag=KfdClassify
41//! DEBUG created shadow page address=0x0000000005a37e10 original_gfn=0x0000000000005a37 shadow_gfn=0x00000000001100fd view=1
42//! DEBUG active breakpoint inserted active=3 gfn=0x0000000000005a35 ctx=0xfffff8010bc35520 @ 0x00000000001aa000 view=1 global=true key=() tag=KfdIsLayerEmpty
43//! DEBUG created shadow page address=0x0000000005a35520 original_gfn=0x0000000000005a35 shadow_gfn=0x00000000001100fe view=1
44//! DEBUG active breakpoint inserted active=4 gfn=0x00000000000c81de ctx=0x00007ffbdc4f12a0 @ 0x00000000c39da000 view=1 global=true key=() tag=SslGenerateSessionKeys
45//! DEBUG created shadow page address=0x00000000c81de2a0 original_gfn=0x00000000000c81de shadow_gfn=0x00000000001100ff view=1
46//! ...
47//!  INFO reactor{vcpu=1 view=1 pid=1844 tid=7672}:interrupt:KfdClassify: protocol=IPPROTO_UDP local_ip=192.168.1.1 local_port=60066 remote_ip=8.8.8.8 remote_port=53 pid=1844
48//!  INFO reactor{vcpu=1 view=1 pid=1844 tid=7672}:interrupt:KfdClassify: protocol=IPPROTO_UDP local_ip=192.168.1.1 local_port=60066 remote_ip=8.8.8.8 remote_port=53 pid=1844
49//!  INFO reactor{vcpu=1 view=1 pid=8120 tid=3616}:interrupt:KfdClassify: protocol=IPPROTO_TCP local_ip=192.168.1.1 local_port=55598 remote_ip=54.91.177.181 remote_port=443 pid=8120
50//!  INFO reactor{vcpu=3 view=1 pid=4 tid=0}:interrupt:KfdClassify: protocol=IPPROTO_TCP local_ip=192.168.1.1 local_port=55598 remote_ip=54.91.177.181 remote_port=443 pid=8120
51//!  INFO reactor{vcpu=0 view=1 pid=664 tid=788}:interrupt:SslGenerateSessionKeys: client_random="4a8979877a9386a10d1e488eea06eb33a6650008538514ba0d91d1a8ad50d45e" secret="61b835e7246e00bcca42f2067d0efebbb3809897118c6f20025056afbb7cd89d3cded737d15c26ab04f9b339d65810d5"
52//! ...
53//!  INFO reactor{vcpu=3 view=1 pid=2500 tid=5140}:interrupt:NtWriteFile: handle=0x00000000000004b4 path="\\Device\\HarddiskVolume2\\Windows\\System32\\config\\systemprofile\\AppData\\LocalLow\\Microsoft\\CryptnetUrlCache\\MetaData\\57C8EDB95DF3F0AD4EE2DC2B8CFD4157"
54//!  INFO reactor{vcpu=3 view=1 pid=2500 tid=5140}:interrupt:NtWriteFile: handle=0x00000000000004b4 path="\\Device\\HarddiskVolume2\\Windows\\System32\\config\\systemprofile\\AppData\\LocalLow\\Microsoft\\CryptnetUrlCache\\MetaData\\FB0D848F74F70BB2EAA93746D24D9749"
55//!  INFO reactor{vcpu=0 view=1 pid=5432 tid=2112}:interrupt:NtWriteFile: handle=0x00000000000018c0 path="\\Device\\HarddiskVolume2\\Users\\John\\AppData\\Local\\Microsoft\\Edge\\User Data\\Edge-Local-State-Tmp-53f0e37f-8e8f-4a36-87aa-4b0582035293.tmp"
56//! ...
57//! DEBUG active breakpoints removed active=3 gfn=0x00000000000c81de view=1 breakpoints={((), AddressContext { va: 0x00007ffbdc4f12a0, root: 0x00000000c39da000 }): {Breakpoint { ctx: AddressContext { va: 0x00007ffbdc4f12a0, root: 0x00000000c39da000 }, view: View(1), global: true, key: (), tag: SslGenerateSessionKeys }}}
58//! DEBUG active breakpoints removed active=2 gfn=0x00000000000031ed view=1 breakpoints={((), AddressContext { va: 0xfffff801073ed180, root: 0x00000000001aa000 }): {Breakpoint { ctx: AddressContext { va: 0xfffff801073ed180, root: 0x00000000001aa000 }, view: View(1), global: true, key: (), tag: NtWriteFile }}}
59//! DEBUG active breakpoints removed active=1 gfn=0x0000000000005a37 view=1 breakpoints={((), AddressContext { va: 0xfffff8010bc37e10, root: 0x00000000001aa000 }): {Breakpoint { ctx: AddressContext { va: 0xfffff8010bc37e10, root: 0x00000000001aa000 }, view: View(1), global: true, key: (), tag: KfdClassify }}}
60//! DEBUG active breakpoints removed active=0 gfn=0x0000000000005a35 view=1 breakpoints={((), AddressContext { va: 0xfffff8010bc35520, root: 0x00000000001aa000 }): {Breakpoint { ctx: AddressContext { va: 0xfffff8010bc35520, root: 0x00000000001aa000 }, view: View(1), global: true, key: (), tag: KfdIsLayerEmpty }}}
61//! ...
62//!  INFO hit counts NtWriteFile=773 KfdClassify=30 KfdIsLayerEmpty=2131 SslGenerateSessionKeys=3
63//! ```
64//!
65
66#![expect(non_snake_case)]
67
68mod ncrypt;
69mod netio;
70mod ntoskrnl;
71
72use std::sync::{Arc, atomic::AtomicBool};
73
74use anyhow::{Context as _, Error};
75use isr::cache::IsrCache;
76use tracing_subscriber::EnvFilter;
77use vmi::{
78    VcpuId, VmiContext, VmiCore, VmiError, VmiOs, VmiRead, VmiSession,
79    arch::amd64::Amd64,
80    driver::xen::VmiXenDriver,
81    os::{
82        VmiOsProcess as _,
83        windows::{ArchAdapter, WindowsOs, WindowsProcess},
84    },
85    utils::reactor::{Action, Reactor, ReactorHandler, define_events, define_modules},
86};
87use xen::{XenDomainId, XenStore};
88
89fn match_lsass<Driver>(process: &WindowsProcess<Driver>) -> Result<bool, VmiError>
90where
91    Driver: VmiRead,
92    Driver::Architecture: ArchAdapter<Driver>,
93{
94    // Match "lsass.exe" in SessionId 0.
95    Ok(
96        matches!(process.session()?, Some(session) if session.id()? == 0)
97            && process.name()?.eq_ignore_ascii_case("lsass.exe"),
98    )
99}
100
101define_modules! {
102    /// Modules used by the reactor.
103    #[os(
104        <Driver: VmiRead> WindowsOs<Driver>
105        where Driver::Architecture: ArchAdapter<Driver>
106    )]
107    enum Module {
108        /// `netio.sys`
109        #[module(name = "netio.sys")]
110        NetioSys,
111
112        /// `ncrypt.dll` in `lsass.exe`.
113        // Note that `.., process = "lsass.exe"` would also work, but this
114        // demonstrates how to use a custom predicate.
115        #[module(name = "ncrypt.dll", mode(user, process = match_lsass))]
116        NcryptDll,
117    }
118
119    #[resolver]
120    struct ModuleResolver;
121
122    #[cache]
123    struct SymbolCache;
124}
125
126define_events! {
127    /// Events monitored by the reactor.
128    enum Event in Module {
129        /// `nt!NtWriteFile`
130        NtWriteFile,
131
132        // `netio.sys`
133        NetioSys {
134            /// `netio.sys!KfdClassify`
135            KfdClassify,
136
137            /// `netio.sys!KfdIsLayerEmpty`
138            KfdIsLayerEmpty,
139        },
140
141        // `ncrypt.dll`
142        NcryptDll {
143            /// `ncrypt.dll!SslGenerateSessionKeys`
144            SslGenerateSessionKeys,
145        },
146    }
147}
148
149#[derive(Default)]
150struct NetIo {
151    NtWriteFile_counter: u64,
152    KfdClassify_counter: u64,
153    KfdIsLayerEmpty_counter: u64,
154    SslGenerateSessionKeys_counter: u64,
155}
156
157impl NetIo {
158    #[tracing::instrument(skip_all)]
159    fn NtWriteFile<Driver>(
160        &mut self,
161        vmi: &VmiContext<WindowsOs<Driver>>,
162    ) -> Result<Action<<WindowsOs<Driver> as VmiOs>::Architecture>, VmiError>
163    where
164        Driver: VmiRead,
165        Driver::Architecture: ArchAdapter<Driver>,
166    {
167        self.NtWriteFile_counter += 1;
168        ntoskrnl::NtWriteFile(vmi)
169    }
170
171    #[tracing::instrument(skip_all)]
172    fn KfdClassify<Driver>(
173        &mut self,
174        vmi: &VmiContext<WindowsOs<Driver>>,
175    ) -> Result<Action<<WindowsOs<Driver> as VmiOs>::Architecture>, VmiError>
176    where
177        Driver: VmiRead,
178        Driver::Architecture: ArchAdapter<Driver>,
179    {
180        self.KfdClassify_counter += 1;
181        netio::KfdClassify(vmi)
182    }
183
184    #[tracing::instrument(skip_all)]
185    fn KfdIsLayerEmpty<Driver>(
186        &mut self,
187        vmi: &VmiContext<WindowsOs<Driver>>,
188    ) -> Result<Action<<WindowsOs<Driver> as VmiOs>::Architecture>, VmiError>
189    where
190        Driver: VmiRead,
191        Driver::Architecture: ArchAdapter<Driver>,
192    {
193        self.KfdIsLayerEmpty_counter += 1;
194        netio::KfdIsLayerEmpty(vmi)
195    }
196
197    #[tracing::instrument(skip_all)]
198    fn SslGenerateSessionKeys<Driver>(
199        &mut self,
200        vmi: &VmiContext<WindowsOs<Driver>>,
201    ) -> Result<Action<<WindowsOs<Driver> as VmiOs>::Architecture>, VmiError>
202    where
203        Driver: VmiRead,
204        Driver::Architecture: ArchAdapter<Driver>,
205    {
206        self.SslGenerateSessionKeys_counter += 1;
207        ncrypt::SslGenerateSessionKeys(vmi)
208    }
209}
210
211impl Drop for NetIo {
212    fn drop(&mut self) {
213        tracing::info!(
214            NtWriteFile = self.NtWriteFile_counter,
215            KfdClassify = self.KfdClassify_counter,
216            KfdIsLayerEmpty = self.KfdIsLayerEmpty_counter,
217            SslGenerateSessionKeys = self.SslGenerateSessionKeys_counter,
218            "hit counts"
219        );
220    }
221}
222
223impl<Driver> ReactorHandler<WindowsOs<Driver>> for NetIo
224where
225    Driver: VmiRead,
226    Driver::Architecture: ArchAdapter<Driver>,
227{
228    type Output = ();
229    type Event = Event;
230
231    fn handle_event(
232        &mut self,
233        vmi: &VmiContext<WindowsOs<Driver>>,
234        event: Self::Event,
235    ) -> Result<Action<<WindowsOs<Driver> as VmiOs>::Architecture, Self::Output>, VmiError> {
236        match event {
237            Self::Event::NtWriteFile => self.NtWriteFile(vmi),
238            Self::Event::KfdClassify => self.KfdClassify(vmi),
239            Self::Event::KfdIsLayerEmpty => self.KfdIsLayerEmpty(vmi),
240            Self::Event::SslGenerateSessionKeys => self.SslGenerateSessionKeys(vmi),
241        }
242    }
243}
244
245fn main() -> Result<(), Error> {
246    let filter = EnvFilter::default()
247        .add_directive(tracing::Level::DEBUG.into())
248        .add_directive("reqwest=warn".parse()?)
249        .add_directive("rustls=warn".parse()?);
250
251    tracing_subscriber::fmt()
252        .with_env_filter(filter)
253        .with_target(false)
254        .init();
255
256    let domain_id = match std::env::var("VMI_XEN_DOMAIN_ID") {
257        Ok(domain_id) => XenDomainId(
258            domain_id
259                .parse()
260                .context("invalid VMI_XEN_DOMAIN_ID environment variable")?,
261        ),
262        Err(_) => {
263            let domain_name = std::env::var("VMI_XEN_DOMAIN_NAME")
264                .context("invalid VMI_XEN_DOMAIN_NAME environment variable")?;
265
266            tracing::info!(%domain_name, "resolving domain ID");
267
268            match XenStore::new()?.domain_id_from_name(&domain_name)? {
269                Some(domain_id) => domain_id,
270                None => return Err(anyhow::anyhow!("domain not found: {domain_name}")),
271            }
272        }
273    };
274
275    // Setup VMI.
276    tracing::info!(%domain_id, "setting up VMI");
277    let driver = VmiXenDriver::<Amd64>::new(domain_id)?;
278    let core = VmiCore::new(driver)?;
279
280    // Try to find the kernel information.
281    // This is necessary in order to load the profile.
282    let kernel_info = {
283        let _pause_guard = core.pause_guard()?;
284        let registers = core.registers(VcpuId(0))?;
285
286        WindowsOs::find_kernel(&core, &registers)?.context("cannot find kernel information")?
287    };
288
289    // Load the kernel profile.
290    // The profile contains offsets to kernel functions and data structures.
291    tracing::info!(codeview = ?kernel_info.codeview, "loading kernel profile");
292    let isr = IsrCache::new("cache")?;
293    let entry = isr.entry_from_codeview(kernel_info.codeview)?;
294    let profile = entry.profile()?;
295
296    // Create the VMI session.
297    tracing::info!("creating VMI session");
298    let terminate_flag = Arc::new(AtomicBool::new(false));
299    signal_hook::flag::register(signal_hook::consts::SIGHUP, terminate_flag.clone())?;
300    signal_hook::flag::register(signal_hook::consts::SIGINT, terminate_flag.clone())?;
301    signal_hook::flag::register(signal_hook::consts::SIGALRM, terminate_flag.clone())?;
302    signal_hook::flag::register(signal_hook::consts::SIGTERM, terminate_flag.clone())?;
303
304    let os = WindowsOs::<VmiXenDriver<Amd64>>::new(&profile)?;
305    let session = VmiSession::new(&core, &os);
306
307    let handler = NetIo::default();
308
309    //
310    // The following `let ncrypt_* = ...` lines demonstrate how to manually
311    // resolve a module, load its profile (symbols) and add it to the resolver
312    // via `with_module(_in_process)`.
313    //
314    // Note that this is not strictly necessary, as `ModuleResolver::resolve()`
315    // will automatically resolve modules if they are not explicitly added.
316    //
317    // Manually resolving modules can be useful in cases where you want to deal
318    // with the resolved information (base address, profile) in other places.
319    //
320
321    let ncrypt_resolved = {
322        let paused = session.pause_guard()?;
323        let vmi = paused.state();
324
325        // Calling `resolve_user_module(&vmi, &isr, "ncrypt.dll", "lsass.exe")`
326        // would also work, but this demonstrates how to use a custom predicate.
327        //
328        // Also, `match_lsass` is more strict, because it specifically looks
329        // for "lsass.exe" in SessionId 0 (therefore, avoiding potential false
330        // positives or potential malicious processes).
331        vmi::utils::resolver::resolve_user_module(&vmi, &isr, "ncrypt.dll", match_lsass)?
332            .context("ncrypt.dll not found in lsass.exe")?
333    };
334
335    let ncrypt_process = ncrypt_resolved
336        .process
337        .context("resolved ncrypt.dll is not associated with a process")?;
338
339    let ncrypt_entry = isr
340        .entry_from_codeview(ncrypt_resolved.debug_signature)
341        .context("cannot find symbols for ncrypt.dll")?;
342
343    let ncrypt_profile = ncrypt_entry
344        .profile()
345        .context("cannot load profile for ncrypt.dll")?;
346
347    // The `SymbolCache` holds the resolved `isr::Entry` items.
348    let mut cache = SymbolCache::default();
349    let modules = ModuleResolver::default()
350        // `with_kernel` MUST be called if `Event` variants reference kernel
351        // symbols - like `NtWriteFile` in this example.
352        //
353        // This is because the "kernel" module is always optional.
354        .with_kernel(kernel_info.base_address, profile)
355        .with_module_in_process(
356            Module::NcryptDll,
357            ncrypt_process,
358            ncrypt_resolved.image_base,
359            ncrypt_profile,
360        )
361        // This will automatically resolve the `netio.sys` module and load
362        // its profile.
363        //
364        // Note that if we hadn't called `with_module_in_process` for
365        // `ncrypt.dll`, it would also be automatically resolved here.
366        .resolve(&session, &isr, &mut cache)?;
367
368    // Finally, we collect the events according to the resolved information
369    // and the metadata.
370    //
371    // For example, if some module/event is marked as `optional` and the
372    // resolver fails to resolve it, then it will simply not be included
373    // in the `events`.
374    let events = modules.into_events()?;
375
376    // And we're ready to create the reactor!
377    session.handle(|session| {
378        Ok(Reactor::new(session, handler, events)?.with_termination_flag(terminate_flag))
379    })?;
380
381    Ok(())
382}