windows_recipe_writefile_advanced/windows-recipe-writefile-advanced.rs
1//! # Advanced [`recipe!`] example
2//!
3//! This example demonstrates how to write a recipe with multiple steps
4//! and advanced control flow using [`RecipeControlFlow`].
5//!
6//! The recipe is injected into the `explorer.exe` process and writes
7//! a file in chunks to the guest.
8//!
9//! # Possible log output
10//!
11//! > Note the page faults, which are automatically handled by the
12//! > [`InjectorHandler`] and injected back into the guest.
13//!
14//! ```text
15//! DEBUG domain_id=XenDomainId(104)
16//! DEBUG found MZ base_address=0xfffff80002861000
17//! INFO profile already exists profile_path="cache/windows/ntkrnlmp.pdb/3844dbb920174967be7aa4a2c20430fa2/profile.json"
18//! INFO Creating VMI session
19//! INFO found explorer.exe pid=1248 object=0xfffffa80030e9060
20//! DEBUG injector{vcpu=0 rip=0x0000000077c618ca}:memory_access: thread hijacked current_tid=2776
21//! DEBUG injector{vcpu=0 rip=0x0000000077c618ca}:memory_access: recipe step index=0
22//! INFO injector{vcpu=0 rip=0x0000000077c618ca}:memory_access: step 1: kernel32!CreateFileA() target_path="C:\\Users\\John\\Desktop\\test.txt"
23//! DEBUG injector{vcpu=0 rip=0x0000000077c618ca}:memory_access: recipe step index=1
24//! INFO injector{vcpu=0 rip=0x0000000077c618ca}:memory_access: step 2: kernel32!WriteFile() handle=0x0000000000000000
25//! DEBUG injector{vcpu=0 rip=0x0000000077c618ca}:memory_access: recipe step index=2
26//! DEBUG injector{vcpu=0 rip=0x0000000077c618ca}:memory_access: recipe step index=2 (x20)
27//! WARN injecting page fault pf=AddressContext { va: 0x0000000006358d70, root: 0x0000000070749000 }
28//! DEBUG injector{vcpu=0 rip=0x0000000077c618ca}:memory_access: recipe step index=2
29//! DEBUG injector{vcpu=0 rip=0x0000000077c618ca}:memory_access: recipe step index=2 (x6)
30//! WARN injecting page fault pf=AddressContext { va: 0x0000000006356fb0, root: 0x0000000070749000 }
31//! DEBUG injector{vcpu=0 rip=0x0000000077c618ca}:memory_access: recipe step index=2
32//! DEBUG injector{vcpu=0 rip=0x0000000077c618ca}:memory_access: recipe step index=2 (x4)
33//! WARN injecting page fault pf=AddressContext { va: 0x0000000006355eb0, root: 0x0000000070749000 }
34//! DEBUG injector{vcpu=2 rip=0x0000000077c618ca}:memory_access: recipe step index=2
35//! DEBUG injector{vcpu=2 rip=0x0000000077c618ca}:memory_access: recipe step index=2 (x4)
36//! WARN injecting page fault pf=AddressContext { va: 0x0000000006354db0, root: 0x0000000070749000 }
37//! DEBUG injector{vcpu=2 rip=0x0000000077c618ca}:memory_access: recipe step index=2
38//! DEBUG injector{vcpu=2 rip=0x0000000077c618ca}:memory_access: recipe step index=2 (x4)
39//! WARN injecting page fault pf=AddressContext { va: 0x0000000006353cb0, root: 0x0000000070749000 }
40//! DEBUG injector{vcpu=2 rip=0x0000000077c618ca}:memory_access: recipe step index=2
41//! DEBUG injector{vcpu=2 rip=0x0000000077c618ca}:memory_access: recipe step index=2 (x4)
42//! WARN injecting page fault pf=AddressContext { va: 0x0000000006352ff0, root: 0x0000000070749000 }
43//! DEBUG injector{vcpu=2 rip=0x0000000077c618ca}:memory_access: recipe step index=2
44//! DEBUG injector{vcpu=2 rip=0x0000000077c618ca}:memory_access: recipe step index=2 (x8)
45//! DEBUG injector{vcpu=2 rip=0x0000000077c618ca}:memory_access: recipe step index=3
46//! INFO injector{vcpu=2 rip=0x0000000077c618ca}:memory_access: step 4: kernel32!WriteFile() number_of_bytes_written=26
47//! INFO injector{vcpu=2 rip=0x0000000077c618ca}:memory_access: step 4: kernel32!CloseHandle() handle=0x0000000000000bac
48//! DEBUG injector{vcpu=2 rip=0x0000000077c618ca}:memory_access: recipe finished result=0x0000000000000001
49//! ```
50
51mod common;
52
53use vmi::{
54 Hex, Va, VcpuId, VmiDriver,
55 arch::amd64::Amd64,
56 os::{VmiOsProcess as _, windows::WindowsOs},
57 utils::injector::{InjectorHandler, Recipe, RecipeControlFlow, recipe},
58};
59
60#[derive(Debug, Default)]
61pub struct GuestFile {
62 /// Target path in the guest to write the file.
63 target_path: String,
64
65 /// Content to write to the file.
66 content: Vec<u8>,
67
68 /// The size of the chunk to write to the file.
69 chunk_size: usize,
70
71 /// Handle to the file.
72 /// Assigned in 2nd step.
73 handle: u64,
74
75 /// The number of bytes written to the file.
76 /// Assigned in 2nd step and used in 3rd step.
77 bytes_written_ptr: Va,
78
79 /// The total number of bytes written to the file.
80 bytes_written_total: u32,
81}
82
83impl GuestFile {
84 pub fn new(target_path: impl AsRef<str>, content: impl AsRef<[u8]>) -> Self {
85 Self {
86 target_path: target_path.as_ref().to_string(),
87 content: content.as_ref().to_vec(),
88 chunk_size: 1024,
89
90 // Mutable fields.
91 handle: 0,
92 bytes_written_ptr: Va::default(),
93 bytes_written_total: 0,
94 }
95 }
96}
97
98/// Create a recipe to write a file to the guest.
99///
100/// # Equivalent C pseudo-code
101///
102/// ```c
103/// const char* target_path = "...\\test.txt";
104/// const char content[] = "...";
105///
106/// HANDLE handle = CreateFileA(target_path, // lpFileName
107/// GENERIC_WRITE, // dwDesiredAccess
108/// 0, // dwShareMode
109/// NULL, // lpSecurityAttributes
110/// CREATE_ALWAYS, // dwCreationDisposition
111/// FILE_ATTRIBUTE_NORMAL, // dwFlagsAndAttributes
112/// NULL); // hTemplateFile
113///
114/// if (handle == INVALID_HANDLE_VALUE) {
115/// printf("kernel32!CreateFileA() failed\n");
116/// return;
117/// }
118///
119/// DWORD bytes_written;
120/// DWORD bytes_written_total = 0;
121///
122/// while (bytes_written_total < sizeof(content)) {
123/// if (!WriteFile(handle, content, sizeof(content), &bytes_written, 0)) {
124/// printf("kernel32!WriteFile() failed\n");
125/// return;
126/// }
127///
128/// bytes_written_total += bytes_written;
129/// }
130///
131/// CloseHandle(handle);
132/// ```
133pub fn recipe_factory<Driver>(data: GuestFile) -> Recipe<Driver, WindowsOs<Driver>, GuestFile>
134where
135 Driver: VmiDriver<Architecture = Amd64>,
136{
137 recipe![
138 Recipe::<_, WindowsOs<Driver>, _>::new(data),
139 //
140 // Step 1:
141 // - Create a file.
142 //
143 {
144 tracing::info!(
145 target_path = data![target_path],
146 "step 1: kernel32!CreateFileA()"
147 );
148
149 const GENERIC_WRITE: u64 = 0x40000000;
150 const CREATE_ALWAYS: u64 = 2;
151 const FILE_ATTRIBUTE_NORMAL: u64 = 0x80;
152
153 inject! {
154 kernel32!CreateFileA(
155 &data![target_path], // lpFileName
156 GENERIC_WRITE, // dwDesiredAccess
157 0, // dwShareMode
158 0, // lpSecurityAttributes
159 CREATE_ALWAYS, // dwCreationDisposition
160 FILE_ATTRIBUTE_NORMAL, // dwFlagsAndAttributes
161 0 // hTemplateFile
162 )
163 }
164 },
165 //
166 // Step 2:
167 // - Verify the file handle
168 // - Write the first chunk to the file
169 //
170 {
171 let return_value = registers!().rax;
172
173 const INVALID_HANDLE_VALUE: u64 = 0xffff_ffff_ffff_ffff;
174
175 if return_value == INVALID_HANDLE_VALUE {
176 tracing::error!(
177 return_value = %Hex(return_value),
178 "step 2: kernel32!CreateFileA() failed"
179 );
180
181 return Ok(RecipeControlFlow::Break);
182 }
183
184 tracing::info!(
185 handle = %Hex(data![handle]),
186 "step 2: kernel32!WriteFile()"
187 );
188
189 // Save the handle.
190 data![handle] = return_value;
191
192 // Allocate a value on the stack to store the output parameter.
193 data![bytes_written_ptr] = copy_to_stack!(0u64)?;
194
195 // Get the first chunk of content.
196 let content = &data![content];
197 let chunk_size = usize::min(content.len(), data![chunk_size]);
198 let chunk = content[..chunk_size].to_vec();
199
200 inject! {
201 kernel32!WriteFile(
202 data![handle], // hFile
203 chunk, // lpBuffer
204 chunk.len() as u64, // nNumberOfBytesToWrite
205 data![bytes_written_ptr], // lpNumberOfBytesWritten
206 0 // lpOverlapped
207 )
208 }
209 },
210 //
211 // Step 3:
212 // - Verify that the `WriteFile()` call succeeded
213 // - Write the next chunk to the file
214 // - Repeat this step until all content is written
215 //
216 {
217 let return_value = registers!().rax;
218
219 if return_value == 0 {
220 tracing::error!(
221 return_value = %Hex(return_value),
222 "step 3: kernel32!WriteFile() failed"
223 );
224
225 return Ok(RecipeControlFlow::Break);
226 }
227
228 // Read the number of bytes written and update the total.
229 let number_of_bytes_written = vmi!().read_u32(data![bytes_written_ptr])?;
230 data![bytes_written_total] += number_of_bytes_written;
231
232 let bytes_written_total = data![bytes_written_total];
233 let content = &data![content];
234
235 // If all content is written, move to the next step.
236 if bytes_written_total >= content.len() as u32 {
237 return Ok(RecipeControlFlow::Continue);
238 }
239
240 // Get the next chunk of content.
241 let remaining = content.len() - bytes_written_total as usize;
242 let chunk_size = usize::min(remaining, data![chunk_size]);
243 let chunk = &content[bytes_written_total as usize..];
244 let chunk = chunk[..chunk_size].to_vec();
245
246 // Allocate a value on the stack to store the output parameter.
247 data![bytes_written_ptr] = copy_to_stack!(0u64)?;
248
249 inject! {
250 kernel32!WriteFile(
251 data![handle], // hFile
252 chunk, // lpBuffer
253 chunk.len() as u64, // nNumberOfBytesToWrite
254 data![bytes_written_ptr], // lpNumberOfBytesWritten
255 0 // lpOverlapped
256 )
257 }?;
258
259 Ok(RecipeControlFlow::Repeat)
260 },
261 //
262 // Step 4:
263 // - Verify that the last `WriteFile()` call succeeded
264 // - Close the file handle
265 //
266 {
267 let return_value = registers!().rax;
268
269 if return_value == 0 {
270 tracing::error!(
271 return_value = %Hex(return_value),
272 "step 4: kernel32!WriteFile() failed"
273 );
274
275 // Don't exit, we want to close the handle.
276 // return Ok(RecipeControlFlow::Break);
277 }
278
279 // Read the number of bytes written.
280 let number_of_bytes_written = vmi!().read_u32(data![bytes_written_ptr])?;
281 tracing::info!(number_of_bytes_written, "step 4: kernel32!WriteFile()");
282
283 tracing::info!(
284 handle = %Hex(data![handle]),
285 "step 4: kernel32!CloseHandle()"
286 );
287
288 inject! {
289 kernel32!CloseHandle(
290 data![handle] // hObject
291 )
292 }
293 },
294 ]
295}
296
297fn main() -> Result<(), Box<dyn std::error::Error>> {
298 let (session, profile) = common::create_vmi_session()?;
299
300 let explorer_pid = {
301 // This block is used to drop the pause guard after the PID is found.
302 // If the `session.handle()` would be called with the VM paused, no
303 // events would be triggered.
304 let _pause_guard = session.pause_guard()?;
305
306 let registers = session.registers(VcpuId(0))?;
307 let vmi = session.with_registers(®isters);
308
309 let explorer = match common::find_process(&vmi, "explorer.exe")? {
310 Some(explorer) => explorer,
311 None => {
312 tracing::error!("explorer.exe not found");
313 return Ok(());
314 }
315 };
316
317 tracing::info!(
318 pid = %explorer.id()?,
319 object = %explorer.object()?,
320 "found explorer.exe"
321 );
322
323 explorer.id()?
324 };
325
326 let mut content = Vec::new();
327 for c in 'A'..='Z' {
328 content.extend((0..2049).map(|_| c as u8).collect::<Vec<_>>());
329 }
330
331 session.handle(|session| {
332 InjectorHandler::new(
333 session,
334 &profile,
335 explorer_pid,
336 recipe_factory(GuestFile::new(
337 "C:\\Users\\John\\Desktop\\test.txt",
338 content,
339 )),
340 )
341 })?;
342
343 Ok(())
344}