Skip to main content

specter/memory/manipulation/
checksum.rs

1//! # Hook Integrity Checking
2//!
3//! This module provides self-checksumming capabilities to detect if installed hooks
4//! have been tampered with by or security tools.
5
6use once_cell::sync::Lazy;
7use parking_lot::Mutex;
8use std::collections::HashMap;
9use std::sync::Arc;
10use std::sync::atomic::{AtomicBool, Ordering};
11use std::thread::{self, JoinHandle};
12use std::time::Duration;
13
14#[cfg(feature = "dev_release")]
15use crate::utils::logger;
16
17use crate::memory::info::protection;
18
19/// FNV-1a offset basis for 64-bit
20const FNV_OFFSET_BASIS: u64 = 0xcbf29ce484222325;
21/// FNV-1a prime for 64-bit
22const FNV_PRIME: u64 = 0x100000001b3;
23
24/// Errors that can occur during integrity checking
25#[derive(Debug, Clone, Copy, PartialEq, Eq)]
26pub enum IntegrityError {
27    InvalidAddress,
28    InvalidLength,
29    MonitorAlreadyRunning,
30    MonitorNotRunning,
31}
32
33/// Represents a checksum for an installed hook
34#[derive(Clone, Debug)]
35pub struct HookChecksum {
36    /// Address of the hook redirect (the 16-byte branch at target)
37    pub target: usize,
38    /// Expected FNV-1a hash of the hook bytes
39    pub expected_hash: u64,
40    /// Length of bytes to check
41    pub length: usize,
42}
43
44impl HookChecksum {
45    /// Creates a new checksum for a hook at the given address
46    ///
47    /// # Arguments
48    /// * `target` - The address where the hook redirect is installed
49    /// * `len` - Number of bytes to checksum (typically 4 or 16)
50    ///
51    /// # Safety
52    /// Caller must ensure target address is valid and readable for `len` bytes
53    #[inline]
54    pub unsafe fn new_unchecked(target: usize, len: usize) -> Self {
55        unsafe {
56            let hash = Self::compute_unchecked(target, len);
57            Self {
58                target,
59                expected_hash: hash,
60                length: len,
61            }
62        }
63    }
64
65    /// Creates a new checksum with validation
66    pub fn new(target: usize, len: usize) -> Result<Self, IntegrityError> {
67        if target == 0 || len == 0 {
68            return Err(IntegrityError::InvalidAddress);
69        }
70        if len > 4096 {
71            return Err(IntegrityError::InvalidLength);
72        }
73
74        // Perform a safe read test
75        if !Self::is_readable(target, len) {
76            return Err(IntegrityError::InvalidAddress);
77        }
78
79        Ok(unsafe { Self::new_unchecked(target, len) })
80    }
81
82    /// Checks if memory region is readable
83    fn is_readable(addr: usize, len: usize) -> bool {
84        match protection::get_region_info(addr) {
85            Ok(info) => {
86                let is_accessible = info.protection.is_readable();
87                // Check if the region fully covers the requested range
88                let end_addr = addr + len;
89                let region_end = info.address + info.size;
90                let in_range = end_addr <= region_end;
91
92                is_accessible && in_range
93            }
94            Err(_) => false,
95        }
96    }
97
98    /// Computes FNV-1a hash of bytes at the given address
99    ///
100    /// # Safety
101    /// Caller must ensure target address is valid and readable for `len` bytes
102    #[inline]
103    unsafe fn compute_unchecked(target: usize, len: usize) -> u64 {
104        unsafe {
105            let mut hash = FNV_OFFSET_BASIS;
106            let ptr = target as *const u8;
107
108            // Use slice for better optimization
109            let slice = std::slice::from_raw_parts(ptr, len);
110            for &byte in slice {
111                hash ^= byte as u64;
112                hash = hash.wrapping_mul(FNV_PRIME);
113            }
114
115            hash
116        }
117    }
118
119    /// Verifies that the hook bytes have not been modified
120    ///
121    /// # Returns
122    /// * `bool` - `true` if intact, `false` if tampered
123    #[inline]
124    pub fn verify(&self) -> bool {
125        unsafe {
126            let current = Self::compute_unchecked(self.target, self.length);
127            current == self.expected_hash
128        }
129    }
130
131    /// Returns the target address
132    #[inline]
133    pub fn target(&self) -> usize {
134        self.target
135    }
136
137    /// Updates the expected hash to current memory state
138    pub fn update(&mut self) {
139        unsafe {
140            self.expected_hash = Self::compute_unchecked(self.target, self.length);
141        }
142    }
143}
144
145/// Global registry of hook checksums
146static CHECKSUMS: Lazy<Mutex<HashMap<usize, HookChecksum>>> =
147    Lazy::new(|| Mutex::new(HashMap::new()));
148
149/// Registers a checksum for a newly installed hook
150///
151/// # Arguments
152/// * `target` - The hook target address
153/// * `len` - Number of bytes in the hook redirect
154///
155/// Note: Automatically starts the background integrity monitor on first registration
156pub fn register(target: usize, len: usize) -> Result<(), IntegrityError> {
157    let checksum = HookChecksum::new(target, len)?;
158
159    let mut checksums = CHECKSUMS.lock();
160    let was_empty = checksums.is_empty();
161    checksums.insert(target, checksum);
162    drop(checksums); // Release lock early
163
164    if was_empty && !is_monitor_running() {
165        let _ = start_monitor(5000, Some(default_tamper_callback));
166    }
167
168    Ok(())
169}
170
171/// Removes a checksum when a hook is uninstalled
172///
173/// # Arguments
174/// * `target` - The hook target address
175pub fn unregister(target: usize) -> bool {
176    CHECKSUMS.lock().remove(&target).is_some()
177}
178
179/// Verifies a single hook's integrity
180///
181/// # Arguments
182/// * `target` - The hook target address
183///
184/// # Returns
185/// * `Option<bool>` - `Some(true)` if intact, `Some(false)` if tampered, `None` if not registered
186pub fn verify(target: usize) -> Option<bool> {
187    CHECKSUMS.lock().get(&target).map(|c| c.verify())
188}
189
190/// Verifies all registered hooks and returns addresses of tampered ones
191///
192/// # Returns
193/// * `Vec<usize>` - List of addresses where tampering was detected
194pub fn verify_all() -> Vec<usize> {
195    CHECKSUMS
196        .lock()
197        .values()
198        .filter(|c| !c.verify())
199        .map(|c| c.target)
200        .collect()
201}
202
203/// Returns the number of registered checksums
204pub fn count() -> usize {
205    CHECKSUMS.lock().len()
206}
207
208/// Clears all registered checksums
209pub fn clear() {
210    CHECKSUMS.lock().clear();
211}
212
213/// Result of integrity verification
214#[derive(Debug, Clone)]
215pub struct IntegrityReport {
216    /// Total hooks checked
217    pub total: usize,
218    /// Number of intact hooks
219    pub intact: usize,
220    /// Addresses of tampered hooks
221    pub tampered: Vec<usize>,
222}
223
224impl IntegrityReport {
225    /// Returns true if all hooks are intact
226    #[inline]
227    pub fn is_clean(&self) -> bool {
228        self.tampered.is_empty()
229    }
230
231    /// Returns the percentage of intact hooks
232    pub fn integrity_percentage(&self) -> f64 {
233        if self.total == 0 {
234            return 100.0;
235        }
236        (self.intact as f64 / self.total as f64) * 100.0
237    }
238}
239
240/// Performs a full integrity scan and returns a detailed report
241///
242/// # Returns
243/// * `IntegrityReport` - Detailed results of the scan
244pub fn scan() -> IntegrityReport {
245    let checksums = CHECKSUMS.lock();
246    let total = checksums.len();
247    let tampered: Vec<usize> = checksums
248        .values()
249        .filter(|c| !c.verify())
250        .map(|c| c.target)
251        .collect();
252    let intact = total - tampered.len();
253
254    IntegrityReport {
255        total,
256        intact,
257        tampered,
258    }
259}
260
261/// Monitor thread state
262struct MonitorState {
263    running: AtomicBool,
264    handle: Mutex<Option<JoinHandle<()>>>,
265}
266
267static MONITOR_STATE: Lazy<Arc<MonitorState>> = Lazy::new(|| {
268    Arc::new(MonitorState {
269        running: AtomicBool::new(false),
270        handle: Mutex::new(None),
271    })
272});
273
274/// Callback type for when tampering is detected
275pub type TamperCallback = fn(tampered: &[usize]);
276
277/// Default callback that restores tampered hooks
278///
279/// When tampering is detected, this callback:
280/// 1. Logs a warning (in dev builds)
281/// 2. Re-writes the hook redirect bytes to restore functionality
282/// 3. Updates the checksum with the new bytes
283fn default_tamper_callback(tampered: &[usize]) {
284    use super::hook::restore_hook_bytes;
285
286    for &addr in tampered {
287        #[cfg(feature = "dev_release")]
288        logger::warning(&format!("Hook tampered at {:#x}, restoring...", addr));
289
290        if restore_hook_bytes(addr) {
291            // Update checksum in a single lock acquisition
292            let mut checksums = CHECKSUMS.lock();
293            if let Some(checksum) = checksums.get_mut(&addr) {
294                checksum.update();
295            }
296            drop(checksums);
297
298            #[cfg(feature = "dev_release")]
299            logger::info(&format!("Hook restored at {:#x}", addr));
300        } else {
301            #[cfg(feature = "dev_release")]
302            logger::error(&format!("Failed to restore hook at {:#x}", addr));
303        }
304    }
305}
306
307/// Starts a background thread that periodically verifies hook integrity
308///
309/// # Arguments
310/// * `interval_ms` - How often to check (in milliseconds)
311/// * `on_tamper` - Optional callback when tampering is detected
312///
313/// # Returns
314/// * `Result<(), IntegrityError>` - Ok if monitor started, Err if already running
315pub fn start_monitor(
316    interval_ms: u64,
317    on_tamper: Option<TamperCallback>,
318) -> Result<(), IntegrityError> {
319    let state = Arc::clone(&MONITOR_STATE);
320
321    if state.running.swap(true, Ordering::SeqCst) {
322        return Err(IntegrityError::MonitorAlreadyRunning);
323    }
324
325    let callback = on_tamper.unwrap_or(default_tamper_callback);
326    let interval = Duration::from_millis(interval_ms);
327    let state_clone = Arc::clone(&state);
328
329    let handle = thread::spawn(move || {
330        #[cfg(feature = "dev_release")]
331        logger::info("Integrity monitor started");
332
333        while state_clone.running.load(Ordering::Relaxed) {
334            thread::sleep(interval);
335
336            if count() == 0 {
337                continue;
338            }
339
340            let tampered = verify_all();
341            if !tampered.is_empty() {
342                callback(&tampered);
343            }
344        }
345
346        #[cfg(feature = "dev_release")]
347        logger::info("Integrity monitor stopped");
348    });
349
350    *state.handle.lock() = Some(handle);
351    Ok(())
352}
353
354/// Stops the background integrity monitor
355///
356/// # Returns
357/// * `Result<(), IntegrityError>` - Ok if monitor was running and is now stopped
358pub fn stop_monitor() -> Result<(), IntegrityError> {
359    let state = Arc::clone(&MONITOR_STATE);
360
361    if !state.running.swap(false, Ordering::SeqCst) {
362        return Err(IntegrityError::MonitorNotRunning);
363    }
364
365    // Wait for thread to finish
366    if let Some(handle) = state.handle.lock().take() {
367        let _ = handle.join();
368    }
369
370    Ok(())
371}
372
373/// Checks if the background monitor is running
374#[inline]
375pub fn is_monitor_running() -> bool {
376    MONITOR_STATE.running.load(Ordering::Relaxed)
377}