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, ®isters)?.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}