wraith/manipulation/syscall/
table.rs

1//! Syscall lookup table
2//!
3//! Provides fast lookup of syscall information by name, hash, or SSN.
4
5use super::enumerator::{enumerate_syscalls, EnumeratedSyscall};
6use crate::error::{Result, WraithError};
7use crate::util::hash::djb2_hash;
8
9#[cfg(feature = "std")]
10use std::collections::HashMap;
11
12#[cfg(all(not(feature = "std"), feature = "alloc"))]
13use alloc::{collections::BTreeMap, format, string::String, vec::Vec};
14
15#[cfg(feature = "std")]
16use std::{format, string::String, vec::Vec};
17
18/// syscall entry in the table
19#[derive(Debug, Clone)]
20pub struct SyscallEntry {
21    /// function name
22    pub name: String,
23    /// hash of name for fast lookup
24    pub name_hash: u32,
25    /// syscall service number
26    pub ssn: u16,
27    /// function address in ntdll
28    pub address: usize,
29    /// address of syscall instruction (for indirect calls)
30    pub syscall_address: Option<usize>,
31}
32
33impl From<EnumeratedSyscall> for SyscallEntry {
34    fn from(sc: EnumeratedSyscall) -> Self {
35        Self {
36            name: sc.name,
37            name_hash: sc.name_hash,
38            ssn: sc.ssn,
39            address: sc.address,
40            syscall_address: sc.syscall_address,
41        }
42    }
43}
44
45/// syscall lookup table
46#[cfg(feature = "std")]
47pub struct SyscallTable {
48    /// entries by name hash
49    by_hash: HashMap<u32, SyscallEntry>,
50    /// entries by SSN
51    by_ssn: HashMap<u16, SyscallEntry>,
52    /// all entries in order
53    entries: Vec<SyscallEntry>,
54}
55
56/// syscall lookup table (no_std version using BTreeMap)
57#[cfg(all(not(feature = "std"), feature = "alloc"))]
58pub struct SyscallTable {
59    /// entries by name hash
60    by_hash: BTreeMap<u32, SyscallEntry>,
61    /// entries by SSN
62    by_ssn: BTreeMap<u16, SyscallEntry>,
63    /// all entries in order
64    entries: Vec<SyscallEntry>,
65}
66
67impl SyscallTable {
68    /// enumerate and build syscall table
69    #[cfg(feature = "std")]
70    pub fn enumerate() -> Result<Self> {
71        let syscalls = enumerate_syscalls()?;
72
73        let mut by_hash = HashMap::with_capacity(syscalls.len());
74        let mut by_ssn = HashMap::with_capacity(syscalls.len());
75        let mut entries = Vec::with_capacity(syscalls.len());
76
77        for sc in syscalls {
78            let entry = SyscallEntry::from(sc);
79
80            by_hash.insert(entry.name_hash, entry.clone());
81            by_ssn.insert(entry.ssn, entry.clone());
82            entries.push(entry);
83        }
84
85        Ok(Self {
86            by_hash,
87            by_ssn,
88            entries,
89        })
90    }
91
92    /// enumerate and build syscall table (no_std version)
93    #[cfg(all(not(feature = "std"), feature = "alloc"))]
94    pub fn enumerate() -> Result<Self> {
95        let syscalls = enumerate_syscalls()?;
96
97        let mut by_hash = BTreeMap::new();
98        let mut by_ssn = BTreeMap::new();
99        let mut entries = Vec::with_capacity(syscalls.len());
100
101        for sc in syscalls {
102            let entry = SyscallEntry::from(sc);
103
104            by_hash.insert(entry.name_hash, entry.clone());
105            by_ssn.insert(entry.ssn, entry.clone());
106            entries.push(entry);
107        }
108
109        Ok(Self {
110            by_hash,
111            by_ssn,
112            entries,
113        })
114    }
115
116    /// get syscall by name
117    pub fn get(&self, name: &str) -> Option<&SyscallEntry> {
118        let hash = djb2_hash(name.as_bytes());
119        self.by_hash.get(&hash)
120    }
121
122    /// get syscall by hash
123    pub fn get_by_hash(&self, hash: u32) -> Option<&SyscallEntry> {
124        self.by_hash.get(&hash)
125    }
126
127    /// get syscall by SSN
128    pub fn get_by_ssn(&self, ssn: u16) -> Option<&SyscallEntry> {
129        self.by_ssn.get(&ssn)
130    }
131
132    /// get all entries
133    pub fn entries(&self) -> &[SyscallEntry] {
134        &self.entries
135    }
136
137    /// number of syscalls
138    pub fn len(&self) -> usize {
139        self.entries.len()
140    }
141
142    /// check if empty
143    pub fn is_empty(&self) -> bool {
144        self.entries.is_empty()
145    }
146
147    /// get SSN for syscall name
148    pub fn get_ssn(&self, name: &str) -> Option<u16> {
149        self.get(name).map(|e| e.ssn)
150    }
151
152    /// get syscall instruction address for indirect calls
153    pub fn get_syscall_address(&self, name: &str) -> Option<usize> {
154        self.get(name).and_then(|e| e.syscall_address)
155    }
156
157    /// find syscall containing address (for detecting which syscall is hooked)
158    pub fn find_by_address(&self, addr: usize) -> Option<&SyscallEntry> {
159        // typical syscall stub is ~32 bytes
160        self.entries
161            .iter()
162            .find(|e| addr >= e.address && addr < e.address + 32)
163    }
164
165    /// get syscall or return error
166    pub fn require(&self, name: &str) -> Result<&SyscallEntry> {
167        self.get(name).ok_or_else(|| WraithError::SyscallNotFound {
168            name: name.to_string(),
169        })
170    }
171
172    /// get syscall by hash or return error
173    pub fn require_by_hash(&self, hash: u32) -> Result<&SyscallEntry> {
174        self.get_by_hash(hash)
175            .ok_or_else(|| WraithError::SyscallNotFound {
176                name: format!("hash {hash:#x}"),
177            })
178    }
179}
180
181/// common syscall name hashes (computed at compile time)
182pub mod hashes {
183    use crate::util::hash::djb2_hash;
184
185    pub const NT_OPEN_PROCESS: u32 = djb2_hash(b"NtOpenProcess");
186    pub const NT_CLOSE: u32 = djb2_hash(b"NtClose");
187    pub const NT_READ_VIRTUAL_MEMORY: u32 = djb2_hash(b"NtReadVirtualMemory");
188    pub const NT_WRITE_VIRTUAL_MEMORY: u32 = djb2_hash(b"NtWriteVirtualMemory");
189    pub const NT_ALLOCATE_VIRTUAL_MEMORY: u32 = djb2_hash(b"NtAllocateVirtualMemory");
190    pub const NT_FREE_VIRTUAL_MEMORY: u32 = djb2_hash(b"NtFreeVirtualMemory");
191    pub const NT_PROTECT_VIRTUAL_MEMORY: u32 = djb2_hash(b"NtProtectVirtualMemory");
192    pub const NT_QUERY_INFORMATION_PROCESS: u32 = djb2_hash(b"NtQueryInformationProcess");
193    pub const NT_SET_INFORMATION_THREAD: u32 = djb2_hash(b"NtSetInformationThread");
194    pub const NT_CREATE_THREAD_EX: u32 = djb2_hash(b"NtCreateThreadEx");
195    pub const NT_QUERY_SYSTEM_INFORMATION: u32 = djb2_hash(b"NtQuerySystemInformation");
196    pub const NT_QUERY_VIRTUAL_MEMORY: u32 = djb2_hash(b"NtQueryVirtualMemory");
197    pub const NT_OPEN_FILE: u32 = djb2_hash(b"NtOpenFile");
198    pub const NT_CREATE_FILE: u32 = djb2_hash(b"NtCreateFile");
199    pub const NT_READ_FILE: u32 = djb2_hash(b"NtReadFile");
200    pub const NT_WRITE_FILE: u32 = djb2_hash(b"NtWriteFile");
201    pub const NT_CREATE_SECTION: u32 = djb2_hash(b"NtCreateSection");
202    pub const NT_MAP_VIEW_OF_SECTION: u32 = djb2_hash(b"NtMapViewOfSection");
203    pub const NT_UNMAP_VIEW_OF_SECTION: u32 = djb2_hash(b"NtUnmapViewOfSection");
204}
205
206#[cfg(test)]
207mod tests {
208    use super::*;
209
210    #[test]
211    fn test_enumerate_table() {
212        let table = SyscallTable::enumerate().expect("should enumerate");
213        assert!(!table.is_empty());
214    }
215
216    #[test]
217    fn test_lookup_by_name() {
218        let table = SyscallTable::enumerate().expect("should enumerate");
219
220        let entry = table.get("NtClose").expect("should find NtClose");
221        assert_eq!(entry.name, "NtClose");
222    }
223
224    #[test]
225    fn test_lookup_by_hash() {
226        let table = SyscallTable::enumerate().expect("should enumerate");
227
228        let hash = djb2_hash(b"NtClose");
229        let entry = table.get_by_hash(hash).expect("should find by hash");
230        assert_eq!(entry.name, "NtClose");
231    }
232
233    #[test]
234    fn test_precomputed_hash() {
235        let table = SyscallTable::enumerate().expect("should enumerate");
236
237        let entry = table
238            .get_by_hash(hashes::NT_CLOSE)
239            .expect("should find by precomputed hash");
240        assert_eq!(entry.name, "NtClose");
241    }
242
243    #[test]
244    fn test_require() {
245        let table = SyscallTable::enumerate().expect("should enumerate");
246
247        assert!(table.require("NtClose").is_ok());
248        assert!(table.require("NonExistentSyscall").is_err());
249    }
250}