wraith/manipulation/syscall/
enumerator.rs1#[cfg(all(not(feature = "std"), feature = "alloc"))]
7use alloc::{string::{String, ToString}, vec::Vec};
8
9#[cfg(feature = "std")]
10use std::{string::{String, ToString}, vec::Vec};
11
12use crate::error::{Result, WraithError};
13use crate::navigation::{Module, ModuleQuery};
14use crate::structures::pe::{DataDirectoryType, ExportDirectory};
15use crate::structures::Peb;
16use crate::util::hash::djb2_hash;
17
18mod patterns {
20 #[cfg(target_arch = "x86_64")]
27 pub const MOV_R10_RCX: [u8; 3] = [0x4C, 0x8B, 0xD1];
28
29 pub const MOV_EAX: u8 = 0xB8;
30
31 #[cfg(target_arch = "x86_64")]
32 pub const SYSCALL: [u8; 2] = [0x0F, 0x05];
33
34 #[cfg(target_arch = "x86")]
36 pub const INT_2E: [u8; 2] = [0xCD, 0x2E];
37
38 #[cfg(target_arch = "x86")]
39 pub const SYSENTER: [u8; 2] = [0x0F, 0x34];
40}
41
42pub struct SyscallEnumerator<'a> {
44 ntdll: Module<'a>,
45}
46
47impl<'a> SyscallEnumerator<'a> {
48 pub fn new(ntdll: Module<'a>) -> Self {
50 Self { ntdll }
51 }
52
53 pub fn enumerate(&self) -> Result<Vec<EnumeratedSyscall>> {
55 let mut syscalls = Vec::new();
56
57 let nt = self.ntdll.nt_headers()?;
58 let export_dir = nt
59 .data_directory(DataDirectoryType::Export.index())
60 .ok_or(WraithError::SyscallEnumerationFailed {
61 reason: "no export directory".into(),
62 })?;
63
64 if !export_dir.is_present() {
65 return Err(WraithError::SyscallEnumerationFailed {
66 reason: "export directory not present".into(),
67 });
68 }
69
70 let base = self.ntdll.base();
71 let exports = unsafe {
73 &*((base + export_dir.virtual_address as usize) as *const ExportDirectory)
74 };
75
76 let num_names = exports.number_of_names as usize;
77 let names = base + exports.address_of_names as usize;
78 let ordinals = base + exports.address_of_name_ordinals as usize;
79 let functions = base + exports.address_of_functions as usize;
80
81 for i in 0..num_names {
82 let name_rva = unsafe { *((names + i * 4) as *const u32) };
84 let name_ptr = (base + name_rva as usize) as *const u8;
85
86 let name = unsafe {
88 let mut len = 0;
89 while *name_ptr.add(len) != 0 && len < 256 {
90 len += 1;
91 }
92 let bytes = core::slice::from_raw_parts(name_ptr, len);
93 match core::str::from_utf8(bytes) {
94 Ok(s) => s,
95 Err(_) => continue, }
97 };
98
99 if !name.starts_with("Nt") && !name.starts_with("Zw") {
101 continue;
102 }
103
104 if matches!(
106 name,
107 "NtCurrentTeb"
108 | "NtCurrentPeb"
109 | "NtGetTickCount"
110 | "NtdllDefWindowProc_A"
111 | "NtdllDefWindowProc_W"
112 | "NtdllDialogWndProc_A"
113 | "NtdllDialogWndProc_W"
114 ) {
115 continue;
116 }
117
118 let ordinal = unsafe { *((ordinals + i * 2) as *const u16) };
119 let func_rva = unsafe { *((functions + ordinal as usize * 4) as *const u32) };
120 let func_addr = base + func_rva as usize;
121
122 if func_rva >= export_dir.virtual_address
124 && func_rva < export_dir.virtual_address + export_dir.size
125 {
126 continue;
127 }
128
129 if let Some(ssn) = self.extract_ssn(func_addr) {
131 syscalls.push(EnumeratedSyscall {
132 name: name.to_string(),
133 name_hash: djb2_hash(name.as_bytes()),
134 ssn,
135 address: func_addr,
136 syscall_address: self.find_syscall_instruction(func_addr),
137 });
138 }
139 }
140
141 syscalls.sort_by_key(|s| s.ssn);
143
144 Ok(syscalls)
145 }
146
147 #[cfg(target_arch = "x86_64")]
149 fn extract_ssn(&self, addr: usize) -> Option<u16> {
150 let bytes = unsafe { core::slice::from_raw_parts(addr as *const u8, 32) };
152
153 if bytes.len() >= 8
155 && bytes[0..3] == patterns::MOV_R10_RCX
156 && bytes[3] == patterns::MOV_EAX
157 {
158 let ssn = u16::from_le_bytes([bytes[4], bytes[5]]);
159 return Some(ssn);
160 }
161
162 for i in 0..20 {
164 if i + 2 < bytes.len() && bytes[i] == patterns::MOV_EAX {
165 let ssn = u16::from_le_bytes([bytes[i + 1], bytes[i + 2]]);
166 if ssn < 0x1000 {
167 return Some(ssn);
168 }
169 }
170 }
171
172 None
173 }
174
175 #[cfg(target_arch = "x86")]
177 fn extract_ssn(&self, addr: usize) -> Option<u16> {
178 let bytes = unsafe { core::slice::from_raw_parts(addr as *const u8, 32) };
180
181 if bytes.len() >= 5 && bytes[0] == patterns::MOV_EAX {
183 let ssn = u16::from_le_bytes([bytes[1], bytes[2]]);
184 return Some(ssn);
185 }
186
187 None
188 }
189
190 #[cfg(target_arch = "x86_64")]
192 fn find_syscall_instruction(&self, func_addr: usize) -> Option<usize> {
193 let bytes = unsafe { core::slice::from_raw_parts(func_addr as *const u8, 32) };
195
196 for i in 0..30 {
198 if i + 1 < bytes.len() && bytes[i..].starts_with(&patterns::SYSCALL) {
199 return Some(func_addr + i);
200 }
201 }
202
203 None
204 }
205
206 #[cfg(target_arch = "x86")]
208 fn find_syscall_instruction(&self, func_addr: usize) -> Option<usize> {
209 let bytes = unsafe { core::slice::from_raw_parts(func_addr as *const u8, 64) };
211
212 for i in 0..60 {
214 if i + 1 < bytes.len()
215 && (bytes[i..].starts_with(&patterns::INT_2E)
216 || bytes[i..].starts_with(&patterns::SYSENTER))
217 {
218 return Some(func_addr + i);
219 }
220 }
221
222 None
223 }
224
225 #[allow(dead_code)]
230 pub fn resolve_hooked_ssn(&self, target_addr: usize) -> Option<u16> {
231 for offset in 1..=20u16 {
233 let check_addr = target_addr.wrapping_sub(offset as usize * 32);
235 if let Some(ssn) = self.extract_ssn(check_addr) {
236 return Some(ssn.wrapping_add(offset));
237 }
238 }
239
240 for offset in 1..=20u16 {
242 let check_addr = target_addr + (offset as usize * 32);
243 if let Some(ssn) = self.extract_ssn(check_addr) {
244 return ssn.checked_sub(offset);
245 }
246 }
247
248 None
249 }
250}
251
252#[derive(Debug, Clone)]
254pub struct EnumeratedSyscall {
255 pub name: String,
257 pub name_hash: u32,
259 pub ssn: u16,
261 pub address: usize,
263 pub syscall_address: Option<usize>,
265}
266
267pub fn enumerate_syscalls() -> Result<Vec<EnumeratedSyscall>> {
269 let peb = Peb::current()?;
270 let query = ModuleQuery::new(&peb);
271 let ntdll = query.ntdll().map_err(|_| WraithError::NtdllNotFound)?;
272
273 let enumerator = SyscallEnumerator::new(ntdll);
274 enumerator.enumerate()
275}
276
277#[cfg(test)]
278mod tests {
279 use super::*;
280
281 #[test]
282 fn test_enumerate_syscalls() {
283 let syscalls = enumerate_syscalls().expect("should enumerate syscalls");
284 assert!(!syscalls.is_empty(), "should find at least some syscalls");
285
286 let nt_close = syscalls.iter().find(|s| s.name == "NtClose");
288 assert!(nt_close.is_some(), "should find NtClose");
289
290 let close = nt_close.unwrap();
292 assert!(close.ssn < 0x500, "NtClose SSN should be reasonable");
293 }
294
295 #[test]
296 fn test_ssn_ordering() {
297 let syscalls = enumerate_syscalls().expect("should enumerate syscalls");
298
299 for i in 1..syscalls.len() {
301 assert!(
302 syscalls[i].ssn >= syscalls[i - 1].ssn,
303 "SSNs should be sorted"
304 );
305 }
306 }
307}