use once_cell::sync::Lazy;
use parking_lot::Mutex;
use std::collections::HashMap;
use std::sync::Arc;
use std::sync::atomic::{AtomicBool, Ordering};
use std::thread::{self, JoinHandle};
use std::time::Duration;
#[cfg(debug_assertions)]
use crate::utils::logger;
use crate::memory::info::protection;
const FNV_OFFSET_BASIS: u64 = 0xcbf29ce484222325;
const FNV_PRIME: u64 = 0x100000001b3;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum IntegrityError {
InvalidAddress,
InvalidLength,
MonitorAlreadyRunning,
MonitorNotRunning,
}
#[derive(Clone, Debug)]
pub struct HookChecksum {
pub target: usize,
pub expected_hash: u64,
pub length: usize,
}
impl HookChecksum {
#[inline]
pub unsafe fn new_unchecked(target: usize, len: usize) -> Self {
unsafe {
let hash = Self::compute_unchecked(target, len);
Self {
target,
expected_hash: hash,
length: len,
}
}
}
pub fn new(target: usize, len: usize) -> Result<Self, IntegrityError> {
if target == 0 || len == 0 {
return Err(IntegrityError::InvalidAddress);
}
if len > 4096 {
return Err(IntegrityError::InvalidLength);
}
if !Self::is_readable(target, len) {
return Err(IntegrityError::InvalidAddress);
}
Ok(unsafe { Self::new_unchecked(target, len) })
}
fn is_readable(addr: usize, len: usize) -> bool {
match protection::get_region_info(addr) {
Ok(info) => {
let is_accessible = info.protection.is_readable();
let end_addr = addr + len;
let region_end = info.address + info.size;
let in_range = end_addr <= region_end;
is_accessible && in_range
}
Err(_) => false,
}
}
#[inline]
unsafe fn compute_unchecked(target: usize, len: usize) -> u64 {
unsafe {
let mut hash = FNV_OFFSET_BASIS;
let ptr = target as *const u8;
let slice = std::slice::from_raw_parts(ptr, len);
for &byte in slice {
hash ^= byte as u64;
hash = hash.wrapping_mul(FNV_PRIME);
}
hash
}
}
#[inline]
pub fn verify(&self) -> bool {
unsafe {
let current = Self::compute_unchecked(self.target, self.length);
current == self.expected_hash
}
}
#[inline]
pub fn target(&self) -> usize {
self.target
}
pub fn update(&mut self) {
unsafe {
self.expected_hash = Self::compute_unchecked(self.target, self.length);
}
}
}
static CHECKSUMS: Lazy<Mutex<HashMap<usize, HookChecksum>>> =
Lazy::new(|| Mutex::new(HashMap::new()));
pub fn register(target: usize, len: usize) -> Result<(), IntegrityError> {
let checksum = HookChecksum::new(target, len)?;
let mut checksums = CHECKSUMS.lock();
let was_empty = checksums.is_empty();
checksums.insert(target, checksum);
drop(checksums);
if was_empty && !is_monitor_running() {
let _ = start_monitor(5000, Some(default_tamper_callback));
}
Ok(())
}
pub fn unregister(target: usize) -> bool {
CHECKSUMS.lock().remove(&target).is_some()
}
pub fn verify(target: usize) -> Option<bool> {
CHECKSUMS.lock().get(&target).map(|c| c.verify())
}
pub fn verify_all() -> Vec<usize> {
CHECKSUMS
.lock()
.values()
.filter(|c| !c.verify())
.map(|c| c.target)
.collect()
}
pub fn count() -> usize {
CHECKSUMS.lock().len()
}
pub fn clear() {
CHECKSUMS.lock().clear();
}
#[derive(Debug, Clone)]
pub struct IntegrityReport {
pub total: usize,
pub intact: usize,
pub tampered: Vec<usize>,
}
impl IntegrityReport {
#[inline]
pub fn is_clean(&self) -> bool {
self.tampered.is_empty()
}
pub fn integrity_percentage(&self) -> f64 {
if self.total == 0 {
return 100.0;
}
(self.intact as f64 / self.total as f64) * 100.0
}
}
pub fn scan() -> IntegrityReport {
let checksums = CHECKSUMS.lock();
let total = checksums.len();
let tampered: Vec<usize> = checksums
.values()
.filter(|c| !c.verify())
.map(|c| c.target)
.collect();
let intact = total - tampered.len();
IntegrityReport {
total,
intact,
tampered,
}
}
struct MonitorState {
running: AtomicBool,
handle: Mutex<Option<JoinHandle<()>>>,
}
static MONITOR_STATE: Lazy<Arc<MonitorState>> = Lazy::new(|| {
Arc::new(MonitorState {
running: AtomicBool::new(false),
handle: Mutex::new(None),
})
});
pub type TamperCallback = fn(tampered: &[usize]);
fn default_tamper_callback(tampered: &[usize]) {
use super::hook::restore_hook_bytes;
for &addr in tampered {
#[cfg(debug_assertions)]
logger::warning(&format!("Hook tampered at {:#x}, restoring...", addr));
if restore_hook_bytes(addr) {
let mut checksums = CHECKSUMS.lock();
if let Some(checksum) = checksums.get_mut(&addr) {
checksum.update();
}
drop(checksums);
#[cfg(debug_assertions)]
logger::info(&format!("Hook restored at {:#x}", addr));
} else {
#[cfg(debug_assertions)]
logger::error(&format!("Failed to restore hook at {:#x}", addr));
}
}
}
pub fn start_monitor(
interval_ms: u64,
on_tamper: Option<TamperCallback>,
) -> Result<(), IntegrityError> {
let state = Arc::clone(&MONITOR_STATE);
if state.running.swap(true, Ordering::SeqCst) {
return Err(IntegrityError::MonitorAlreadyRunning);
}
let callback = on_tamper.unwrap_or(default_tamper_callback);
let interval = Duration::from_millis(interval_ms);
let state_clone = Arc::clone(&state);
let handle = thread::spawn(move || {
#[cfg(debug_assertions)]
logger::info("Integrity monitor started");
while state_clone.running.load(Ordering::Relaxed) {
thread::sleep(interval);
if count() == 0 {
continue;
}
let tampered = verify_all();
if !tampered.is_empty() {
callback(&tampered);
}
}
#[cfg(debug_assertions)]
logger::info("Integrity monitor stopped");
});
*state.handle.lock() = Some(handle);
Ok(())
}
pub fn stop_monitor() -> Result<(), IntegrityError> {
let state = Arc::clone(&MONITOR_STATE);
if !state.running.swap(false, Ordering::SeqCst) {
return Err(IntegrityError::MonitorNotRunning);
}
if let Some(handle) = state.handle.lock().take() {
let _ = handle.join();
}
Ok(())
}
#[inline]
pub fn is_monitor_running() -> bool {
MONITOR_STATE.running.load(Ordering::Relaxed)
}