feat: implement comprehensive process hollowing detection

This commit is contained in:
Adir Shitrit
2025-11-08 11:16:42 +02:00
parent 7eddbb575a
commit bae68652e1
3 changed files with 274 additions and 1 deletions

View File

@@ -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<u32, ProcessBaseline>,
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,

261
ghost-core/src/hollowing.rs Normal file
View File

@@ -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<HollowingIndicator>,
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<Option<HollowingDetection>> {
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<HollowingIndicator> {
// 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<HollowingIndicator> {
// 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<HollowingIndicator> {
// 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<HollowingIndicator> {
// 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<HollowingIndicator> {
// 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()
}
}

View File

@@ -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;