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