wraith/manipulation/remote/
modules.rs1#[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#[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
27pub struct RemoteModule {
29 pub info: RemoteModuleInfo,
30 process_handle: usize,
31}
32
33impl RemoteModule {
34 pub fn base(&self) -> usize {
36 self.info.base
37 }
38
39 pub fn size(&self) -> usize {
41 self.info.size
42 }
43
44 pub fn name(&self) -> &str {
46 &self.info.name
47 }
48
49 pub fn path(&self) -> &str {
51 &self.info.path
52 }
53
54 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 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
84pub 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 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 #[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 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 let next = process.read_value::<usize>(current)?;
127 if next == current {
128 break; }
130 current = next;
131 }
132
133 Ok(modules)
134}
135
136fn read_ldr_entry(process: &RemoteProcess, entry_address: usize) -> Result<RemoteModuleInfo> {
137 #[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 #[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 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
206pub 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
227pub 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 let status = unsafe {
247 syscall.call5(
248 process.handle(),
249 0, 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
271pub 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 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}