wraith/manipulation/hooks/
integrity.rs

1//! Function integrity verification
2//!
3//! Stores checksums of function prologues and verifies they haven't
4//! been modified at runtime. Useful for detecting hooks installed
5//! after initial recording.
6
7use crate::error::Result;
8use crate::navigation::Module;
9use crate::structures::pe::{DataDirectoryType, ExportDirectory};
10use std::collections::HashMap;
11
12/// stores checksums of function prologues for integrity checking
13pub struct IntegrityChecker {
14    checksums: HashMap<usize, u64>,
15    prologue_size: usize,
16}
17
18impl IntegrityChecker {
19    /// create new integrity checker with custom prologue size
20    pub fn new(prologue_size: usize) -> Self {
21        Self {
22            checksums: HashMap::new(),
23            prologue_size,
24        }
25    }
26
27    /// create with default prologue size (32 bytes)
28    pub fn with_default_size() -> Self {
29        Self::new(32)
30    }
31
32    /// get configured prologue size
33    pub fn prologue_size(&self) -> usize {
34        self.prologue_size
35    }
36
37    /// number of recorded functions
38    pub fn recorded_count(&self) -> usize {
39        self.checksums.len()
40    }
41
42    /// record checksum of function at address
43    pub fn record(&mut self, addr: usize) {
44        let checksum = self.compute_checksum(addr);
45        self.checksums.insert(addr, checksum);
46    }
47
48    /// record checksums for specific addresses
49    pub fn record_addresses(&mut self, addresses: &[usize]) {
50        for &addr in addresses {
51            self.record(addr);
52        }
53    }
54
55    /// record checksums for all exports in module
56    pub fn record_module(&mut self, module: &Module) -> Result<usize> {
57        let nt = module.nt_headers()?;
58        let export_dir = match nt.data_directory(DataDirectoryType::Export.index()) {
59            Some(dir) if dir.is_present() => dir,
60            _ => return Ok(0),
61        };
62
63        let base = module.base();
64        // SAFETY: export directory is present and valid for loaded modules
65        let exports = unsafe {
66            &*((base + export_dir.virtual_address as usize) as *const ExportDirectory)
67        };
68
69        let num_funcs = exports.number_of_functions as usize;
70        let functions_va = base + exports.address_of_functions as usize;
71        let mut count = 0;
72
73        for i in 0..num_funcs {
74            // SAFETY: iterating within bounds
75            let func_rva = unsafe { *((functions_va + i * 4) as *const u32) };
76            if func_rva != 0 {
77                // skip forwarded exports
78                if func_rva >= export_dir.virtual_address
79                    && func_rva < export_dir.virtual_address + export_dir.size
80                {
81                    continue;
82                }
83
84                let func_addr = base + func_rva as usize;
85                self.record(func_addr);
86                count += 1;
87            }
88        }
89
90        Ok(count)
91    }
92
93    /// record specific exports by name
94    pub fn record_exports(&mut self, module: &Module, names: &[&str]) -> Result<usize> {
95        let mut count = 0;
96        for name in names {
97            if let Ok(addr) = module.get_export(name) {
98                self.record(addr);
99                count += 1;
100            }
101        }
102        Ok(count)
103    }
104
105    /// verify function hasn't been modified
106    pub fn verify(&self, addr: usize) -> bool {
107        match self.checksums.get(&addr) {
108            Some(&expected) => {
109                let current = self.compute_checksum(addr);
110                current == expected
111            }
112            None => true, // not recorded, can't verify - assume ok
113        }
114    }
115
116    /// verify all recorded functions, returning list of modified addresses
117    pub fn verify_all(&self) -> Vec<usize> {
118        self.checksums
119            .keys()
120            .filter(|&&addr| !self.verify(addr))
121            .copied()
122            .collect()
123    }
124
125    /// get addresses of all modified functions
126    pub fn get_modified(&self) -> Vec<usize> {
127        self.verify_all()
128    }
129
130    /// check if a specific address was recorded
131    pub fn is_recorded(&self, addr: usize) -> bool {
132        self.checksums.contains_key(&addr)
133    }
134
135    /// remove a recorded address
136    pub fn unrecord(&mut self, addr: usize) -> bool {
137        self.checksums.remove(&addr).is_some()
138    }
139
140    /// clear all recorded checksums
141    pub fn clear(&mut self) {
142        self.checksums.clear();
143    }
144
145    /// compute FNV-1a hash of bytes at address
146    fn compute_checksum(&self, addr: usize) -> u64 {
147        // SAFETY: caller ensures address is valid and readable
148        let bytes = unsafe { core::slice::from_raw_parts(addr as *const u8, self.prologue_size) };
149
150        // FNV-1a hash
151        const FNV_OFFSET: u64 = 0xcbf29ce484222325;
152        const FNV_PRIME: u64 = 0x100000001b3;
153
154        let mut hash = FNV_OFFSET;
155        for &byte in bytes {
156            hash ^= byte as u64;
157            hash = hash.wrapping_mul(FNV_PRIME);
158        }
159        hash
160    }
161}
162
163impl Default for IntegrityChecker {
164    fn default() -> Self {
165        Self::with_default_size()
166    }
167}
168
169/// monitor for continuous integrity checking
170pub struct IntegrityMonitor {
171    checker: IntegrityChecker,
172    module_name: String,
173}
174
175impl IntegrityMonitor {
176    /// create monitor for a module
177    pub fn for_module(module: &Module) -> Result<Self> {
178        let mut checker = IntegrityChecker::with_default_size();
179        checker.record_module(module)?;
180
181        Ok(Self {
182            checker,
183            module_name: module.name(),
184        })
185    }
186
187    /// create monitor for specific functions
188    pub fn for_exports(module: &Module, exports: &[&str]) -> Result<Self> {
189        let mut checker = IntegrityChecker::with_default_size();
190        checker.record_exports(module, exports)?;
191
192        Ok(Self {
193            checker,
194            module_name: module.name(),
195        })
196    }
197
198    /// check for modifications
199    pub fn check(&self) -> Vec<usize> {
200        self.checker.get_modified()
201    }
202
203    /// get module name
204    pub fn module_name(&self) -> &str {
205        &self.module_name
206    }
207
208    /// number of monitored functions
209    pub fn monitored_count(&self) -> usize {
210        self.checker.recorded_count()
211    }
212}
213
214#[cfg(test)]
215mod tests {
216    use super::*;
217
218    #[test]
219    fn test_fnv1a_consistency() {
220        let checker = IntegrityChecker::new(8);
221
222        // same input should produce same hash
223        let data = [0x90u8; 8]; // nops
224        let addr = data.as_ptr() as usize;
225
226        let hash1 = checker.compute_checksum(addr);
227        let hash2 = checker.compute_checksum(addr);
228
229        assert_eq!(hash1, hash2);
230    }
231}