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