wraith/manipulation/remote/
modules.rs

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