wraith/navigation/
module_query.rs

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