wraith/manipulation/remote/
modules.rs1use 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#[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
21pub struct RemoteModule {
23 pub info: RemoteModuleInfo,
24 process_handle: usize,
25}
26
27impl RemoteModule {
28 pub fn base(&self) -> usize {
30 self.info.base
31 }
32
33 pub fn size(&self) -> usize {
35 self.info.size
36 }
37
38 pub fn name(&self) -> &str {
40 &self.info.name
41 }
42
43 pub fn path(&self) -> &str {
45 &self.info.path
46 }
47
48 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 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
78pub 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 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 #[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 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 let next = process.read_value::<usize>(current)?;
121 if next == current {
122 break; }
124 current = next;
125 }
126
127 Ok(modules)
128}
129
130fn read_ldr_entry(process: &RemoteProcess, entry_address: usize) -> Result<RemoteModuleInfo> {
131 #[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 #[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 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
200pub 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
221pub 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 let status = unsafe {
241 syscall.call5(
242 process.handle(),
243 0, 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
265pub 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 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}