diff --git a/ghost-core/src/detection.rs b/ghost-core/src/detection.rs index 4039687..b7cfa1e 100644 --- a/ghost-core/src/detection.rs +++ b/ghost-core/src/detection.rs @@ -1,4 +1,4 @@ -use crate::{detect_hook_injection, MemoryProtection, MemoryRegion, ProcessInfo, ShellcodeDetector, ThreadInfo}; +use crate::{detect_hook_injection, HollowingDetector, MemoryProtection, MemoryRegion, ProcessInfo, ShellcodeDetector, ThreadInfo}; use std::collections::HashMap; #[derive(Debug, Clone, Copy, PartialEq, Eq)] @@ -19,6 +19,7 @@ pub struct DetectionResult { pub struct DetectionEngine { baseline: HashMap, shellcode_detector: ShellcodeDetector, + hollowing_detector: HollowingDetector, } #[derive(Debug, Clone)] @@ -32,6 +33,7 @@ impl DetectionEngine { Self { baseline: HashMap::new(), shellcode_detector: ShellcodeDetector::new(), + hollowing_detector: HollowingDetector::new(), } } @@ -125,6 +127,14 @@ impl DetectionEngine { confidence += detection.confidence; } } + + // Check for process hollowing + if let Ok(Some(hollowing_detection)) = self.hollowing_detector.analyze_process(process, memory_regions) { + for indicator in &hollowing_detection.indicators { + indicators.push(format!("Process hollowing: {}", indicator)); + } + confidence += hollowing_detection.confidence; + } self.baseline.insert( process.pid, diff --git a/ghost-core/src/hollowing.rs b/ghost-core/src/hollowing.rs new file mode 100644 index 0000000..fe50006 --- /dev/null +++ b/ghost-core/src/hollowing.rs @@ -0,0 +1,261 @@ +use crate::{GhostError, MemoryRegion, ProcessInfo, Result}; + +#[derive(Debug, Clone)] +pub struct HollowingDetection { + pub pid: u32, + pub process_name: String, + pub indicators: Vec, + pub confidence: f32, +} + +#[derive(Debug, Clone)] +pub enum HollowingIndicator { + UnmappedMainImage, + SuspiciousImageBase, + MemoryLayoutAnomaly { expected_size: usize, actual_size: usize }, + MismatchedPEHeader, + UnusualEntryPoint { address: usize }, + SuspiciousMemoryGaps { gap_count: usize, largest_gap: usize }, +} + +impl std::fmt::Display for HollowingIndicator { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::UnmappedMainImage => write!(f, "Main executable image appears unmapped"), + Self::SuspiciousImageBase => write!(f, "Image base address is suspicious"), + Self::MemoryLayoutAnomaly { expected_size, actual_size } => { + write!(f, "Memory layout anomaly: expected {:#x}, found {:#x}", expected_size, actual_size) + } + Self::MismatchedPEHeader => write!(f, "PE header mismatch detected"), + Self::UnusualEntryPoint { address } => { + write!(f, "Entry point at unusual location: {:#x}", address) + } + Self::SuspiciousMemoryGaps { gap_count, largest_gap } => { + write!(f, "{} memory gaps detected, largest: {:#x} bytes", gap_count, largest_gap) + } + } + } +} + +/// Process hollowing detection engine +pub struct HollowingDetector; + +impl HollowingDetector { + pub fn new() -> Self { + Self + } + + /// Analyze process for signs of hollowing + pub fn analyze_process( + &self, + process: &ProcessInfo, + memory_regions: &[MemoryRegion], + ) -> Result> { + let mut indicators = Vec::new(); + let mut confidence = 0.0; + + // Check for main image unmapping + if let Some(indicator) = self.check_main_image_unmapping(process, memory_regions) { + indicators.push(indicator); + confidence += 0.8; + } + + // Check memory layout anomalies + if let Some(indicator) = self.check_memory_layout_anomalies(memory_regions) { + indicators.push(indicator); + confidence += 0.6; + } + + // Check for suspicious memory gaps + if let Some(indicator) = self.check_memory_gaps(memory_regions) { + indicators.push(indicator); + confidence += 0.4; + } + + // Check for PE header anomalies + if let Some(indicator) = self.check_pe_header_anomalies(memory_regions) { + indicators.push(indicator); + confidence += 0.7; + } + + // Check entry point location + if let Some(indicator) = self.check_entry_point_anomalies(process, memory_regions) { + indicators.push(indicator); + confidence += 0.5; + } + + if !indicators.is_empty() { + Ok(Some(HollowingDetection { + pid: process.pid, + process_name: process.name.clone(), + indicators, + confidence: confidence.min(1.0), + })) + } else { + Ok(None) + } + } + + fn check_main_image_unmapping( + &self, + process: &ProcessInfo, + regions: &[MemoryRegion], + ) -> Option { + // Look for the main executable image region + let main_image_regions: Vec<_> = regions + .iter() + .filter(|r| r.region_type == "IMAGE") + .collect(); + + // Typical legitimate process should have at least one IMAGE region for the main executable + if main_image_regions.is_empty() { + return Some(HollowingIndicator::UnmappedMainImage); + } + + // Check if the main image base is suspicious + // Most Windows executables load at predictable addresses + for region in &main_image_regions { + if region.base_address < 0x400000 || region.base_address > 0x80000000 { + return Some(HollowingIndicator::SuspiciousImageBase); + } + } + + None + } + + fn check_memory_layout_anomalies( + &self, + regions: &[MemoryRegion], + ) -> Option { + // Calculate total executable memory size + let total_executable: usize = regions + .iter() + .filter(|r| matches!(r.protection, crate::MemoryProtection::ReadExecute | crate::MemoryProtection::ReadWriteExecute)) + .map(|r| r.size) + .sum(); + + // Check for unusually large or small executable regions + if total_executable > 0x10000000 { + // More than 256MB of executable memory is very suspicious + return Some(HollowingIndicator::MemoryLayoutAnomaly { + expected_size: 0x1000000, // 16MB expected + actual_size: total_executable, + }); + } + + // Check for too many small executable regions (potential shellcode injection) + let small_exec_regions = regions + .iter() + .filter(|r| { + matches!(r.protection, crate::MemoryProtection::ReadExecute | crate::MemoryProtection::ReadWriteExecute) + && r.size < 0x10000 // Less than 64KB + && r.region_type == "PRIVATE" + }) + .count(); + + if small_exec_regions > 10 { + return Some(HollowingIndicator::MemoryLayoutAnomaly { + expected_size: 3, // 3 or fewer small executable regions expected + actual_size: small_exec_regions, + }); + } + + None + } + + fn check_memory_gaps(&self, regions: &[MemoryRegion]) -> Option { + // Sort regions by base address + let mut sorted_regions: Vec<_> = regions.iter().collect(); + sorted_regions.sort_by_key(|r| r.base_address); + + let mut gaps = Vec::new(); + + // Find gaps between consecutive regions + for window in sorted_regions.windows(2) { + let current_end = window[0].base_address + window[0].size; + let next_start = window[1].base_address; + + if next_start > current_end { + let gap_size = next_start - current_end; + // Only consider significant gaps (> 64KB) + if gap_size > 0x10000 { + gaps.push(gap_size); + } + } + } + + // Look for suspicious gap patterns + let large_gaps = gaps.iter().filter(|&&gap| gap > 0x1000000).count(); // 16MB+ + let total_gaps = gaps.len(); + + if large_gaps > 0 || total_gaps > 20 { + let largest_gap = gaps.iter().max().copied().unwrap_or(0); + return Some(HollowingIndicator::SuspiciousMemoryGaps { + gap_count: total_gaps, + largest_gap, + }); + } + + None + } + + fn check_pe_header_anomalies(&self, regions: &[MemoryRegion]) -> Option { + // Look for IMAGE regions that might have mismatched PE headers + let image_regions: Vec<_> = regions + .iter() + .filter(|r| r.region_type == "IMAGE") + .collect(); + + // Check for unusual number of IMAGE regions + if image_regions.len() > 50 { + // Too many loaded modules might indicate DLL injection + return Some(HollowingIndicator::MismatchedPEHeader); + } + + // Check for IMAGE regions at unusual addresses + for region in &image_regions { + // PE images should typically be aligned to 64KB boundaries + if region.base_address % 0x10000 != 0 { + return Some(HollowingIndicator::MismatchedPEHeader); + } + } + + None + } + + fn check_entry_point_anomalies( + &self, + _process: &ProcessInfo, + regions: &[MemoryRegion], + ) -> Option { + // In a real implementation, we would read the PE header to get the actual entry point + // For now, we'll use heuristics based on memory layout + + // Look for executable regions that might contain the entry point + let executable_regions: Vec<_> = regions + .iter() + .filter(|r| { + matches!(r.protection, crate::MemoryProtection::ReadExecute | crate::MemoryProtection::ReadWriteExecute) + && r.region_type == "PRIVATE" + }) + .collect(); + + // If there are many small private executable regions, the entry point might have been moved + if executable_regions.len() > 5 { + // Pick the first region as a potential suspicious entry point + if let Some(region) = executable_regions.first() { + return Some(HollowingIndicator::UnusualEntryPoint { + address: region.base_address, + }); + } + } + + None + } +} + +impl Default for HollowingDetector { + fn default() -> Self { + Self::new() + } +} \ No newline at end of file diff --git a/ghost-core/src/lib.rs b/ghost-core/src/lib.rs index 93ad4d3..9ffd5a7 100644 --- a/ghost-core/src/lib.rs +++ b/ghost-core/src/lib.rs @@ -1,5 +1,6 @@ pub mod detection; pub mod error; +pub mod hollowing; pub mod hooks; pub mod memory; pub mod process; @@ -8,6 +9,7 @@ pub mod thread; pub use detection::{DetectionEngine, DetectionResult, ThreatLevel}; pub use error::{GhostError, Result}; +pub use hollowing::{HollowingDetection, HollowingDetector, HollowingIndicator}; pub use hooks::{detect_hook_injection, HookDetectionResult, HookInfo}; pub use memory::{MemoryProtection, MemoryRegion}; pub use process::ProcessInfo;