From a6e75fddcdca65beb5dabb8d38f46a21256e8a49 Mon Sep 17 00:00:00 2001 From: Laxyny Date: Tue, 23 Jun 2026 11:07:26 +0200 Subject: [PATCH] Fix CI failures and harden scanner coverage --- Cargo.lock | 83 +++-- Cargo.toml | 2 +- bindings/python/src/lib.rs | 44 +-- core/examples/list_processes.rs | 9 +- core/examples/memory_scanner.rs | 23 +- core/src/debugger/mod.rs | 7 +- core/src/lib.rs | 4 +- core/src/memory/mod.rs | 10 +- core/src/memory/unix.rs | 18 +- core/src/memory/windows.rs | 74 ++--- core/src/platform/mod.rs | 4 +- core/src/pointer/mod.rs | 37 +-- core/src/process/mod.rs | 14 +- core/src/process/windows.rs | 38 ++- core/src/scanner/engine.rs | 12 +- core/src/scanner/mod.rs | 457 +++++++++++++++++++--------- core/src/scanner/patterns.rs | 2 +- core/src/types.rs | 6 +- inspectra-gui/src-tauri/src/main.rs | 149 +++++---- 19 files changed, 573 insertions(+), 420 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index cb7486b..9a70071 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -251,9 +251,9 @@ checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" [[package]] name = "bytes" -version = "1.10.1" +version = "1.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a" +checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" [[package]] name = "cairo-rs" @@ -332,7 +332,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d067ad48b8650848b989a59a86c6c36a995d02d2bf778d45c3c5d57bc2718f02" dependencies = [ "smallvec", - "target-lexicon", + "target-lexicon 0.12.16", ] [[package]] @@ -1545,15 +1545,6 @@ dependencies = [ "serde_core", ] -[[package]] -name = "indoc" -version = "2.0.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "79cf5c93f93228cf8efb3ba362535fb11199ac548a09ce117c9b1adc3030d706" -dependencies = [ - "rustversion", -] - [[package]] name = "infer" version = "0.13.0" @@ -1965,9 +1956,9 @@ dependencies = [ [[package]] name = "num-conv" -version = "0.1.0" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" +checksum = "521739c6d2bac4aa25192232afe6841231376b2b26d4d9fae5ecf8ca5772e441" [[package]] name = "num-traits" @@ -2394,37 +2385,32 @@ dependencies = [ [[package]] name = "pyo3" -version = "0.20.3" +version = "0.29.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "53bdbb96d49157e65d45cc287af5f32ffadd5f4761438b527b055fb0d4bb8233" +checksum = "cd274650b21d4bfc26a0a47587962c1edb425f69287324355cd040c3ea66071c" dependencies = [ - "cfg-if", - "indoc", "libc", - "memoffset", - "parking_lot", + "once_cell", "portable-atomic", "pyo3-build-config", "pyo3-ffi", "pyo3-macros", - "unindent", ] [[package]] name = "pyo3-build-config" -version = "0.20.3" +version = "0.29.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "deaa5745de3f5231ce10517a1f5dd97d53e5a2fd77aa6b5842292085831d48d7" +checksum = "c5e2a7d2f0d013342f295c048ad19237add5154a55b1c5a254c0ec93d4109078" dependencies = [ - "once_cell", - "target-lexicon", + "target-lexicon 0.13.5", ] [[package]] name = "pyo3-ffi" -version = "0.20.3" +version = "0.29.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62b42531d03e08d4ef1f6e85a2ed422eb678b8cd62b762e53891c05faf0d4afa" +checksum = "ca85c467da1bbc8d866eea5deff9cf29ea5f7785054a17da36e65bda9c05845b" dependencies = [ "libc", "pyo3-build-config", @@ -2432,9 +2418,9 @@ dependencies = [ [[package]] name = "pyo3-macros" -version = "0.20.3" +version = "0.29.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7305c720fa01b8055ec95e484a6eca7a83c841267f0dd5280f0c8b8551d2c158" +checksum = "9ac53762fd065daa3194dd09337a38bd793a188100fd1a9304c4ab312d901771" dependencies = [ "proc-macro2", "pyo3-macros-backend", @@ -2444,13 +2430,12 @@ dependencies = [ [[package]] name = "pyo3-macros-backend" -version = "0.20.3" +version = "0.29.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7c7e9b68bb9c3149c5b0cade5d07f953d6d125eb4337723c4ccdb665f1f96185" +checksum = "4ca3a1557399783172dc5bf39cfca835157732532cba56b71d2292161e53b362" dependencies = [ - "heck 0.4.1", + "heck 0.5.0", "proc-macro2", - "pyo3-build-config", "quote", "syn 2.0.108", ] @@ -3192,9 +3177,9 @@ dependencies = [ [[package]] name = "tar" -version = "0.4.44" +version = "0.4.45" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d863878d212c87a19c1a610eb53bb01fe12951c0501cf5a0d65f724914a667a" +checksum = "22692a6476a21fa75fdfc11d452fda482af402c008cdbaf3476414e122040973" dependencies = [ "filetime", "libc", @@ -3207,6 +3192,12 @@ version = "0.12.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1" +[[package]] +name = "target-lexicon" +version = "0.13.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "adb6935a6f5c20170eeceb1a3835a49e12e19d792f6dd344ccc76a985ca5a6ca" + [[package]] name = "tauri" version = "1.8.3" @@ -3471,30 +3462,30 @@ dependencies = [ [[package]] name = "time" -version = "0.3.44" +version = "0.3.47" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91e7d9e3bb61134e77bde20dd4825b97c010155709965fedf0f49bb138e52a9d" +checksum = "743bd48c283afc0388f9b8827b976905fb217ad9e647fae3a379a9283c4def2c" dependencies = [ "deranged", "itoa 1.0.15", "num-conv", "powerfmt", - "serde", + "serde_core", "time-core", "time-macros", ] [[package]] name = "time-core" -version = "0.1.6" +version = "0.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "40868e7c1d2f0b8d73e4a8c7f0ff63af4f6d19be117e90bd73eb1d62cf831c6b" +checksum = "7694e1cfe791f8d31026952abf09c69ca6f6fa4e1a1229e18988f06a04a12dca" [[package]] name = "time-macros" -version = "0.2.24" +version = "0.2.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "30cfb0125f12d9c277f35663a0a33f8c30190f4e4574868a330595412d34ebf3" +checksum = "2e70e4c5a0e0a8a4823ad65dfe1a6930e4f4d756dcd9dd7939022b5e8c501215" dependencies = [ "num-conv", "time-core", @@ -3716,12 +3707,6 @@ version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" -[[package]] -name = "unindent" -version = "0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7264e107f553ccae879d21fbea1d6724ac785e8c3bfc762137959b5802826ef3" - [[package]] name = "url" version = "2.5.7" @@ -4019,7 +4004,7 @@ version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" dependencies = [ - "windows-sys 0.61.2", + "windows-sys 0.48.0", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index a360c66..acdf03e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -38,7 +38,7 @@ windows = { version = "0.54", features = [ libc = "0.2" # Scripting integration -pyo3 = { version = "0.20", features = ["extension-module"] } +pyo3 = { version = "0.29", features = ["extension-module"] } # UI/IPC tauri = { version = "1.5", features = ["api-all"] } diff --git a/bindings/python/src/lib.rs b/bindings/python/src/lib.rs index ddc110c..6e13b00 100644 --- a/bindings/python/src/lib.rs +++ b/bindings/python/src/lib.rs @@ -1,8 +1,8 @@ //! Python bindings for Inspectra +#![allow(non_local_definitions)] use pyo3::prelude::*; -use pyo3::types::PyList; -use inspectra_core; +use pyo3::types::{PyList, PyModule}; /// Process information exposed to Python #[pyclass] @@ -31,43 +31,49 @@ impl ProcessManager { } /// List all processes - fn list_processes(&self, py: Python) -> PyResult { + fn list_processes(&self, py: Python<'_>) -> PyResult> { let processes = self .manager .list_processes() .map_err(|e| PyErr::new::(e.to_string()))?; - let list = PyList::empty_bound(py); + let list = PyList::empty(py); for proc in processes { - let py_proc = Py::new(py, ProcessInfo { - pid: proc.pid, - name: proc.name, - path: proc.path.unwrap_or_default(), - })?; + let py_proc = Py::new( + py, + ProcessInfo { + pid: proc.pid, + name: proc.name, + path: proc.path, + }, + )?; list.append(py_proc)?; } - Ok(list.into()) + Ok(list.into_any().unbind()) } /// Find processes by name - fn find_by_name(&self, py: Python, name: &str) -> PyResult { + fn find_by_name(&self, py: Python<'_>, name: &str) -> PyResult> { let processes = self .manager .find_by_name(name) .map_err(|e| PyErr::new::(e.to_string()))?; - let list = PyList::empty_bound(py); + let list = PyList::empty(py); for proc in processes { - let py_proc = Py::new(py, ProcessInfo { - pid: proc.pid, - name: proc.name, - path: proc.path.unwrap_or_default(), - })?; + let py_proc = Py::new( + py, + ProcessInfo { + pid: proc.pid, + name: proc.name, + path: proc.path, + }, + )?; list.append(py_proc)?; } - Ok(list.into()) + Ok(list.into_any().unbind()) } } @@ -100,7 +106,7 @@ impl Scanner { /// Initialize the Inspectra Python module #[pymodule] -fn inspectra(_py: Python, m: &PyModule) -> PyResult<()> { +fn inspectra(m: &Bound<'_, PyModule>) -> PyResult<()> { m.add_class::()?; m.add_class::()?; m.add_class::()?; diff --git a/core/examples/list_processes.rs b/core/examples/list_processes.rs index 78cf273..ec3f32d 100644 --- a/core/examples/list_processes.rs +++ b/core/examples/list_processes.rs @@ -18,16 +18,11 @@ fn main() -> Result<(), Box> { sorted_processes.sort_by(|a, b| a.name.cmp(&b.name)); // Display - println!("{:<10} {:<30} {}", "PID", "Name", "Path"); + println!("{:<10} {:<30} Path", "PID", "Name"); println!("{}", "=".repeat(80)); for proc in sorted_processes.iter().take(20) { - println!( - "{:<10} {:<30} {}", - proc.pid, - proc.name, - proc.path - ); + println!("{:<10} {:<30} {}", proc.pid, proc.name, proc.path); } println!("\nTotal: {} processes", sorted_processes.len()); diff --git a/core/examples/memory_scanner.rs b/core/examples/memory_scanner.rs index 2e396a7..1bdaef3 100644 --- a/core/examples/memory_scanner.rs +++ b/core/examples/memory_scanner.rs @@ -29,8 +29,12 @@ fn main() -> Result<(), Box> { eprintln!("No process found with name: {}", input); return Ok(()); } - println!("Found {} processes, using first: {} (PID: {})", - procs.len(), procs[0].name, procs[0].pid); + println!( + "Found {} processes, using first: {} (PID: {})", + procs.len(), + procs[0].name, + procs[0].pid + ); manager.attach(procs[0].pid)? }; @@ -40,8 +44,10 @@ fn main() -> Result<(), Box> { let mem = memory::create_memory(handle.as_ref())?; // Configure scanner - let mut config = ScanConfig::default(); - config.data_type = DataType::I32; + let config = ScanConfig { + data_type: DataType::I32, + ..Default::default() + }; let mut scanner = Scanner::new(mem, config); @@ -60,11 +66,12 @@ fn main() -> Result<(), Box> { if let Ok(value) = value_input.parse::() { println!("Scanning for value: {}", value); - - let results = scanner.scan(&value.to_le_bytes())?; - + + let bytes = value.to_le_bytes(); + let results = scanner.scan(Some(&bytes), None)?; + println!("Found {} results", results.len()); - + // Display first 10 results for (i, result) in results.iter().take(10).enumerate() { println!(" [{}] Address: 0x{:X}", i, result.address); diff --git a/core/src/debugger/mod.rs b/core/src/debugger/mod.rs index 0c862d5..d339462 100644 --- a/core/src/debugger/mod.rs +++ b/core/src/debugger/mod.rs @@ -120,10 +120,9 @@ impl CodePatcher { use crate::types::Protection; // Allocate executable memory - let address = self.memory.allocate( - shellcode.len(), - Protection::new(true, true, true), - )?; + let address = self + .memory + .allocate(shellcode.len(), Protection::new(true, true, true))?; // Write shellcode self.memory.write(address, shellcode)?; diff --git a/core/src/lib.rs b/core/src/lib.rs index ad371c2..b8015ac 100644 --- a/core/src/lib.rs +++ b/core/src/lib.rs @@ -5,13 +5,13 @@ //! This crate provides the core functionality for process introspection, //! memory scanning, pointer analysis, and runtime manipulation. +pub mod debugger; pub mod error; pub mod memory; pub mod platform; +pub mod pointer; pub mod process; pub mod scanner; -pub mod pointer; -pub mod debugger; pub mod types; pub use error::{InspectraError, Result}; diff --git a/core/src/memory/mod.rs b/core/src/memory/mod.rs index a337909..6fec3cd 100644 --- a/core/src/memory/mod.rs +++ b/core/src/memory/mod.rs @@ -4,15 +4,15 @@ use crate::error::Result; use crate::process::ProcessHandle; use crate::types::{Address, MemoryRegion, Protection, Size}; -#[cfg(windows)] -mod windows; #[cfg(unix)] mod unix; - #[cfg(windows)] -pub use self::windows::*; +mod windows; + #[cfg(unix)] pub use self::unix::*; +#[cfg(windows)] +pub use self::windows::*; /// Memory reader/writer trait pub trait Memory: Send + Sync { @@ -56,7 +56,7 @@ mod tests { fn test_memory_regions() { let manager = process::get_process_manager(); let current_pid = std::process::id(); - + if let Ok(handle) = manager.attach(current_pid) { if let Ok(memory) = create_memory(handle.as_ref()) { let regions = memory.query_regions().unwrap(); diff --git a/core/src/memory/unix.rs b/core/src/memory/unix.rs index 7e7fb3f..b4393d2 100644 --- a/core/src/memory/unix.rs +++ b/core/src/memory/unix.rs @@ -13,9 +13,7 @@ pub struct UnixMemory { impl UnixMemory { pub fn new(process: &dyn ProcessHandle) -> Result { - Ok(Self { - pid: process.pid(), - }) + Ok(Self { pid: process.pid() }) } fn parse_maps_line(line: &str) -> Option { @@ -94,22 +92,24 @@ impl Memory for UnixMemory { let content = fs::read_to_string(&maps_path) .map_err(|e| InspectraError::memory(format!("Cannot read maps: {}", e)))?; - Ok(content - .lines() - .filter_map(Self::parse_maps_line) - .collect()) + Ok(content.lines().filter_map(Self::parse_maps_line).collect()) } fn query_region(&self, address: Address) -> Result { let regions = self.query_regions()?; - + regions .into_iter() .find(|r| address >= r.base_address && address < r.base_address + r.size) .ok_or_else(|| InspectraError::InvalidAddress(address)) } - fn protect(&self, _address: Address, _size: Size, _protection: Protection) -> Result { + fn protect( + &self, + _address: Address, + _size: Size, + _protection: Protection, + ) -> Result { // mprotect would require ptrace or other system calls Err(InspectraError::platform( "Memory protection not yet implemented on Unix", diff --git a/core/src/memory/windows.rs b/core/src/memory/windows.rs index 03ef281..ab1c086 100644 --- a/core/src/memory/windows.rs +++ b/core/src/memory/windows.rs @@ -3,16 +3,16 @@ use super::Memory; use crate::error::{InspectraError, Result}; use crate::process::ProcessHandle; -use crate::types::{Address, MemoryRegion, Protection, RegionType, Size, Pid}; +use crate::types::{Address, MemoryRegion, Pid, Protection, RegionType, Size}; +use std::sync::Mutex; +use windows::Win32::Foundation::HANDLE; use windows::Win32::System::Diagnostics::Debug::{ReadProcessMemory, WriteProcessMemory}; use windows::Win32::System::Memory::{ VirtualAllocEx, VirtualFreeEx, VirtualProtectEx, VirtualQueryEx, MEMORY_BASIC_INFORMATION, - MEM_COMMIT, MEM_IMAGE, MEM_MAPPED, MEM_PRIVATE, MEM_RELEASE, MEM_RESERVE, - PAGE_EXECUTE, PAGE_EXECUTE_READ, PAGE_EXECUTE_READWRITE, PAGE_NOACCESS, PAGE_READONLY, - PAGE_READWRITE, PAGE_PROTECTION_FLAGS, + MEM_COMMIT, MEM_IMAGE, MEM_MAPPED, MEM_PRIVATE, MEM_RELEASE, MEM_RESERVE, PAGE_EXECUTE, + PAGE_EXECUTE_READ, PAGE_EXECUTE_READWRITE, PAGE_NOACCESS, PAGE_PROTECTION_FLAGS, PAGE_READONLY, + PAGE_READWRITE, }; -use windows::Win32::Foundation::HANDLE; -use std::sync::Mutex; pub struct WindowsMemory { pid: Pid, @@ -22,32 +22,39 @@ pub struct WindowsMemory { impl WindowsMemory { pub fn new(process: &dyn ProcessHandle) -> Result { // Get the raw handle from the process - let raw_handle = process.as_raw_handle() + let raw_handle = process + .as_raw_handle() .ok_or_else(|| InspectraError::memory("Failed to get process handle"))?; - + let pid = process.pid(); - + // Use the handle directly - we'll recreate it if needed Ok(Self { pid, - handle: Mutex::new(unsafe { HANDLE(raw_handle as isize) }), + handle: Mutex::new(HANDLE(raw_handle as isize)), }) } - + fn ensure_handle(&self) -> Result { // Check if handle is still valid, if not recreate it let mut handle = self.handle.lock().unwrap(); unsafe { use windows::Win32::System::Threading::OpenProcess; - use windows::Win32::System::Threading::{PROCESS_VM_READ, PROCESS_VM_WRITE, PROCESS_VM_OPERATION, PROCESS_QUERY_INFORMATION}; - + use windows::Win32::System::Threading::{ + PROCESS_QUERY_INFORMATION, PROCESS_VM_OPERATION, PROCESS_VM_READ, PROCESS_VM_WRITE, + }; + // Try to open process again if handle is invalid if handle.is_invalid() { let new_handle = OpenProcess( - PROCESS_VM_READ | PROCESS_VM_WRITE | PROCESS_VM_OPERATION | PROCESS_QUERY_INFORMATION, + PROCESS_VM_READ + | PROCESS_VM_WRITE + | PROCESS_VM_OPERATION + | PROCESS_QUERY_INFORMATION, false, self.pid, - ).map_err(|e| InspectraError::memory(format!("Failed to reopen process: {}", e)))?; + ) + .map_err(|e| InspectraError::memory(format!("Failed to reopen process: {}", e)))?; *handle = new_handle; } } @@ -71,21 +78,24 @@ impl WindowsMemory { || prot == PAGE_READWRITE || prot == PAGE_EXECUTE_READ || prot == PAGE_EXECUTE_READWRITE; - + let write = prot == PAGE_READWRITE || prot == PAGE_EXECUTE_READWRITE; - - let execute = prot == PAGE_EXECUTE - || prot == PAGE_EXECUTE_READ - || prot == PAGE_EXECUTE_READWRITE; - Protection { read, write, execute } + let execute = + prot == PAGE_EXECUTE || prot == PAGE_EXECUTE_READ || prot == PAGE_EXECUTE_READWRITE; + + Protection { + read, + write, + execute, + } } } impl Memory for WindowsMemory { fn read(&self, address: Address, size: Size) -> Result> { let handle = self.ensure_handle()?; - + unsafe { let mut buffer = vec![0u8; size]; let mut bytes_read = 0; @@ -106,7 +116,7 @@ impl Memory for WindowsMemory { fn write(&self, address: Address, data: &[u8]) -> Result { let handle = self.ensure_handle()?; - + unsafe { let mut bytes_written = 0; @@ -125,7 +135,7 @@ impl Memory for WindowsMemory { fn query_regions(&self) -> Result> { let handle = self.ensure_handle()?; - + let mut regions = Vec::new(); let mut address: usize = 0; @@ -175,7 +185,7 @@ impl Memory for WindowsMemory { fn query_region(&self, address: Address) -> Result { let handle = self.ensure_handle()?; - + unsafe { let mut mbi = MEMORY_BASIC_INFORMATION::default(); let result = VirtualQueryEx( @@ -211,7 +221,7 @@ impl Memory for WindowsMemory { fn protect(&self, address: Address, size: Size, protection: Protection) -> Result { let handle = self.ensure_handle()?; - + unsafe { let new_protect = Self::convert_protection(protection); let mut old_protect = PAGE_PROTECTION_FLAGS::default(); @@ -231,16 +241,10 @@ impl Memory for WindowsMemory { fn allocate(&self, size: Size, protection: Protection) -> Result
{ let handle = self.ensure_handle()?; - + unsafe { let protect = Self::convert_protection(protection); - let address = VirtualAllocEx( - handle, - None, - size, - MEM_COMMIT | MEM_RESERVE, - protect, - ); + let address = VirtualAllocEx(handle, None, size, MEM_COMMIT | MEM_RESERVE, protect); if address.is_null() { return Err(InspectraError::memory("Allocation failed")); @@ -252,7 +256,7 @@ impl Memory for WindowsMemory { fn free(&self, address: Address) -> Result<()> { let handle = self.ensure_handle()?; - + unsafe { VirtualFreeEx(handle, address as *mut _, 0, MEM_RELEASE) .map_err(|e| InspectraError::memory(format!("Free failed: {}", e)))?; diff --git a/core/src/platform/mod.rs b/core/src/platform/mod.rs index 15154d6..fc0e785 100644 --- a/core/src/platform/mod.rs +++ b/core/src/platform/mod.rs @@ -2,10 +2,10 @@ use crate::error::Result; -#[cfg(windows)] -pub mod windows; #[cfg(unix)] pub mod unix; +#[cfg(windows)] +pub mod windows; /// Platform information #[derive(Debug, Clone)] diff --git a/core/src/pointer/mod.rs b/core/src/pointer/mod.rs index 6cf4a54..f87e566 100644 --- a/core/src/pointer/mod.rs +++ b/core/src/pointer/mod.rs @@ -34,7 +34,7 @@ impl PointerChain { let bytes = memory.read(current_address, std::mem::size_of::())?; current_address = usize::from_le_bytes(bytes.try_into().unwrap()); } - + // Apply offset current_address = (current_address as isize + offset) as usize; } @@ -84,11 +84,7 @@ impl PointerScanner { } /// Scan for pointers to a target address - pub fn scan( - &self, - memory: &dyn Memory, - target_address: Address, - ) -> Result> { + pub fn scan(&self, memory: &dyn Memory, target_address: Address) -> Result> { let mut results = Vec::new(); let regions = memory.query_regions()?; @@ -106,29 +102,20 @@ impl PointerScanner { break; } - let bytes: [u8; std::mem::size_of::()] = - data[i..i + std::mem::size_of::()] - .try_into() - .unwrap(); + let bytes: [u8; std::mem::size_of::()] = data + [i..i + std::mem::size_of::()] + .try_into() + .unwrap(); let value = usize::from_le_bytes(bytes); let pointer_address = region.base_address + i; - pointer_map - .entry(value) - .or_insert_with(Vec::new) - .push(pointer_address); + pointer_map.entry(value).or_default().push(pointer_address); } } } // Build pointer chains - self.build_chains( - &pointer_map, - target_address, - Vec::new(), - 0, - &mut results, - ); + self.build_chains(&pointer_map, target_address, Vec::new(), 0, &mut results); Ok(results) } @@ -160,13 +147,7 @@ impl PointerScanner { results.push(PointerChain::new(pointer_addr, offsets.clone())); } else { // Continue searching - self.build_chains( - pointer_map, - pointer_addr, - offsets, - level + 1, - results, - ); + self.build_chains(pointer_map, pointer_addr, offsets, level + 1, results); } } } diff --git a/core/src/process/mod.rs b/core/src/process/mod.rs index 8eed509..092bca5 100644 --- a/core/src/process/mod.rs +++ b/core/src/process/mod.rs @@ -4,15 +4,15 @@ use crate::error::Result; use crate::types::{Architecture, Pid}; use serde::{Deserialize, Serialize}; -#[cfg(windows)] -mod windows; #[cfg(unix)] mod unix; - #[cfg(windows)] -pub use self::windows::*; +mod windows; + #[cfg(unix)] pub use self::unix::*; +#[cfg(windows)] +pub use self::windows::*; /// Process information #[derive(Debug, Clone, Serialize, Deserialize)] @@ -38,11 +38,11 @@ pub trait ProcessHandle: Send + Sync { /// Terminate the process (requires elevated privileges) fn terminate(&self) -> Result<()>; - + /// Get the raw handle (platform-specific) #[cfg(windows)] fn as_raw_handle(&self) -> Option<*mut std::ffi::c_void>; - + #[cfg(not(windows))] fn as_raw_handle(&self) -> Option<*mut std::ffi::c_void> { None @@ -50,7 +50,7 @@ pub trait ProcessHandle: Send + Sync { } /// Process manager for listing and attaching to processes -pub trait ProcessManager { +pub trait ProcessManager: Send + Sync { /// List all running processes fn list_processes(&self) -> Result>; diff --git a/core/src/process/windows.rs b/core/src/process/windows.rs index 457ff23..c70b90c 100644 --- a/core/src/process/windows.rs +++ b/core/src/process/windows.rs @@ -6,13 +6,12 @@ use crate::types::{Architecture, Pid}; use std::sync::Arc; use windows::Win32::Foundation::{CloseHandle, HANDLE}; use windows::Win32::System::Diagnostics::ToolHelp::{ - CreateToolhelp32Snapshot, Process32FirstW, Process32NextW, PROCESSENTRY32W, - TH32CS_SNAPPROCESS, + CreateToolhelp32Snapshot, Process32FirstW, Process32NextW, PROCESSENTRY32W, TH32CS_SNAPPROCESS, }; use windows::Win32::System::Threading::{ - OpenProcess, TerminateProcess, PROCESS_QUERY_INFORMATION, PROCESS_QUERY_LIMITED_INFORMATION, - PROCESS_VM_OPERATION, PROCESS_VM_READ, PROCESS_VM_WRITE, QueryFullProcessImageNameW, - PROCESS_NAME_WIN32, + OpenProcess, QueryFullProcessImageNameW, TerminateProcess, PROCESS_NAME_WIN32, + PROCESS_QUERY_INFORMATION, PROCESS_QUERY_LIMITED_INFORMATION, PROCESS_VM_OPERATION, + PROCESS_VM_READ, PROCESS_VM_WRITE, }; pub struct WindowsProcessManager; @@ -23,11 +22,18 @@ impl WindowsProcessManager { } } +impl Default for WindowsProcessManager { + fn default() -> Self { + Self::new() + } +} + impl ProcessManager for WindowsProcessManager { fn list_processes(&self) -> Result> { unsafe { - let snapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0) - .map_err(|e| InspectraError::process(format!("Failed to create snapshot: {}", e)))?; + let snapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0).map_err(|e| { + InspectraError::process(format!("Failed to create snapshot: {}", e)) + })?; let mut processes = Vec::new(); let mut entry = PROCESSENTRY32W { @@ -46,7 +52,7 @@ impl ProcessManager for WindowsProcessManager { ); let pid = entry.th32ProcessID; - + // Try to get the full path of the process let path = get_process_path(pid).unwrap_or_default(); @@ -102,15 +108,17 @@ impl ProcessManager for WindowsProcessManager { fn get_process_path(pid: Pid) -> Result { unsafe { use windows::core::PWSTR; - + // Try with limited information first (works for more processes) let handle = OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, false, pid) .or_else(|_| OpenProcess(PROCESS_QUERY_INFORMATION, false, pid)) - .map_err(|e| InspectraError::process(format!("Failed to open process {}: {}", pid, e)))?; + .map_err(|e| { + InspectraError::process(format!("Failed to open process {}: {}", pid, e)) + })?; let mut buffer = vec![0u16; 1024]; let mut size = buffer.len() as u32; - + let pwstr = PWSTR::from_raw(buffer.as_mut_ptr()); if QueryFullProcessImageNameW(handle, PROCESS_NAME_WIN32, pwstr, &mut size).is_ok() { let len = buffer.iter().position(|&c| c == 0).unwrap_or(size as usize); @@ -118,7 +126,7 @@ fn get_process_path(pid: Pid) -> Result { let _ = CloseHandle(handle); return Ok(path); } - + let _ = CloseHandle(handle); Ok(String::new()) } @@ -149,7 +157,7 @@ impl ProcessHandle for WindowsProcessHandle { fn is_alive(&self) -> bool { // Check if handle is still valid - self.handle.0.is_invalid() == false + !self.handle.0.is_invalid() } fn info(&self) -> Result { @@ -170,8 +178,8 @@ impl ProcessHandle for WindowsProcessHandle { Ok(()) } } - + fn as_raw_handle(&self) -> Option<*mut std::ffi::c_void> { - Some(self.handle.0.0 as *mut _) + Some(self.handle.0 .0 as *mut _) } } diff --git a/core/src/scanner/engine.rs b/core/src/scanner/engine.rs index 1e07d55..da38420 100644 --- a/core/src/scanner/engine.rs +++ b/core/src/scanner/engine.rs @@ -61,14 +61,10 @@ pub fn value_to_bytes(value: &str, data_type: DataType) -> Result, Strin /// Convert bytes to display string pub fn bytes_to_string(bytes: &[u8], data_type: DataType) -> String { match data_type { - DataType::I8 if bytes.len() >= 1 => i8::from_le_bytes([bytes[0]]).to_string(), - DataType::U8 if bytes.len() >= 1 => bytes[0].to_string(), - DataType::I16 if bytes.len() >= 2 => { - i16::from_le_bytes([bytes[0], bytes[1]]).to_string() - } - DataType::U16 if bytes.len() >= 2 => { - u16::from_le_bytes([bytes[0], bytes[1]]).to_string() - } + DataType::I8 if !bytes.is_empty() => i8::from_le_bytes([bytes[0]]).to_string(), + DataType::U8 if !bytes.is_empty() => bytes[0].to_string(), + DataType::I16 if bytes.len() >= 2 => i16::from_le_bytes([bytes[0], bytes[1]]).to_string(), + DataType::U16 if bytes.len() >= 2 => u16::from_le_bytes([bytes[0], bytes[1]]).to_string(), DataType::I32 if bytes.len() >= 4 => { i32::from_le_bytes([bytes[0], bytes[1], bytes[2], bytes[3]]).to_string() } diff --git a/core/src/scanner/mod.rs b/core/src/scanner/mod.rs index c0edd8b..6045cdb 100644 --- a/core/src/scanner/mod.rs +++ b/core/src/scanner/mod.rs @@ -51,15 +51,19 @@ impl Scanner { } /// Perform initial scan - pub fn scan(&mut self, value: Option<&[u8]>, range: Option<(f64, f64)>) -> Result> { + pub fn scan( + &mut self, + value: Option<&[u8]>, + range: Option<(f64, f64)>, + ) -> Result> { let regions = self.memory.query_regions()?; - + // Filter regions based on writable_only let regions_to_scan: Vec<_> = regions .into_iter() .filter(|r| !self.config.writable_only || r.protection.write) .collect(); - + // Use threading for large scans if self.config.thread_count > 1 && regions_to_scan.len() > 1 { self.scan_parallel(®ions_to_scan, value, range) @@ -67,14 +71,20 @@ impl Scanner { self.scan_sequential(®ions_to_scan, value, range) } } - + /// Sequential scan (single-threaded) - fn scan_sequential(&mut self, regions: &[crate::types::MemoryRegion], value: Option<&[u8]>, range: Option<(f64, f64)>) -> Result> { + fn scan_sequential( + &mut self, + regions: &[crate::types::MemoryRegion], + value: Option<&[u8]>, + range: Option<(f64, f64)>, + ) -> Result> { let mut results = Vec::new(); for region in regions { if let Ok(data) = self.memory.read(region.base_address, region.size) { - let region_results = self.scan_region_data(&data, region.base_address, value, range); + let region_results = + self.scan_region_data(&data, region.base_address, value, range); results.extend(region_results); } } @@ -82,52 +92,63 @@ impl Scanner { self.results = results.clone(); Ok(results) } - + /// Parallel scan (multi-threaded) - fn scan_parallel(&mut self, regions: &[crate::types::MemoryRegion], value: Option<&[u8]>, range: Option<(f64, f64)>) -> Result> { + fn scan_parallel( + &mut self, + regions: &[crate::types::MemoryRegion], + value: Option<&[u8]>, + range: Option<(f64, f64)>, + ) -> Result> { use std::sync::mpsc; use std::thread; - + let (tx, rx) = mpsc::channel(); let memory = Arc::clone(&self.memory); let config = self.config.clone(); let value_clone = value.map(|v| v.to_vec()); - + // Split regions into chunks for each thread - let chunk_size = (regions.len() + self.config.thread_count - 1) / self.config.thread_count; + let chunk_size = regions.len().div_ceil(self.config.thread_count); let mut handles = Vec::new(); - + for chunk in regions.chunks(chunk_size) { let tx = tx.clone(); let memory = Arc::clone(&memory); let config = config.clone(); let value_clone = value_clone.clone(); let chunk = chunk.to_vec(); - + let handle = thread::spawn(move || { let mut chunk_results = Vec::new(); - + for region in chunk { if let Ok(data) = memory.read(region.base_address, region.size) { - let region_results = Self::scan_region_data_static(&data, region.base_address, &config, value_clone.as_deref(), range); + let region_results = Self::scan_region_data_static( + &data, + region.base_address, + &config, + value_clone.as_deref(), + range, + ); chunk_results.extend(region_results); } } - + tx.send(chunk_results).ok(); }); - + handles.push(handle); } - + drop(tx); // Close sender so receiver knows when done - + // Collect results from all threads let mut results = Vec::new(); for handle in handles { handle.join().ok(); } - + // Collect all results from channel while let Ok(chunk_results) = rx.recv() { results.extend(chunk_results); @@ -136,13 +157,17 @@ impl Scanner { self.results = results.clone(); Ok(results) } - + /// Scan a single region's data - fn scan_region_data(&self, data: &[u8], base_address: usize, value: Option<&[u8]>, range: Option<(f64, f64)>) -> Vec { + fn scan_region_data( + &self, + data: &[u8], + base_address: usize, + value: Option<&[u8]>, + range: Option<(f64, f64)>, + ) -> Vec { match self.config.scan_type { - ScanType::Unknown => { - self.scan_buffer_unknown(data, base_address) - } + ScanType::Unknown => self.scan_buffer_unknown(data, base_address), ScanType::Exact => { if let Some(val) = value { self.scan_buffer(data, base_address, val) @@ -173,15 +198,19 @@ impl Scanner { } } } - + /// Static version for threading - fn scan_region_data_static(data: &[u8], base_address: usize, config: &ScanConfig, value: Option<&[u8]>, range: Option<(f64, f64)>) -> Vec { + fn scan_region_data_static( + data: &[u8], + base_address: usize, + config: &ScanConfig, + value: Option<&[u8]>, + range: Option<(f64, f64)>, + ) -> Vec { // Create a temporary scanner instance for scanning // This is a workaround since we can't easily pass &self to threads match config.scan_type { - ScanType::Unknown => { - Self::scan_buffer_unknown_static(data, base_address, config) - } + ScanType::Unknown => Self::scan_buffer_unknown_static(data, base_address, config), ScanType::Exact => { if let Some(val) = value { Self::scan_buffer_static(data, base_address, val, config) @@ -214,17 +243,21 @@ impl Scanner { } /// Rescan previous results - pub fn rescan(&mut self, value: Option<&[u8]>, range: Option<(f64, f64)>) -> Result> { + pub fn rescan( + &mut self, + value: Option<&[u8]>, + range: Option<(f64, f64)>, + ) -> Result> { let mut new_results = Vec::new(); - let value_size = value.map(|v| v.len()).unwrap_or_else(|| { - match self.config.data_type { + let value_size = value + .map(|v| v.len()) + .unwrap_or_else(|| match self.config.data_type { DataType::I8 | DataType::U8 => 1, DataType::I16 | DataType::U16 => 2, DataType::I32 | DataType::U32 | DataType::F32 => 4, DataType::I64 | DataType::U64 | DataType::F64 => 8, DataType::String | DataType::WString | DataType::Bytes => 16, - } - }); + }); for result in &self.results { if let Ok(data) = self.memory.read(result.address, value_size) { @@ -243,18 +276,10 @@ impl Scanner { false } } - ScanType::Changed => { - self.compare_value(&data, &result.value, None, None) - } - ScanType::Unchanged => { - self.compare_value(&data, &result.value, None, None) - } - ScanType::Increased => { - self.compare_value(&data, &result.value, None, None) - } - ScanType::Decreased => { - self.compare_value(&data, &result.value, None, None) - } + ScanType::Changed => self.compare_value(&data, &result.value, None, None), + ScanType::Unchanged => self.compare_value(&data, &result.value, None, None), + ScanType::Increased => self.compare_value(&data, &result.value, None, None), + ScanType::Decreased => self.compare_value(&data, &result.value, None, None), ScanType::Bigger => { if let Some(val) = value { self.compare_value(&data, val, None, None) @@ -273,11 +298,7 @@ impl Scanner { }; if matches { - new_results.push(ScanResult::new( - result.address, - data, - result.data_type, - )); + new_results.push(ScanResult::new(result.address, data, result.data_type)); } } } @@ -325,7 +346,7 @@ impl Scanner { DataType::I64 | DataType::U64 | DataType::F64 => 8, DataType::String | DataType::WString | DataType::Bytes => 16, }; - + let alignment = if self.config.aligned { match self.config.data_type { DataType::I16 | DataType::U16 => 2, @@ -352,10 +373,15 @@ impl Scanner { } /// Scan buffer for comparison (bigger/smaller) - fn scan_buffer_comparison(&self, buffer: &[u8], base_address: usize, target: &[u8]) -> Vec { + fn scan_buffer_comparison( + &self, + buffer: &[u8], + base_address: usize, + target: &[u8], + ) -> Vec { let mut results = Vec::new(); let size = target.len(); - + let alignment = if self.config.aligned { match self.config.data_type { DataType::I16 | DataType::U16 => 2, @@ -384,7 +410,13 @@ impl Scanner { } /// Scan buffer for range values - fn scan_buffer_range(&self, buffer: &[u8], base_address: usize, min: f64, max: f64) -> Vec { + fn scan_buffer_range( + &self, + buffer: &[u8], + base_address: usize, + min: f64, + max: f64, + ) -> Vec { let mut results = Vec::new(); let size = match self.config.data_type { DataType::I8 | DataType::U8 => 1, @@ -393,7 +425,7 @@ impl Scanner { DataType::I64 | DataType::U64 | DataType::F64 => 8, _ => return results, }; - + let alignment = if self.config.aligned { match self.config.data_type { DataType::I16 | DataType::U16 => 2, @@ -422,7 +454,13 @@ impl Scanner { } /// Compare values based on scan type - fn compare_value(&self, current: &[u8], target: &[u8], min: Option, max: Option) -> bool { + fn compare_value( + &self, + current: &[u8], + target: &[u8], + min: Option, + max: Option, + ) -> bool { match self.config.scan_type { ScanType::Exact => { if target.is_empty() { @@ -489,41 +527,28 @@ impl Scanner { /// Convert bytes to numeric value for comparison fn bytes_to_numeric(&self, bytes: &[u8]) -> f64 { match self.config.data_type { - DataType::I8 if bytes.len() >= 1 => i8::from_le_bytes([bytes[0]]) as f64, - DataType::U8 if bytes.len() >= 1 => bytes[0] as f64, - DataType::I16 if bytes.len() >= 2 => { - i16::from_le_bytes([bytes[0], bytes[1]]) as f64 - } - DataType::U16 if bytes.len() >= 2 => { - u16::from_le_bytes([bytes[0], bytes[1]]) as f64 - } + DataType::I8 if !bytes.is_empty() => i8::from_le_bytes([bytes[0]]) as f64, + DataType::U8 if !bytes.is_empty() => bytes[0] as f64, + DataType::I16 if bytes.len() >= 2 => i16::from_le_bytes([bytes[0], bytes[1]]) as f64, + DataType::U16 if bytes.len() >= 2 => u16::from_le_bytes([bytes[0], bytes[1]]) as f64, DataType::I32 if bytes.len() >= 4 => { i32::from_le_bytes([bytes[0], bytes[1], bytes[2], bytes[3]]) as f64 } DataType::U32 if bytes.len() >= 4 => { u32::from_le_bytes([bytes[0], bytes[1], bytes[2], bytes[3]]) as f64 } - DataType::I64 if bytes.len() >= 8 => { - i64::from_le_bytes([ - bytes[0], bytes[1], bytes[2], bytes[3], - bytes[4], bytes[5], bytes[6], bytes[7], - ]) as f64 - } - DataType::U64 if bytes.len() >= 8 => { - u64::from_le_bytes([ - bytes[0], bytes[1], bytes[2], bytes[3], - bytes[4], bytes[5], bytes[6], bytes[7], - ]) as f64 - } + DataType::I64 if bytes.len() >= 8 => i64::from_le_bytes([ + bytes[0], bytes[1], bytes[2], bytes[3], bytes[4], bytes[5], bytes[6], bytes[7], + ]) as f64, + DataType::U64 if bytes.len() >= 8 => u64::from_le_bytes([ + bytes[0], bytes[1], bytes[2], bytes[3], bytes[4], bytes[5], bytes[6], bytes[7], + ]) as f64, DataType::F32 if bytes.len() >= 4 => { f32::from_le_bytes([bytes[0], bytes[1], bytes[2], bytes[3]]) as f64 } - DataType::F64 if bytes.len() >= 8 => { - f64::from_le_bytes([ - bytes[0], bytes[1], bytes[2], bytes[3], - bytes[4], bytes[5], bytes[6], bytes[7], - ]) - } + DataType::F64 if bytes.len() >= 8 => f64::from_le_bytes([ + bytes[0], bytes[1], bytes[2], bytes[3], bytes[4], bytes[5], bytes[6], bytes[7], + ]), _ => 0.0, } } @@ -542,10 +567,15 @@ impl Scanner { pub fn set_results(&mut self, results: Vec) { self.results = results; } - + // Static versions for threading - - fn scan_buffer_static(buffer: &[u8], base_address: usize, value: &[u8], config: &ScanConfig) -> Vec { + + fn scan_buffer_static( + buffer: &[u8], + base_address: usize, + value: &[u8], + config: &ScanConfig, + ) -> Vec { let mut results = Vec::new(); let alignment = if config.aligned { match config.data_type { @@ -572,8 +602,12 @@ impl Scanner { results } - - fn scan_buffer_unknown_static(buffer: &[u8], base_address: usize, config: &ScanConfig) -> Vec { + + fn scan_buffer_unknown_static( + buffer: &[u8], + base_address: usize, + config: &ScanConfig, + ) -> Vec { let mut results = Vec::new(); let size = match config.data_type { DataType::I8 | DataType::U8 => 1, @@ -582,7 +616,7 @@ impl Scanner { DataType::I64 | DataType::U64 | DataType::F64 => 8, DataType::String | DataType::WString | DataType::Bytes => 16, }; - + let alignment = if config.aligned { match config.data_type { DataType::I16 | DataType::U16 => 2, @@ -597,18 +631,19 @@ impl Scanner { let mut i = 0; while i + size <= buffer.len() { let value = buffer[i..i + size].to_vec(); - results.push(ScanResult::new( - base_address + i, - value, - config.data_type, - )); + results.push(ScanResult::new(base_address + i, value, config.data_type)); i += alignment; } results } - - fn scan_buffer_comparison_static(buffer: &[u8], base_address: usize, target: &[u8], config: &ScanConfig) -> Vec { + + fn scan_buffer_comparison_static( + buffer: &[u8], + base_address: usize, + target: &[u8], + config: &ScanConfig, + ) -> Vec { let mut results = Vec::new(); let size = match config.data_type { DataType::I8 | DataType::U8 => 1, @@ -617,7 +652,7 @@ impl Scanner { DataType::I64 | DataType::U64 | DataType::F64 => 8, _ => return results, }; - + let alignment = if config.aligned { match config.data_type { DataType::I16 | DataType::U16 => 2, @@ -634,7 +669,7 @@ impl Scanner { while i + size <= buffer.len() { let value = buffer[i..i + size].to_vec(); let current_val = Self::bytes_to_f64(&value, &config.data_type); - + let matches = match config.scan_type { ScanType::Bigger => current_val > target_val, ScanType::Smaller => current_val < target_val, @@ -642,21 +677,23 @@ impl Scanner { ScanType::Decreased => current_val < target_val, _ => false, }; - + if matches { - results.push(ScanResult::new( - base_address + i, - value, - config.data_type, - )); + results.push(ScanResult::new(base_address + i, value, config.data_type)); } i += alignment; } results } - - fn scan_buffer_range_static(buffer: &[u8], base_address: usize, min: f64, max: f64, config: &ScanConfig) -> Vec { + + fn scan_buffer_range_static( + buffer: &[u8], + base_address: usize, + min: f64, + max: f64, + config: &ScanConfig, + ) -> Vec { let mut results = Vec::new(); let size = match config.data_type { DataType::I8 | DataType::U8 => 1, @@ -665,7 +702,7 @@ impl Scanner { DataType::I64 | DataType::U64 | DataType::F64 => 8, _ => return results, }; - + let alignment = if config.aligned { match config.data_type { DataType::I16 | DataType::U16 => 2, @@ -682,55 +719,38 @@ impl Scanner { let value = buffer[i..i + size].to_vec(); let val = Self::bytes_to_f64(&value, &config.data_type); if val >= min && val <= max { - results.push(ScanResult::new( - base_address + i, - value, - config.data_type, - )); + results.push(ScanResult::new(base_address + i, value, config.data_type)); } i += alignment; } results } - + fn bytes_to_f64(bytes: &[u8], data_type: &DataType) -> f64 { match data_type { - DataType::I8 if bytes.len() >= 1 => i8::from_le_bytes([bytes[0]]) as f64, - DataType::U8 if bytes.len() >= 1 => bytes[0] as f64, - DataType::I16 if bytes.len() >= 2 => { - i16::from_le_bytes([bytes[0], bytes[1]]) as f64 - } - DataType::U16 if bytes.len() >= 2 => { - u16::from_le_bytes([bytes[0], bytes[1]]) as f64 - } + DataType::I8 if !bytes.is_empty() => i8::from_le_bytes([bytes[0]]) as f64, + DataType::U8 if !bytes.is_empty() => bytes[0] as f64, + DataType::I16 if bytes.len() >= 2 => i16::from_le_bytes([bytes[0], bytes[1]]) as f64, + DataType::U16 if bytes.len() >= 2 => u16::from_le_bytes([bytes[0], bytes[1]]) as f64, DataType::I32 if bytes.len() >= 4 => { i32::from_le_bytes([bytes[0], bytes[1], bytes[2], bytes[3]]) as f64 } DataType::U32 if bytes.len() >= 4 => { u32::from_le_bytes([bytes[0], bytes[1], bytes[2], bytes[3]]) as f64 } - DataType::I64 if bytes.len() >= 8 => { - i64::from_le_bytes([ - bytes[0], bytes[1], bytes[2], bytes[3], - bytes[4], bytes[5], bytes[6], bytes[7], - ]) as f64 - } - DataType::U64 if bytes.len() >= 8 => { - u64::from_le_bytes([ - bytes[0], bytes[1], bytes[2], bytes[3], - bytes[4], bytes[5], bytes[6], bytes[7], - ]) as f64 - } + DataType::I64 if bytes.len() >= 8 => i64::from_le_bytes([ + bytes[0], bytes[1], bytes[2], bytes[3], bytes[4], bytes[5], bytes[6], bytes[7], + ]) as f64, + DataType::U64 if bytes.len() >= 8 => u64::from_le_bytes([ + bytes[0], bytes[1], bytes[2], bytes[3], bytes[4], bytes[5], bytes[6], bytes[7], + ]) as f64, DataType::F32 if bytes.len() >= 4 => { f32::from_le_bytes([bytes[0], bytes[1], bytes[2], bytes[3]]) as f64 } - DataType::F64 if bytes.len() >= 8 => { - f64::from_le_bytes([ - bytes[0], bytes[1], bytes[2], bytes[3], - bytes[4], bytes[5], bytes[6], bytes[7], - ]) - } + DataType::F64 if bytes.len() >= 8 => f64::from_le_bytes([ + bytes[0], bytes[1], bytes[2], bytes[3], bytes[4], bytes[5], bytes[6], bytes[7], + ]), _ => 0.0, } } @@ -747,14 +767,112 @@ mod num_cpus { #[cfg(test)] mod tests { use super::*; + use crate::error::{InspectraError, Result}; use crate::memory; + use crate::memory::Memory; use crate::process; + use crate::types::{MemoryRegion, Protection, RegionType}; + use std::sync::{Arc, Mutex}; + + const BASE_ADDRESS: usize = 0x1000; + + struct MockMemory { + data: Arc>>, + protection: Protection, + } + + impl MockMemory { + fn new(data: Vec, protection: Protection) -> Self { + Self { + data: Arc::new(Mutex::new(data)), + protection, + } + } + + fn region(&self) -> MemoryRegion { + MemoryRegion { + base_address: BASE_ADDRESS, + size: self.data.lock().unwrap().len(), + protection: self.protection, + region_type: RegionType::Private, + module_name: Some("mock".to_string()), + } + } + } + + impl Memory for MockMemory { + fn read(&self, address: usize, size: usize) -> Result> { + let data = self.data.lock().unwrap(); + let offset = address + .checked_sub(BASE_ADDRESS) + .ok_or(InspectraError::InvalidAddress(address))?; + let end = offset + size; + + if end > data.len() { + return Err(InspectraError::memory("Read exceeds mock memory")); + } + + Ok(data[offset..end].to_vec()) + } + + fn write(&self, address: usize, bytes: &[u8]) -> Result { + let mut data = self.data.lock().unwrap(); + let offset = address + .checked_sub(BASE_ADDRESS) + .ok_or(InspectraError::InvalidAddress(address))?; + let end = offset + bytes.len(); + + if end > data.len() { + return Err(InspectraError::memory("Write exceeds mock memory")); + } + + data[offset..end].copy_from_slice(bytes); + Ok(bytes.len()) + } + + fn query_regions(&self) -> Result> { + Ok(vec![self.region()]) + } + + fn query_region(&self, address: usize) -> Result { + let region = self.region(); + if address >= region.base_address && address < region.base_address + region.size { + Ok(region) + } else { + Err(InspectraError::InvalidAddress(address)) + } + } + + fn protect( + &self, + _address: usize, + _size: usize, + _protection: Protection, + ) -> Result { + Ok(self.protection) + } + + fn allocate(&self, _size: usize, _protection: Protection) -> Result { + Err(InspectraError::memory("Mock allocation is not supported")) + } + + fn free(&self, _address: usize) -> Result<()> { + Ok(()) + } + } + + fn i32_buffer(values: &[i32]) -> Vec { + values + .iter() + .flat_map(|value| value.to_le_bytes()) + .collect() + } #[test] fn test_scanner_creation() { let manager = process::get_process_manager(); let current_pid = std::process::id(); - + if let Ok(handle) = manager.attach(current_pid) { if let Ok(mem) = memory::create_memory(handle.as_ref()) { let scanner = Scanner::new(mem, ScanConfig::default()); @@ -762,4 +880,61 @@ mod tests { } } } + + #[test] + fn exact_i32_scan_finds_aligned_matches() { + let mem = MockMemory::new(i32_buffer(&[7, 42, 13, 42]), Protection::read_write()); + let config = ScanConfig { + data_type: DataType::I32, + thread_count: 1, + aligned: true, + ..Default::default() + }; + let mut scanner = Scanner::new(Box::new(mem), config); + + let needle = 42_i32.to_le_bytes(); + let results = scanner.scan(Some(&needle), None).unwrap(); + let addresses: Vec<_> = results.iter().map(|result| result.address).collect(); + + assert_eq!(addresses, vec![BASE_ADDRESS + 4, BASE_ADDRESS + 12]); + } + + #[test] + fn range_i32_scan_filters_numeric_values() { + let mem = MockMemory::new(i32_buffer(&[1, 5, 10, 15]), Protection::read_write()); + let config = ScanConfig { + data_type: DataType::I32, + scan_type: ScanType::Range, + thread_count: 1, + aligned: true, + ..Default::default() + }; + let mut scanner = Scanner::new(Box::new(mem), config); + + let results = scanner.scan(None, Some((4.0, 12.0))).unwrap(); + let values: Vec<_> = results + .iter() + .map(|result| i32::from_le_bytes(result.value.clone().try_into().unwrap())) + .collect(); + + assert_eq!(values, vec![5, 10]); + } + + #[test] + fn writable_only_scan_skips_read_only_regions() { + let mem = MockMemory::new(i32_buffer(&[42]), Protection::read_only()); + let config = ScanConfig { + data_type: DataType::I32, + writable_only: true, + thread_count: 1, + aligned: true, + ..Default::default() + }; + let mut scanner = Scanner::new(Box::new(mem), config); + + let needle = 42_i32.to_le_bytes(); + let results = scanner.scan(Some(&needle), None).unwrap(); + + assert!(results.is_empty()); + } } diff --git a/core/src/scanner/patterns.rs b/core/src/scanner/patterns.rs index 4d95625..8df0e5d 100644 --- a/core/src/scanner/patterns.rs +++ b/core/src/scanner/patterns.rs @@ -45,7 +45,7 @@ impl Pattern { /// Find all occurrences in buffer pub fn find_all(&self, buffer: &[u8]) -> Vec { let mut results = Vec::new(); - + for i in 0..=buffer.len().saturating_sub(self.bytes.len()) { if self.matches(&buffer[i..]) { results.push(i); diff --git a/core/src/types.rs b/core/src/types.rs index 46d326e..d2a9851 100644 --- a/core/src/types.rs +++ b/core/src/types.rs @@ -52,7 +52,11 @@ pub struct Protection { impl Protection { pub fn new(read: bool, write: bool, execute: bool) -> Self { - Self { read, write, execute } + Self { + read, + write, + execute, + } } pub fn read_only() -> Self { diff --git a/inspectra-gui/src-tauri/src/main.rs b/inspectra-gui/src-tauri/src/main.rs index 64e086b..852369a 100644 --- a/inspectra-gui/src-tauri/src/main.rs +++ b/inspectra-gui/src-tauri/src/main.rs @@ -35,9 +35,7 @@ type SharedState = Arc>; #[tauri::command] fn list_processes() -> Result, String> { let manager = process::get_process_manager(); - let processes = manager - .list_processes() - .map_err(|e| e.to_string())?; + let processes = manager.list_processes().map_err(|e| e.to_string())?; // Extract icons with better error handling Ok(processes @@ -49,7 +47,7 @@ fn list_processes() -> Result, String> { } else { None }; - + ProcessInfo { pid: p.pid, name: p.name, @@ -69,12 +67,12 @@ fn extract_process_icon_robust(path: &str, _pid: u32) -> Option { use windows::core::PCWSTR; use windows::Win32::Storage::FileSystem::FILE_FLAGS_AND_ATTRIBUTES; use windows::Win32::UI::Shell::{SHGetFileInfoW, SHFILEINFOW, SHGFI_ICON, SHGFI_SMALLICON}; - use windows::Win32::UI::WindowsAndMessaging::{HICON, DestroyIcon}; - + use windows::Win32::UI::WindowsAndMessaging::{DestroyIcon, HICON}; + if path.is_empty() { return None; } - + unsafe { // Convert path to wide string let wide_path: Vec = OsStr::new(path).encode_wide().chain(Some(0)).collect(); @@ -82,9 +80,9 @@ fn extract_process_icon_robust(path: &str, _pid: u32) -> Option { return None; } let path_ptr = PCWSTR::from_raw(wide_path.as_ptr()); - + let mut file_info: SHFILEINFOW = std::mem::zeroed(); - + // Get icon handle - use SHGFI_SMALLICON for 16x16 icons let result = SHGetFileInfoW( path_ptr, @@ -93,30 +91,24 @@ fn extract_process_icon_robust(path: &str, _pid: u32) -> Option { std::mem::size_of::() as u32, SHGFI_ICON | SHGFI_SMALLICON, ); - + if result == 0 { return None; } - + let hicon: HICON = file_info.hIcon; if hicon.is_invalid() { return None; } - + // Convert icon to PNG base64 using DrawIconEx method (more reliable) let icon_result = icon_to_png_base64_draw(hicon); let _ = DestroyIcon(hicon); - + icon_result.map(|data| format!("data:image/png;base64,{}", data)) } } -// Keep old function for fallback -#[cfg(windows)] -fn extract_process_icon(path: &str, pid: u32) -> Option { - extract_process_icon_robust(path, pid) -} - // Test function to verify icon extraction #[cfg(windows)] #[allow(dead_code)] @@ -125,7 +117,7 @@ fn test_icon_extraction() { "C:\\Windows\\System32\\notepad.exe", "C:\\Windows\\System32\\calc.exe", ]; - + for path in test_paths { if let Some(icon) = extract_process_icon_robust(path, 0) { println!("Successfully extracted icon from: {}", path); @@ -137,28 +129,29 @@ fn test_icon_extraction() { } #[cfg(windows)] -unsafe fn icon_to_png_base64_draw(hicon: windows::Win32::UI::WindowsAndMessaging::HICON) -> Option { +unsafe fn icon_to_png_base64_draw( + hicon: windows::Win32::UI::WindowsAndMessaging::HICON, +) -> Option { + use image::RgbaImage; use windows::Win32::Graphics::Gdi::{ - GetDC, CreateCompatibleDC, CreateCompatibleBitmap, SelectObject, - GetDIBits, BITMAPINFO, BITMAPINFOHEADER, DIB_RGB_COLORS, - DeleteObject, DeleteDC, ReleaseDC + CreateCompatibleBitmap, CreateCompatibleDC, DeleteDC, DeleteObject, GetDC, GetDIBits, + ReleaseDC, SelectObject, BITMAPINFO, BITMAPINFOHEADER, DIB_RGB_COLORS, }; use windows::Win32::UI::WindowsAndMessaging::{DrawIconEx, DI_NORMAL}; - use image::RgbaImage; - + const ICON_SIZE: i32 = 32; // Use 32x32 for better quality - + let hdc = GetDC(None); if hdc.is_invalid() { return None; } - + let hdc_mem = CreateCompatibleDC(hdc); if hdc_mem.is_invalid() { ReleaseDC(None, hdc); return None; } - + // Create a 32-bit bitmap let hbmp = CreateCompatibleBitmap(hdc, ICON_SIZE, ICON_SIZE); if hbmp.is_invalid() { @@ -166,29 +159,21 @@ unsafe fn icon_to_png_base64_draw(hicon: windows::Win32::UI::WindowsAndMessaging ReleaseDC(None, hdc); return None; } - + let _old_bmp = SelectObject(hdc_mem, hbmp); - + // Draw the icon onto the bitmap let drawn = DrawIconEx( - hdc_mem, - 0, - 0, - hicon, - ICON_SIZE, - ICON_SIZE, - 0, - None, - DI_NORMAL, + hdc_mem, 0, 0, hicon, ICON_SIZE, ICON_SIZE, 0, None, DI_NORMAL, ); - + if drawn.is_err() { DeleteObject(hbmp); DeleteDC(hdc_mem); ReleaseDC(None, hdc); return None; } - + // Prepare bitmap info for GetDIBits let mut bmp_info = BITMAPINFO { bmiHeader: BITMAPINFOHEADER { @@ -206,11 +191,11 @@ unsafe fn icon_to_png_base64_draw(hicon: windows::Win32::UI::WindowsAndMessaging }, bmiColors: [std::mem::zeroed(); 1], }; - + // Allocate buffer for pixel data (BGRA format) let size = (ICON_SIZE * ICON_SIZE * 4) as usize; let mut bits: Vec = vec![0; size]; - + // Get the bitmap bits let result = GetDIBits( hdc_mem, @@ -221,23 +206,23 @@ unsafe fn icon_to_png_base64_draw(hicon: windows::Win32::UI::WindowsAndMessaging &mut bmp_info, DIB_RGB_COLORS, ); - + // Cleanup SelectObject(hdc_mem, _old_bmp); DeleteObject(hbmp); DeleteDC(hdc_mem); ReleaseDC(None, hdc); - + if result == 0 { return None; } - + // Convert BGRA to RGBA for i in (0..bits.len()).step_by(4) { // Swap B and R channels: BGRA -> RGBA bits.swap(i, i + 2); } - + // Create image and encode to PNG if let Some(img) = RgbaImage::from_raw(ICON_SIZE as u32, ICON_SIZE as u32, bits) { let mut png_data = Vec::new(); @@ -250,8 +235,10 @@ unsafe fn icon_to_png_base64_draw(hicon: windows::Win32::UI::WindowsAndMessaging ICON_SIZE as u32, image::ColorType::Rgba8, image::ImageFormat::Png, - ).is_ok() { - use base64::{Engine as _, engine::general_purpose}; + ) + .is_ok() + { + use base64::{engine::general_purpose, Engine as _}; let encoded = general_purpose::STANDARD.encode(&png_data); // Verify the encoded data is not empty if !encoded.is_empty() && encoded.len() > 100 { @@ -260,7 +247,7 @@ unsafe fn icon_to_png_base64_draw(hicon: windows::Win32::UI::WindowsAndMessaging } } } - + None } @@ -269,7 +256,6 @@ fn extract_process_icon(_path: &str, _pid: u32) -> Option { None } - #[tauri::command] fn attach_process(state: tauri::State, pid: u32) -> Result { let manager = process::get_process_manager(); @@ -288,6 +274,7 @@ fn attach_process(state: tauri::State, pid: u32) -> Result, scan_type: String, @@ -299,9 +286,7 @@ fn scan_memory( aligned: bool, ) -> Result, String> { let app_state = state.lock().unwrap(); - let pid = app_state - .attached_pid - .ok_or("No process attached")?; + let pid = app_state.attached_pid.ok_or("No process attached")?; drop(app_state); // Recreate handle - it's safe to reopen @@ -334,11 +319,13 @@ fn scan_memory( _ => types::ScanType::Exact, }; - let mut config = scanner::ScanConfig::default(); - config.data_type = dt; - config.scan_type = st; - config.writable_only = writable_only; - config.aligned = aligned; + let config = scanner::ScanConfig { + data_type: dt, + scan_type: st, + writable_only, + aligned, + ..Default::default() + }; let mut scanner = scanner::Scanner::new(mem, config); @@ -354,7 +341,9 @@ fn scan_memory( None }; - let results = scanner.scan(value_bytes.as_deref(), range).map_err(|e| e.to_string())?; + let results = scanner + .scan(value_bytes.as_deref(), range) + .map_err(|e| e.to_string())?; let frontend_results: Vec = results .iter() @@ -381,9 +370,7 @@ fn rescan_memory( range_max: Option, ) -> Result, String> { let app_state = state.lock().unwrap(); - let pid = app_state - .attached_pid - .ok_or("No process attached")?; + let pid = app_state.attached_pid.ok_or("No process attached")?; drop(app_state); // Recreate handle - it's safe to reopen @@ -416,12 +403,14 @@ fn rescan_memory( _ => types::ScanType::Exact, }; - let mut config = scanner::ScanConfig::default(); - config.data_type = dt; - config.scan_type = st; + let config = scanner::ScanConfig { + data_type: dt, + scan_type: st, + ..Default::default() + }; let mut scanner = scanner::Scanner::new(mem, config); - + // Restore previous results let app_state = state.lock().unwrap(); scanner.set_results(app_state.scan_results.clone()); @@ -439,7 +428,9 @@ fn rescan_memory( None }; - let results = scanner.rescan(value_bytes.as_deref(), range).map_err(|e| e.to_string())?; + let results = scanner + .rescan(value_bytes.as_deref(), range) + .map_err(|e| e.to_string())?; let frontend_results: Vec = results .iter() @@ -458,8 +449,8 @@ fn rescan_memory( #[tauri::command] fn read_memory(pid: u32, address: String, size: usize) -> Result, String> { - let addr = if address.starts_with("0x") { - usize::from_str_radix(&address[2..], 16) + let addr = if let Some(hex) = address.strip_prefix("0x") { + usize::from_str_radix(hex, 16) } else { address.parse() } @@ -473,18 +464,20 @@ fn read_memory(pid: u32, address: String, size: usize) -> Result, String } #[tauri::command] -fn write_memory(state: tauri::State, address: String, data: Vec) -> Result { - let addr = if address.starts_with("0x") { - usize::from_str_radix(&address[2..], 16) +fn write_memory( + state: tauri::State, + address: String, + data: Vec, +) -> Result { + let addr = if let Some(hex) = address.strip_prefix("0x") { + usize::from_str_radix(hex, 16) } else { address.parse() } .map_err(|e| format!("Invalid address: {}", e))?; let app_state = state.lock().unwrap(); - let pid = app_state - .attached_pid - .ok_or("No process attached")?; + let pid = app_state.attached_pid.ok_or("No process attached")?; drop(app_state); // Recreate handle @@ -502,7 +495,7 @@ fn get_memory_regions(pid: u32) -> Result, String> { let manager = process::get_process_manager(); let handle = manager.attach(pid).map_err(|e| e.to_string())?; let mem = memory::create_memory(handle.as_ref()).map_err(|e| e.to_string())?; - + mem.query_regions().map_err(|e| e.to_string()) }