wraith/navigation/
module_query.rs

1//! Query interfaces for finding modules
2
3use super::module::{Module, ModuleHandle};
4use super::module_iter::{ModuleIterator, ModuleListType};
5use crate::error::{Result, WraithError};
6use crate::structures::Peb;
7use crate::util::hash::djb2_hash_lowercase;
8
9/// module query builder
10pub struct ModuleQuery<'a> {
11    peb: &'a Peb,
12}
13
14impl<'a> ModuleQuery<'a> {
15    /// create new query for given PEB
16    pub fn new(peb: &'a Peb) -> Self {
17        Self { peb }
18    }
19
20    /// find module by name (case-insensitive)
21    ///
22    /// searches by base name (e.g., "ntdll.dll", not full path)
23    pub fn find_by_name(&self, name: &str) -> Result<Module<'a>> {
24        let name_lower = name.to_lowercase();
25
26        for module in ModuleIterator::new(self.peb, ModuleListType::InLoadOrder)? {
27            if module.name_lowercase() == name_lower {
28                return Ok(module);
29            }
30        }
31
32        Err(WraithError::ModuleNotFound { name: name.into() })
33    }
34
35    /// find module by hash of name (for API hashing)
36    ///
37    /// hash is computed lowercase
38    pub fn find_by_hash(&self, hash: u32) -> Result<Module<'a>> {
39        for module in ModuleIterator::new(self.peb, ModuleListType::InLoadOrder)? {
40            let name = module.name();
41            let name_bytes = name.as_bytes();
42            if djb2_hash_lowercase(name_bytes) == hash {
43                return Ok(module);
44            }
45        }
46
47        Err(WraithError::ModuleNotFound {
48            name: format!("hash {hash:#x}"),
49        })
50    }
51
52    /// find module containing an address
53    pub fn find_by_address(&self, address: usize) -> Result<Module<'a>> {
54        for module in ModuleIterator::new(self.peb, ModuleListType::InLoadOrder)? {
55            if module.contains(address) {
56                return Ok(module);
57            }
58        }
59
60        Err(WraithError::AddressNotInModule {
61            address: address as u64,
62        })
63    }
64
65    /// find module by base address (exact match)
66    pub fn find_by_base(&self, base: usize) -> Result<Module<'a>> {
67        for module in ModuleIterator::new(self.peb, ModuleListType::InLoadOrder)? {
68            if module.base() == base {
69                return Ok(module);
70            }
71        }
72
73        Err(WraithError::AddressNotInModule {
74            address: base as u64,
75        })
76    }
77
78    /// find module by partial path match
79    pub fn find_by_path_contains(&self, substring: &str) -> Result<Module<'a>> {
80        let sub_lower = substring.to_lowercase();
81
82        for module in ModuleIterator::new(self.peb, ModuleListType::InLoadOrder)? {
83            if module.full_path().to_lowercase().contains(&sub_lower) {
84                return Ok(module);
85            }
86        }
87
88        Err(WraithError::ModuleNotFound {
89            name: substring.into(),
90        })
91    }
92
93    /// get ntdll.dll module
94    pub fn ntdll(&self) -> Result<Module<'a>> {
95        self.find_by_name("ntdll.dll")
96    }
97
98    /// get kernel32.dll module
99    pub fn kernel32(&self) -> Result<Module<'a>> {
100        self.find_by_name("kernel32.dll")
101    }
102
103    /// get kernelbase.dll module
104    pub fn kernelbase(&self) -> Result<Module<'a>> {
105        self.find_by_name("kernelbase.dll")
106    }
107
108    /// get current executable module
109    pub fn current_module(&self) -> Result<Module<'a>> {
110        let image_base = self.peb.image_base() as usize;
111        self.find_by_base(image_base)
112    }
113
114    /// find all modules matching a predicate
115    pub fn find_all<F>(&self, predicate: F) -> Result<Vec<Module<'a>>>
116    where
117        F: Fn(&Module) -> bool,
118    {
119        let mut results = Vec::new();
120
121        for module in ModuleIterator::new(self.peb, ModuleListType::InLoadOrder)? {
122            if predicate(&module) {
123                results.push(module);
124            }
125        }
126
127        Ok(results)
128    }
129
130    /// check if a module is loaded
131    pub fn is_loaded(&self, name: &str) -> bool {
132        self.find_by_name(name).is_ok()
133    }
134}
135
136/// convenient function to find ntdll
137pub fn get_ntdll() -> Result<ModuleHandle> {
138    let peb = Peb::current()?;
139    let query = ModuleQuery::new(&peb);
140    let module = query.ntdll()?;
141
142    // SAFETY: converting borrowed module to handle - we know the module exists
143    unsafe {
144        ModuleHandle::from_raw(module.as_ldr_entry() as *const _ as *mut _)
145            .ok_or(WraithError::NullPointer {
146                context: "ntdll entry",
147            })
148    }
149}
150
151/// convenient function to find kernel32
152pub fn get_kernel32() -> Result<ModuleHandle> {
153    let peb = Peb::current()?;
154    let query = ModuleQuery::new(&peb);
155    let module = query.kernel32()?;
156
157    // SAFETY: converting borrowed module to handle - we know the module exists
158    unsafe {
159        ModuleHandle::from_raw(module.as_ldr_entry() as *const _ as *mut _)
160            .ok_or(WraithError::NullPointer {
161                context: "kernel32 entry",
162            })
163    }
164}
165
166/// get export from a module by name
167pub fn get_proc_address(module_name: &str, proc_name: &str) -> Result<usize> {
168    let peb = Peb::current()?;
169    let query = ModuleQuery::new(&peb);
170    let module = query.find_by_name(module_name)?;
171    module.get_export(proc_name)
172}
173
174/// get export from ntdll by name
175pub fn get_ntdll_export(proc_name: &str) -> Result<usize> {
176    get_proc_address("ntdll.dll", proc_name)
177}
178
179#[cfg(test)]
180mod tests {
181    use super::*;
182
183    #[test]
184    fn test_find_ntdll() {
185        let peb = Peb::current().expect("should get PEB");
186        let query = ModuleQuery::new(&peb);
187        let ntdll = query.ntdll().expect("should find ntdll");
188        assert!(ntdll.name_lowercase().contains("ntdll"));
189    }
190
191    #[test]
192    fn test_find_kernel32() {
193        let peb = Peb::current().expect("should get PEB");
194        let query = ModuleQuery::new(&peb);
195        let kernel32 = query.kernel32().expect("should find kernel32");
196        assert!(kernel32.name_lowercase().contains("kernel32"));
197    }
198
199    #[test]
200    fn test_find_by_address() {
201        let peb = Peb::current().expect("should get PEB");
202        let query = ModuleQuery::new(&peb);
203
204        // use address of a function we know exists
205        let addr = test_find_by_address as usize;
206        let module = query.find_by_address(addr).expect("should find module");
207
208        // should be our executable
209        assert!(module.contains(addr));
210    }
211
212    #[test]
213    fn test_get_ntdll_handle() {
214        let handle = get_ntdll().expect("should get ntdll handle");
215        let module = handle.as_module();
216        assert!(module.name_lowercase().contains("ntdll"));
217    }
218
219    #[test]
220    fn test_get_export() {
221        let peb = Peb::current().expect("should get PEB");
222        let query = ModuleQuery::new(&peb);
223        let ntdll = query.ntdll().expect("should find ntdll");
224
225        let addr = ntdll.get_export("NtClose").expect("should find NtClose");
226        assert!(addr > ntdll.base());
227        assert!(addr < ntdll.base() + ntdll.size());
228    }
229
230    #[test]
231    fn test_is_loaded() {
232        let peb = Peb::current().expect("should get PEB");
233        let query = ModuleQuery::new(&peb);
234
235        assert!(query.is_loaded("ntdll.dll"));
236        assert!(query.is_loaded("kernel32.dll"));
237        assert!(!query.is_loaded("nonexistent_module_12345.dll"));
238    }
239}