Skip to main content

decant_memflow/
backend.rs

1use std::sync::Mutex;
2
3use decant_backend::{BackendError, MemoryBackend, Result};
4use decant_inject::guest::{
5    GuestCapabilities, GuestInjectError, GuestMemoryBackend, GuestMemoryRegion, GuestModuleInfo,
6    GuestProcessInfo,
7};
8use decant_protocol::{MemRegion, ModuleInfo, Pid, ProcessInfo};
9
10use memflow::prelude::v1::*;
11
12const PAGE_SIZE: usize = 0x1000;
13
14pub struct MemflowBackend {
15    os: Mutex<OsInstanceArcBox<'static>>,
16    proc_cache: Mutex<Option<(u32, IntoProcessInstanceArcBox<'static>)>>,
17    connector: String,
18}
19
20fn other<E: std::fmt::Debug>(e: E) -> BackendError {
21    BackendError::Other(format!("memflow: {e:?}"))
22}
23
24fn guest_other<E: std::fmt::Display>(e: E) -> GuestInjectError {
25    GuestInjectError::Backend(e.to_string())
26}
27
28impl MemflowBackend {
29    pub fn connect(connector: &str) -> anyhow::Result<Self> {
30        let mut inventory = Inventory::scan();
31        let connector_args = std::env::var("DECANT_CONNECTOR_ARGS")
32            .ok()
33            .filter(|s| !s.is_empty());
34        let os_args = std::env::var("DECANT_OS_ARGS")
35            .ok()
36            .filter(|s| !s.is_empty());
37
38        let mut builder = inventory.builder().connector(connector);
39        if let Some(a) = connector_args {
40            let args: ConnectorArgs = a
41                .parse()
42                .map_err(|e| anyhow::anyhow!("parsing DECANT_CONNECTOR_ARGS {a:?}: {e:?}"))?;
43            builder = builder.args(args);
44        }
45
46        let builder = builder.os("win32");
47        let os = if let Some(a) = os_args {
48            let args: OsArgs = a
49                .parse()
50                .map_err(|e| anyhow::anyhow!("parsing DECANT_OS_ARGS {a:?}: {e:?}"))?;
51            builder.args(args).build()
52        } else {
53            builder.build()
54        }
55        .map_err(|e| {
56            anyhow::anyhow!(
57                "building memflow OS via the {connector:?} connector: {e:?}. Check the {connector} \
58                 plugin is in MEMFLOW_PLUGIN_PATH and the VM is running. The qemu connector needs \
59                 ptrace access (CAP_SYS_PTRACE on the daemon, or root); the kvm connector needs root \
60                 and the memflow kernel module."
61            )
62        })?;
63
64        Ok(MemflowBackend {
65            os: Mutex::new(os),
66            proc_cache: Mutex::new(None),
67            connector: connector.to_string(),
68        })
69    }
70
71    pub fn connector(&self) -> &str {
72        &self.connector
73    }
74
75    // Reuse a resolved process across calls; re-resolving per read makes memflow
76    // rebuild the address translation every time and dominates scan latency. Keyed by
77    // pid, refreshed when the pid changes.
78    fn with_process<R>(
79        &self,
80        pid: Pid,
81        f: impl FnOnce(&mut IntoProcessInstanceArcBox<'static>) -> Result<R>,
82    ) -> Result<R> {
83        let os = self.os.lock().unwrap();
84        let mut cache = self.proc_cache.lock().unwrap();
85        if cache.as_ref().map(|(p, _)| *p) != Some(pid.0) {
86            let proc =
87                os.clone()
88                    .into_process_by_pid(pid.0)
89                    .map_err(|_| BackendError::NoSuchProcess {
90                        pid: Some(pid.0),
91                        name: None,
92                    })?;
93            *cache = Some((pid.0, proc));
94        }
95        f(&mut cache.as_mut().unwrap().1)
96    }
97
98    fn clear_process_cache(&self, pid: Pid) {
99        let mut cache = self.proc_cache.lock().unwrap();
100        if cache.as_ref().map(|(p, _)| *p) == Some(pid.0) {
101            *cache = None;
102        }
103    }
104
105    fn write_once(&self, pid: Pid, addr: u64, data: &[u8]) -> Result<usize> {
106        self.with_process(pid, |proc| {
107            proc.write_raw(Address::from(addr), data)
108                .map_err(|e| BackendError::WriteFailed {
109                    addr,
110                    reason: format!("{e:?}"),
111                })?;
112            Ok(data.len())
113        })
114    }
115
116    fn write_paged(&self, pid: Pid, addr: u64, data: &[u8]) -> Result<usize> {
117        let mut offset = 0usize;
118        while offset < data.len() {
119            let cur = addr + offset as u64;
120            let page_remaining = PAGE_SIZE - (cur as usize & (PAGE_SIZE - 1));
121            let chunk_len = page_remaining.min(data.len() - offset);
122            self.write_once(pid, cur, &data[offset..offset + chunk_len])?;
123            offset += chunk_len;
124        }
125        Ok(data.len())
126    }
127}
128
129impl MemoryBackend for MemflowBackend {
130    fn list_processes(&self) -> Result<Vec<ProcessInfo>> {
131        let mut os = self.os.lock().unwrap();
132        let infos = os.process_info_list().map_err(other)?;
133        Ok(infos
134            .into_iter()
135            .map(|i| ProcessInfo {
136                pid: Pid(i.pid),
137                name: i.name.to_string(),
138            })
139            .collect())
140    }
141
142    fn process_by_pid(&self, pid: Pid) -> Result<ProcessInfo> {
143        let mut os = self.os.lock().unwrap();
144        match os.process_info_by_pid(pid.0) {
145            Ok(i) => Ok(ProcessInfo {
146                pid: Pid(i.pid),
147                name: i.name.to_string(),
148            }),
149            Err(_) => Err(BackendError::NoSuchProcess {
150                pid: Some(pid.0),
151                name: None,
152            }),
153        }
154    }
155
156    fn process_by_name(&self, name: &str) -> Result<ProcessInfo> {
157        let mut os = self.os.lock().unwrap();
158        match os.process_info_by_name(name) {
159            Ok(i) => Ok(ProcessInfo {
160                pid: Pid(i.pid),
161                name: i.name.to_string(),
162            }),
163            Err(_) => Err(BackendError::NoSuchProcess {
164                pid: None,
165                name: Some(name.to_string()),
166            }),
167        }
168    }
169
170    fn module_list(&self, pid: Pid) -> Result<Vec<ModuleInfo>> {
171        let mut os = self.os.lock().unwrap();
172        let mut proc = os
173            .process_by_pid(pid.0)
174            .map_err(|_| BackendError::NoSuchProcess {
175                pid: Some(pid.0),
176                name: None,
177            })?;
178        let mods = proc.module_list().map_err(other)?;
179        Ok(mods.into_iter().map(module_to_info).collect())
180    }
181
182    fn module_by_name(&self, pid: Pid, name: &str) -> Result<ModuleInfo> {
183        let mut os = self.os.lock().unwrap();
184        let mut proc = os
185            .process_by_pid(pid.0)
186            .map_err(|_| BackendError::NoSuchProcess {
187                pid: Some(pid.0),
188                name: None,
189            })?;
190        let m = module_by_name_ci(&mut proc, pid, name)?;
191        Ok(module_to_info(m))
192    }
193
194    fn module_exports(&self, pid: Pid, module: &str) -> Result<Vec<(String, u64)>> {
195        let mut os = self.os.lock().unwrap();
196        let mut proc = os
197            .process_by_pid(pid.0)
198            .map_err(|_| BackendError::NoSuchProcess {
199                pid: Some(pid.0),
200                name: None,
201            })?;
202        let m = module_by_name_ci(&mut proc, pid, module)?;
203        let exports = proc.module_export_list(&m).map_err(other)?;
204        let base = m.base.to_umem();
205        Ok(exports
206            .into_iter()
207            .map(|e| (e.name.to_string(), base + e.offset))
208            .collect())
209    }
210
211    fn read(&self, pid: Pid, addr: u64, len: usize) -> Result<Vec<u8>> {
212        self.with_process(pid, |proc| {
213            proc.read_raw(Address::from(addr), len)
214                .map_err(|e| BackendError::ReadFailed {
215                    addr,
216                    len: len as u64,
217                    reason: format!("{e:?}"),
218                })
219        })
220    }
221
222    fn write(&self, pid: Pid, addr: u64, data: &[u8]) -> Result<usize> {
223        match self.write_once(pid, addr, data) {
224            Ok(written) => Ok(written),
225            Err(first) => {
226                self.clear_process_cache(pid);
227                match self.write_once(pid, addr, data) {
228                    Ok(written) => Ok(written),
229                    Err(second) => {
230                        self.clear_process_cache(pid);
231                        self.write_paged(pid, addr, data).map_err(|third| {
232                            BackendError::WriteFailed {
233                                addr,
234                                reason: format!(
235                                    "{third}; after full-write retry failures: {first}; {second}"
236                                ),
237                            }
238                        })
239                    }
240                }
241            }
242        }
243    }
244
245    fn memory_map(&self, pid: Pid) -> Result<Vec<MemRegion>> {
246        let mut os = self.os.lock().unwrap();
247        let mut proc = os
248            .process_by_pid(pid.0)
249            .map_err(|_| BackendError::NoSuchProcess {
250                pid: Some(pid.0),
251                name: None,
252            })?;
253        let ranges = proc.mapped_mem_vec(-1);
254        Ok(ranges
255            .into_iter()
256            .map(
257                |CTup3(addr, size, page_type): CTup3<Address, umem, PageType>| MemRegion {
258                    base: addr.to_umem(),
259                    size,
260                    readable: true,
261                    writable: page_type.contains(PageType::WRITEABLE),
262                    executable: !page_type.contains(PageType::NOEXEC),
263                },
264            )
265            .collect())
266    }
267}
268
269impl GuestMemoryBackend for MemflowBackend {
270    fn capabilities(&self) -> GuestCapabilities {
271        GuestCapabilities::memflow_guest_injection()
272    }
273
274    fn list_processes(&self) -> std::result::Result<Vec<GuestProcessInfo>, GuestInjectError> {
275        <Self as MemoryBackend>::list_processes(self)
276            .map(|processes| {
277                processes
278                    .into_iter()
279                    .map(|p| GuestProcessInfo {
280                        pid: p.pid.0,
281                        name: p.name,
282                    })
283                    .collect()
284            })
285            .map_err(guest_other)
286    }
287
288    fn module_list(&self, pid: u32) -> std::result::Result<Vec<GuestModuleInfo>, GuestInjectError> {
289        <Self as MemoryBackend>::module_list(self, Pid(pid))
290            .map(|modules| {
291                modules
292                    .into_iter()
293                    .map(|m| GuestModuleInfo {
294                        name: m.name,
295                        base: m.base,
296                        size: m.size,
297                    })
298                    .collect()
299            })
300            .map_err(guest_other)
301    }
302
303    fn module_exports(
304        &self,
305        pid: u32,
306        module: &str,
307    ) -> std::result::Result<Vec<(String, u64)>, GuestInjectError> {
308        <Self as MemoryBackend>::module_exports(self, Pid(pid), module).map_err(guest_other)
309    }
310
311    fn memory_map(
312        &self,
313        pid: u32,
314    ) -> std::result::Result<Vec<GuestMemoryRegion>, GuestInjectError> {
315        <Self as MemoryBackend>::memory_map(self, Pid(pid))
316            .map(|regions| {
317                regions
318                    .into_iter()
319                    .map(|r| GuestMemoryRegion {
320                        base: r.base,
321                        size: r.size,
322                        readable: r.readable,
323                        writable: r.writable,
324                        executable: r.executable,
325                    })
326                    .collect()
327            })
328            .map_err(guest_other)
329    }
330
331    fn read(
332        &self,
333        pid: u32,
334        addr: u64,
335        len: usize,
336    ) -> std::result::Result<Vec<u8>, GuestInjectError> {
337        <Self as MemoryBackend>::read(self, Pid(pid), addr, len).map_err(guest_other)
338    }
339
340    fn write(&self, pid: u32, addr: u64, data: &[u8]) -> std::result::Result<(), GuestInjectError> {
341        <Self as MemoryBackend>::write(self, Pid(pid), addr, data)
342            .map(|_| ())
343            .map_err(guest_other)
344    }
345
346    fn spoof_vad_type(
347        &self,
348        pid: u32,
349        base: u64,
350        size: u64,
351    ) -> std::result::Result<(), GuestInjectError> {
352        let _ = (pid, base, size);
353        Err(GuestInjectError::Unsupported {
354            operation: "VAD type spoofing",
355            reason: "the memflow backend does not expose a versioned, validated Windows VAD mutation API".into(),
356        })
357    }
358}
359
360fn module_by_name_ci<P: memflow::os::Process + ?Sized>(
361    proc: &mut P,
362    pid: Pid,
363    name: &str,
364) -> Result<ModuleInfo_> {
365    if let Ok(module) = proc.module_by_name(name) {
366        return Ok(module);
367    }
368    let modules = proc.module_list().map_err(other)?;
369    modules
370        .into_iter()
371        .find(|module| module_matches(module, name))
372        .ok_or_else(|| BackendError::NoSuchModule {
373            pid: pid.0,
374            module: name.to_string(),
375        })
376}
377
378fn module_matches(module: &ModuleInfo_, name: &str) -> bool {
379    let module_name = module.name.to_string();
380    if module_name.eq_ignore_ascii_case(name) {
381        return true;
382    }
383    module
384        .path
385        .to_string()
386        .rsplit(['\\', '/'])
387        .next()
388        .is_some_and(|base| base.eq_ignore_ascii_case(name))
389}
390
391fn module_to_info(m: ModuleInfo_) -> ModuleInfo {
392    ModuleInfo {
393        name: m.name.to_string(),
394        base: m.base.to_umem(),
395        size: m.size,
396    }
397}
398
399// aliased to avoid clash with our wire ModuleInfo
400use memflow::os::module::ModuleInfo as ModuleInfo_;