diff --git a/Cargo.lock b/Cargo.lock index 9813848..643209a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -575,9 +575,9 @@ dependencies = [ [[package]] name = "hashbrown" -version = "0.15.2" +version = "0.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289" +checksum = "5419bdc4f6a9207fbeba6d11b604d481addf78ecd10c11ad51e76c2f6482748d" [[package]] name = "hermit-abi" @@ -638,12 +638,12 @@ checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" [[package]] name = "indexmap" -version = "2.9.0" +version = "2.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cea70ddb795996207ad57735b50c5982d8844f38ba9ee5f1aedcfb708a2aa11e" +checksum = "6717a8d2a5a929a1a2eb43a12812498ed141a0bcfb7e8f7844fbdbe4303bba9f" dependencies = [ "equivalent", - "hashbrown 0.15.2", + "hashbrown 0.16.0", ] [[package]] @@ -726,9 +726,9 @@ checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" [[package]] name = "libc" -version = "0.2.172" +version = "0.2.182" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d750af042f7ef4f724306de029d18836c26c1765a54a6a3f094cbd23a7267ffa" +checksum = "6800badb6cb2082ffd7b6a67e6125bb39f18782f793520caee8cb8846be06112" [[package]] name = "libloading" @@ -824,9 +824,9 @@ checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" [[package]] name = "memflow" -version = "0.2.3" +version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df612ab27a15bc64554a6bc93cf80493b9bff753834aebea044089ffdf6295b6" +checksum = "0b5a164dd29bb697a512c389215acf4899a3e671a72844acb04d1ddac6ba8d7f" dependencies = [ "abi_stable", "bitflags 1.3.2", @@ -858,9 +858,9 @@ dependencies = [ [[package]] name = "memflow-derive" -version = "0.2.0" +version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d766f6681f968c92eb0359fc4bc99039ebe2568df4bb884c7cb7b16023e94d32" +checksum = "d894dc2b0bbce37b81280e1b36da4db3e524e4df5a57d5899b4bd943f489b506" dependencies = [ "darling", "proc-macro-crate", @@ -1012,12 +1012,11 @@ checksum = "b4596b6d070b27117e987119b4dac604f3c58cfb0b191112e24771b2faeac1a6" [[package]] name = "proc-macro-crate" -version = "2.0.2" +version = "3.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b00f26d3400549137f92511a46ac1cd8ce37cb5598a96d382381458b992a5d24" +checksum = "219cb19e96be00ab2e37d6e299658a0cfa83e52429179969b0f0121b4ac46983" dependencies = [ - "toml_datetime", - "toml_edit", + "toml_edit 0.23.7", ] [[package]] @@ -1223,18 +1222,28 @@ checksum = "388a1df253eca08550bef6c72392cfe7c30914bf41df5269b68cbd6ff8f570a3" [[package]] name = "serde" -version = "1.0.219" +version = "1.0.228" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.219" +version = "1.0.228" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" dependencies = [ "proc-macro2", "quote", @@ -1345,8 +1354,8 @@ checksum = "185d8ab0dfbb35cf1399a6344d8484209c088f75f8f68230da55d48d95d43e3d" dependencies = [ "serde", "serde_spanned", - "toml_datetime", - "toml_edit", + "toml_datetime 0.6.3", + "toml_edit 0.20.2", ] [[package]] @@ -1358,6 +1367,15 @@ dependencies = [ "serde", ] +[[package]] +name = "toml_datetime" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2cdb639ebbc97961c51720f858597f7f24c4fc295327923af55b74c3c724533" +dependencies = [ + "serde_core", +] + [[package]] name = "toml_edit" version = "0.20.2" @@ -1367,8 +1385,29 @@ dependencies = [ "indexmap", "serde", "serde_spanned", - "toml_datetime", - "winnow", + "toml_datetime 0.6.3", + "winnow 0.5.40", +] + +[[package]] +name = "toml_edit" +version = "0.23.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6485ef6d0d9b5d0ec17244ff7eb05310113c3f316f2d14200d4de56b3cb98f8d" +dependencies = [ + "indexmap", + "toml_datetime 0.7.3", + "toml_parser", + "winnow 0.7.13", +] + +[[package]] +name = "toml_parser" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0cbe268d35bdb4bb5a56a2de88d0ad0eb70af5384a99d648cd4b3d04039800e" +dependencies = [ + "winnow 0.7.13", ] [[package]] @@ -1841,6 +1880,15 @@ dependencies = [ "memchr", ] +[[package]] +name = "winnow" +version = "0.7.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21a0236b59786fed61e2a80582dd500fe61f18b5dca67a4a067d0bc9039339cf" +dependencies = [ + "memchr", +] + [[package]] name = "x86_64" version = "0.14.13" diff --git a/Cargo.toml b/Cargo.toml index 1edf470..bd65bc1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "memflow-native" version = "0.2.6" -authors = ["Aurimas Blažulionis <0x60@pm.me>", "ko1N "] +authors = ["Aurimas Blažulionis <0x60@pm.me>", "ko1N ", "k1nd0ne "] edition = "2021" build = "build.rs" description = "System call based proxy-OS for memflow" @@ -47,7 +47,6 @@ windows = { version = "0.61", features = [ "Win32_UI_Input", "Win32_UI_Input_KeyboardAndMouse", ] } - [target.'cfg(target_os = "macos")'.dependencies] mac-sys-info = "0.1" libproc = "0.14" diff --git a/src/linux/process.rs b/src/linux/process.rs index 93ba5aa..0df66e0 100644 --- a/src/linux/process.rs +++ b/src/linux/process.rs @@ -108,6 +108,28 @@ impl LinuxProcess { MMapPath::Other(s) => s.as_str().into(), } } + + #[cfg(memflow_plugin_api = "2")] + fn collect_envars(&self) -> Result> { + let path = format!("/proc/{}/environ", self.pid); + let data = std::fs::read(path) + .map_err(|_| Error(ErrorOrigin::OsLayer, ErrorKind::EnvarNotFound))?; + + let mut out = Vec::new(); + for entry in data.split(|b| *b == 0).filter(|entry| !entry.is_empty()) { + let entry = String::from_utf8_lossy(entry); + if let Some((name, value)) = entry.split_once('=') { + out.push(EnvVarInfo { + name: ReprCString::from(name), + value: ReprCString::from(value), + address: Address::NULL, + arch: self.info.proc_arch, + }); + } + } + + Ok(out) + } } cglue_impl_group!(LinuxProcess, ProcessInstance, {}); @@ -276,6 +298,42 @@ impl Process for LinuxProcess { } } } + #[cfg(memflow_plugin_api = "2")] + fn envar_list_callback( + &mut self, + target_arch: Option<&ArchitectureIdent>, + mut callback: EnvVarCallback, + ) -> Result<()> { + if let Some(arch) = target_arch { + if *arch != self.info.proc_arch { + return Ok(()); + } + } + + for envar in self.collect_envars()? { + if !callback.call(envar) { + break; + } + } + + Ok(()) + } + + #[cfg(memflow_plugin_api = "2")] + fn environment_block_address(&mut self, _architecture: ArchitectureIdent) -> Result
{ + // Linux does not expose a stable public env-block pointer through procfs. + Ok(Address::NULL) + } + + #[cfg(memflow_plugin_api = "2")] + fn envar_list_from_address( + &mut self, + _env_block: Address, + architecture: ArchitectureIdent, + callback: EnvVarCallback, + ) -> Result<()> { + self.envar_list_callback(Some(&architecture), callback) + } } impl MemoryView for LinuxProcess { diff --git a/src/macos/mem.rs b/src/macos/mem.rs index 4070623..4c8cf41 100644 --- a/src/macos/mem.rs +++ b/src/macos/mem.rs @@ -15,7 +15,9 @@ fn get_task(pid: u32) -> Result { let mut task = MACH_PORT_NULL; let res = task_for_pid(mach_task_self(), pid as i32, &mut task as *mut mach_port_t); if res != KERN_SUCCESS { - log::error!("Could not get task: {res}"); + log::warn!( + "task_for_pid permission denied/unavailable for pid {pid} (kern_return={res})" + ); Err(Error(ErrorOrigin::OsLayer, ErrorKind::Unknown)) } else { Ok(task) diff --git a/src/macos/mod.rs b/src/macos/mod.rs index 92e103c..4c91d27 100644 --- a/src/macos/mod.rs +++ b/src/macos/mod.rs @@ -32,6 +32,8 @@ pub use process::MacProcess; pub(super) struct ProcArgs { pub exec_path: String, pub argv: Vec, + #[cfg(memflow_plugin_api = "2")] + pub environ: Vec<(String, String)>, } fn argmax() -> usize { @@ -58,6 +60,8 @@ fn argmax() -> usize { } pub(super) fn read_procargs2(pid: Pid) -> Result> { + // Read raw `KERN_PROCARGS2` for the target pid. + // This is shared by process-info building and env/argv enumeration to keep parsing consistent. let mut scratch = vec![0u8; argmax()]; let mut mib: [c_int; 3] = [CTL_KERN, KERN_PROCARGS2, pid as _]; @@ -82,6 +86,34 @@ pub(super) fn read_procargs2(pid: Pid) -> Result> { Ok(scratch) } +#[cfg(memflow_plugin_api = "2")] +fn parse_environ(buf: &[u8]) -> Vec<(String, String)> { + let mut environ = Vec::new(); + let mut idx = 0; + + while idx < buf.len() { + let start = idx; + while idx < buf.len() && buf[idx] != 0 { + idx += 1; + } + + if idx == start { + break; + } + + let entry = String::from_utf8_lossy(&buf[start..idx]); + if let Some((name, value)) = entry.split_once('=') { + environ.push((name.to_string(), value.to_string())); + } + + if idx < buf.len() { + idx += 1; + } + } + + environ +} + pub(super) fn parse_procargs2(data: &[u8]) -> Result { if data.len() < 4 { return Err(Error(ErrorOrigin::OsLayer, ErrorKind::Unknown)); @@ -111,22 +143,24 @@ pub(super) fn parse_procargs2(data: &[u8]) -> Result { if idx >= buf.len() { break; } - let start = idx; while idx < buf.len() && buf[idx] != 0 { idx += 1; } - if idx > start { argv.push(String::from_utf8_lossy(&buf[start..idx]).into_owned()); } - if idx < buf.len() { idx += 1; } } - Ok(ProcArgs { exec_path, argv }) + Ok(ProcArgs { + exec_path, + argv, + #[cfg(memflow_plugin_api = "2")] + environ: parse_environ(&buf[idx..]), + }) } fn get_arch() -> ArchitectureIdent { @@ -206,6 +240,8 @@ impl Os for MacOs { } fn process_info_by_pid(&mut self, pid: Pid) -> Result { + // We query BSD info with the target `pid`. + // Using the caller pid here breaks name-based filtering. let bsd_info = lp::proc_pid::pidinfo::(pid as _, 0).map_err(|e| { error!("bsd_info: {e}"); @@ -223,6 +259,7 @@ impl Os for MacOs { .collect::>(); let fallback_path = String::from_utf8_lossy(&fallback_path).into_owned(); + // Prefer parsed procargs for executable path + argv, but always keep a safe fallback. match read_procargs2(pid).and_then(|d| parse_procargs2(&d)) { Ok(parsed) => { let path = if parsed.exec_path.is_empty() { @@ -279,7 +316,7 @@ impl Os for MacOs { /// /// # Arguments /// * `callback` - where to pass each matching module to. This is an opaque callback. - fn module_address_list_callback(&mut self, mut callback: AddressCallback) -> Result<()> { + fn module_address_list_callback(&mut self, _callback: AddressCallback) -> Result<()> { // TODO: build this with OSKextCopyLoadedKextInfo. /*self.cached_modules = procfs::modules() .map_err(|_| Error(ErrorOrigin::OsLayer, ErrorKind::UnableToReadDir))? @@ -298,7 +335,7 @@ impl Os for MacOs { /// /// # Arguments /// * `address` - address where module's information resides in - fn module_by_address(&mut self, address: Address) -> Result { + fn module_by_address(&mut self, _address: Address) -> Result { /*self.cached_modules .get(address.to_umem() as usize) .map(|km| ModuleInfo { diff --git a/src/macos/process.rs b/src/macos/process.rs index 8a0f171..f064dec 100644 --- a/src/macos/process.rs +++ b/src/macos/process.rs @@ -16,6 +16,7 @@ const PROC_PIDREGIONPATHINFO: i32 = 8; use core::mem::MaybeUninit; use itertools::Itertools; +use std::ffi::CStr; #[repr(C)] #[allow(non_camel_case_types)] @@ -116,8 +117,83 @@ impl Clone for MacProcess { } impl MacProcess { + /// Fallback module discovery based on `PROC_PIDREGIONPATHINFO`. + /// + /// This is used when dyld/task-port based module enumeration is unavailable. + /// We walk VM regions, keep file-backed entries with absolute paths, and merge + /// contiguous regions that resolve to the same path into a single module span. + /// The result is best-effort and may be less precise than dyld metadata. + fn update_cached_module_maps_from_regions(&mut self) -> Result<()> { + log::info!( + "Using region-path module fallback for pid {} (dyld/task-port path unavailable)", + self.info.pid + ); + self.cached_module_maps.clear(); + + let mut start = Address::NULL; + let end = Address::invalid(); + let mut prwpi: proc_regionwithpathinfo = unsafe { MaybeUninit::zeroed().assume_init() }; + + while start < end { + let size = core::mem::size_of::() as _; + let ret = unsafe { + proc_pidinfo( + self.info.pid as _, + PROC_PIDREGIONPATHINFO, + start.to_umem() as _, + &mut prwpi as *mut proc_regionwithpathinfo as *mut _, + size, + ) + }; + + if ret <= 0 { + break; + } + if ret < size { + return Err(Error(ErrorOrigin::OsLayer, ErrorKind::Unknown)); + } + + let region_start = Address::from(prwpi.prp_prinfo.pri_address); + let region_size = prwpi.prp_prinfo.pri_size as umem; + + if region_size == 0 { + break; + } + + let path = unsafe { + CStr::from_ptr(prwpi.prp_vip.vip_path.as_ptr().cast::()) + .to_string_lossy() + .into_owned() + }; + + // Only keep concrete filesystem mappings; anonymous/synthetic regions are ignored. + if !path.is_empty() && path.starts_with('/') { + if let Some(last) = self.cached_module_maps.last_mut() { + // Coalesce neighboring regions for the same file into one stable entry. + if last.2 == path && last.0 + last.1 == region_start { + last.1 += region_size; + } else { + self.cached_module_maps + .push((region_start, region_size, path)); + } + } else { + self.cached_module_maps + .push((region_start, region_size, path)); + } + } + + start = region_start + region_size; + } + + self.cached_module_maps.sort_by_key(|v| v.0); + + Ok(()) + } + pub fn try_new(info: ProcessInfo) -> Result { Ok(Self { + // Do not fail process construction just because task_for_pid is denied. + // This keeps name/pid process selection usable for metadata/envar paths. virt_mem: ProcessVirtualMemory::try_new(&info).unwrap_or_else(|e| { log::warn!("Unable to get task port for pid {}: {e:?}", info.pid); ProcessVirtualMemory::new_unavailable(info.pid) @@ -129,178 +205,201 @@ impl MacProcess { } pub fn update_cached_module_maps(&mut self) -> Result<()> { - let mut info: task_dyld_info = unsafe { MaybeUninit::zeroed().assume_init() }; - - self.cached_module_maps.clear(); + let dyld_result = (|| { + let mut info: task_dyld_info = unsafe { MaybeUninit::zeroed().assume_init() }; - let mut count = - (core::mem::size_of::() / core::mem::size_of::()) as _; - let port = self.virt_mem.ensure_port()?; - let ret = unsafe { - task_info( - port, - TASK_DYLD_INFO, - &mut info as *mut task_dyld_info as *mut _, - &mut count, - ) - }; - - if ret != KERN_SUCCESS { - return Err(Error(ErrorOrigin::OsLayer, ErrorKind::Unknown)); - } + self.cached_module_maps.clear(); - // 0 -> 32-bit fmt - // 1 -> 64-bit fmt - // We need to verify that the format is the same as our native pointer width (usize size), - // so that we don't misread nonsense. - if 4 * (1 + info.all_image_info_format) as usize != core::mem::size_of::() { - return Err(Error(ErrorOrigin::OsLayer, ErrorKind::NotSupported)); - } + let mut count = + (core::mem::size_of::() / core::mem::size_of::()) as _; + let port = self.virt_mem.ensure_port()?; + let ret = unsafe { + task_info( + port, + TASK_DYLD_INFO, + &mut info as *mut task_dyld_info as *mut _, + &mut count, + ) + }; - let infos = self.read::(info.all_image_info_addr.into())?; - - let mut left = infos.info_array_count as usize; - let mut info_buf = vec![dyld_image_info::default(); core::cmp::min(128, left)]; - - while left > 0 { - let pos = infos.info_array_count as usize - left; - - let size = core::cmp::min(left, info_buf.len()); - - self.read_into( - Address::from( - infos.dyld_image_info + pos * core::mem::size_of::(), - ), - &mut info_buf[..size], - )?; - left -= size; - - // And now, let's process the elements - for i in &info_buf[..size] { - // TODO: do the string reads concurrently - let name = self.read_utf8_lossy(i.image_file_path.into(), 4096)?; - - let start = Address::from(i.image_load_address); - let mut end = start; - - // Now, we need to figure out the size of the image. To do this, iterate through - // proc_regioninfo and grab all entries with identical (non-zero) inode number. - let mut prwpi: proc_regionwithpathinfo = - unsafe { MaybeUninit::zeroed().assume_init() }; - let mut last_ino = 0; - - loop { - let size = core::mem::size_of::() as _; - let ret = unsafe { - proc_pidinfo( - self.info.pid as _, - PROC_PIDREGIONPATHINFO, - end.to_umem() as _, - &mut prwpi as *mut proc_regionwithpathinfo as *mut _, - size, - ) - }; - - if ret <= 0 { - break; - } - if ret < size { - return Err(Error(ErrorOrigin::OsLayer, ErrorKind::Unknown)); - } + if ret != KERN_SUCCESS { + return Err(Error(ErrorOrigin::OsLayer, ErrorKind::Unknown)); + } - let ino = prwpi.prp_vip.vip_vi.vi_stat.vst_ino; + // 0 -> 32-bit fmt + // 1 -> 64-bit fmt + // We need to verify that the format is the same as our native pointer width (usize size), + // so that we don't misread nonsense. + if 4 * (1 + info.all_image_info_format) as usize != core::mem::size_of::() { + return Err(Error(ErrorOrigin::OsLayer, ErrorKind::NotSupported)); + } - // FIXME: if we get ino 0 at the start, this usually indicates that we are - // dealing with a submap - submaps are how dyld's are shared across processes, - // meaning, practically shared libraries from dyld cache will be reported to be - // of size 0. - if ino == 0 || (last_ino != 0 && ino != last_ino) { - break; - } + let infos = self.read::(info.all_image_info_addr.into())?; + + let mut left = infos.info_array_count as usize; + let mut info_buf = vec![dyld_image_info::default(); core::cmp::min(128, left)]; + + while left > 0 { + let pos = infos.info_array_count as usize - left; + + let size = core::cmp::min(left, info_buf.len()); + + self.read_into( + Address::from( + infos.dyld_image_info + pos * core::mem::size_of::(), + ), + &mut info_buf[..size], + )?; + left -= size; + + // And now, let's process the elements + for i in &info_buf[..size] { + // TODO: do the string reads concurrently + let name = self.read_utf8_lossy(i.image_file_path.into(), 4096)?; + + let start = Address::from(i.image_load_address); + let mut end = start; + + // Now, we need to figure out the size of the image. To do this, iterate through + // proc_regioninfo and grab all entries with identical (non-zero) inode number. + let mut prwpi: proc_regionwithpathinfo = + unsafe { MaybeUninit::zeroed().assume_init() }; + let mut last_ino = 0; + + loop { + let size = core::mem::size_of::() as _; + let ret = unsafe { + proc_pidinfo( + self.info.pid as _, + PROC_PIDREGIONPATHINFO, + end.to_umem() as _, + &mut prwpi as *mut proc_regionwithpathinfo as *mut _, + size, + ) + }; - let len = prwpi.prp_prinfo.pri_size as umem; - end = Address::from(prwpi.prp_prinfo.pri_address) + len; + if ret <= 0 { + break; + } + if ret < size { + return Err(Error(ErrorOrigin::OsLayer, ErrorKind::Unknown)); + } - last_ino = ino; - } + let ino = prwpi.prp_vip.vip_vi.vi_stat.vst_ino; - let mut mod_sz = (end - start) as umem; + // FIXME: if we get ino 0 at the start, this usually indicates that we are + // dealing with a submap - submaps are how dyld's are shared across processes, + // meaning, practically shared libraries from dyld cache will be reported to be + // of size 0. + if ino == 0 || (last_ino != 0 && ino != last_ino) { + break; + } - // FIXME: figure out a way without parsing the mach file... - let _ = (|| { - let header = self.read::(start)?; + let len = prwpi.prp_prinfo.pri_size as umem; + end = Address::from(prwpi.prp_prinfo.pri_address) + len; - if header.sizeofcmds as usize > size::mb(16) { - return Err(ErrorKind::Unknown.into()); + last_ino = ino; } - let cmdaddr = start - + core::mem::size_of::() - + if header.magic == 0xfeedfacf { - 4 - } else if header.magic == 0xfeedface { - 0 - } else { + let mut mod_sz = (end - start) as umem; + + // FIXME: figure out a way without parsing the mach file... + let _ = (|| { + let header = self.read::(start)?; + + if header.sizeofcmds as usize > size::mb(16) { return Err(ErrorKind::Unknown.into()); - }; + } - let mut cmds = vec![0; header.sizeofcmds as usize]; - - self.read_raw_into(cmdaddr, &mut cmds[..])?; - - let view = DataView::from(&cmds[..]); - - let mut cmdoff = 0; - - let mut base_addr = None; - let mut all_seg_sz = 0; - - for _ in 0..header.ncmds { - let hdr = view.read::(cmdoff); - - if let Some((addr, sz, seg)) = if hdr.ty == LC_SEGMENT { - let addr = view.read::(cmdoff + 24); - let sz = view.read::(cmdoff + 24 + 4); - Some(( - addr as umem, - sz as umem, - view.read::(cmdoff + 24 + 16), - )) - } else if hdr.ty == LC_SEGMENT_64 { - let addr = view.read::(cmdoff + 24); - let sz = view.read::(cmdoff + 24 + 8); - Some(( - addr as umem, - sz as umem, - view.read::(cmdoff + 24 + 32), - )) - } else { - None - } { - // Skip __PAGEZERO segment that has no sections - // TODO: should we also check for protection flags? - if seg.nsects != 0 { - if base_addr.is_none() { - base_addr = Some(addr); + let cmdaddr = start + + core::mem::size_of::() + + if header.magic == 0xfeedfacf { + 4 + } else if header.magic == 0xfeedface { + 0 + } else { + return Err(ErrorKind::Unknown.into()); + }; + + let mut cmds = vec![0; header.sizeofcmds as usize]; + + self.read_raw_into(cmdaddr, &mut cmds[..])?; + + let view = DataView::from(&cmds[..]); + + let mut cmdoff = 0; + + let mut base_addr = None; + let mut all_seg_sz = 0; + + for _ in 0..header.ncmds { + let hdr = view.read::(cmdoff); + + if let Some((addr, sz, seg)) = if hdr.ty == LC_SEGMENT { + let addr = view.read::(cmdoff + 24); + let sz = view.read::(cmdoff + 24 + 4); + Some(( + addr as umem, + sz as umem, + view.read::(cmdoff + 24 + 16), + )) + } else if hdr.ty == LC_SEGMENT_64 { + let addr = view.read::(cmdoff + 24); + let sz = view.read::(cmdoff + 24 + 8); + Some(( + addr as umem, + sz as umem, + view.read::(cmdoff + 24 + 32), + )) + } else { + None + } { + // Skip __PAGEZERO segment that has no sections + // TODO: should we also check for protection flags? + if seg.nsects != 0 { + if base_addr.is_none() { + base_addr = Some(addr); + } + all_seg_sz = + core::cmp::max(all_seg_sz, addr - base_addr.unwrap() + sz); } - all_seg_sz = - core::cmp::max(all_seg_sz, addr - base_addr.unwrap() + sz); } - } - cmdoff += hdr.sz as usize; - } + cmdoff += hdr.sz as usize; + } - mod_sz = core::cmp::max(all_seg_sz, mod_sz); + mod_sz = core::cmp::max(all_seg_sz, mod_sz); - Result::Ok(()) - })(); + Result::Ok(()) + })(); - self.cached_module_maps.push((start, mod_sz, name)); + self.cached_module_maps.push((start, mod_sz, name)); + } } + + self.cached_module_maps.sort_by_key(|v| v.0); + + Ok(()) + })(); + + if dyld_result.is_ok() { + return Ok(()); } - self.cached_module_maps.sort_by_key(|v| v.0); + let dyld_err = dyld_result.err().unwrap(); + log::warn!( + "Falling back to proc region module enumeration for pid {}", + self.info.pid + ); + self.update_cached_module_maps_from_regions()?; + + if self.cached_module_maps.is_empty() { + log::warn!( + "Region-path module fallback produced no modules for pid {}; returning original dyld error", + self.info.pid + ); + return Err(dyld_err); + } Ok(()) } @@ -383,7 +482,6 @@ impl MacProcess { } self.cached_maps.sort_by_key(|v| v.0); - Ok(()) } } @@ -551,6 +649,56 @@ impl Process for MacProcess { .map(<_>::into) .feed_into(out); } + + #[cfg(memflow_plugin_api = "2")] + fn envar_list_callback( + &mut self, + target_arch: Option<&ArchitectureIdent>, + mut callback: EnvVarCallback, + ) -> Result<()> { + if let Some(arch) = target_arch { + if *arch != self.info.proc_arch { + return Ok(()); + } + } + + let parsed = + super::read_procargs2(self.info.pid).and_then(|data| super::parse_procargs2(&data))?; + + for (name, value) in parsed.environ { + let info = EnvVarInfo { + name: name.into(), + value: value.into(), + address: memflow::types::Address::from(0), + arch: self.info.proc_arch, + }; + + if !callback.call(info) { + break; + } + } + + Ok(()) + } + + #[cfg(memflow_plugin_api = "2")] + fn environment_block_address(&mut self, _architecture: ArchitectureIdent) -> Result
{ + // macOS does not expose a stable public env-block pointer like Windows. + // Return a sentinel and enumerate via sysctl in `envar_list_from_address`. + Ok(Address::NULL) + } + + #[cfg(memflow_plugin_api = "2")] + fn envar_list_from_address( + &mut self, + _env_block: Address, + architecture: ArchitectureIdent, + callback: EnvVarCallback, + ) -> Result<()> { + // Same rationale as Linux: we can’t use env_block directly, but we *can* + // enumerate via sysctl. + self.envar_list_callback(Some(&architecture), callback) + } } impl MemoryView for MacProcess { diff --git a/src/windows/process.rs b/src/windows/process.rs index 4465edd..d031256 100644 --- a/src/windows/process.rs +++ b/src/windows/process.rs @@ -44,6 +44,92 @@ impl WindowsProcess { cached_modules: vec![], }) } + + #[cfg(memflow_plugin_api = "2")] + fn collect_envars(&mut self) -> Result> { + // Step 1: get PEB base via NtQueryInformationProcess + let mut pbi = PROCESS_BASIC_INFORMATION::default(); + let status = unsafe { + NtQueryInformationProcess( + **self.virt_mem.handle, + ProcessBasicInformation, + &mut pbi as *mut _ as _, + size_of_val(&pbi) as _, + ptr::null_mut(), + ) + }; + if status.is_err() { + return Err(conv_ntstatus(status)); + } + + // Step 2: detect WOW64 (32-bit process on 64-bit OS) + let mut wow64_peb = 0usize; + let wow64_status = unsafe { + NtQueryInformationProcess( + **self.virt_mem.handle, + ProcessWow64Information, + &mut wow64_peb as *mut _ as _, + size_of_val(&wow64_peb) as _, + ptr::null_mut(), + ) + }; + let is_32bit = + wow64_status.is_ok() && wow64_status != STATUS_INVALID_INFO_CLASS && wow64_peb != 0; + + // Step 3: read ProcessParameters pointer from PEB + // PEB64 ProcessParameters at +0x20, PEB32 at +0x10 + let proc_params = if is_32bit { + let peb32 = Address::from(wow64_peb as umem); + self.read_addr32(peb32 + 0x10usize).data_part()? + } else { + let peb64 = Address::from(pbi.PebBaseAddress as umem); + self.read_addr64(peb64 + 0x20usize).data_part()? + }; + + // Step 4: read Environment pointer from ProcessParameters + // RTL_USER_PROCESS_PARAMETERS64 Environment at +0x80, 32-bit at +0x48 + let env_addr = if is_32bit { + self.read_addr32(proc_params + 0x48usize).data_part()? + } else { + self.read_addr64(proc_params + 0x80usize).data_part()? + }; + + // Step 5: read the UTF-16LE environment block (NUL-NUL terminated, cap at 256 KB) + let mut buf = vec![0u8; 256 * 1024]; + let _ = self.read_raw_into(env_addr, &mut buf); + + // Step 6: parse UTF-16LE "KEY=VALUE\0" entries + let words: Vec = buf + .chunks_exact(2) + .map(|c| u16::from_le_bytes([c[0], c[1]])) + .collect(); + + let mut out = Vec::new(); + let mut i = 0; + while i < words.len() { + if words[i] == 0 { + break; + } + let start = i; + while i < words.len() && words[i] != 0 { + i += 1; + } + let entry = String::from_utf16_lossy(&words[start..i]); + if let Some((name, value)) = entry.split_once('=') { + if !name.is_empty() { + out.push(EnvVarInfo { + name: ReprCString::from(name), + value: ReprCString::from(value), + address: env_addr + (start * 2) as umem, + arch: self.info.proc_arch, + }); + } + } + i += 1; + } + + Ok(out) + } } cglue_impl_group!(WindowsProcess, ProcessInstance, {}); @@ -275,6 +361,52 @@ impl Process for WindowsProcess { memflow::os::util::module_section_list_callback(&mut self.virt_mem, info, callback) } + #[cfg(memflow_plugin_api = "2")] + fn envar_list_callback( + &mut self, + target_arch: Option<&ArchitectureIdent>, + mut callback: EnvVarCallback, + ) -> Result<()> { + if let Some(target_arch) = target_arch { + if *target_arch != self.info.proc_arch { + return Ok(()); + } + } + + for ev in self.collect_envars()? { + if !callback.call(ev) { + break; + } + } + + Ok(()) + } + + #[cfg(memflow_plugin_api = "2")] + fn environment_block_address(&mut self, _architecture: ArchitectureIdent) -> Result
{ + Err(Error(ErrorOrigin::OsLayer, ErrorKind::NotSupported)) + } + + #[cfg(memflow_plugin_api = "2")] + fn envar_list_from_address( + &mut self, + _env_block: Address, + architecture: ArchitectureIdent, + mut callback: EnvVarCallback, + ) -> Result<()> { + if architecture != self.info.proc_arch { + return Ok(()); + } + + for ev in self.collect_envars()? { + if !callback.call(ev) { + break; + } + } + + Ok(()) + } + /// Retrieves the process info fn info(&self) -> &ProcessInfo { &self.info