Skip to main content

ud_emulator/
context.rs

1//! Optional emulation-context layer.
2//!
3//! Sits on the [`HostState`](crate::win32::HostState) and provides
4//! per-instance environmental surfaces the guest can query
5//! through Win32 stubs:
6//!
7//! * [`VirtualFs`] — a read/write in-memory filesystem. The
8//!   guest can `CreateFileA` / `ReadFile` / `WriteFile` /
9//!   `CloseHandle` against it; nothing ever touches the host
10//!   filesystem. Used both for "feed a fixture to the program"
11//!   workflows (analyse a sample that wants to load a config
12//!   file) and for "capture what the program would write"
13//!   workflows (malware that stages payloads).
14//! * [`VirtualRegistry`] — a read/write key-value tree
15//!   modelling the Windows registry. Same intent: the guest
16//!   `Reg*` API calls observe whatever the analyst pre-staged
17//!   and writes land in-memory.
18//!
19//! Both pieces are optional. A sandbox with no `Context`
20//! surfaces presents the same fail-soft Win32 stubs as before;
21//! attaching one swaps the no-op fail-soft for "consult the
22//! virtual surface". Future additions (virtual network,
23//! virtual clock, ...) hang off [`Context`] the same way.
24//!
25//! The contract is bounded-only: no `Context` surface can
26//! reach the host's filesystem, registry, network, or
27//! clock — every byte the guest sees came from the
28//! analyst-controlled `Context` or from the emulator's
29//! synthesised state. That's the whole point of running
30//! untrusted code through this stack.
31
32use std::collections::BTreeMap;
33
34/// Top-level optional context layer. Owned by
35/// [`HostState`](crate::win32::HostState); each guest call to a
36/// Win32 stub backed by a virtual surface goes through here.
37#[derive(Debug, Default, Clone)]
38pub struct Context {
39    /// In-memory filesystem, if attached.
40    pub vfs: Option<VirtualFs>,
41    /// In-memory registry, if attached.
42    pub registry: Option<VirtualRegistry>,
43}
44
45impl Context {
46    /// Empty context — no virtual filesystem, no virtual
47    /// registry. Equivalent to [`Default::default`].
48    #[must_use]
49    pub fn new() -> Self {
50        Self::default()
51    }
52
53    /// Builder: attach the given VFS.
54    #[must_use]
55    pub fn with_vfs(mut self, vfs: VirtualFs) -> Self {
56        self.vfs = Some(vfs);
57        self
58    }
59
60    /// Builder: attach the given registry.
61    #[must_use]
62    pub fn with_registry(mut self, reg: VirtualRegistry) -> Self {
63        self.registry = Some(reg);
64        self
65    }
66}
67
68// ============================================================
69// Virtual filesystem
70// ============================================================
71
72/// In-memory filesystem the guest can read and write through
73/// the Win32 file APIs. Paths are normalised to lowercase +
74/// forward slashes internally so `"C:\\Windows\\foo.ini"`,
75/// `"c:/windows/foo.ini"`, and `"C:/WINDOWS/Foo.INI"` all
76/// reference the same file — matching the case-insensitive
77/// behaviour of Windows.
78///
79/// The handle space is local to the VFS; opened handles are
80/// returned as `u32` values starting at [`HANDLE_BASE`].
81#[derive(Debug, Default, Clone)]
82pub struct VirtualFs {
83    files: BTreeMap<String, Vec<u8>>,
84    open: BTreeMap<u32, FileHandle>,
85    next_handle: u32,
86}
87
88/// One open file handle's state.
89#[derive(Debug, Clone)]
90pub struct FileHandle {
91    /// Normalised path the handle refers to.
92    pub path: String,
93    /// Current file pointer, in bytes from start.
94    pub pos: u64,
95    /// Access mode the handle was opened with.
96    pub access: FileAccess,
97}
98
99/// What the guest asked for when opening the file. Mirrors
100/// the Win32 `GENERIC_READ` / `GENERIC_WRITE` axes — the
101/// virtual filesystem honours the access bits so a
102/// `GENERIC_READ`-only handle can't `WriteFile`.
103#[derive(Debug, Clone, Copy, PartialEq, Eq)]
104pub enum FileAccess {
105    Read,
106    Write,
107    ReadWrite,
108}
109
110impl FileAccess {
111    /// Map a Win32 `dwDesiredAccess` bitset to the matching
112    /// [`FileAccess`]. `GENERIC_READ = 0x80000000`,
113    /// `GENERIC_WRITE = 0x40000000`.
114    #[must_use]
115    pub fn from_win32_desired_access(flags: u32) -> Self {
116        let read = flags & 0x8000_0000 != 0;
117        let write = flags & 0x4000_0000 != 0;
118        match (read, write) {
119            (true, true) => FileAccess::ReadWrite,
120            (false, true) => FileAccess::Write,
121            _ => FileAccess::Read, // default to read on (0,0) too
122        }
123    }
124
125    fn allows_read(self) -> bool {
126        matches!(self, FileAccess::Read | FileAccess::ReadWrite)
127    }
128    fn allows_write(self) -> bool {
129        matches!(self, FileAccess::Write | FileAccess::ReadWrite)
130    }
131}
132
133/// First handle [`VirtualFs::open`] hands back. The base is
134/// well above the kernel32 heap arena and the synthetic HIC
135/// space so the kinds don't collide.
136pub const HANDLE_BASE: u32 = 0x6800_0000;
137
138impl VirtualFs {
139    #[must_use]
140    pub fn new() -> Self {
141        Self::default()
142    }
143
144    /// Insert a file. Overwrites whatever was at `path`
145    /// before.
146    pub fn insert(&mut self, path: &str, bytes: Vec<u8>) {
147        self.files.insert(normalize_path(path), bytes);
148    }
149
150    /// Read a file by path. Returns `None` if not present.
151    /// Doesn't open a handle — just peeks.
152    #[must_use]
153    pub fn read(&self, path: &str) -> Option<&[u8]> {
154        self.files.get(&normalize_path(path)).map(Vec::as_slice)
155    }
156
157    /// True iff a file exists at this path.
158    #[must_use]
159    pub fn contains(&self, path: &str) -> bool {
160        self.files.contains_key(&normalize_path(path))
161    }
162
163    /// Replace a file's bytes by path. Inserts if absent.
164    /// Doesn't go through a handle.
165    pub fn write_path(&mut self, path: &str, bytes: Vec<u8>) {
166        self.files.insert(normalize_path(path), bytes);
167    }
168
169    /// Remove a file. Returns `true` if it was present.
170    pub fn remove(&mut self, path: &str) -> bool {
171        self.files.remove(&normalize_path(path)).is_some()
172    }
173
174    /// Iterate over every (path, byte-count) pair.
175    pub fn list(&self) -> impl Iterator<Item = (&str, usize)> {
176        self.files.iter().map(|(k, v)| (k.as_str(), v.len()))
177    }
178
179    /// Open a handle to the file at `path`. Returns `None`
180    /// when the file doesn't exist (and the caller asked for
181    /// read-only access). For write access, the file is
182    /// created if absent.
183    pub fn open(&mut self, path: &str, access: FileAccess) -> Option<u32> {
184        let key = normalize_path(path);
185        let exists = self.files.contains_key(&key);
186        if !exists {
187            if !access.allows_write() {
188                return None;
189            }
190            self.files.insert(key.clone(), Vec::new());
191        }
192        let handle = HANDLE_BASE.wrapping_add(self.next_handle);
193        self.next_handle = self.next_handle.wrapping_add(1);
194        self.open.insert(
195            handle,
196            FileHandle {
197                path: key,
198                pos: 0,
199                access,
200            },
201        );
202        Some(handle)
203    }
204
205    /// Close a handle. Returns `true` if the handle was
206    /// known. Successive `close` calls are tolerated (they
207    /// return `false`).
208    pub fn close(&mut self, handle: u32) -> bool {
209        self.open.remove(&handle).is_some()
210    }
211
212    /// Read up to `buf.len()` bytes from the file at the
213    /// handle's current position. Advances the position by
214    /// the read length. Returns the number of bytes read
215    /// (`0` at EOF), or `None` if the handle is unknown or
216    /// not readable.
217    pub fn read_handle(&mut self, handle: u32, buf: &mut [u8]) -> Option<usize> {
218        let fh = self.open.get_mut(&handle)?;
219        if !fh.access.allows_read() {
220            return None;
221        }
222        let file = self.files.get(&fh.path)?;
223        let pos = fh.pos as usize;
224        if pos >= file.len() {
225            return Some(0);
226        }
227        let n = buf.len().min(file.len() - pos);
228        buf[..n].copy_from_slice(&file[pos..pos + n]);
229        fh.pos = fh.pos.wrapping_add(n as u64);
230        Some(n)
231    }
232
233    /// Write `data` to the file at the handle's current
234    /// position. Extends the file as needed. Returns the
235    /// number of bytes written, or `None` if the handle is
236    /// unknown or not writable.
237    pub fn write_handle(&mut self, handle: u32, data: &[u8]) -> Option<usize> {
238        let fh = self.open.get_mut(&handle)?;
239        if !fh.access.allows_write() {
240            return None;
241        }
242        let file = self.files.get_mut(&fh.path)?;
243        let pos = fh.pos as usize;
244        if pos + data.len() > file.len() {
245            file.resize(pos + data.len(), 0);
246        }
247        file[pos..pos + data.len()].copy_from_slice(data);
248        fh.pos = fh.pos.wrapping_add(data.len() as u64);
249        Some(data.len())
250    }
251
252    /// Move the handle's file pointer to `pos`. Returns the
253    /// new position, or `None` if the handle is unknown.
254    pub fn seek(&mut self, handle: u32, pos: u64) -> Option<u64> {
255        let fh = self.open.get_mut(&handle)?;
256        fh.pos = pos;
257        Some(pos)
258    }
259
260    /// Current size of the file the handle refers to.
261    /// Returns `None` if the handle is unknown.
262    #[must_use]
263    pub fn size(&self, handle: u32) -> Option<u64> {
264        let fh = self.open.get(&handle)?;
265        let file = self.files.get(&fh.path)?;
266        Some(file.len() as u64)
267    }
268
269    /// True iff `handle` was minted by this VFS (and still
270    /// open). Used by the Win32 stubs to disambiguate
271    /// VFS-backed handles from heap / event / semaphore
272    /// handles when `CloseHandle` is called.
273    #[must_use]
274    pub fn owns(&self, handle: u32) -> bool {
275        self.open.contains_key(&handle)
276    }
277}
278
279/// Normalise a Windows-style path for case-insensitive lookup.
280/// Lowercases the whole string and converts `\` to `/`. Drive
281/// letters stay attached (`C:` stays `c:`).
282fn normalize_path(path: &str) -> String {
283    let mut out = String::with_capacity(path.len());
284    for c in path.chars() {
285        if c == '\\' {
286            out.push('/');
287        } else {
288            out.extend(c.to_lowercase());
289        }
290    }
291    out
292}
293
294// ============================================================
295// Virtual registry
296// ============================================================
297
298/// In-memory Windows registry tree. Keys are referenced by
299/// their full path (`"HKEY_LOCAL_MACHINE\\SOFTWARE\\Microsoft"`).
300/// Each key holds a set of named values; the value names are
301/// matched case-insensitively per the Win32 contract.
302///
303/// The handle space is local to the registry; opened keys
304/// return synthetic `HKEY` values starting at
305/// [`HKEY_USER_BASE`]. The four predefined hive roots
306/// (`HKEY_LOCAL_MACHINE`, `HKEY_CURRENT_USER`,
307/// `HKEY_CLASSES_ROOT`, `HKEY_USERS`) live at
308/// [`HKLM`] / [`HKCU`] / [`HKCR`] / [`HKU`] respectively.
309#[derive(Debug, Default, Clone)]
310pub struct VirtualRegistry {
311    /// All known keys, indexed by full path (case-folded).
312    keys: BTreeMap<String, RegistryKey>,
313    /// Open handle → (key path, …).
314    open: BTreeMap<u32, OpenKey>,
315    next_handle: u32,
316}
317
318/// One open registry key.
319#[derive(Debug, Clone)]
320pub struct OpenKey {
321    pub path: String,
322}
323
324/// One registry key — a bag of named values.
325#[derive(Debug, Default, Clone)]
326pub struct RegistryKey {
327    values: BTreeMap<String, RegistryValue>,
328}
329
330/// One typed registry value. Mirrors the standard `REG_SZ` /
331/// `REG_DWORD` / `REG_BINARY` / `REG_MULTI_SZ` shapes.
332#[derive(Debug, Clone, PartialEq, Eq)]
333pub enum RegistryValue {
334    /// `REG_SZ` — null-terminated string.
335    Sz(String),
336    /// `REG_EXPAND_SZ` — string with embedded `%VAR%`
337    /// references the loader is expected to expand.
338    ExpandSz(String),
339    /// `REG_DWORD` — 32-bit integer.
340    Dword(u32),
341    /// `REG_QWORD` — 64-bit integer.
342    Qword(u64),
343    /// `REG_BINARY` — opaque bytes.
344    Binary(Vec<u8>),
345    /// `REG_MULTI_SZ` — `\0`-separated list, terminated by
346    /// an extra `\0`.
347    MultiSz(Vec<String>),
348}
349
350/// Predefined hive handles. Win32 docs spell them as negative
351/// pointers, but we hand back ordinary `u32` values; the
352/// codec doesn't care what the bits are, only that closing
353/// + reopening lands a different key correctly.
354pub const HKEY_CLASSES_ROOT: u32 = 0x8000_0000;
355pub const HKEY_CURRENT_USER: u32 = 0x8000_0001;
356pub const HKEY_LOCAL_MACHINE: u32 = 0x8000_0002;
357pub const HKEY_USERS: u32 = 0x8000_0003;
358/// Short aliases the docs use interchangeably.
359pub const HKCR: u32 = HKEY_CLASSES_ROOT;
360pub const HKCU: u32 = HKEY_CURRENT_USER;
361pub const HKLM: u32 = HKEY_LOCAL_MACHINE;
362pub const HKU: u32 = HKEY_USERS;
363
364/// First handle [`VirtualRegistry::open_key`] hands back.
365pub const HKEY_USER_BASE: u32 = 0x6900_0000;
366
367impl VirtualRegistry {
368    #[must_use]
369    pub fn new() -> Self {
370        Self::default()
371    }
372
373    /// Set a value on a key, creating the key if absent.
374    pub fn set_value(&mut self, key_path: &str, name: &str, value: RegistryValue) {
375        let key = normalize_path(key_path);
376        let entry = self.keys.entry(key).or_default();
377        entry.values.insert(name.to_ascii_lowercase(), value);
378    }
379
380    /// Read a value by `(key, name)`. Case-insensitive.
381    #[must_use]
382    pub fn get_value(&self, key_path: &str, name: &str) -> Option<&RegistryValue> {
383        let key = normalize_path(key_path);
384        self.keys
385            .get(&key)
386            .and_then(|k| k.values.get(&name.to_ascii_lowercase()))
387    }
388
389    /// Iterate every `(key_path, value_name, value)` triple in
390    /// the virtual registry. Suitable for "what did the guest
391    /// write?" reports.
392    pub fn all_values(&self) -> impl Iterator<Item = (&str, &str, &RegistryValue)> {
393        self.keys.iter().flat_map(|(key_path, key)| {
394            key.values
395                .iter()
396                .map(move |(name, value)| (key_path.as_str(), name.as_str(), value))
397        })
398    }
399
400    /// True iff the named key exists.
401    #[must_use]
402    pub fn contains_key(&self, key_path: &str) -> bool {
403        self.keys.contains_key(&normalize_path(key_path))
404    }
405
406    /// Resolve a predefined hive handle to its canonical
407    /// path string.
408    #[must_use]
409    pub fn predefined_path(hkey: u32) -> Option<&'static str> {
410        match hkey {
411            HKEY_CLASSES_ROOT => Some("hkey_classes_root"),
412            HKEY_CURRENT_USER => Some("hkey_current_user"),
413            HKEY_LOCAL_MACHINE => Some("hkey_local_machine"),
414            HKEY_USERS => Some("hkey_users"),
415            _ => None,
416        }
417    }
418
419    /// Open a subkey under `base_hkey` (which may be a
420    /// predefined hive or a previously-returned user handle).
421    /// Returns `None` if the key doesn't exist.
422    pub fn open_key(&mut self, base_hkey: u32, subkey: &str) -> Option<u32> {
423        let base_path = if let Some(p) = Self::predefined_path(base_hkey) {
424            p.to_string()
425        } else {
426            self.open.get(&base_hkey)?.path.clone()
427        };
428        let combined = if subkey.is_empty() {
429            base_path
430        } else {
431            format!("{}/{}", base_path, normalize_path(subkey))
432        };
433        if !self.keys.contains_key(&combined) {
434            return None;
435        }
436        let h = HKEY_USER_BASE.wrapping_add(self.next_handle);
437        self.next_handle = self.next_handle.wrapping_add(1);
438        self.open.insert(h, OpenKey { path: combined });
439        Some(h)
440    }
441
442    /// Close a previously-opened handle. Predefined hives
443    /// (`HKLM` etc.) are tolerated — the call is a no-op.
444    /// Returns `true` if the handle was a user handle that
445    /// was closed.
446    pub fn close_key(&mut self, hkey: u32) -> bool {
447        if Self::predefined_path(hkey).is_some() {
448            return true;
449        }
450        self.open.remove(&hkey).is_some()
451    }
452
453    /// True iff `hkey` was minted by this registry (and still
454    /// open). Used by the Win32 stubs to disambiguate
455    /// registry handles from VFS / heap / event handles when
456    /// `CloseHandle` is called.
457    #[must_use]
458    pub fn owns(&self, hkey: u32) -> bool {
459        Self::predefined_path(hkey).is_some() || self.open.contains_key(&hkey)
460    }
461
462    /// Map an open handle back to its canonical key path.
463    /// Returns `None` if `hkey` isn't known.
464    #[must_use]
465    pub fn path_of(&self, hkey: u32) -> Option<&str> {
466        if let Some(p) = Self::predefined_path(hkey) {
467            return Some(p);
468        }
469        self.open.get(&hkey).map(|k| k.path.as_str())
470    }
471}
472
473#[cfg(test)]
474mod tests {
475    use super::*;
476
477    #[test]
478    fn vfs_insert_and_read_case_insensitive() {
479        let mut vfs = VirtualFs::new();
480        vfs.insert("C:\\Windows\\foo.ini", b"hello".to_vec());
481        assert_eq!(vfs.read("C:\\Windows\\foo.ini"), Some(&b"hello"[..]));
482        assert_eq!(vfs.read("c:/windows/FOO.INI"), Some(&b"hello"[..]));
483        assert_eq!(vfs.read("nope.txt"), None);
484    }
485
486    #[test]
487    fn vfs_open_read_close() {
488        let mut vfs = VirtualFs::new();
489        vfs.insert("a.txt", b"hello world".to_vec());
490        let h = vfs.open("a.txt", FileAccess::Read).expect("opens");
491        let mut buf = [0u8; 5];
492        assert_eq!(vfs.read_handle(h, &mut buf), Some(5));
493        assert_eq!(&buf, b"hello");
494        assert_eq!(vfs.read_handle(h, &mut buf), Some(5));
495        assert_eq!(&buf, b" worl");
496        let mut tail = [0u8; 5];
497        assert_eq!(vfs.read_handle(h, &mut tail), Some(1));
498        assert_eq!(&tail[..1], b"d");
499        assert_eq!(vfs.read_handle(h, &mut buf), Some(0));
500        assert!(vfs.close(h));
501    }
502
503    #[test]
504    fn vfs_write_extends_and_round_trips() {
505        let mut vfs = VirtualFs::new();
506        let h = vfs.open("new.txt", FileAccess::Write).expect("opens");
507        assert_eq!(vfs.write_handle(h, b"hello").unwrap(), 5);
508        assert_eq!(vfs.write_handle(h, b" world").unwrap(), 6);
509        vfs.close(h);
510        assert_eq!(vfs.read("new.txt"), Some(&b"hello world"[..]));
511    }
512
513    #[test]
514    fn vfs_read_only_handle_cannot_write() {
515        let mut vfs = VirtualFs::new();
516        vfs.insert("a.txt", b"hi".to_vec());
517        let h = vfs.open("a.txt", FileAccess::Read).unwrap();
518        assert!(vfs.write_handle(h, b"!").is_none());
519    }
520
521    #[test]
522    fn vfs_open_nonexistent_read_returns_none() {
523        let mut vfs = VirtualFs::new();
524        assert!(vfs.open("missing.txt", FileAccess::Read).is_none());
525    }
526
527    #[test]
528    fn registry_set_get_case_insensitive() {
529        let mut reg = VirtualRegistry::new();
530        reg.set_value(
531            "HKLM\\Software\\Foo",
532            "Version",
533            RegistryValue::Sz("1.2.3".into()),
534        );
535        assert_eq!(
536            reg.get_value("hklm/software/foo", "version"),
537            Some(&RegistryValue::Sz("1.2.3".into()))
538        );
539        assert_eq!(
540            reg.get_value("HKLM\\Software\\Foo", "VERSION"),
541            Some(&RegistryValue::Sz("1.2.3".into()))
542        );
543    }
544
545    #[test]
546    fn registry_open_close_round_trip() {
547        let mut reg = VirtualRegistry::new();
548        reg.set_value(
549            "hkey_local_machine/software/foo",
550            "x",
551            RegistryValue::Dword(1),
552        );
553        let h = reg.open_key(HKLM, "Software\\Foo").expect("opens");
554        assert!(reg.owns(h));
555        assert_eq!(reg.path_of(h), Some("hkey_local_machine/software/foo"));
556        assert!(reg.close_key(h));
557    }
558
559    #[test]
560    fn context_builders() {
561        let mut vfs = VirtualFs::new();
562        vfs.insert("a.txt", b"x".to_vec());
563        let mut reg = VirtualRegistry::new();
564        reg.set_value("hklm", "v", RegistryValue::Dword(1));
565        let ctx = Context::new().with_vfs(vfs).with_registry(reg);
566        assert!(ctx.vfs.is_some());
567        assert!(ctx.registry.is_some());
568    }
569}