use anyhow::Result; use log::{debug, info}; use rayon::prelude::*; use std::sync::{Arc, Mutex}; use crate::memory::MemoryReader; use crate::process::{MemoryRegion, Process}; #[derive(Clone, Debug)] pub enum ScanValueType { #[allow(dead_code)] U8(u8), #[allow(dead_code)] U16(u16), #[allow(dead_code)] U32(u32), #[allow(dead_code)] U64(u64), #[allow(dead_code)] I8(i8), #[allow(dead_code)] I16(i16), I32(i32), #[allow(dead_code)] I64(i64), #[allow(dead_code)] F32(f32), #[allow(dead_code)] F64(f64), } #[derive(Clone, Debug)] pub struct MemoryMatch { pub address: usize, #[allow(dead_code)] pub value_type: ScanValueType, } pub struct MemoryScanner { reader: MemoryReader, regions: Vec, matches: Vec, } impl MemoryScanner { pub fn new(process: Process) -> Result { let regions = process.get_memory_maps()?; let reader = MemoryReader::new(process); println!( "Successfully attached to process. Found {} readable memory regions.", regions.len() ); Ok(Self { reader, regions, matches: Vec::new(), }) } pub fn scan_for_value(&mut self, value: ScanValueType) -> Result<()> { let matches = Arc::new(Mutex::new(Vec::new())); self.regions .par_iter() .enumerate() .for_each(|(idx, region)| { println!( "{:02}/{:03} searching {:x} - {:x}...", idx + 1, self.regions.len(), region.start_addr, region.end_addr ); match self.scan_region(region, &value) { Ok(region_matches) => { let mut matches_guard = matches.lock().unwrap(); matches_guard.extend(region_matches); } Err(e) => { debug!( "Error scanning region {:x}-{:x}: {}", region.start_addr, region.end_addr, e ); } } print!("........ok\n"); }); self.matches = Arc::try_unwrap(matches) .expect("Failed to unwrap Arc") .into_inner() .expect("Failed to unlock Mutex"); info!("we currently have {} matches.", self.matches.len()); Ok(()) } pub fn get_matches_count(&self) -> usize { self.matches.len() } fn scan_region( &self, region: &MemoryRegion, value: &ScanValueType, ) -> Result> { // Basic implementation that scans a single region for matches let mut region_matches = Vec::new(); match self.reader.read_region(region) { Ok(memory) => { // Simple implementation searching for i32 values if let ScanValueType::I32(target) = value { let target_bytes = target.to_ne_bytes(); for i in (0..memory.len().saturating_sub(4)).step_by(4) { let mut matches = true; for j in 0..4 { if memory[i + j] != target_bytes[j] { matches = false; break; } } if matches { region_matches.push(MemoryMatch { address: region.start_addr + i, value_type: value.clone(), }); } } } // TODO missing handlers for all other types } Err(e) => { println!( "Error reading region {:x}-{:x}: {}", region.start_addr, region.end_addr, e ); } } Ok(region_matches) } pub fn filter_matches(&mut self, value: ScanValueType) -> Result<()> { let old_matches = std::mem::take(&mut self.matches); for m in old_matches { let addr = m.address; let data = match &value { ScanValueType::I32(_) => { match self.reader.read_memory(addr, 4) { Ok(data) => data, Err(_) => continue, // Skip if we can't read } } _ => continue, }; let matched = match &value { ScanValueType::I32(target) => { if data.len() >= 4 { let value_bytes = target.to_ne_bytes(); data[0] == value_bytes[0] && data[1] == value_bytes[1] && data[2] == value_bytes[2] && data[3] == value_bytes[3] } else { false } } _ => false, }; if matched { self.matches.push(MemoryMatch { address: addr, value_type: value.clone(), }); } } println!("..........ok"); info!("we currently have {} matches.", self.matches.len()); Ok(()) } pub fn set_value(&self, index: usize, value: i32) -> Result<()> { if index >= self.matches.len() { anyhow::bail!( "Index {} out of range (have {} matches)", index, self.matches.len() ); } let address = self.matches[index].address; let data = value.to_ne_bytes(); self.reader.write_memory(address, &data) } }