wraith/manipulation/remote/
modules.rs

1//! Remote module enumeration
2
3#[cfg(all(not(feature = "std"), feature = "alloc"))]
4use alloc::{format, string::String, vec, vec::Vec};
5
6#[cfg(feature = "std")]
7use std::{format, string::String, vec, vec::Vec};
8
9use super::process::RemoteProcess;
10use crate::error::{Result, WraithError};
11use crate::manipulation::syscall::{
12    get_syscall_table, nt_success, DirectSyscall,
13};
14use crate::structures::offsets::PebOffsets;
15use crate::version::WindowsVersion;
16
17/// information about a remote module
18#[derive(Debug, Clone)]
19pub struct RemoteModuleInfo {
20    pub name: String,
21    pub path: String,
22    pub base: usize,
23    pub size: usize,
24    pub entry_point: usize,
25}
26
27/// wrapper for remote module operations
28pub struct RemoteModule {
29    pub info: RemoteModuleInfo,
30    process_handle: usize,
31}
32
33impl RemoteModule {
34    /// get module base address
35    pub fn base(&self) -> usize {
36        self.info.base
37    }
38
39    /// get module size
40    pub fn size(&self) -> usize {
41        self.info.size
42    }
43
44    /// get module name
45    pub fn name(&self) -> &str {
46        &self.info.name
47    }
48
49    /// get full path
50    pub fn path(&self) -> &str {
51        &self.info.path
52    }
53
54    /// read memory from within this module
55    pub fn read(&self, rva: usize, buffer: &mut [u8]) -> Result<usize> {
56        let address = self.info.base + rva;
57        let mut bytes_read: usize = 0;
58
59        let table = get_syscall_table()?;
60        let syscall = DirectSyscall::from_table(table, "NtReadVirtualMemory")?;
61
62        // SAFETY: buffer is valid
63        let status = unsafe {
64            syscall.call5(
65                self.process_handle,
66                address,
67                buffer.as_mut_ptr() as usize,
68                buffer.len(),
69                &mut bytes_read as *mut usize as usize,
70            )
71        };
72
73        if nt_success(status) {
74            Ok(bytes_read)
75        } else {
76            Err(WraithError::ReadFailed {
77                address: u64::try_from(address).unwrap_or(u64::MAX),
78                size: buffer.len(),
79            })
80        }
81    }
82}
83
84/// enumerate modules in a remote process
85pub fn enumerate_remote_modules(process: &RemoteProcess) -> Result<Vec<RemoteModuleInfo>> {
86    let peb_address = get_remote_peb(process)?;
87    let version = WindowsVersion::current()?;
88    let offsets = PebOffsets::for_version(&version)?;
89
90    // read PEB.Ldr pointer
91    let ldr_ptr = process.read_value::<usize>(peb_address + offsets.ldr)?;
92    if ldr_ptr == 0 {
93        return Err(WraithError::RemoteModuleEnumFailed {
94            reason: "null Ldr pointer".into(),
95        });
96    }
97
98    // in PEB_LDR_DATA, InLoadOrderModuleList is at offset 0x10 (x64) or 0x0C (x86)
99    #[cfg(target_arch = "x86_64")]
100    const LDR_MODULE_LIST_OFFSET: usize = 0x10;
101    #[cfg(target_arch = "x86")]
102    const LDR_MODULE_LIST_OFFSET: usize = 0x0C;
103
104    let list_head = ldr_ptr + LDR_MODULE_LIST_OFFSET;
105
106    // read head.Flink to get first entry
107    let first_entry = process.read_value::<usize>(list_head)?;
108    if first_entry == 0 || first_entry == list_head {
109        return Ok(Vec::new());
110    }
111
112    let mut modules = Vec::new();
113    let mut current = first_entry;
114    let max_iterations = 4096;
115
116    for _ in 0..max_iterations {
117        if current == list_head || current == 0 {
118            break;
119        }
120
121        if let Ok(module) = read_ldr_entry(process, current) {
122            modules.push(module);
123        }
124
125        // read Flink to get next entry
126        let next = process.read_value::<usize>(current)?;
127        if next == current {
128            break; // corrupted list
129        }
130        current = next;
131    }
132
133    Ok(modules)
134}
135
136fn read_ldr_entry(process: &RemoteProcess, entry_address: usize) -> Result<RemoteModuleInfo> {
137    // LDR_DATA_TABLE_ENTRY offsets (InLoadOrderLinks is at offset 0)
138    #[cfg(target_arch = "x86_64")]
139    const DLL_BASE_OFFSET: usize = 0x30;
140    #[cfg(target_arch = "x86_64")]
141    const SIZE_OFFSET: usize = 0x40;
142    #[cfg(target_arch = "x86_64")]
143    const ENTRY_POINT_OFFSET: usize = 0x38;
144    #[cfg(target_arch = "x86_64")]
145    const FULL_DLL_NAME_OFFSET: usize = 0x48;
146    #[cfg(target_arch = "x86_64")]
147    const BASE_DLL_NAME_OFFSET: usize = 0x58;
148
149    #[cfg(target_arch = "x86")]
150    const DLL_BASE_OFFSET: usize = 0x18;
151    #[cfg(target_arch = "x86")]
152    const SIZE_OFFSET: usize = 0x20;
153    #[cfg(target_arch = "x86")]
154    const ENTRY_POINT_OFFSET: usize = 0x1C;
155    #[cfg(target_arch = "x86")]
156    const FULL_DLL_NAME_OFFSET: usize = 0x24;
157    #[cfg(target_arch = "x86")]
158    const BASE_DLL_NAME_OFFSET: usize = 0x2C;
159
160    let base = process.read_value::<usize>(entry_address + DLL_BASE_OFFSET)?;
161    let size = process.read_value::<u32>(entry_address + SIZE_OFFSET)? as usize;
162    let entry_point = process.read_value::<usize>(entry_address + ENTRY_POINT_OFFSET)?;
163
164    let name = read_unicode_string(process, entry_address + BASE_DLL_NAME_OFFSET)
165        .unwrap_or_else(|_| String::from("<unknown>"));
166    let path = read_unicode_string(process, entry_address + FULL_DLL_NAME_OFFSET)
167        .unwrap_or_else(|_| String::new());
168
169    Ok(RemoteModuleInfo {
170        name,
171        path,
172        base,
173        size,
174        entry_point,
175    })
176}
177
178fn read_unicode_string(process: &RemoteProcess, address: usize) -> Result<String> {
179    // UNICODE_STRING: Length (u16), MaxLength (u16), padding (u32 on x64), Buffer (ptr)
180    #[cfg(target_arch = "x86_64")]
181    const BUFFER_OFFSET: usize = 8;
182    #[cfg(target_arch = "x86")]
183    const BUFFER_OFFSET: usize = 4;
184
185    let length = process.read_value::<u16>(address)? as usize;
186    if length == 0 || length > 520 {
187        return Ok(String::new());
188    }
189
190    let buffer_ptr = process.read_value::<usize>(address + BUFFER_OFFSET)?;
191    if buffer_ptr == 0 {
192        return Ok(String::new());
193    }
194
195    // read the wide string
196    let mut buffer = vec![0u16; length / 2];
197    let byte_buffer = unsafe {
198        core::slice::from_raw_parts_mut(buffer.as_mut_ptr() as *mut u8, length)
199    };
200
201    process.read(buffer_ptr, byte_buffer)?;
202
203    Ok(String::from_utf16_lossy(&buffer))
204}
205
206/// find a specific module in a remote process
207pub fn find_remote_module(process: &RemoteProcess, name: &str) -> Result<RemoteModule> {
208    let modules = enumerate_remote_modules(process)?;
209    let name_lower = name.to_lowercase();
210
211    for module in modules {
212        if module.name.to_lowercase() == name_lower
213            || module.name.to_lowercase().starts_with(&name_lower)
214        {
215            return Ok(RemoteModule {
216                info: module,
217                process_handle: process.handle(),
218            });
219        }
220    }
221
222    Err(WraithError::ModuleNotFound {
223        name: name.to_string(),
224    })
225}
226
227/// get PEB address of remote process
228pub fn get_remote_peb(process: &RemoteProcess) -> Result<usize> {
229    let table = get_syscall_table()?;
230    let syscall = DirectSyscall::from_table(table, "NtQueryInformationProcess")?;
231
232    #[repr(C)]
233    struct ProcessBasicInfo {
234        exit_status: i32,
235        peb_base: usize,
236        affinity_mask: usize,
237        base_priority: i32,
238        unique_pid: usize,
239        inherited_from_pid: usize,
240    }
241
242    let mut info = core::mem::MaybeUninit::<ProcessBasicInfo>::uninit();
243    let mut return_length: u32 = 0;
244
245    // SAFETY: buffer is correctly sized
246    let status = unsafe {
247        syscall.call5(
248            process.handle(),
249            0, // ProcessBasicInformation
250            info.as_mut_ptr() as usize,
251            core::mem::size_of::<ProcessBasicInfo>(),
252            &mut return_length as *mut u32 as usize,
253        )
254    };
255
256    if nt_success(status) {
257        let info = unsafe { info.assume_init() };
258        if info.peb_base == 0 {
259            return Err(WraithError::RemoteModuleEnumFailed {
260                reason: "null PEB address".into(),
261            });
262        }
263        Ok(info.peb_base)
264    } else {
265        Err(WraithError::RemoteModuleEnumFailed {
266            reason: format!("NtQueryInformationProcess failed: {:#x}", status as u32),
267        })
268    }
269}
270
271/// get image base from remote PEB
272pub fn get_remote_image_base(process: &RemoteProcess) -> Result<usize> {
273    let peb_address = get_remote_peb(process)?;
274    let version = WindowsVersion::current()?;
275    let offsets = PebOffsets::for_version(&version)?;
276
277    process.read_value::<usize>(peb_address + offsets.image_base)
278}
279
280#[cfg(test)]
281mod tests {
282    use super::*;
283    use crate::manipulation::remote::ProcessAccess;
284
285    #[test]
286    fn test_get_remote_peb_self() {
287        let pid = std::process::id();
288        let proc = RemoteProcess::open(pid, ProcessAccess::read_only());
289        assert!(proc.is_ok());
290
291        let proc = proc.unwrap();
292        let peb = get_remote_peb(&proc);
293        assert!(peb.is_ok());
294        assert!(peb.unwrap() != 0);
295    }
296
297    #[test]
298    fn test_enumerate_modules_self() {
299        let pid = std::process::id();
300        let proc = RemoteProcess::open(pid, ProcessAccess::read_only()).unwrap();
301        let modules = enumerate_remote_modules(&proc);
302        assert!(modules.is_ok());
303
304        let modules = modules.unwrap();
305        assert!(!modules.is_empty(), "should have at least one module");
306
307        // should find ntdll
308        let has_ntdll = modules.iter().any(|m| {
309            m.name.to_lowercase().contains("ntdll")
310        });
311        assert!(has_ntdll, "should find ntdll.dll");
312    }
313}