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(&registers);
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}