From b1f098571d5ea140c1bd1feea6b9b37f2e3ba4d2 Mon Sep 17 00:00:00 2001 From: pandaadir05 Date: Mon, 17 Nov 2025 22:02:41 +0200 Subject: [PATCH] feat: Add PE header validation and LD_PRELOAD detection --- .gitignore | 5 + BUILD.md | 62 ++- README.md | 91 ++-- docs/DETECTION_METHODS.md | 62 ++- docs/MITRE_ATTACK_COVERAGE.md | 83 +++- docs/PERFORMANCE_GUIDE.md | 353 ++++++--------- ghost-core/Cargo.toml | 2 + ghost-core/src/hollowing.rs | 84 +++- ghost-core/src/hooks.rs | 612 +++++++++++++++++++++---- ghost-core/src/memory.rs | 680 +++++++++++++++++++++++++++- ghost-core/src/process.rs | 108 ++++- ghost-core/src/shellcode.rs | 336 +++++++++++--- ghost-core/src/testing.rs | 2 + ghost-core/src/thread.rs | 424 ++++++++++++++++- ghost-core/tests/detection_tests.rs | 263 ++++++++++- 15 files changed, 2708 insertions(+), 459 deletions(-) diff --git a/.gitignore b/.gitignore index c51b3da..683e136 100644 --- a/.gitignore +++ b/.gitignore @@ -32,3 +32,8 @@ config.local.toml #Ai stuff .claude/ +.claude-plugins/ +.claude-embeddings/ +.claude*.cache/ +.claude*/ + diff --git a/BUILD.md b/BUILD.md index 817facb..dc7a3d6 100644 --- a/BUILD.md +++ b/BUILD.md @@ -5,7 +5,7 @@ ### Windows - Rust toolchain (MSVC target) - Visual Studio Build Tools with C++ workload -- Windows SDK +- Windows SDK (for windows crate bindings) Install via: ```powershell @@ -15,22 +15,76 @@ rustup default stable-msvc ### Linux - Rust toolchain - GCC/Clang -- libelf-dev (for eBPF) +- libc development headers + +```bash +# Debian/Ubuntu +sudo apt install build-essential + +# RHEL/Fedora +sudo dnf groupinstall "Development Tools" +``` ### macOS - Rust toolchain -- Xcode Command Line Tools +- Xcode Command Line Tools (for libc bindings) + +```bash +xcode-select --install +``` ## Building ```bash +# Release build (recommended for performance) cargo build --release + +# Debug build +cargo build + +# Check for compilation errors without full build +cargo check ``` ## Running ```bash +# CLI interface cargo run --bin ghost-cli + +# Terminal UI +cargo run --bin ghost-tui + +# With arguments +cargo run --bin ghost-cli -- --pid 1234 +cargo run --bin ghost-cli -- --config examples/ghost.toml ``` -Note: Requires elevated privileges for full process memory access. +## Testing + +```bash +# Run all tests +cargo test + +# Run specific test module +cargo test --package ghost-core detection_tests + +# Run with output +cargo test -- --nocapture +``` + +## Documentation + +```bash +# Generate and open documentation +cargo doc --open + +# Generate without dependencies +cargo doc --no-deps --open +``` + +## Platform Notes + +- **Windows**: Requires elevated privileges (Administrator) for full process memory access +- **Linux**: Requires appropriate permissions to read /proc/[pid]/mem (root or ptrace capability) +- **macOS**: Some features require System Integrity Protection (SIP) to be adjusted for full functionality diff --git a/README.md b/README.md index 60b3370..2277bf9 100644 --- a/README.md +++ b/README.md @@ -4,62 +4,64 @@ Cross-platform process injection detection framework written in Rust. ## Overview -Ghost is a comprehensive security framework for detecting process injection, memory manipulation, and advanced evasion techniques in running processes. It combines kernel-level monitoring with behavioral analysis, machine learning, and threat intelligence to provide enterprise-grade detection capabilities. +Ghost is a security framework for detecting process injection, memory manipulation, and suspicious process behavior. It provides memory analysis, behavioral monitoring, and MITRE ATT&CK technique mapping for security research and defensive purposes. ## Features -- **Multi-layer detection**: Memory analysis, behavioral patterns, and ML-based anomaly detection -- **MITRE ATT&CK mapping**: Automatic technique classification using the ATT&CK framework -- **Threat intelligence**: Integration with threat feeds for IOC correlation and attribution -- **Cross-platform**: Windows (full support), Linux (with eBPF), macOS (planned) -- **Real-time monitoring**: Continuous scanning with configurable intervals -- **Low overhead**: Performance-optimized for production environments +- **Memory Analysis**: RWX region detection, shellcode pattern scanning, memory protection analysis +- **MITRE ATT&CK Mapping**: Technique identification using the ATT&CK framework +- **Cross-platform Support**: + - **Windows**: Process enumeration, memory reading (ReadProcessMemory), thread analysis (NtQueryInformationThread), inline hook detection, PE header validation + - **Linux**: Process enumeration via procfs, memory region analysis (/proc/[pid]/maps), thread state monitoring, LD_PRELOAD detection, ptrace detection + - **macOS**: Process enumeration via sysctl/KERN_PROC_ALL +- **Real-time Monitoring**: Continuous scanning with configurable intervals +- **Threat Intelligence**: IOC storage and correlation framework ## Architecture ``` ghost/ -├── ghost-core/ # Core detection engine (21 modules) +├── ghost-core/ # Core detection engine and platform abstractions ├── ghost-cli/ # Command-line interface -├── ghost-tui/ # Interactive terminal UI +├── ghost-tui/ # Interactive terminal UI (Ratatui-based) ├── examples/ # Configuration examples └── docs/ # Technical documentation ``` ### Core Modules -- **Detection Engine**: Orchestrates all analysis components -- **Memory Analysis**: RWX region detection, shellcode patterns -- **Process Hollowing**: PE header validation, memory gap analysis -- **Thread Analysis**: Start address validation, behavioral patterns -- **Evasion Detection**: Anti-debugging, VM detection, obfuscation -- **MITRE ATT&CK Engine**: Technique mapping and threat actor profiling -- **Threat Intelligence**: IOC matching and campaign correlation +- **Detection Engine** ([detection.rs](ghost-core/src/detection.rs)): Orchestrates analysis and threat scoring +- **Memory Analysis** ([memory.rs](ghost-core/src/memory.rs)): Platform-specific memory enumeration and reading +- **Process Enumeration** ([process.rs](ghost-core/src/process.rs)): Cross-platform process listing +- **Thread Analysis** ([thread.rs](ghost-core/src/thread.rs)): Thread enumeration with start address and creation time +- **Hook Detection** ([hooks.rs](ghost-core/src/hooks.rs)): Inline hook detection via JMP pattern analysis +- **MITRE ATT&CK** ([mitre.rs](ghost-core/src/mitre.rs)): Technique mapping and categorization +- **Configuration** ([config.rs](ghost-core/src/config.rs)): TOML-based configuration with validation ## Supported Detection Techniques ### Process Injection (T1055) - RWX memory region detection - Private executable memory analysis -- Remote thread creation monitoring -- SetWindowsHookEx injection (T1055.001) -- Thread hijacking (T1055.003) -- APC injection patterns (T1055.004) -- Process hollowing (T1055.012) -- Reflective DLL injection +- Thread count anomaly detection +- Inline hook detection (JMP patches on ntdll.dll, kernel32.dll, user32.dll) +- LD_PRELOAD and LD_LIBRARY_PATH detection (Linux) +- Ptrace injection detection (Linux) +- SetWindowsHookEx hook enumeration +- Thread hijacking indicators (T1055.003) +- Process hollowing detection with PE header validation (T1055.012) -### Evasion Techniques -- Anti-debugging detection -- Virtual machine detection attempts -- Code obfuscation analysis -- Timing-based analysis evasion -- Environment fingerprinting +### Memory Analysis +- Memory protection flags (R/W/X combinations) +- Region type classification (IMAGE, PRIVATE, MAPPED, HEAP, STACK) +- Small executable region detection (shellcode indicators) +- Memory region size anomalies -### Behavioral Anomalies -- Thread count deviations -- Memory allocation patterns -- API call sequences -- Process relationship analysis +### Behavioral Monitoring +- Thread count changes from baseline +- New thread creation detection +- Process parent-child relationships +- System process identification ## Installation @@ -192,6 +194,27 @@ Please review [SECURITY.md](SECURITY.md) for: - Security considerations - Threat model +## Platform Support Matrix + +| Feature | Windows | Linux | macOS | +|---------|---------|-------|-------| +| Process Enumeration | CreateToolhelp32Snapshot | /proc filesystem | sysctl KERN_PROC_ALL | +| Memory Enumeration | VirtualQueryEx | /proc/[pid]/maps | Not implemented | +| Memory Reading | ReadProcessMemory | /proc/[pid]/mem | Not implemented | +| Thread Enumeration | Thread32First/Next | /proc/[pid]/task | Not implemented | +| Thread Start Address | NtQueryInformationThread | /proc/[pid]/task/[tid]/syscall | Not implemented | +| Thread Creation Time | GetThreadTimes | /proc/[pid]/task/[tid]/stat | Not implemented | +| Hook Detection | Inline JMP pattern scanning | LD_PRELOAD/ptrace detection | Not applicable | +| PE Header Validation | Full PE validation | Not applicable | Not applicable | +| Process Path | GetProcessImageFileNameW | /proc/[pid]/exe | proc_pidpath | + ## Status -Active development. Core detection engine stable. Windows support complete. Linux eBPF support in progress. macOS Endpoint Security framework planned. +Active development. Core detection engine functional with cross-platform abstractions. Windows support most complete. Linux support via procfs. macOS has process enumeration but limited memory/thread analysis. + +### Known Limitations + +- macOS memory enumeration and reading not yet implemented (requires vm_read and mach_vm_region) +- Windows SetWindowsHookEx chain enumeration requires parsing undocumented USER32.dll structures +- Shellcode pattern matching currently uses heuristics (no actual signature database) +- No kernel-level monitoring (all userspace APIs) diff --git a/docs/DETECTION_METHODS.md b/docs/DETECTION_METHODS.md index 33abbf0..c38d972 100644 --- a/docs/DETECTION_METHODS.md +++ b/docs/DETECTION_METHODS.md @@ -46,11 +46,37 @@ Monitors thread count changes over time. Sudden increases may indicate CreateRem Threads created by external processes via CreateRemoteThread or NtCreateThreadEx. -**Detection Logic** (Planned): -- Compare thread creator PID with owner PID -- Check thread start addresses against known modules +**Detection Logic**: +- Enumerate threads using CreateToolhelp32Snapshot (Windows) or /proc/[pid]/task (Linux) +- Get thread start addresses via NtQueryInformationThread (Windows) or /proc syscall file (Linux) +- Get thread creation times via GetThreadTimes (Windows) or stat parsing (Linux) +- Track thread state (Running, Waiting, Suspended, Terminated) - Flag threads starting in private memory regions +## Hook Detection + +### Inline API Hooks + +**MITRE ATT&CK**: T1055.003 + +Detects JMP patches at the start of critical API functions. + +**Detection Logic**: +- Enumerate loaded modules in target process (EnumProcessModulesEx) +- Check entry points of critical APIs (ntdll, kernel32, user32) +- Detect common hook patterns: + - JMP rel32 (E9 xx xx xx xx) + - JMP [rip+disp32] (FF 25 xx xx xx xx) + - MOV RAX, imm64; JMP RAX (48 B8 ... FF E0) + - PUSH imm32; RET (68 xx xx xx xx C3) + +**Critical APIs Monitored**: +- NtCreateThread, NtCreateThreadEx +- NtAllocateVirtualMemory, NtWriteVirtualMemory, NtProtectVirtualMemory +- VirtualAllocEx, WriteProcessMemory, CreateRemoteThread +- LoadLibraryA, LoadLibraryW +- SetWindowsHookExA, SetWindowsHookExW + ## Heuristic Analysis ### Confidence Scoring @@ -74,23 +100,37 @@ Ghost uses weighted confidence scoring: ### Windows - [x] Classic DLL injection detection -- [x] Memory region analysis -- [x] Thread enumeration +- [x] Memory region analysis (VirtualQueryEx) +- [x] Memory reading (ReadProcessMemory) +- [x] Thread enumeration (CreateToolhelp32Snapshot) +- [x] Thread start addresses (NtQueryInformationThread) +- [x] Thread creation times (GetThreadTimes) +- [x] Inline hook detection (JMP pattern scanning) +- [x] Process hollowing heuristics - [ ] APC injection detection -- [ ] Process hollowing detection -- [ ] Hook detection (IAT/EAT) -- [ ] Reflective DLL injection +- [ ] SetWindowsHookEx chain enumeration +- [ ] Reflective DLL injection signature matching ### Linux -- [ ] ptrace injection -- [ ] LD_PRELOAD detection +- [x] Process enumeration (/proc filesystem) +- [x] Memory region analysis (/proc/[pid]/maps) +- [x] Memory reading (/proc/[pid]/mem) +- [x] Thread enumeration (/proc/[pid]/task) +- [x] Thread state detection (stat parsing) +- [x] ptrace injection detection +- [x] LD_PRELOAD detection - [ ] process_vm_writev monitoring - [ ] Shared memory inspection ### macOS -- [ ] DYLD_INSERT_LIBRARIES +- [x] Process enumeration (sysctl KERN_PROC_ALL) +- [x] Process path retrieval (proc_pidpath) +- [ ] Memory enumeration (vm_region) +- [ ] Memory reading (vm_read) +- [ ] Thread enumeration (task_threads) +- [ ] DYLD_INSERT_LIBRARIES detection - [ ] task_for_pid monitoring - [ ] Mach port analysis diff --git a/docs/MITRE_ATTACK_COVERAGE.md b/docs/MITRE_ATTACK_COVERAGE.md index 8339b0d..55dd80e 100644 --- a/docs/MITRE_ATTACK_COVERAGE.md +++ b/docs/MITRE_ATTACK_COVERAGE.md @@ -46,7 +46,9 @@ Ghost detection engine coverage mapped to MITRE ATT&CK framework techniques. - **Indicators**: - Unmapped main executable image - Suspicious memory gaps (>16MB) - - PE header mismatches + - PE header validation (DOS/NT signatures) + - Image base mismatches + - Corrupted PE structures - Unusual entry point locations - Memory layout anomalies - **Confidence**: Very High (0.8-1.0) @@ -121,35 +123,82 @@ Ghost detection engine coverage mapped to MITRE ATT&CK framework techniques. | Technique | Detection Module | Implementation Status | Test Coverage | |-----------|------------------|----------------------|---------------| -| T1055.001 | hooks.rs | ✅ Complete | ✅ Tested | -| T1055.002 | shellcode.rs | ✅ Complete | ✅ Tested | -| T1055.003 | thread.rs | ✅ Complete | ✅ Tested | -| T1055.004 | detection.rs | ⚠️ Partial | ✅ Tested | -| T1055.012 | hollowing.rs | ✅ Complete | ✅ Tested | -| T1027 | shellcode.rs | ✅ Complete | ✅ Tested | -| T1036 | process.rs | ⚠️ Partial | ❌ Pending | -| T1106 | detection.rs | ⚠️ Basic | ❌ Pending | +| T1055.001 | hooks.rs | ✅ Inline hooks + Linux LD_PRELOAD | ❌ Basic | +| T1055.002 | shellcode.rs | ⚠️ Heuristic only | ✅ Basic | +| T1055.003 | thread.rs | ✅ Thread enumeration | ✅ Unit tests | +| T1055.004 | detection.rs | ⚠️ Heuristic only | ✅ Basic | +| T1055.012 | hollowing.rs | ✅ PE header validation | ❌ Pending | +| T1027 | shellcode.rs | ⚠️ Basic patterns | ❌ Pending | +| T1036 | process.rs | ❌ Not implemented | ❌ Pending | +| T1106 | detection.rs | ❌ Not implemented | ❌ Pending | + +**Implementation Status Legend**: +- ✅ Complete: Full implementation with actual API calls +- ⚠️ Partial: Heuristic-based or incomplete implementation +- ❌ Not implemented: Placeholder or missing + +## Current Implementation Details + +### What's Actually Implemented + +1. **Memory Analysis** (memory.rs) + - Windows: VirtualQueryEx, ReadProcessMemory + - Linux: /proc/[pid]/maps parsing, /proc/[pid]/mem reading + - macOS: Not implemented + +2. **Thread Analysis** (thread.rs) + - Windows: Thread32First/Next, NtQueryInformationThread, GetThreadTimes + - Linux: /proc/[pid]/task enumeration, stat parsing + - macOS: Not implemented + +3. **Hook Detection** (hooks.rs) + - Windows: Inline hook detection via JMP pattern scanning + - Linux: LD_PRELOAD detection, LD_LIBRARY_PATH monitoring, ptrace detection + - Detects suspicious library loading from /tmp/, /dev/shm/, etc. + - Does NOT enumerate SetWindowsHookEx chains on Windows + - No IAT/EAT hook scanning (pattern detection only) + +4. **Process Hollowing Detection** (hollowing.rs) + - Windows: Full PE header validation (DOS/NT signatures, image base) + - Detects corrupted PE structures + - Detects image base mismatches + - Memory layout anomaly detection + - Memory gap analysis + +5. **Process Enumeration** (process.rs) + - Windows: CreateToolhelp32Snapshot + - Linux: /proc filesystem + - macOS: sysctl KERN_PROC_ALL + +### What's NOT Implemented + +- Actual shellcode signature database +- Entropy analysis for obfuscation detection +- SetWindowsHookEx chain parsing (Windows) +- APC injection detection +- MITRE ATT&CK technique attribution (framework only) +- process_vm_writev monitoring (Linux) ## Future Enhancements ### High Priority -- **T1055.008** - Ptrace System Calls (Linux) -- **T1055.009** - Proc Memory (Linux) +- **T1055.008** - Ptrace System Calls (Linux) - ✅ Basic detection implemented - **T1055.013** - Process Doppelgänging - **T1055.014** - VDSO Hijacking (Linux) +- Shellcode signature database -### Medium Priority +### Medium Priority - **T1134** - Access Token Manipulation -- **T1548.002** - Bypass User Account Control -- **T1562.001** - Disable or Modify Tools +- SetWindowsHookEx chain enumeration +- IAT/EAT hook scanning +- LD_PRELOAD detection (Linux) - ✅ Implemented ### Research Areas -- Machine learning-based anomaly detection -- Graph analysis of process relationships -- Timeline analysis for attack progression +- Behavioral analysis over time +- Process relationship analysis - Integration with threat intelligence feeds ## References diff --git a/docs/PERFORMANCE_GUIDE.md b/docs/PERFORMANCE_GUIDE.md index f190dc4..f737096 100644 --- a/docs/PERFORMANCE_GUIDE.md +++ b/docs/PERFORMANCE_GUIDE.md @@ -2,298 +2,197 @@ ## Overview -Ghost is designed for high-performance real-time detection with minimal system impact. This guide covers optimization strategies and performance monitoring. +Ghost is designed for process injection detection with configurable performance characteristics. This guide covers actual optimization strategies and expected performance. ## Performance Characteristics -### Detection Engine Performance +### Expected Detection Engine Performance -- **Scan Speed**: 500-1000 processes/second on modern hardware -- **Memory Usage**: 50-100MB base footprint -- **CPU Impact**: <2% during active monitoring -- **Latency**: <10ms detection response time +- **Process Enumeration**: 10-50ms for all system processes +- **Memory Region Analysis**: 1-5ms per process (platform-dependent) +- **Thread Enumeration**: 1-10ms per process +- **Detection Heuristics**: <1ms per process +- **Memory Usage**: ~10-20MB for core engine -### Optimization Techniques +**Note**: Actual performance varies significantly by: +- Number of processes (100-1000+ typical) +- Memory region count per process +- Thread count per process +- Platform (Windows APIs vs Linux procfs) -#### 1. Selective Scanning +### Configuration Options + +#### 1. Selective Detection ```rust -// Configure detection modules based on threat landscape -let mut config = DetectionConfig::new(); -config.enable_shellcode_detection(true); -config.enable_hook_detection(false); // Disable if not needed -config.enable_anomaly_detection(true); +use ghost_core::config::DetectionConfig; + +// Disable expensive detections for performance +let mut config = DetectionConfig::default(); +config.rwx_detection = true; // Fast: O(n) memory regions +config.shellcode_detection = false; // Skip pattern matching +config.hook_detection = false; // Skip module enumeration +config.thread_detection = true; // Moderate: thread enum +config.hollowing_detection = false; // Skip heuristics ``` -#### 2. Batch Processing +#### 2. Preset Modes ```rust -// Process multiple items in batches for efficiency -let processes = enumerate_processes()?; -let results: Vec = processes - .chunks(10) - .flat_map(|chunk| engine.analyze_batch(chunk)) - .collect(); +// Fast scanning mode +let config = DetectionConfig::performance_mode(); + +// Thorough scanning mode +let config = DetectionConfig::thorough_mode(); ``` -#### 3. Memory Pool Management +#### 3. Process Filtering ```rust -// Pre-allocate memory pools to reduce allocations -pub struct MemoryPool { - process_buffers: Vec, - detection_results: Vec, -} +// Skip system processes +config.skip_system_processes = true; + +// Limit memory scan size +config.max_memory_scan_size = 10 * 1024 * 1024; // 10MB per process ``` -## Performance Monitoring +## Performance Considerations -### Built-in Metrics +### Platform-Specific Performance -```rust -use ghost_core::metrics::PerformanceMonitor; +**Windows**: +- CreateToolhelp32Snapshot: Single syscall, fast +- VirtualQueryEx: Iterative, slower for processes with many regions +- ReadProcessMemory: Cross-process, requires proper handles +- NtQueryInformationThread: Undocumented API call per thread -let monitor = PerformanceMonitor::new(); -monitor.start_collection(); +**Linux**: +- /proc enumeration: Directory reads, fast +- /proc/[pid]/maps parsing: File I/O, moderate +- /proc/[pid]/mem reading: Requires ptrace or same user +- /proc/[pid]/task parsing: Per-thread file I/O -// Detection operations... +**macOS**: +- sysctl KERN_PROC_ALL: Single syscall, fast +- Memory/thread analysis: Not yet implemented -let stats = monitor.get_statistics(); -println!("Avg scan time: {:.2}ms", stats.avg_scan_time); -println!("Memory usage: {}MB", stats.memory_usage_mb); -``` - -### Custom Benchmarks +### Running Tests ```bash -# Run comprehensive benchmarks -cargo bench +# Run all tests including performance assertions +cargo test -# Profile specific operations -cargo bench -- shellcode_detection -cargo bench -- process_enumeration +# Run tests with timing output +cargo test -- --nocapture ``` ## Tuning Guidelines -### For High-Volume Environments +### For Continuous Monitoring -1. **Increase batch sizes**: Process 20-50 items per batch -2. **Reduce scan frequency**: 2-5 second intervals -3. **Enable result caching**: Cache stable process states -4. **Use filtered scanning**: Skip known-good processes +1. **Adjust scan interval**: Configure `scan_interval_ms` in DetectionConfig +2. **Skip system processes**: Set `skip_system_processes = true` +3. **Limit memory scans**: Reduce `max_memory_scan_size` +4. **Disable heavy detections**: Turn off hook_detection and shellcode_detection -### For Low-Latency Requirements +### For One-Time Analysis -1. **Decrease batch sizes**: Process 1-5 items per batch -2. **Increase scan frequency**: Sub-second intervals -3. **Disable heavy detections**: Skip complex ML analysis -4. **Use memory-mapped scanning**: Direct memory access - -### Memory Optimization - -```rust -// Configure memory limits -let config = DetectionConfig { - max_memory_usage_mb: 200, - enable_result_compression: true, - cache_size_limit: 1000, - ..Default::default() -}; -``` +1. **Enable all detections**: Use `DetectionConfig::thorough_mode()` +2. **Full memory scanning**: Increase `max_memory_scan_size` +3. **Include system processes**: Set `skip_system_processes = false` ## Platform-Specific Optimizations ### Windows -- Use `SetProcessWorkingSetSize` to limit memory -- Enable `SE_INCREASE_QUOTA_NAME` privilege for better access -- Leverage Windows Performance Toolkit (WPT) for profiling +- Run as Administrator for full process access +- Use `PROCESS_QUERY_LIMITED_INFORMATION` when `PROCESS_QUERY_INFORMATION` fails +- Handle access denied errors gracefully (system processes) ### Linux -- Use `cgroups` for resource isolation -- Enable `CAP_SYS_PTRACE` for enhanced process access -- Leverage `perf` for detailed performance analysis +- Run with appropriate privileges (root or CAP_SYS_PTRACE) +- Handle permission denied for /proc/[pid]/mem gracefully +- Consider using process groups for batch access + +### macOS + +- Limited functionality (process enumeration only) +- Most detection features require kernel extensions or Endpoint Security framework ## Troubleshooting Performance Issues ### High CPU Usage -1. Check scan frequency settings -2. Verify filter effectiveness -3. Profile detection module performance -4. Consider disabling expensive detections +1. Reduce scan frequency (`scan_interval_ms`) +2. Disable thread analysis for each scan +3. Skip memory region enumeration +4. Filter out known-good processes ### High Memory Usage -1. Monitor result cache sizes -2. Check for memory leaks in custom modules -3. Verify proper cleanup of process handles -4. Consider reducing batch sizes +1. Reduce baseline cache size (limited processes tracked) +2. Clear detection history periodically +3. Limit memory reading buffer sizes ### Slow Detection Response -1. Profile individual detection modules -2. Check system resource availability -3. Verify network latency (if applicable) -4. Consider async processing optimization +1. Disable hook detection (expensive module enumeration) +2. Skip shellcode pattern matching +3. Use performance preset mode -## Benchmarking Results +## Current Implementation Limits -### Baseline Performance (Intel i7-9700K, 32GB RAM) +**What's NOT implemented**: +- No performance metrics collection system +- No Prometheus/monitoring integration +- No SIMD-accelerated pattern matching +- No parallel/async process scanning (single-threaded) +- No LRU caching of results +- No batch processing APIs -``` -Process Enumeration: 2.3ms (avg) -Shellcode Detection: 0.8ms per process -Hook Detection: 1.2ms per process -Anomaly Analysis: 3.5ms per process -Full Scan (100 proc): 847ms total -``` +**Current architecture**: +- Sequential process scanning +- Simple HashMap for baseline tracking +- Basic confidence scoring +- Manual timer-based intervals (TUI) -### Memory Usage - -``` -Base Engine: 45MB -+ Shellcode Patterns: +12MB -+ ML Models: +23MB -+ Result Cache: +15MB (1000 entries) -Total Runtime: 95MB typical -``` - -## Advanced Optimizations - -### SIMD Acceleration +## Testing Performance ```rust -// Enable SIMD for pattern matching -#[cfg(target_feature = "avx2")] -use std::arch::x86_64::*; +#[test] +fn test_detection_performance() { + use std::time::Instant; -// Vectorized shellcode scanning -unsafe fn simd_pattern_search(data: &[u8], pattern: &[u8]) -> bool { - // AVX2 accelerated pattern matching -} -``` + let mut engine = DetectionEngine::new().unwrap(); + let process = ProcessInfo::new(1234, 4, "test.exe".to_string()); + let regions = vec![/* test regions */]; -### Multi-threading - -```rust -use rayon::prelude::*; - -// Parallel process analysis -let results: Vec = processes - .par_iter() - .map(|process| engine.analyze_process(process)) - .collect(); -``` - -### Caching Strategies - -```rust -use lru::LruCache; - -pub struct DetectionCache { - process_hashes: LruCache, - shellcode_results: LruCache, - anomaly_profiles: LruCache, -} -``` - -## Monitoring Dashboard Integration - -### Prometheus Metrics - -```rust -use prometheus::{Counter, Histogram, Gauge}; - -lazy_static! { - static ref SCAN_DURATION: Histogram = Histogram::new( - "ghost_scan_duration_seconds", - "Time spent scanning processes" - ).unwrap(); - - static ref DETECTIONS_TOTAL: Counter = Counter::new( - "ghost_detections_total", - "Total number of detections" - ).unwrap(); -} -``` - -### Real-time Monitoring - -```rust -// WebSocket-based real-time metrics -pub struct MetricsServer { - connections: Vec, - metrics_collector: PerformanceMonitor, -} - -impl MetricsServer { - pub async fn broadcast_metrics(&self) { - let metrics = self.metrics_collector.get_real_time_stats(); - let json = serde_json::to_string(&metrics).unwrap(); - - for connection in &self.connections { - connection.send(json.clone()).await.ok(); - } + let start = Instant::now(); + for _ in 0..100 { + engine.analyze_process(&process, ®ions, None); } + let duration = start.elapsed(); + + // Should complete 100 analyses in under 100ms + assert!(duration.as_millis() < 100); } ``` ## Best Practices -1. **Profile First**: Always benchmark before optimizing -2. **Measure Impact**: Quantify optimization effectiveness -3. **Monitor Production**: Continuous performance monitoring -4. **Gradual Tuning**: Make incremental adjustments -5. **Document Changes**: Track optimization history +1. **Start with defaults**: Use `DetectionConfig::default()` initially +2. **Profile specific modules**: Identify which detection is slow +3. **Adjust based on needs**: Disable features you don't need +4. **Handle errors gracefully**: Processes may exit during scan +5. **Test on target hardware**: Performance varies by system -## Performance Testing Framework +## Future Performance Improvements -```rust -#[cfg(test)] -mod performance_tests { - use super::*; - use std::time::Instant; - - #[test] - fn benchmark_full_system_scan() { - let engine = DetectionEngine::new().unwrap(); - let start = Instant::now(); - - let results = engine.scan_all_processes().unwrap(); - let duration = start.elapsed(); - - assert!(duration.as_millis() < 5000, "Scan took too long"); - assert!(results.len() > 0, "No processes detected"); - } - - #[test] - fn memory_usage_benchmark() { - let initial = get_memory_usage(); - let engine = DetectionEngine::new().unwrap(); - - // Perform operations - for _ in 0..1000 { - engine.analyze_dummy_process(); - } - - let final_usage = get_memory_usage(); - let growth = final_usage - initial; - - assert!(growth < 50_000_000, "Memory usage grew too much: {}MB", - growth / 1_000_000); - } -} -``` - -## Conclusion - -Ghost's performance can be fine-tuned for various deployment scenarios. Regular monitoring and benchmarking ensure optimal operation while maintaining security effectiveness. - -For additional performance support, see: - -- [Profiling Guide](PROFILING.md) -- [Deployment Strategies](DEPLOYMENT.md) -- [Scaling Recommendations](SCALING.md) \ No newline at end of file +Potential enhancements (not yet implemented): +- Parallel process analysis using rayon +- Async I/O for file system operations (Linux) +- Result caching with TTL +- Incremental scanning (only changed processes) +- Memory-mapped file parsing +- SIMD pattern matching for shellcode \ No newline at end of file diff --git a/ghost-core/Cargo.toml b/ghost-core/Cargo.toml index 1e0e098..d565d46 100644 --- a/ghost-core/Cargo.toml +++ b/ghost-core/Cargo.toml @@ -19,9 +19,11 @@ toml = "0.8" windows = { version = "0.58", features = [ "Win32_Foundation", "Win32_System_Diagnostics_ToolHelp", + "Win32_System_Diagnostics_Debug", "Win32_System_Threading", "Win32_System_ProcessStatus", "Win32_System_Memory", + "Win32_System_LibraryLoader", "Win32_Security", ] } diff --git a/ghost-core/src/hollowing.rs b/ghost-core/src/hollowing.rs index fe50006..cdf52bd 100644 --- a/ghost-core/src/hollowing.rs +++ b/ghost-core/src/hollowing.rs @@ -1,5 +1,8 @@ use crate::{GhostError, MemoryRegion, ProcessInfo, Result}; +#[cfg(windows)] +use crate::memory::{validate_pe_header, read_pe_header_info, PEHeaderValidation}; + #[derive(Debug, Clone)] pub struct HollowingDetection { pub pid: u32, @@ -14,6 +17,8 @@ pub enum HollowingIndicator { SuspiciousImageBase, MemoryLayoutAnomaly { expected_size: usize, actual_size: usize }, MismatchedPEHeader, + InvalidPEHeader { validation: String }, + CorruptedPEStructure { address: usize, reason: String }, UnusualEntryPoint { address: usize }, SuspiciousMemoryGaps { gap_count: usize, largest_gap: usize }, } @@ -27,6 +32,12 @@ impl std::fmt::Display for HollowingIndicator { write!(f, "Memory layout anomaly: expected {:#x}, found {:#x}", expected_size, actual_size) } Self::MismatchedPEHeader => write!(f, "PE header mismatch detected"), + Self::InvalidPEHeader { validation } => { + write!(f, "Invalid PE header: {}", validation) + } + Self::CorruptedPEStructure { address, reason } => { + write!(f, "Corrupted PE structure at {:#x}: {}", address, reason) + } Self::UnusualEntryPoint { address } => { write!(f, "Entry point at unusual location: {:#x}", address) } @@ -72,12 +83,18 @@ impl HollowingDetector { confidence += 0.4; } - // Check for PE header anomalies + // Check for PE header anomalies (heuristic-based) if let Some(indicator) = self.check_pe_header_anomalies(memory_regions) { indicators.push(indicator); confidence += 0.7; } + // Validate actual PE headers (deep inspection) + if let Some(indicator) = self.validate_pe_headers(process.pid, memory_regions) { + indicators.push(indicator); + confidence += 0.9; // Higher confidence for actual PE validation + } + // Check entry point location if let Some(indicator) = self.check_entry_point_anomalies(process, memory_regions) { indicators.push(indicator); @@ -223,6 +240,71 @@ impl HollowingDetector { None } + #[cfg(windows)] + fn validate_pe_headers(&self, pid: u32, regions: &[MemoryRegion]) -> Option { + // Focus on main executable IMAGE regions + let image_regions: Vec<_> = regions + .iter() + .filter(|r| r.region_type == "IMAGE") + .take(5) // Check first 5 IMAGE regions (main exe + critical DLLs) + .collect(); + + for region in image_regions { + match validate_pe_header(pid, region.base_address) { + Ok(validation) => { + match validation { + PEHeaderValidation::Valid => continue, + PEHeaderValidation::InvalidDosSignature => { + return Some(HollowingIndicator::InvalidPEHeader { + validation: "Invalid DOS signature (not MZ)".to_string(), + }); + } + PEHeaderValidation::InvalidNtSignature => { + return Some(HollowingIndicator::InvalidPEHeader { + validation: "Invalid NT signature (not PE)".to_string(), + }); + } + PEHeaderValidation::InvalidHeaderOffset => { + return Some(HollowingIndicator::InvalidPEHeader { + validation: "Invalid PE header offset".to_string(), + }); + } + PEHeaderValidation::MismatchedImageBase => { + return Some(HollowingIndicator::CorruptedPEStructure { + address: region.base_address, + reason: "Image base mismatch - possible hollowing".to_string(), + }); + } + PEHeaderValidation::SuspiciousEntryPoint => { + return Some(HollowingIndicator::InvalidPEHeader { + validation: "Suspicious entry point location".to_string(), + }); + } + PEHeaderValidation::CorruptedHeader => { + return Some(HollowingIndicator::CorruptedPEStructure { + address: region.base_address, + reason: "Corrupted PE header structure".to_string(), + }); + } + PEHeaderValidation::NotPE => continue, + } + } + Err(_) => { + // Could not read memory - might be suspicious but don't report + continue; + } + } + } + + None + } + + #[cfg(not(windows))] + fn validate_pe_headers(&self, _pid: u32, _regions: &[MemoryRegion]) -> Option { + // PE validation is Windows-specific + None + } + fn check_entry_point_anomalies( &self, _process: &ProcessInfo, diff --git a/ghost-core/src/hooks.rs b/ghost-core/src/hooks.rs index 75fc205..6b9d227 100644 --- a/ghost-core/src/hooks.rs +++ b/ghost-core/src/hooks.rs @@ -1,130 +1,335 @@ +//! Hook detection for identifying SetWindowsHookEx and inline hook-based injection. +//! +//! This module detects Windows message hooks and inline API hooks that are commonly +//! used for process injection (T1055.003, T1055.012). +//! On Linux, it detects LD_PRELOAD and LD_LIBRARY_PATH based injection. + use crate::{GhostError, Result}; +use serde::{Deserialize, Serialize}; use std::collections::HashMap; -#[derive(Debug, Clone)] -pub struct HookInfo { - pub hook_type: u32, - pub thread_id: u32, - pub hook_proc: usize, - pub module_name: String, +/// Type of hook detected. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +pub enum HookType { + /// SetWindowsHookEx hook (message hook). + WindowsHook(u32), + /// Inline/detour hook (JMP patch). + InlineHook, + /// Import Address Table (IAT) hook. + IATHook, + /// Export Address Table (EAT) hook. + EATHook, + /// LD_PRELOAD based library injection (Linux). + LdPreload, + /// LD_LIBRARY_PATH manipulation (Linux). + LdLibraryPath, + /// Ptrace-based injection (Linux). + PtraceInjection, } -#[derive(Debug, Clone)] +impl std::fmt::Display for HookType { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::WindowsHook(id) => write!(f, "WindowsHook({})", id), + Self::InlineHook => write!(f, "InlineHook"), + Self::IATHook => write!(f, "IATHook"), + Self::EATHook => write!(f, "EATHook"), + Self::LdPreload => write!(f, "LD_PRELOAD"), + Self::LdLibraryPath => write!(f, "LD_LIBRARY_PATH"), + Self::PtraceInjection => write!(f, "PtraceInjection"), + } + } +} + +/// Information about a detected hook. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct HookInfo { + /// Type of hook. + pub hook_type: HookType, + /// Thread ID (for message hooks) or 0 for system-wide. + pub thread_id: u32, + /// Address of the hook procedure. + pub hook_proc: usize, + /// Original address (for inline/IAT hooks). + pub original_address: usize, + /// Module containing the hook procedure. + pub module_name: String, + /// Function being hooked (for inline/IAT hooks). + pub hooked_function: String, +} + +/// Result of hook detection analysis. +#[derive(Debug, Clone, Serialize, Deserialize)] pub struct HookDetectionResult { + /// List of detected hooks. pub hooks: Vec, + /// Number of suspicious hooks. pub suspicious_count: usize, + /// Number of global/system-wide hooks. pub global_hooks: usize, + /// Number of inline API hooks detected. + pub inline_hooks: usize, } #[cfg(windows)] mod platform { - use super::{HookDetectionResult, HookInfo}; + use super::{HookDetectionResult, HookInfo, HookType}; use crate::{GhostError, Result}; use std::collections::HashMap; - use windows::Win32::Foundation::{GetLastError, HWND}; - use windows::Win32::System::LibraryLoader::{GetModuleFileNameW, GetModuleHandleW}; + use windows::Win32::Foundation::CloseHandle; + use windows::Win32::System::Diagnostics::Debug::ReadProcessMemory; + use windows::Win32::System::LibraryLoader::{ + GetModuleHandleW, GetProcAddress, LoadLibraryW, + }; + use windows::Win32::System::ProcessStatus::{ + EnumProcessModulesEx, GetModuleBaseNameW, GetModuleInformation, LIST_MODULES_ALL, + MODULEINFO, + }; + use windows::Win32::System::Threading::{OpenProcess, PROCESS_QUERY_INFORMATION, PROCESS_VM_READ}; use windows::Win32::UI::WindowsAndMessaging::{ - EnumWindows, GetWindowThreadProcessId, HC_ACTION, HOOKPROC, WH_CALLWNDPROC, - WH_CALLWNDPROCRET, WH_CBT, WH_DEBUG, WH_FOREGROUNDIDLE, WH_GETMESSAGE, WH_JOURNALPLAYBACK, - WH_JOURNALRECORD, WH_KEYBOARD, WH_KEYBOARD_LL, WH_MOUSE, WH_MOUSE_LL, WH_MSGFILTER, - WH_SHELL, WH_SYSMSGFILTER, + WH_CALLWNDPROC, WH_CALLWNDPROCRET, WH_CBT, WH_DEBUG, WH_FOREGROUNDIDLE, WH_GETMESSAGE, + WH_JOURNALPLAYBACK, WH_JOURNALRECORD, WH_KEYBOARD, WH_KEYBOARD_LL, WH_MOUSE, + WH_MOUSE_LL, WH_MSGFILTER, WH_SHELL, WH_SYSMSGFILTER, }; - /// Detect Windows hook-based injection techniques + /// Critical APIs commonly hooked for injection. + const CRITICAL_APIS: &[(&str, &str)] = &[ + ("ntdll.dll", "NtCreateThread"), + ("ntdll.dll", "NtCreateThreadEx"), + ("ntdll.dll", "NtAllocateVirtualMemory"), + ("ntdll.dll", "NtWriteVirtualMemory"), + ("ntdll.dll", "NtProtectVirtualMemory"), + ("ntdll.dll", "NtQueueApcThread"), + ("kernel32.dll", "VirtualAllocEx"), + ("kernel32.dll", "WriteProcessMemory"), + ("kernel32.dll", "CreateRemoteThread"), + ("kernel32.dll", "LoadLibraryA"), + ("kernel32.dll", "LoadLibraryW"), + ("user32.dll", "SetWindowsHookExA"), + ("user32.dll", "SetWindowsHookExW"), + ]; + + /// Detect Windows hook-based injection techniques. pub fn detect_hook_injection(target_pid: u32) -> Result { let mut hooks = Vec::new(); let mut suspicious_count = 0; let mut global_hooks = 0; + let mut inline_hooks = 0; - // This is a simplified implementation - real hook detection requires - // more sophisticated techniques like parsing USER32.dll's hook table - // or using undocumented APIs. For now, we'll detect based on heuristics. - - // Check for global hooks that might be used for injection - if let Ok(global_hook_count) = count_global_hooks() { - global_hooks = global_hook_count; - if global_hook_count > 5 { - suspicious_count += 1; + // Detect inline hooks in critical APIs + match detect_inline_hooks(target_pid) { + Ok(inline) => { + inline_hooks = inline.len(); + for hook in inline { + if is_suspicious_inline_hook(&hook) { + suspicious_count += 1; + } + hooks.push(hook); + } + } + Err(e) => { + log::debug!("Failed to detect inline hooks: {}", e); } } - // Check for hooks targeting specific process - if let Ok(process_hooks) = enumerate_process_hooks(target_pid) { - for hook in process_hooks { - // Check if hook procedure is in suspicious location - if is_suspicious_hook(&hook) { - suspicious_count += 1; - } - hooks.push(hook); - } + // Estimate global hooks based on system state + global_hooks = estimate_global_hooks(); + if global_hooks > 10 { + suspicious_count += 1; } Ok(HookDetectionResult { hooks, suspicious_count, global_hooks, + inline_hooks, }) } - fn count_global_hooks() -> Result { - // In a real implementation, this would examine the global hook chain - // by parsing USER32.dll internal structures or using WinAPIOverride - // For now, return a realistic count based on typical system state - Ok(3) // Typical Windows system has 2-4 global hooks - } - - fn enumerate_process_hooks(pid: u32) -> Result> { + /// Detect inline (detour) hooks by checking for JMP instructions at API entry points. + fn detect_inline_hooks(target_pid: u32) -> Result> { let mut hooks = Vec::new(); - // Real implementation would: - // 1. Enumerate all threads in the process - // 2. Check each thread's hook chain - // 3. Validate hook procedures and their locations - // 4. Cross-reference with loaded modules + unsafe { + let handle = OpenProcess(PROCESS_QUERY_INFORMATION | PROCESS_VM_READ, false, target_pid) + .map_err(|e| GhostError::Process { + message: format!("Failed to open process: {}", e), + })?; - // Simplified detection: check for common hook types that might indicate injection - let common_injection_hooks = vec![ - (WH_CALLWNDPROC.0, "WH_CALLWNDPROC"), - (WH_GETMESSAGE.0, "WH_GETMESSAGE"), - (WH_CBT.0, "WH_CBT"), - (WH_KEYBOARD_LL.0, "WH_KEYBOARD_LL"), - (WH_MOUSE_LL.0, "WH_MOUSE_LL"), - ]; + // Get loaded modules in target process + let mut modules = [windows::Win32::Foundation::HMODULE::default(); 1024]; + let mut cb_needed = 0u32; - // This is a placeholder - real hook enumeration requires low-level API calls - // or kernel debugging interfaces - for (hook_type, _name) in common_injection_hooks { - if might_have_hook(pid, hook_type) { - hooks.push(HookInfo { - hook_type, - thread_id: 0, // Would get actual thread ID - hook_proc: 0, // Would get actual procedure address - module_name: "unknown".to_string(), + let result = EnumProcessModulesEx( + handle, + modules.as_mut_ptr(), + (modules.len() * std::mem::size_of::()) as u32, + &mut cb_needed, + LIST_MODULES_ALL, + ); + + if result.is_err() { + let _ = CloseHandle(handle); + return Err(GhostError::Process { + message: "Failed to enumerate process modules".to_string(), }); } + + let module_count = + (cb_needed as usize) / std::mem::size_of::(); + + // Check each critical API for hooks + for (module_name, func_name) in CRITICAL_APIS { + // Find the module in target process + for i in 0..module_count { + let mut name_buffer = [0u16; 256]; + if GetModuleBaseNameW(handle, modules[i], &mut name_buffer) == 0 { + continue; + } + + let mod_name = String::from_utf16_lossy( + &name_buffer[..name_buffer + .iter() + .position(|&c| c == 0) + .unwrap_or(name_buffer.len())], + ) + .to_lowercase(); + + if !mod_name.contains(&module_name.to_lowercase().replace(".dll", "")) { + continue; + } + + // Get module info + let mut mod_info = MODULEINFO::default(); + if GetModuleInformation( + handle, + modules[i], + &mut mod_info, + std::mem::size_of::() as u32, + ) + .is_err() + { + continue; + } + + // Get function address from our process (assume same base address) + let local_module = match GetModuleHandleW(windows::core::PCWSTR::from_raw( + module_name + .encode_utf16() + .chain(std::iter::once(0)) + .collect::>() + .as_ptr(), + )) { + Ok(h) => h, + Err(_) => continue, + }; + + let func_addr = match GetProcAddress( + local_module, + windows::core::PCSTR::from_raw( + std::ffi::CString::new(*func_name) + .unwrap() + .as_bytes_with_nul() + .as_ptr(), + ), + ) { + Some(addr) => addr as usize, + None => continue, + }; + + // Calculate offset from module base + let offset = func_addr - local_module.0 as usize; + let target_func_addr = mod_info.lpBaseOfDll as usize + offset; + + // Read first bytes of function in target process + let mut buffer = [0u8; 16]; + let mut bytes_read = 0usize; + + if ReadProcessMemory( + handle, + target_func_addr as *const _, + buffer.as_mut_ptr() as *mut _, + buffer.len(), + Some(&mut bytes_read), + ) + .is_ok() + && bytes_read >= 5 + { + // Check for common hook patterns + if let Some(hook) = detect_hook_pattern(&buffer, target_func_addr) { + hooks.push(HookInfo { + hook_type: HookType::InlineHook, + thread_id: 0, + hook_proc: hook, + original_address: target_func_addr, + module_name: module_name.to_string(), + hooked_function: func_name.to_string(), + }); + } + } + } + } + + let _ = CloseHandle(handle); } Ok(hooks) } - fn might_have_hook(pid: u32, hook_type: u32) -> bool { - // Heuristic: certain processes are more likely to have hooks - // This is a simplified check - real implementation would examine memory - hook_type == WH_KEYBOARD_LL.0 || hook_type == WH_MOUSE_LL.0 - } - - fn is_suspicious_hook(hook: &HookInfo) -> bool { - // Check for hooks with suspicious characteristics - match hook.hook_type { - t if t == WH_CALLWNDPROC.0 => true, // Often used for injection - t if t == WH_GETMESSAGE.0 => true, // Common injection vector - t if t == WH_CBT.0 => true, // Can be used maliciously - t if t == WH_DEBUG.0 => true, // Debugging hooks are suspicious - _ => false, + /// Detect common hook patterns in function prologue. + fn detect_hook_pattern(bytes: &[u8], base_addr: usize) -> Option { + if bytes.len() < 5 { + return None; } + + // JMP rel32 (E9 xx xx xx xx) + if bytes[0] == 0xE9 { + let offset = i32::from_le_bytes([bytes[1], bytes[2], bytes[3], bytes[4]]); + let target = (base_addr as i64 + 5 + offset as i64) as usize; + return Some(target); + } + + // JMP [rip+disp32] (FF 25 xx xx xx xx) - 64-bit + if bytes.len() >= 6 && bytes[0] == 0xFF && bytes[1] == 0x25 { + // This is an indirect jump, would need to read the target address + return Some(0xFFFFFFFF); // Indicate hook detected but target unknown + } + + // MOV RAX, imm64; JMP RAX (48 B8 ... FF E0) + if bytes.len() >= 12 + && bytes[0] == 0x48 + && bytes[1] == 0xB8 + && bytes[10] == 0xFF + && bytes[11] == 0xE0 + { + let target = u64::from_le_bytes([ + bytes[2], bytes[3], bytes[4], bytes[5], bytes[6], bytes[7], bytes[8], bytes[9], + ]) as usize; + return Some(target); + } + + // PUSH imm32; RET (68 xx xx xx xx C3) - 32-bit style + if bytes.len() >= 6 && bytes[0] == 0x68 && bytes[5] == 0xC3 { + let target = u32::from_le_bytes([bytes[1], bytes[2], bytes[3], bytes[4]]) as usize; + return Some(target); + } + + None } - /// Get hook type name for display + fn is_suspicious_inline_hook(hook: &HookInfo) -> bool { + // All inline hooks are suspicious in security context + matches!(hook.hook_type, HookType::InlineHook | HookType::IATHook) + } + + fn estimate_global_hooks() -> usize { + // In a full implementation, this would enumerate the global hook chain + // by parsing USER32.dll's internal structures. + // Return typical value for now. + 3 + } + + /// Get hook type name for display. pub fn get_hook_type_name(hook_type: u32) -> &'static str { match hook_type { t if t == WH_CALLWNDPROC.0 => "WH_CALLWNDPROC", @@ -147,14 +352,259 @@ mod platform { } } -#[cfg(not(windows))] +#[cfg(target_os = "linux")] +mod platform { + use super::{HookDetectionResult, HookInfo, HookType}; + use crate::{GhostError, Result}; + use std::fs; + use std::path::Path; + + /// Detect hook injection on Linux (LD_PRELOAD, LD_LIBRARY_PATH, ptrace). + pub fn detect_hook_injection(target_pid: u32) -> Result { + let mut hooks = Vec::new(); + let mut suspicious_count = 0; + + // Check for LD_PRELOAD in process environment + if let Ok(ld_preload_hooks) = detect_ld_preload(target_pid) { + suspicious_count += ld_preload_hooks.len(); + hooks.extend(ld_preload_hooks); + } + + // Check for LD_LIBRARY_PATH manipulation + if let Ok(ld_library_path_hooks) = detect_ld_library_path(target_pid) { + suspicious_count += ld_library_path_hooks.len(); + hooks.extend(ld_library_path_hooks); + } + + // Check for ptrace attachment + if let Ok(ptrace_detected) = detect_ptrace_attachment(target_pid) { + if ptrace_detected { + suspicious_count += 1; + hooks.push(HookInfo { + hook_type: HookType::PtraceInjection, + thread_id: 0, + hook_proc: 0, + original_address: 0, + module_name: "ptrace".to_string(), + hooked_function: "process_vm_writev/ptrace".to_string(), + }); + } + } + + // Check loaded libraries for suspicious patterns + if let Ok(suspicious_libs) = detect_suspicious_libraries(target_pid) { + hooks.extend(suspicious_libs); + } + + Ok(HookDetectionResult { + hooks, + suspicious_count, + global_hooks: 0, + inline_hooks: 0, + }) + } + + /// Detect LD_PRELOAD environment variable in process. + fn detect_ld_preload(pid: u32) -> Result> { + let environ_path = format!("/proc/{}/environ", pid); + let environ_content = fs::read_to_string(&environ_path).map_err(|e| { + GhostError::Process { + message: format!("Failed to read process environment: {}", e), + } + })?; + + let mut hooks = Vec::new(); + + // Environment variables are null-separated + for env_var in environ_content.split('\0') { + if env_var.starts_with("LD_PRELOAD=") { + let libraries = env_var.strip_prefix("LD_PRELOAD=").unwrap_or(""); + + // Multiple libraries can be separated by spaces or colons + for lib in libraries.split(&[' ', ':'][..]) { + if !lib.is_empty() { + hooks.push(HookInfo { + hook_type: HookType::LdPreload, + thread_id: 0, + hook_proc: 0, + original_address: 0, + module_name: lib.to_string(), + hooked_function: "LD_PRELOAD".to_string(), + }); + } + } + } + } + + Ok(hooks) + } + + /// Detect LD_LIBRARY_PATH environment variable manipulation. + fn detect_ld_library_path(pid: u32) -> Result> { + let environ_path = format!("/proc/{}/environ", pid); + let environ_content = fs::read_to_string(&environ_path).map_err(|e| { + GhostError::Process { + message: format!("Failed to read process environment: {}", e), + } + })?; + + let mut hooks = Vec::new(); + + for env_var in environ_content.split('\0') { + if env_var.starts_with("LD_LIBRARY_PATH=") { + let paths = env_var.strip_prefix("LD_LIBRARY_PATH=").unwrap_or(""); + + // Check for suspicious paths + for path in paths.split(':') { + if is_suspicious_library_path(path) { + hooks.push(HookInfo { + hook_type: HookType::LdLibraryPath, + thread_id: 0, + hook_proc: 0, + original_address: 0, + module_name: path.to_string(), + hooked_function: "LD_LIBRARY_PATH".to_string(), + }); + } + } + } + } + + Ok(hooks) + } + + /// Check if a library path is suspicious. + fn is_suspicious_library_path(path: &str) -> bool { + // Suspicious patterns + let suspicious_patterns = [ + "/tmp/", + "/dev/shm/", + "/var/tmp/", + ".", + "..", + "/home/", + ]; + + suspicious_patterns.iter().any(|&pattern| path.contains(pattern)) + } + + /// Detect ptrace attachment (debugging/injection). + fn detect_ptrace_attachment(pid: u32) -> Result { + let status_path = format!("/proc/{}/status", pid); + let status_content = fs::read_to_string(&status_path).map_err(|e| { + GhostError::Process { + message: format!("Failed to read process status: {}", e), + } + })?; + + // Look for TracerPid field + for line in status_content.lines() { + if line.starts_with("TracerPid:") { + let tracer_pid = line + .split_whitespace() + .nth(1) + .and_then(|s| s.parse::().ok()) + .unwrap_or(0); + + // Non-zero TracerPid means the process is being traced + if tracer_pid != 0 { + return Ok(true); + } + } + } + + Ok(false) + } + + /// Detect suspicious loaded libraries. + fn detect_suspicious_libraries(pid: u32) -> Result> { + let maps_path = format!("/proc/{}/maps", pid); + let maps_content = fs::read_to_string(&maps_path).map_err(|e| { + GhostError::Process { + message: format!("Failed to read process maps: {}", e), + } + })?; + + let mut hooks = Vec::new(); + let mut seen_libraries = std::collections::HashSet::new(); + + for line in maps_content.lines() { + let parts: Vec<&str> = line.split_whitespace().collect(); + if parts.len() < 6 { + continue; + } + + let pathname = parts[5..].join(" "); + + // Check if it's a shared library + if pathname.ends_with(".so") || pathname.contains(".so.") { + // Skip if already seen + if !seen_libraries.insert(pathname.clone()) { + continue; + } + + // Check for suspicious library locations + if is_suspicious_library(&pathname) { + hooks.push(HookInfo { + hook_type: HookType::InlineHook, // Generic classification + thread_id: 0, + hook_proc: 0, + original_address: 0, + module_name: pathname.clone(), + hooked_function: "suspicious_library".to_string(), + }); + } + } + } + + Ok(hooks) + } + + /// Check if a library path is suspicious. + fn is_suspicious_library(path: &str) -> bool { + // Libraries in these locations are often used for injection + let suspicious_locations = [ + "/tmp/", + "/dev/shm/", + "/var/tmp/", + "/home/", + ]; + + // Check if library is in a suspicious location + if suspicious_locations.iter().any(|&loc| path.starts_with(loc)) { + return true; + } + + // Check for libraries with suspicious names + let suspicious_names = [ + "inject", + "hook", + "cheat", + "hack", + "rootkit", + ]; + + let path_lower = path.to_lowercase(); + suspicious_names.iter().any(|&name| path_lower.contains(name)) + } + + pub fn get_hook_type_name(_hook_type: u32) -> &'static str { + "LINUX_HOOK" + } +} + +#[cfg(not(any(windows, target_os = "linux")))] mod platform { use super::HookDetectionResult; use crate::{GhostError, Result}; pub fn detect_hook_injection(_target_pid: u32) -> Result { - Err(GhostError::Detection { - message: "Hook detection not implemented for this platform".to_string(), + // Hook detection is not implemented for this platform + Ok(HookDetectionResult { + hooks: Vec::new(), + suspicious_count: 0, + global_hooks: 0, + inline_hooks: 0, }) } diff --git a/ghost-core/src/memory.rs b/ghost-core/src/memory.rs index 6f02eb5..93b5616 100644 --- a/ghost-core/src/memory.rs +++ b/ghost-core/src/memory.rs @@ -1,6 +1,120 @@ +//! Memory region enumeration and analysis. +//! +//! This module provides cross-platform memory introspection capabilities, +//! allowing analysis of process memory layouts, protection flags, and content. + +use serde::{Deserialize, Serialize}; use std::fmt; -#[derive(Debug, Clone, Copy, PartialEq, Eq)] +/// PE header constants +#[cfg(windows)] +pub const IMAGE_DOS_SIGNATURE: u16 = 0x5A4D; // "MZ" +#[cfg(windows)] +pub const IMAGE_NT_SIGNATURE: u32 = 0x00004550; // "PE\0\0" + +/// DOS header structure (first 64 bytes of a PE file) +#[derive(Debug, Clone, Copy)] +#[repr(C)] +pub struct ImageDosHeader { + pub e_magic: u16, // Magic number ("MZ") + pub e_cblp: u16, // Bytes on last page + pub e_cp: u16, // Pages in file + pub e_crlc: u16, // Relocations + pub e_cparhdr: u16, // Size of header in paragraphs + pub e_minalloc: u16, // Minimum extra paragraphs + pub e_maxalloc: u16, // Maximum extra paragraphs + pub e_ss: u16, // Initial SS value + pub e_sp: u16, // Initial SP value + pub e_csum: u16, // Checksum + pub e_ip: u16, // Initial IP value + pub e_cs: u16, // Initial CS value + pub e_lfarlc: u16, // File address of relocation table + pub e_ovno: u16, // Overlay number + pub e_res: [u16; 4], // Reserved + pub e_oemid: u16, // OEM identifier + pub e_oeminfo: u16, // OEM information + pub e_res2: [u16; 10], // Reserved + pub e_lfanew: i32, // File address of new exe header +} + +/// PE file header structure +#[derive(Debug, Clone, Copy)] +#[repr(C)] +pub struct ImageFileHeader { + pub machine: u16, + pub number_of_sections: u16, + pub time_date_stamp: u32, + pub pointer_to_symbol_table: u32, + pub number_of_symbols: u32, + pub size_of_optional_header: u16, + pub characteristics: u16, +} + +/// PE optional header structure (64-bit) +#[derive(Debug, Clone, Copy)] +#[repr(C)] +pub struct ImageOptionalHeader64 { + pub magic: u16, + pub major_linker_version: u8, + pub minor_linker_version: u8, + pub size_of_code: u32, + pub size_of_initialized_data: u32, + pub size_of_uninitialized_data: u32, + pub address_of_entry_point: u32, + pub base_of_code: u32, + pub image_base: u64, + pub section_alignment: u32, + pub file_alignment: u32, + pub major_operating_system_version: u16, + pub minor_operating_system_version: u16, + pub major_image_version: u16, + pub minor_image_version: u16, + pub major_subsystem_version: u16, + pub minor_subsystem_version: u16, + pub win32_version_value: u32, + pub size_of_image: u32, + pub size_of_headers: u32, + pub check_sum: u32, + pub subsystem: u16, + pub dll_characteristics: u16, + pub size_of_stack_reserve: u64, + pub size_of_stack_commit: u64, + pub size_of_heap_reserve: u64, + pub size_of_heap_commit: u64, + pub loader_flags: u32, + pub number_of_rva_and_sizes: u32, +} + +/// PE header validation result +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub enum PEHeaderValidation { + Valid, + InvalidDosSignature, + InvalidNtSignature, + InvalidHeaderOffset, + MismatchedImageBase, + SuspiciousEntryPoint, + CorruptedHeader, + NotPE, +} + +impl fmt::Display for PEHeaderValidation { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::Valid => write!(f, "Valid PE header"), + Self::InvalidDosSignature => write!(f, "Invalid DOS signature"), + Self::InvalidNtSignature => write!(f, "Invalid NT signature"), + Self::InvalidHeaderOffset => write!(f, "Invalid header offset"), + Self::MismatchedImageBase => write!(f, "Image base mismatch"), + Self::SuspiciousEntryPoint => write!(f, "Suspicious entry point"), + Self::CorruptedHeader => write!(f, "Corrupted PE header"), + Self::NotPE => write!(f, "Not a PE file"), + } + } +} + +/// Memory protection flags for a memory region. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] pub enum MemoryProtection { NoAccess, ReadOnly, @@ -28,11 +142,16 @@ impl fmt::Display for MemoryProtection { } } -#[derive(Debug, Clone)] +/// Information about a memory region within a process. +#[derive(Debug, Clone, Serialize, Deserialize)] pub struct MemoryRegion { + /// Base address of the memory region. pub base_address: usize, + /// Size of the region in bytes. pub size: usize, + /// Memory protection flags. pub protection: MemoryProtection, + /// Type of memory region (IMAGE, MAPPED, PRIVATE, etc.). pub region_type: String, } @@ -49,18 +168,217 @@ impl fmt::Display for MemoryRegion { } } +/// Validates a PE header in process memory +#[cfg(windows)] +pub fn validate_pe_header(pid: u32, base_address: usize) -> anyhow::Result { + use std::mem; + + // Read DOS header + let dos_header_size = mem::size_of::(); + let dos_header_bytes = read_process_memory(pid, base_address, dos_header_size)?; + + if dos_header_bytes.len() < dos_header_size { + return Ok(PEHeaderValidation::CorruptedHeader); + } + + let dos_header = unsafe { + std::ptr::read(dos_header_bytes.as_ptr() as *const ImageDosHeader) + }; + + // Validate DOS signature + if dos_header.e_magic != IMAGE_DOS_SIGNATURE { + return Ok(PEHeaderValidation::InvalidDosSignature); + } + + // Validate e_lfanew offset (should be reasonable) + if dos_header.e_lfanew < 0 || dos_header.e_lfanew > 0x1000 { + return Ok(PEHeaderValidation::InvalidHeaderOffset); + } + + // Read NT headers + let nt_header_address = base_address.wrapping_add(dos_header.e_lfanew as usize); + + // Read NT signature (4 bytes) + let nt_sig_bytes = read_process_memory(pid, nt_header_address, 4)?; + if nt_sig_bytes.len() < 4 { + return Ok(PEHeaderValidation::CorruptedHeader); + } + + let nt_signature = u32::from_le_bytes([ + nt_sig_bytes[0], + nt_sig_bytes[1], + nt_sig_bytes[2], + nt_sig_bytes[3], + ]); + + if nt_signature != IMAGE_NT_SIGNATURE { + return Ok(PEHeaderValidation::InvalidNtSignature); + } + + // Read file header + let file_header_address = nt_header_address + 4; + let file_header_size = mem::size_of::(); + let file_header_bytes = read_process_memory(pid, file_header_address, file_header_size)?; + + if file_header_bytes.len() < file_header_size { + return Ok(PEHeaderValidation::CorruptedHeader); + } + + let file_header = unsafe { + std::ptr::read(file_header_bytes.as_ptr() as *const ImageFileHeader) + }; + + // Read optional header (64-bit) + let optional_header_address = file_header_address + file_header_size; + let optional_header_size = mem::size_of::(); + let optional_header_bytes = read_process_memory(pid, optional_header_address, optional_header_size)?; + + if optional_header_bytes.len() < optional_header_size { + return Ok(PEHeaderValidation::CorruptedHeader); + } + + let optional_header = unsafe { + std::ptr::read(optional_header_bytes.as_ptr() as *const ImageOptionalHeader64) + }; + + // Validate image base matches memory address + if optional_header.image_base != base_address as u64 { + return Ok(PEHeaderValidation::MismatchedImageBase); + } + + // Validate entry point (should be within the image) + let entry_point_rva = optional_header.address_of_entry_point; + if entry_point_rva == 0 || entry_point_rva >= optional_header.size_of_image { + return Ok(PEHeaderValidation::SuspiciousEntryPoint); + } + + // Additional validation: check if sections count is reasonable + if file_header.number_of_sections > 96 { + return Ok(PEHeaderValidation::CorruptedHeader); + } + + Ok(PEHeaderValidation::Valid) +} + +/// Stub for non-Windows platforms +#[cfg(not(windows))] +pub fn validate_pe_header(_pid: u32, _base_address: usize) -> anyhow::Result { + Ok(PEHeaderValidation::NotPE) +} + +/// Gets PE header information from process memory +#[cfg(windows)] +pub fn read_pe_header_info(pid: u32, base_address: usize) -> anyhow::Result> { + use std::mem; + + let dos_header_size = mem::size_of::(); + let dos_header_bytes = read_process_memory(pid, base_address, dos_header_size)?; + + if dos_header_bytes.len() < dos_header_size { + return Ok(None); + } + + let dos_header = unsafe { + std::ptr::read(dos_header_bytes.as_ptr() as *const ImageDosHeader) + }; + + if dos_header.e_magic != IMAGE_DOS_SIGNATURE { + return Ok(None); + } + + if dos_header.e_lfanew < 0 || dos_header.e_lfanew > 0x1000 { + return Ok(None); + } + + let nt_header_address = base_address.wrapping_add(dos_header.e_lfanew as usize); + + // Read NT signature + let nt_sig_bytes = read_process_memory(pid, nt_header_address, 4)?; + if nt_sig_bytes.len() < 4 { + return Ok(None); + } + + let nt_signature = u32::from_le_bytes([ + nt_sig_bytes[0], + nt_sig_bytes[1], + nt_sig_bytes[2], + nt_sig_bytes[3], + ]); + + if nt_signature != IMAGE_NT_SIGNATURE { + return Ok(None); + } + + // Read file header + let file_header_address = nt_header_address + 4; + let file_header_size = mem::size_of::(); + let file_header_bytes = read_process_memory(pid, file_header_address, file_header_size)?; + + if file_header_bytes.len() < file_header_size { + return Ok(None); + } + + let file_header = unsafe { + std::ptr::read(file_header_bytes.as_ptr() as *const ImageFileHeader) + }; + + // Read optional header + let optional_header_address = file_header_address + file_header_size; + let optional_header_size = mem::size_of::(); + let optional_header_bytes = read_process_memory(pid, optional_header_address, optional_header_size)?; + + if optional_header_bytes.len() < optional_header_size { + return Ok(None); + } + + let optional_header = unsafe { + std::ptr::read(optional_header_bytes.as_ptr() as *const ImageOptionalHeader64) + }; + + Ok(Some(PEHeaderInfo { + dos_signature: dos_header.e_magic, + nt_signature, + machine: file_header.machine, + number_of_sections: file_header.number_of_sections, + image_base: optional_header.image_base, + entry_point_rva: optional_header.address_of_entry_point, + size_of_image: optional_header.size_of_image, + size_of_headers: optional_header.size_of_headers, + })) +} + +/// PE header information extracted from process memory +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct PEHeaderInfo { + pub dos_signature: u16, + pub nt_signature: u32, + pub machine: u16, + pub number_of_sections: u16, + pub image_base: u64, + pub entry_point_rva: u32, + pub size_of_image: u32, + pub size_of_headers: u32, +} + +#[cfg(not(windows))] +pub fn read_pe_header_info(_pid: u32, _base_address: usize) -> anyhow::Result> { + Ok(None) +} + #[cfg(windows)] mod platform { use super::{MemoryProtection, MemoryRegion}; use anyhow::{Context, Result}; - use windows::Win32::Foundation::{CloseHandle, HANDLE}; + use windows::Win32::Foundation::CloseHandle; use windows::Win32::System::Diagnostics::Debug::ReadProcessMemory; use windows::Win32::System::Memory::{ - VirtualQueryEx, MEMORY_BASIC_INFORMATION, MEM_COMMIT, MEM_FREE, MEM_IMAGE, MEM_MAPPED, - MEM_PRIVATE, MEM_RESERVE, PAGE_EXECUTE, PAGE_EXECUTE_READ, PAGE_EXECUTE_READWRITE, - PAGE_EXECUTE_WRITECOPY, PAGE_NOACCESS, PAGE_READONLY, PAGE_READWRITE, PAGE_WRITECOPY, + VirtualQueryEx, MEMORY_BASIC_INFORMATION, MEM_COMMIT, MEM_IMAGE, MEM_MAPPED, MEM_PRIVATE, + PAGE_EXECUTE, PAGE_EXECUTE_READ, PAGE_EXECUTE_READWRITE, PAGE_EXECUTE_WRITECOPY, + PAGE_NOACCESS, PAGE_READONLY, PAGE_READWRITE, PAGE_WRITECOPY, + }; + use windows::Win32::System::Threading::{ + OpenProcess, PROCESS_QUERY_INFORMATION, PROCESS_VM_READ, }; - use windows::Win32::System::Threading::{OpenProcess, PROCESS_QUERY_INFORMATION, PROCESS_VM_READ}; fn parse_protection(protect: u32) -> MemoryProtection { match protect & 0xFF { @@ -80,7 +398,6 @@ mod platform { pub fn enumerate_memory_regions(pid: u32) -> Result> { let mut regions = Vec::new(); - // Skip system process if pid == 0 || pid == 4 { return Ok(regions); } @@ -108,7 +425,7 @@ mod platform { let region_type = if mbi.Type == MEM_IMAGE { "IMAGE" } else if mbi.Type == MEM_MAPPED { - "MAPPED" + "MAPPED" } else if mbi.Type == MEM_PRIVATE { "PRIVATE" } else { @@ -138,19 +455,358 @@ mod platform { Ok(regions) } + + /// Reads memory from a process at the specified address. + /// + /// # Safety + /// + /// This function reads arbitrary process memory. The caller must ensure + /// the address and size are valid for the target process. + pub fn read_process_memory(pid: u32, address: usize, size: usize) -> Result> { + if pid == 0 || pid == 4 { + return Err(anyhow::anyhow!("Cannot read system process memory")); + } + + unsafe { + let handle = OpenProcess(PROCESS_VM_READ, false, pid) + .context("Failed to open process for memory read")?; + + let mut buffer = vec![0u8; size]; + let mut bytes_read = 0usize; + + let success = ReadProcessMemory( + handle, + address as *const _, + buffer.as_mut_ptr() as *mut _, + size, + Some(&mut bytes_read), + ); + + let _ = CloseHandle(handle); + + if success.is_ok() && bytes_read > 0 { + buffer.truncate(bytes_read); + Ok(buffer) + } else { + Err(anyhow::anyhow!( + "Failed to read process memory at {:#x}", + address + )) + } + } + } } -#[cfg(not(windows))] +#[cfg(target_os = "linux")] +mod platform { + use super::{MemoryProtection, MemoryRegion}; + use anyhow::{Context, Result}; + use std::fs; + + pub fn enumerate_memory_regions(pid: u32) -> Result> { + let maps_path = format!("/proc/{}/maps", pid); + let content = fs::read_to_string(&maps_path) + .context(format!("Failed to read {}", maps_path))?; + + let mut regions = Vec::new(); + + for line in content.lines() { + if let Some(region) = parse_maps_line(line) { + regions.push(region); + } + } + + Ok(regions) + } + + fn parse_maps_line(line: &str) -> Option { + let parts: Vec<&str> = line.split_whitespace().collect(); + if parts.is_empty() { + return None; + } + + let addr_range = parts[0]; + let perms = parts.get(1)?; + let pathname = parts.get(5..).map(|p| p.join(" ")).unwrap_or_default(); + + let (start, end) = { + let mut split = addr_range.split('-'); + let start = usize::from_str_radix(split.next()?, 16).ok()?; + let end = usize::from_str_radix(split.next()?, 16).ok()?; + (start, end) + }; + + let protection = parse_linux_perms(perms); + let region_type = determine_region_type(&pathname); + + Some(MemoryRegion { + base_address: start, + size: end.saturating_sub(start), + protection, + region_type, + }) + } + + fn parse_linux_perms(perms: &str) -> MemoryProtection { + let r = perms.contains('r'); + let w = perms.contains('w'); + let x = perms.contains('x'); + + match (r, w, x) { + (false, false, false) => MemoryProtection::NoAccess, + (true, false, false) => MemoryProtection::ReadOnly, + (true, true, false) => MemoryProtection::ReadWrite, + (true, false, true) => MemoryProtection::ReadExecute, + (true, true, true) => MemoryProtection::ReadWriteExecute, + (false, false, true) => MemoryProtection::Execute, + _ => MemoryProtection::Unknown, + } + } + + fn determine_region_type(pathname: &str) -> String { + if pathname.is_empty() || pathname == "[anon]" { + "PRIVATE".to_string() + } else if pathname.starts_with('[') { + match pathname { + "[heap]" => "HEAP".to_string(), + "[stack]" => "STACK".to_string(), + "[vdso]" | "[vvar]" | "[vsyscall]" => "SYSTEM".to_string(), + _ => "SPECIAL".to_string(), + } + } else if pathname.ends_with(".so") || pathname.contains(".so.") { + "IMAGE".to_string() + } else { + "MAPPED".to_string() + } + } + + pub fn read_process_memory(pid: u32, address: usize, size: usize) -> Result> { + let mem_path = format!("/proc/{}/mem", pid); + let mut file = fs::File::open(&mem_path) + .context(format!("Failed to open {}", mem_path))?; + + use std::io::{Read, Seek, SeekFrom}; + file.seek(SeekFrom::Start(address as u64)) + .context("Failed to seek to memory address")?; + + let mut buffer = vec![0u8; size]; + let bytes_read = file.read(&mut buffer).context("Failed to read memory")?; + buffer.truncate(bytes_read); + + Ok(buffer) + } +} + +#[cfg(target_os = "macos")] +mod platform { + use super::{MemoryProtection, MemoryRegion}; + use anyhow::{Context, Result}; + + pub fn enumerate_memory_regions(pid: u32) -> Result> { + use libc::{ + mach_port_t, mach_vm_address_t, mach_vm_size_t, natural_t, vm_region_basic_info_64, + VM_REGION_BASIC_INFO_64, + }; + use std::mem; + + extern "C" { + fn task_for_pid( + target_tport: mach_port_t, + pid: i32, + task: *mut mach_port_t, + ) -> i32; + fn mach_task_self() -> mach_port_t; + fn mach_vm_region( + target_task: mach_port_t, + address: *mut mach_vm_address_t, + size: *mut mach_vm_size_t, + flavor: i32, + info: *mut i32, + info_count: *mut u32, + object_name: *mut mach_port_t, + ) -> i32; + } + + let mut regions = Vec::new(); + + unsafe { + let mut task: mach_port_t = 0; + let kr = task_for_pid(mach_task_self(), pid as i32, &mut task); + + if kr != 0 { + // KERN_SUCCESS = 0 + return Err(anyhow::anyhow!( + "task_for_pid failed with error code {}. Requires root or taskgated entitlement.", + kr + )); + } + + let mut address: mach_vm_address_t = 0; + + loop { + let mut size: mach_vm_size_t = 0; + let mut info: vm_region_basic_info_64 = mem::zeroed(); + let mut info_count = (mem::size_of::() + / mem::size_of::()) as u32; + let mut object_name: mach_port_t = 0; + + let kr = mach_vm_region( + task, + &mut address, + &mut size, + VM_REGION_BASIC_INFO_64, + &mut info as *mut _ as *mut i32, + &mut info_count, + &mut object_name, + ); + + if kr != 0 { + // End of address space or error + break; + } + + let protection = parse_mach_protection(info.protection); + let region_type = determine_mach_region_type(&info); + + regions.push(MemoryRegion { + base_address: address as usize, + size: size as usize, + protection, + region_type, + }); + + // Move to next region + address = address.saturating_add(size); + if address == 0 { + break; + } + } + } + + Ok(regions) + } + + fn parse_mach_protection(prot: i32) -> MemoryProtection { + // VM_PROT_READ = 1, VM_PROT_WRITE = 2, VM_PROT_EXECUTE = 4 + let r = (prot & 1) != 0; + let w = (prot & 2) != 0; + let x = (prot & 4) != 0; + + match (r, w, x) { + (false, false, false) => MemoryProtection::NoAccess, + (true, false, false) => MemoryProtection::ReadOnly, + (true, true, false) => MemoryProtection::ReadWrite, + (true, false, true) => MemoryProtection::ReadExecute, + (true, true, true) => MemoryProtection::ReadWriteExecute, + (false, false, true) => MemoryProtection::Execute, + _ => MemoryProtection::Unknown, + } + } + + fn determine_mach_region_type(info: &libc::vm_region_basic_info_64) -> String { + // Determine region type based on characteristics + if info.shared != 0 { + "SHARED".to_string() + } else if info.reserved != 0 { + "RESERVED".to_string() + } else { + "PRIVATE".to_string() + } + } + + pub fn read_process_memory(pid: u32, address: usize, size: usize) -> Result> { + use libc::mach_port_t; + + extern "C" { + fn task_for_pid( + target_tport: mach_port_t, + pid: i32, + task: *mut mach_port_t, + ) -> i32; + fn mach_task_self() -> mach_port_t; + fn mach_vm_read_overwrite( + target_task: mach_port_t, + address: u64, + size: u64, + data: u64, + out_size: *mut u64, + ) -> i32; + } + + unsafe { + let mut task: mach_port_t = 0; + let kr = task_for_pid(mach_task_self(), pid as i32, &mut task); + + if kr != 0 { + return Err(anyhow::anyhow!( + "task_for_pid failed with error code {}", + kr + )); + } + + let mut buffer = vec![0u8; size]; + let mut out_size: u64 = 0; + + let kr = mach_vm_read_overwrite( + task, + address as u64, + size as u64, + buffer.as_mut_ptr() as u64, + &mut out_size, + ); + + if kr != 0 { + return Err(anyhow::anyhow!( + "mach_vm_read_overwrite failed with error code {}", + kr + )); + } + + buffer.truncate(out_size as usize); + Ok(buffer) + } + } +} + +#[cfg(not(any(windows, target_os = "linux", target_os = "macos")))] mod platform { use super::MemoryRegion; use anyhow::Result; pub fn enumerate_memory_regions(_pid: u32) -> Result> { - // TODO: Implement Linux/macOS memory enumeration - Ok(Vec::new()) + Err(anyhow::anyhow!( + "Memory enumeration not supported on this platform" + )) + } + + pub fn read_process_memory(_pid: u32, _address: usize, _size: usize) -> Result> { + Err(anyhow::anyhow!( + "Memory reading not supported on this platform" + )) } } +/// Enumerates all memory regions for a process. +/// +/// # Platform Support +/// +/// - **Windows**: Uses VirtualQueryEx to enumerate regions. +/// - **Linux**: Parses /proc/[pid]/maps. +/// - **macOS**: Not yet implemented. pub fn enumerate_memory_regions(pid: u32) -> anyhow::Result> { platform::enumerate_memory_regions(pid) } + +/// Reads raw memory content from a process. +/// +/// This function reads up to `size` bytes from the target process at the +/// specified address. Requires appropriate privileges. +/// +/// # Platform Support +/// +/// - **Windows**: Uses ReadProcessMemory API. +/// - **Linux**: Reads from /proc/[pid]/mem. +/// - **macOS**: Not yet implemented. +pub fn read_process_memory(pid: u32, address: usize, size: usize) -> anyhow::Result> { + platform::read_process_memory(pid, address, size) +} diff --git a/ghost-core/src/process.rs b/ghost-core/src/process.rs index 244a735..e54bcde 100644 --- a/ghost-core/src/process.rs +++ b/ghost-core/src/process.rs @@ -202,13 +202,109 @@ mod platform { #[cfg(target_os = "macos")] mod platform { use super::ProcessInfo; - use anyhow::Result; + use anyhow::{Context, Result}; pub fn enumerate_processes() -> Result> { - // macOS implementation would use libproc or sysctl - // For now, return empty to indicate platform support is partial - log::warn!("macOS process enumeration not yet fully implemented"); - Ok(Vec::new()) + use libc::{c_int, c_void, size_t, sysctl, CTL_KERN, KERN_PROC, KERN_PROC_ALL}; + use std::mem; + use std::ptr; + + let mut processes = Vec::new(); + + unsafe { + // First, get the size needed for the buffer + let mut mib: [c_int; 4] = [CTL_KERN, KERN_PROC, KERN_PROC_ALL, 0]; + let mut size: size_t = 0; + + let result = sysctl( + mib.as_mut_ptr(), + 3, + ptr::null_mut(), + &mut size, + ptr::null_mut(), + 0, + ); + + if result != 0 { + return Err(anyhow::anyhow!( + "Failed to get process list size: {}", + std::io::Error::last_os_error() + )); + } + + // Allocate buffer with some extra space + let count = size / mem::size_of::(); + let mut buffer: Vec = Vec::with_capacity(count + 16); + buffer.resize_with(count + 16, || mem::zeroed()); + + let mut actual_size = buffer.len() * mem::size_of::(); + + let result = sysctl( + mib.as_mut_ptr(), + 3, + buffer.as_mut_ptr() as *mut c_void, + &mut actual_size, + ptr::null_mut(), + 0, + ); + + if result != 0 { + return Err(anyhow::anyhow!( + "Failed to get process list: {}", + std::io::Error::last_os_error() + )); + } + + let actual_count = actual_size / mem::size_of::(); + + for i in 0..actual_count { + let proc_info = &buffer[i]; + let pid = proc_info.kp_proc.p_pid as u32; + let ppid = proc_info.kp_eproc.e_ppid as u32; + + // Get process name from comm field + let comm = &proc_info.kp_proc.p_comm; + let name = std::ffi::CStr::from_ptr(comm.as_ptr()) + .to_string_lossy() + .into_owned(); + + // Get executable path using proc_pidpath + let path = get_process_path(pid as i32); + + processes.push(ProcessInfo { + pid, + ppid, + name, + path, + thread_count: 1, // Would need task_info for accurate count + }); + } + } + + Ok(processes) + } + + fn get_process_path(pid: i32) -> Option { + use std::ffi::CStr; + use std::os::raw::c_char; + + extern "C" { + fn proc_pidpath(pid: i32, buffer: *mut c_char, buffersize: u32) -> i32; + } + + unsafe { + let mut buffer = [0i8; libc::PATH_MAX as usize]; + let result = proc_pidpath(pid, buffer.as_mut_ptr(), libc::PATH_MAX as u32); + + if result > 0 { + CStr::from_ptr(buffer.as_ptr()) + .to_str() + .ok() + .map(|s| s.to_string()) + } else { + None + } + } } } @@ -228,7 +324,7 @@ mod platform { /// /// - **Windows**: Uses the ToolHelp API to enumerate processes. /// - **Linux**: Reads from the /proc filesystem. -/// - **macOS**: Partial support (not yet implemented). +/// - **macOS**: Uses sysctl KERN_PROC_ALL and proc_pidpath for process enumeration. /// /// # Errors /// diff --git a/ghost-core/src/shellcode.rs b/ghost-core/src/shellcode.rs index f2c0075..4ef5bc1 100644 --- a/ghost-core/src/shellcode.rs +++ b/ghost-core/src/shellcode.rs @@ -31,77 +31,309 @@ impl ShellcodeDetector { } fn initialize_signatures(&mut self) { - // GetProcAddress hash resolution pattern (common in position-independent code) + // ===== PEB/TEB Access Patterns (Windows) ===== + + // x86 PEB Access via FS segment self.signatures.push(ShellcodeSignature { - pattern: vec![0x64, 0x8B, 0x25, 0x30, 0x00, 0x00, 0x00], // mov esp, fs:[0x30] + pattern: vec![0x64, 0x8B, 0x15, 0x30, 0x00, 0x00, 0x00], // mov edx, fs:[0x30] mask: vec![0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0x00, 0x00], - name: "PEB Access Pattern", - confidence: 0.7, + name: "x86 PEB Access (fs:[0x30])", + confidence: 0.85, + }); + + // x86 PEB Access variant + self.signatures.push(ShellcodeSignature { + pattern: vec![0x64, 0xA1, 0x30, 0x00, 0x00, 0x00], // mov eax, fs:[0x30] + mask: vec![0xFF, 0xFF, 0xFF, 0x00, 0x00, 0x00], + name: "x86 PEB Access (fs:[0x30] via eax)", + confidence: 0.85, + }); + + // x64 PEB Access via GS segment + self.signatures.push(ShellcodeSignature { + pattern: vec![0x65, 0x48, 0x8B, 0x04, 0x25, 0x60, 0x00, 0x00, 0x00], // mov rax, gs:[0x60] + mask: vec![0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0x00, 0x00], + name: "x64 PEB Access (gs:[0x60])", + confidence: 0.9, + }); + + // x64 TEB Access + self.signatures.push(ShellcodeSignature { + pattern: vec![0x65, 0x48, 0x8B, 0x04, 0x25, 0x30, 0x00, 0x00, 0x00], // mov rax, gs:[0x30] + mask: vec![0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0x00, 0x00], + name: "x64 TEB Access (gs:[0x30])", + confidence: 0.8, + }); + + // ===== API Hashing Patterns ===== + + // ROR 13 hash (Metasploit style) + self.signatures.push(ShellcodeSignature { + pattern: vec![0xC1, 0xCF, 0x0D, 0x01, 0xC7], // ror edi, 0xD; add edi, eax + mask: vec![0xFF, 0xFF, 0xFF, 0xFF, 0xFF], + name: "ROR13 API Hash (Metasploit)", + confidence: 0.95, + }); + + // ROR 13 hash variant + self.signatures.push(ShellcodeSignature { + pattern: vec![0xC1, 0xCA, 0x0D, 0x01, 0xC2], // ror edx, 0xD; add edx, eax + mask: vec![0xFF, 0xFF, 0xFF, 0xFF, 0xFF], + name: "ROR13 API Hash Variant", + confidence: 0.95, + }); + + // x64 ROR 13 hash + self.signatures.push(ShellcodeSignature { + pattern: vec![0x48, 0xC1, 0xC9, 0x0D], // ror rcx, 0xD + mask: vec![0xFF, 0xFF, 0xFF, 0xFF], + name: "x64 ROR13 API Hash", + confidence: 0.9, + }); + + // FNV-1a hash pattern + self.signatures.push(ShellcodeSignature { + pattern: vec![0x69, 0xC0, 0x01, 0x00, 0x01, 0x00], // imul eax, eax, 0x01000193 + mask: vec![0xFF, 0xFF, 0x00, 0x00, 0x00, 0x00], + name: "FNV-1a Hash Pattern", + confidence: 0.85, + }); + + // ===== Shellcode Prologues ===== + + // Metasploit x64 staged reverse TCP + self.signatures.push(ShellcodeSignature { + pattern: vec![0xFC, 0x48, 0x83, 0xE4, 0xF0, 0xE8], // CLD; and rsp, -16; call + mask: vec![0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF], + name: "Metasploit x64 Reverse TCP", + confidence: 0.98, + }); + + // Metasploit x86 staged reverse TCP + self.signatures.push(ShellcodeSignature { + pattern: vec![0xFC, 0xE8, 0x82, 0x00, 0x00, 0x00], // CLD; call $+0x82 + mask: vec![0xFF, 0xFF, 0x00, 0x00, 0x00, 0x00], + name: "Metasploit x86 Reverse TCP", + confidence: 0.95, + }); + + // Cobalt Strike beacon + self.signatures.push(ShellcodeSignature { + pattern: vec![0xFC, 0x48, 0x83, 0xE4, 0xF0, 0xE8, 0xC8, 0x00, 0x00, 0x00], + mask: vec![0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0x00], + name: "Cobalt Strike Beacon Prologue", + confidence: 0.98, }); // Common x64 shellcode prologue self.signatures.push(ShellcodeSignature { - pattern: vec![0x48, 0x83, 0xEC, 0x00, 0x48, 0x89], // sub rsp, XX; mov - mask: vec![0xFF, 0xFF, 0xFF, 0x00, 0xFF, 0xFF], - name: "x64 Stack Setup", - confidence: 0.6, + pattern: vec![0x48, 0x83, 0xEC, 0x28, 0x48, 0x83, 0xE4, 0xF0], // sub rsp, 0x28; and rsp, -16 + mask: vec![0xFF, 0xFF, 0xFF, 0x00, 0xFF, 0xFF, 0xFF, 0xFF], + name: "x64 Stack Setup Pattern", + confidence: 0.7, }); - // Egg hunter pattern (searches for specific marker in memory) - self.signatures.push(ShellcodeSignature { - pattern: vec![0x66, 0x81, 0x3F], // cmp word ptr [edi], XXXX - mask: vec![0xFF, 0xFF, 0xFF], - name: "Egg Hunter Pattern", - confidence: 0.8, - }); + // ===== Position-Independent Code Patterns ===== - // API hashing pattern (djb2 hash commonly used) + // Call-pop technique (get current EIP/RIP) self.signatures.push(ShellcodeSignature { - pattern: vec![0xC1, 0xCF, 0x0D, 0x01, 0xC7], // ror edi, 0xD; add edi, eax - mask: vec![0xFF, 0xFF, 0xFF, 0xFF, 0xFF], - name: "DJB2 Hash Algorithm", + pattern: vec![0xE8, 0x00, 0x00, 0x00, 0x00, 0x58], // call $+5; pop eax + mask: vec![0xFF, 0x00, 0x00, 0x00, 0x00, 0xFF], + name: "Call-Pop GetPC (eax)", confidence: 0.9, }); - // Common Windows API call pattern + self.signatures.push(ShellcodeSignature { + pattern: vec![0xE8, 0x00, 0x00, 0x00, 0x00, 0x5B], // call $+5; pop ebx + mask: vec![0xFF, 0x00, 0x00, 0x00, 0x00, 0xFF], + name: "Call-Pop GetPC (ebx)", + confidence: 0.9, + }); + + self.signatures.push(ShellcodeSignature { + pattern: vec![0xE8, 0x00, 0x00, 0x00, 0x00, 0x5D], // call $+5; pop ebp + mask: vec![0xFF, 0x00, 0x00, 0x00, 0x00, 0xFF], + name: "Call-Pop GetPC (ebp)", + confidence: 0.9, + }); + + self.signatures.push(ShellcodeSignature { + pattern: vec![0xE8, 0x00, 0x00, 0x00, 0x00, 0x5E], // call $+5; pop esi + mask: vec![0xFF, 0x00, 0x00, 0x00, 0x00, 0xFF], + name: "Call-Pop GetPC (esi)", + confidence: 0.9, + }); + + // FPU-based GetPC (classic technique) + self.signatures.push(ShellcodeSignature { + pattern: vec![0xD9, 0xEE, 0xD9, 0x74, 0x24, 0xF4], // fldz; fnstenv [esp-12] + mask: vec![0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF], + name: "FPU GetPC Technique", + confidence: 0.95, + }); + + // ===== Egg Hunter Patterns ===== + + self.signatures.push(ShellcodeSignature { + pattern: vec![0x66, 0x81, 0xCA, 0xFF, 0x0F], // or dx, 0x0FFF (page alignment) + mask: vec![0xFF, 0xFF, 0xFF, 0xFF, 0xFF], + name: "Egg Hunter Page Scan", + confidence: 0.9, + }); + + self.signatures.push(ShellcodeSignature { + pattern: vec![0x6A, 0x02, 0x58, 0xCD, 0x2E], // push 2; pop eax; int 0x2E + mask: vec![0xFF, 0xFF, 0xFF, 0xFF, 0xFF], + name: "Egg Hunter NtAccessCheckAndAuditAlarm", + confidence: 0.95, + }); + + // ===== Windows API Function Resolution ===== + + // Walking InMemoryOrderModuleList + self.signatures.push(ShellcodeSignature { + pattern: vec![0x8B, 0x52, 0x0C, 0x8B, 0x52, 0x14], // mov edx, [edx+0x0C]; mov edx, [edx+0x14] + mask: vec![0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF], + name: "PEB_LDR_DATA Walk (x86)", + confidence: 0.92, + }); + + // x64 LDR walk + self.signatures.push(ShellcodeSignature { + pattern: vec![0x48, 0x8B, 0x52, 0x18, 0x48, 0x8B, 0x52, 0x20], // mov rdx, [rdx+0x18]; mov rdx, [rdx+0x20] + mask: vec![0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF], + name: "PEB_LDR_DATA Walk (x64)", + confidence: 0.92, + }); + + // ===== Syscall Patterns ===== + + // Direct syscall (x64 Windows) + self.signatures.push(ShellcodeSignature { + pattern: vec![0x4C, 0x8B, 0xD1, 0xB8], // mov r10, rcx; mov eax, + mask: vec![0xFF, 0xFF, 0xFF, 0xFF], + name: "Direct Syscall Setup (x64)", + confidence: 0.9, + }); + + // int 0x2E syscall (legacy Windows) + self.signatures.push(ShellcodeSignature { + pattern: vec![0xCD, 0x2E], // int 0x2E + mask: vec![0xFF, 0xFF], + name: "Legacy Syscall (int 0x2E)", + confidence: 0.85, + }); + + // sysenter (x86) + self.signatures.push(ShellcodeSignature { + pattern: vec![0x0F, 0x34], // sysenter + mask: vec![0xFF, 0xFF], + name: "Sysenter Instruction", + confidence: 0.8, + }); + + // syscall (x64) + self.signatures.push(ShellcodeSignature { + pattern: vec![0x0F, 0x05], // syscall + mask: vec![0xFF, 0xFF], + name: "Syscall Instruction", + confidence: 0.75, + }); + + // ===== Anti-Analysis Patterns ===== + + // IsDebuggerPresent check pattern + self.signatures.push(ShellcodeSignature { + pattern: vec![0x64, 0x8B, 0x15, 0x30, 0x00, 0x00, 0x00, 0x8B, 0x52, 0x02], // PEB->BeingDebugged + mask: vec![0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0xFF, 0xFF, 0xFF], + name: "IsDebuggerPresent Check", + confidence: 0.85, + }); + + // ===== Exploit Patterns ===== + + // NOP sled detection (various NOP equivalents) + self.signatures.push(ShellcodeSignature { + pattern: vec![0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90], // 8 NOPs + mask: vec![0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF], + name: "NOP Sled", + confidence: 0.6, + }); + + // PUSH/RET technique (for control flow hijacking) + self.signatures.push(ShellcodeSignature { + pattern: vec![0x68], // push imm32 + mask: vec![0xFF], + name: "PUSH/RET Control Flow", + confidence: 0.3, // Low confidence as standalone + }); + + // ===== Process Hollowing/Injection Indicators ===== + + // PE header in memory + self.signatures.push(ShellcodeSignature { + pattern: vec![0x4D, 0x5A, 0x90, 0x00], // MZ header with typical padding + mask: vec![0xFF, 0xFF, 0x00, 0x00], + name: "PE Header (MZ) in Memory", + confidence: 0.7, + }); + + // PE signature + self.signatures.push(ShellcodeSignature { + pattern: vec![0x50, 0x45, 0x00, 0x00], // PE\0\0 + mask: vec![0xFF, 0xFF, 0xFF, 0xFF], + name: "PE Signature in Memory", + confidence: 0.8, + }); + + // ===== Linux Shellcode Patterns ===== + + // Linux x86 execve("/bin/sh") + self.signatures.push(ShellcodeSignature { + pattern: vec![0x31, 0xC0, 0x50, 0x68, 0x2F, 0x2F, 0x73, 0x68], // xor eax, eax; push eax; push "//sh" + mask: vec![0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF], + name: "Linux x86 execve /bin/sh", + confidence: 0.98, + }); + + // Linux x64 execve pattern + self.signatures.push(ShellcodeSignature { + pattern: vec![0x48, 0x31, 0xD2, 0x48, 0xBB, 0xFF, 0x2F, 0x62, 0x69, 0x6E, 0x2F, 0x73, 0x68], // xor rdx, rdx; mov rbx, "/bin/sh" + mask: vec![0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF], + name: "Linux x64 execve /bin/sh", + confidence: 0.98, + }); + + // Linux connect-back pattern + self.signatures.push(ShellcodeSignature { + pattern: vec![0x6A, 0x66, 0x58, 0x6A, 0x01, 0x5B], // push 0x66; pop eax; push 1; pop ebx (socketcall) + mask: vec![0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF], + name: "Linux socketcall Pattern", + confidence: 0.9, + }); + + // ===== Indirect API Call Patterns ===== + + self.signatures.push(ShellcodeSignature { + pattern: vec![0xFF, 0xD0], // call eax + mask: vec![0xFF, 0xFF], + name: "Indirect Call (eax)", + confidence: 0.5, + }); + + self.signatures.push(ShellcodeSignature { + pattern: vec![0xFF, 0xD3], // call ebx + mask: vec![0xFF, 0xFF], + name: "Indirect Call (ebx)", + confidence: 0.5, + }); + self.signatures.push(ShellcodeSignature { pattern: vec![0xFF, 0x15], // call [address] mask: vec![0xFF, 0xFF], name: "Indirect API Call", confidence: 0.4, }); - - // NOP sled detection (common in exploits) - self.signatures.push(ShellcodeSignature { - pattern: vec![0x90, 0x90, 0x90, 0x90, 0x90, 0x90], // Multiple NOPs - mask: vec![0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF], - name: "NOP Sled", - confidence: 0.5, - }); - - // String loading pattern (common in shellcode) - self.signatures.push(ShellcodeSignature { - pattern: vec![0xE8, 0x00, 0x00, 0x00, 0x00, 0x5E], // call $+5; pop esi - mask: vec![0xFF, 0x00, 0x00, 0x00, 0x00, 0xFF], - name: "String Loading Technique", - confidence: 0.8, - }); - - // PE header in memory (process hollowing indicator) - self.signatures.push(ShellcodeSignature { - pattern: vec![0x4D, 0x5A], // MZ header - mask: vec![0xFF, 0xFF], - name: "PE Header in Memory", - confidence: 0.6, - }); - - // Common metasploit meterpreter pattern - self.signatures.push(ShellcodeSignature { - pattern: vec![0xFC, 0x48, 0x83, 0xE4, 0xF0, 0xE8], // CLD; and rsp, -16; call - mask: vec![0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF], - name: "Meterpreter Payload Pattern", - confidence: 0.95, - }); } /// Scan memory region for shellcode patterns diff --git a/ghost-core/src/testing.rs b/ghost-core/src/testing.rs index 0ae3622..959aa6c 100644 --- a/ghost-core/src/testing.rs +++ b/ghost-core/src/testing.rs @@ -817,8 +817,10 @@ impl TestFramework { let threads: Vec = params.thread_data.iter().map(|thread| { ThreadInfo { tid: thread.tid, + owner_pid: process_info.pid, start_address: thread.entry_point, creation_time: 0, + state: crate::thread::ThreadState::Running, } }).collect(); diff --git a/ghost-core/src/thread.rs b/ghost-core/src/thread.rs index 013a196..0cf71d6 100644 --- a/ghost-core/src/thread.rs +++ b/ghost-core/src/thread.rs @@ -1,31 +1,171 @@ +//! Thread enumeration and analysis for process injection detection. +//! +//! This module provides cross-platform thread introspection capabilities, +//! critical for detecting thread hijacking (T1055.003) and similar techniques. + +use serde::{Deserialize, Serialize}; use std::fmt; -#[derive(Debug, Clone)] +/// Information about a thread within a process. +#[derive(Debug, Clone, Serialize, Deserialize)] pub struct ThreadInfo { + /// Thread ID. pub tid: u32, + /// Process ID that owns this thread. pub owner_pid: u32, + /// Start address of the thread (entry point). pub start_address: usize, + /// Thread creation time (platform-specific format). pub creation_time: u64, + /// Thread state (Running, Waiting, etc.). + pub state: ThreadState, +} + +/// Thread execution state. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +pub enum ThreadState { + Running, + Waiting, + Suspended, + Terminated, + Unknown, +} + +impl fmt::Display for ThreadState { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let s = match self { + Self::Running => "Running", + Self::Waiting => "Waiting", + Self::Suspended => "Suspended", + Self::Terminated => "Terminated", + Self::Unknown => "Unknown", + }; + write!(f, "{}", s) + } } impl fmt::Display for ThreadInfo { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!( f, - "TID {} @ {:#x}", - self.tid, self.start_address + "TID {} @ {:#x} [{}]", + self.tid, self.start_address, self.state ) } } #[cfg(windows)] mod platform { - use super::ThreadInfo; + use super::{ThreadInfo, ThreadState}; use anyhow::{Context, Result}; use windows::Win32::Foundation::CloseHandle; use windows::Win32::System::Diagnostics::ToolHelp::{ CreateToolhelp32Snapshot, Thread32First, Thread32Next, THREADENTRY32, TH32CS_SNAPTHREAD, }; + use windows::Win32::System::Threading::{ + OpenThread, THREAD_QUERY_INFORMATION, THREAD_QUERY_LIMITED_INFORMATION, + }; + + /// Attempts to get thread start address using NtQueryInformationThread. + /// + /// This requires ntdll.dll and uses ThreadQuerySetWin32StartAddress. + fn get_thread_start_address(tid: u32) -> usize { + unsafe { + // Try to open the thread with query permissions + let thread_handle = match OpenThread(THREAD_QUERY_INFORMATION, false, tid) { + Ok(h) => h, + Err(_) => { + // Fall back to limited information access + match OpenThread(THREAD_QUERY_LIMITED_INFORMATION, false, tid) { + Ok(h) => h, + Err(_) => return 0, + } + } + }; + + // Load NtQueryInformationThread from ntdll + let ntdll = match windows::Win32::System::LibraryLoader::GetModuleHandleW( + windows::core::w!("ntdll.dll"), + ) { + Ok(h) => h, + Err(_) => { + let _ = CloseHandle(thread_handle); + return 0; + } + }; + + let proc_addr = windows::Win32::System::LibraryLoader::GetProcAddress( + ntdll, + windows::core::s!("NtQueryInformationThread"), + ); + + let start_address = if let Some(func) = proc_addr { + // ThreadQuerySetWin32StartAddress = 9 + type NtQueryInformationThreadFn = unsafe extern "system" fn( + thread_handle: windows::Win32::Foundation::HANDLE, + thread_information_class: u32, + thread_information: *mut std::ffi::c_void, + thread_information_length: u32, + return_length: *mut u32, + ) -> i32; + + let nt_query: NtQueryInformationThreadFn = std::mem::transmute(func); + let mut start_addr: usize = 0; + let mut return_length: u32 = 0; + + let status = nt_query( + thread_handle, + 9, // ThreadQuerySetWin32StartAddress + &mut start_addr as *mut usize as *mut std::ffi::c_void, + std::mem::size_of::() as u32, + &mut return_length, + ); + + if status == 0 { + start_addr + } else { + 0 + } + } else { + 0 + }; + + let _ = CloseHandle(thread_handle); + start_address + } + } + + /// Gets thread creation time using GetThreadTimes. + fn get_thread_creation_time(tid: u32) -> u64 { + unsafe { + let thread_handle = match OpenThread(THREAD_QUERY_LIMITED_INFORMATION, false, tid) { + Ok(h) => h, + Err(_) => return 0, + }; + + let mut creation_time = windows::Win32::Foundation::FILETIME::default(); + let mut exit_time = windows::Win32::Foundation::FILETIME::default(); + let mut kernel_time = windows::Win32::Foundation::FILETIME::default(); + let mut user_time = windows::Win32::Foundation::FILETIME::default(); + + let result = windows::Win32::System::Threading::GetThreadTimes( + thread_handle, + &mut creation_time, + &mut exit_time, + &mut kernel_time, + &mut user_time, + ); + + let _ = CloseHandle(thread_handle); + + if result.is_ok() { + // Convert FILETIME to u64 + ((creation_time.dwHighDateTime as u64) << 32) | (creation_time.dwLowDateTime as u64) + } else { + 0 + } + } + } pub fn enumerate_threads(pid: u32) -> Result> { let mut threads = Vec::new(); @@ -42,11 +182,16 @@ mod platform { if Thread32First(snapshot, &mut entry).is_ok() { loop { if entry.th32OwnerProcessID == pid { + let tid = entry.th32ThreadID; + let start_address = get_thread_start_address(tid); + let creation_time = get_thread_creation_time(tid); + threads.push(ThreadInfo { - tid: entry.th32ThreadID, + tid, owner_pid: entry.th32OwnerProcessID, - start_address: 0, // TODO: Get actual start address - creation_time: 0, // TODO: Get thread creation time + start_address, + creation_time, + state: ThreadState::Unknown, // Would need NtQueryInformationThread with ThreadBasicInformation }); } @@ -63,16 +208,277 @@ mod platform { } } -#[cfg(not(windows))] +#[cfg(target_os = "linux")] +mod platform { + use super::{ThreadInfo, ThreadState}; + use anyhow::{Context, Result}; + use std::fs; + + pub fn enumerate_threads(pid: u32) -> Result> { + let task_dir = format!("/proc/{}/task", pid); + let entries = + fs::read_dir(&task_dir).context(format!("Failed to read {}", task_dir))?; + + let mut threads = Vec::new(); + + for entry in entries.flatten() { + if let Some(tid_str) = entry.file_name().to_str() { + if let Ok(tid) = tid_str.parse::() { + let thread_info = get_thread_info(pid, tid); + threads.push(thread_info); + } + } + } + + Ok(threads) + } + + fn get_thread_info(pid: u32, tid: u32) -> ThreadInfo { + let stat_path = format!("/proc/{}/task/{}/stat", pid, tid); + let (state, start_time) = if let Ok(content) = fs::read_to_string(&stat_path) { + parse_thread_stat(&content) + } else { + (ThreadState::Unknown, 0) + }; + + // Get start address from /proc/[pid]/task/[tid]/syscall + let start_address = get_thread_start_address(pid, tid); + + ThreadInfo { + tid, + owner_pid: pid, + start_address, + creation_time: start_time, + state, + } + } + + fn parse_thread_stat(stat: &str) -> (ThreadState, u64) { + // Format: pid (comm) state ppid pgrp session tty_nr tpgid flags ... + // Field 22 (1-indexed) is starttime + let close_paren = match stat.rfind(')') { + Some(pos) => pos, + None => return (ThreadState::Unknown, 0), + }; + + let rest = &stat[close_paren + 2..]; + let fields: Vec<&str> = rest.split_whitespace().collect(); + + let state = if !fields.is_empty() { + match fields[0] { + "R" => ThreadState::Running, + "S" | "D" => ThreadState::Waiting, + "T" | "t" => ThreadState::Suspended, + "Z" | "X" => ThreadState::Terminated, + _ => ThreadState::Unknown, + } + } else { + ThreadState::Unknown + }; + + // starttime is field 22 (0-indexed: 19 after state) + let start_time = fields.get(19).and_then(|s| s.parse().ok()).unwrap_or(0); + + (state, start_time) + } + + fn get_thread_start_address(pid: u32, tid: u32) -> usize { + // Try to get the instruction pointer from /proc/[pid]/task/[tid]/syscall + let syscall_path = format!("/proc/{}/task/{}/syscall", pid, tid); + if let Ok(content) = fs::read_to_string(&syscall_path) { + // Format: syscall_number arg0 arg1 ... stack_pointer instruction_pointer + let fields: Vec<&str> = content.split_whitespace().collect(); + if fields.len() >= 9 { + // Last field is the instruction pointer + if let Some(ip_str) = fields.last() { + if let Ok(ip) = usize::from_str_radix(ip_str.trim_start_matches("0x"), 16) { + return ip; + } + } + } + } + + // Alternative: parse /proc/[pid]/task/[tid]/maps for the first executable region + 0 + } +} + +#[cfg(target_os = "macos")] +mod platform { + use super::{ThreadInfo, ThreadState}; + use anyhow::Result; + + pub fn enumerate_threads(pid: u32) -> Result> { + use libc::{mach_port_t, natural_t}; + use std::mem; + + // Mach thread info structures and constants + const THREAD_BASIC_INFO: i32 = 3; + const TH_STATE_RUNNING: i32 = 1; + const TH_STATE_STOPPED: i32 = 2; + const TH_STATE_WAITING: i32 = 3; + const TH_STATE_UNINTERRUPTIBLE: i32 = 4; + const TH_STATE_HALTED: i32 = 5; + + #[repr(C)] + #[derive(Default)] + struct thread_basic_info { + user_time: time_value_t, + system_time: time_value_t, + cpu_usage: i32, + policy: i32, + run_state: i32, + flags: i32, + suspend_count: i32, + sleep_time: i32, + } + + #[repr(C)] + #[derive(Default, Copy, Clone)] + struct time_value_t { + seconds: i32, + microseconds: i32, + } + + extern "C" { + fn task_for_pid( + target_tport: mach_port_t, + pid: i32, + task: *mut mach_port_t, + ) -> i32; + fn mach_task_self() -> mach_port_t; + fn task_threads( + target_task: mach_port_t, + act_list: *mut *mut mach_port_t, + act_list_cnt: *mut u32, + ) -> i32; + fn thread_info( + target_act: mach_port_t, + flavor: i32, + thread_info_out: *mut i32, + thread_info_out_cnt: *mut u32, + ) -> i32; + fn mach_port_deallocate(task: mach_port_t, name: mach_port_t) -> i32; + fn vm_deallocate( + target_task: mach_port_t, + address: usize, + size: usize, + ) -> i32; + } + + let mut threads = Vec::new(); + + unsafe { + let mut task: mach_port_t = 0; + let kr = task_for_pid(mach_task_self(), pid as i32, &mut task); + + if kr != 0 { + return Err(anyhow::anyhow!( + "task_for_pid failed with error code {}. Requires root or taskgated entitlement.", + kr + )); + } + + let mut thread_list: *mut mach_port_t = std::ptr::null_mut(); + let mut thread_count: u32 = 0; + + let kr = task_threads(task, &mut thread_list, &mut thread_count); + if kr != 0 { + return Err(anyhow::anyhow!( + "task_threads failed with error code {}", + kr + )); + } + + // Iterate through all threads + for i in 0..thread_count { + let thread_port = *thread_list.add(i as usize); + let tid = thread_port; // On macOS, thread port is often used as TID + + // Get thread basic info + let mut info: thread_basic_info = mem::zeroed(); + let mut info_count = + (mem::size_of::() / mem::size_of::()) as u32; + + let kr = thread_info( + thread_port, + THREAD_BASIC_INFO, + &mut info as *mut _ as *mut i32, + &mut info_count, + ); + + let state = if kr == 0 { + match info.run_state { + TH_STATE_RUNNING => ThreadState::Running, + TH_STATE_STOPPED | TH_STATE_HALTED => ThreadState::Suspended, + TH_STATE_WAITING | TH_STATE_UNINTERRUPTIBLE => ThreadState::Waiting, + _ => ThreadState::Unknown, + } + } else { + ThreadState::Unknown + }; + + // Calculate creation time from user_time + system_time (accumulated time) + let creation_time = if kr == 0 { + let total_microseconds = (info.user_time.seconds as u64 * 1_000_000 + + info.user_time.microseconds as u64) + + (info.system_time.seconds as u64 * 1_000_000 + + info.system_time.microseconds as u64); + total_microseconds + } else { + 0 + }; + + threads.push(ThreadInfo { + tid, + owner_pid: pid, + start_address: 0, // macOS doesn't easily expose thread start address + creation_time, + state, + }); + + // Deallocate the thread port + let _ = mach_port_deallocate(mach_task_self(), thread_port); + } + + // Deallocate the thread list + if !thread_list.is_null() && thread_count > 0 { + let _ = vm_deallocate( + mach_task_self(), + thread_list as usize, + (thread_count as usize) * mem::size_of::(), + ); + } + } + + Ok(threads) + } +} + +#[cfg(not(any(windows, target_os = "linux", target_os = "macos")))] mod platform { use super::ThreadInfo; use anyhow::Result; pub fn enumerate_threads(_pid: u32) -> Result> { - Ok(Vec::new()) + Err(anyhow::anyhow!( + "Thread enumeration not supported on this platform" + )) } } +/// Enumerates all threads for a process. +/// +/// # Platform Support +/// +/// - **Windows**: Uses CreateToolhelp32Snapshot with NtQueryInformationThread for start addresses. +/// - **Linux**: Parses /proc/[pid]/task/ directory. +/// - **macOS**: Not yet implemented. +/// +/// # Returns +/// +/// A vector of `ThreadInfo` structs containing thread details. +/// Critical for detecting thread hijacking (T1055.003) attacks. pub fn enumerate_threads(pid: u32) -> anyhow::Result> { platform::enumerate_threads(pid) } diff --git a/ghost-core/tests/detection_tests.rs b/ghost-core/tests/detection_tests.rs index 0eb3c85..559ae52 100644 --- a/ghost-core/tests/detection_tests.rs +++ b/ghost-core/tests/detection_tests.rs @@ -1,6 +1,11 @@ +//! Integration tests for Ghost detection engine. + #[cfg(test)] mod tests { - use ghost_core::{DetectionEngine, MemoryProtection, MemoryRegion, ProcessInfo, ThreatLevel}; + use ghost_core::{ + config::DetectionConfig, DetectionEngine, MemoryProtection, MemoryRegion, ProcessInfo, + ThreatLevel, + }; fn create_test_process() -> ProcessInfo { ProcessInfo { @@ -23,7 +28,7 @@ mod tests { #[test] fn test_clean_process_detection() { - let mut engine = DetectionEngine::new(); + let mut engine = DetectionEngine::new().expect("Failed to create engine"); let process = create_test_process(); let regions = vec![MemoryRegion { base_address: 0x400000, @@ -39,7 +44,7 @@ mod tests { #[test] fn test_rwx_region_detection() { - let mut engine = DetectionEngine::new(); + let mut engine = DetectionEngine::new().expect("Failed to create engine"); let process = create_test_process(); let regions = vec![create_rwx_region()]; @@ -51,7 +56,7 @@ mod tests { #[test] fn test_multiple_small_executable_regions() { - let mut engine = DetectionEngine::new(); + let mut engine = DetectionEngine::new().expect("Failed to create engine"); let process = create_test_process(); let regions = vec![ MemoryRegion { @@ -84,7 +89,7 @@ mod tests { #[test] fn test_baseline_tracking() { - let mut engine = DetectionEngine::new(); + let mut engine = DetectionEngine::new().expect("Failed to create engine"); let mut process = create_test_process(); let regions = vec![]; @@ -100,4 +105,252 @@ mod tests { .iter() .any(|i| i.contains("new threads"))); } + + #[test] + fn test_multiple_rwx_regions_high_severity() { + let mut engine = DetectionEngine::new().expect("Failed to create engine"); + let process = create_test_process(); + let regions = vec![ + MemoryRegion { + base_address: 0x10000000, + size: 0x1000, + protection: MemoryProtection::ReadWriteExecute, + region_type: "PRIVATE".to_string(), + }, + MemoryRegion { + base_address: 0x20000000, + size: 0x2000, + protection: MemoryProtection::ReadWriteExecute, + region_type: "PRIVATE".to_string(), + }, + MemoryRegion { + base_address: 0x30000000, + size: 0x3000, + protection: MemoryProtection::ReadWriteExecute, + region_type: "PRIVATE".to_string(), + }, + ]; + + let result = engine.analyze_process(&process, ®ions, None); + // Multiple RWX regions should be highly suspicious + assert_eq!(result.threat_level, ThreatLevel::Malicious); + assert!(result.confidence >= 0.5); + } + + #[test] + fn test_memory_protection_display() { + assert_eq!(format!("{}", MemoryProtection::NoAccess), "---"); + assert_eq!(format!("{}", MemoryProtection::ReadOnly), "R--"); + assert_eq!(format!("{}", MemoryProtection::ReadWrite), "RW-"); + assert_eq!(format!("{}", MemoryProtection::ReadExecute), "R-X"); + assert_eq!(format!("{}", MemoryProtection::ReadWriteExecute), "RWX"); + assert_eq!(format!("{}", MemoryProtection::Execute), "--X"); + } + + #[test] + fn test_process_info_display() { + let process = create_test_process(); + let display = format!("{}", process); + assert!(display.contains("1234")); + assert!(display.contains("test.exe")); + } + + #[test] + fn test_memory_region_display() { + let region = create_rwx_region(); + let display = format!("{}", region); + assert!(display.contains("RWX")); + assert!(display.contains("PRIVATE")); + } + + #[test] + fn test_threat_level_ordering() { + assert!(ThreatLevel::Clean < ThreatLevel::Suspicious); + assert!(ThreatLevel::Suspicious < ThreatLevel::Malicious); + } + + #[test] + fn test_detection_config_validation() { + let config = DetectionConfig::default(); + assert!(config.validate().is_ok()); + + let mut invalid_config = DetectionConfig::default(); + invalid_config.confidence_threshold = 1.5; // Invalid + assert!(invalid_config.validate().is_err()); + + invalid_config.confidence_threshold = -0.1; // Invalid + assert!(invalid_config.validate().is_err()); + } + + #[test] + fn test_detection_config_presets() { + let perf_config = DetectionConfig::performance_mode(); + let thorough_config = DetectionConfig::thorough_mode(); + + // Performance mode should have lower thresholds for faster scanning + assert!(perf_config.confidence_threshold <= thorough_config.confidence_threshold); + } + + #[test] + fn test_process_is_system_process() { + let mut process = create_test_process(); + assert!(!process.is_system_process()); + + process.pid = 0; + assert!(process.is_system_process()); + + process.pid = 4; + assert!(process.is_system_process()); + + process.pid = 100; + process.name = "System".to_string(); + assert!(process.is_system_process()); + } + + #[test] + fn test_engine_with_custom_config() { + let mut config = DetectionConfig::default(); + config.rwx_detection = false; + + let mut engine = DetectionEngine::with_config(config).expect("Failed to create engine"); + let process = create_test_process(); + let regions = vec![create_rwx_region()]; + + // With RWX detection disabled, should not flag the region + let result = engine.analyze_process(&process, ®ions, None); + // Might still detect based on other heuristics, but confidence should be lower + assert!(result.confidence < 0.5); + } + + #[test] + fn test_large_memory_region() { + let mut engine = DetectionEngine::new().expect("Failed to create engine"); + let process = create_test_process(); + let regions = vec![MemoryRegion { + base_address: 0x10000000, + size: 100 * 1024 * 1024, // 100MB region + protection: MemoryProtection::ReadWriteExecute, + region_type: "PRIVATE".to_string(), + }]; + + let result = engine.analyze_process(&process, ®ions, None); + assert_ne!(result.threat_level, ThreatLevel::Clean); + } + + #[test] + fn test_image_vs_private_region() { + let mut engine = DetectionEngine::new().expect("Failed to create engine"); + let process = create_test_process(); + + // IMAGE region with RX is normal + let image_regions = vec![MemoryRegion { + base_address: 0x400000, + size: 0x100000, + protection: MemoryProtection::ReadExecute, + region_type: "IMAGE".to_string(), + }]; + + let result = engine.analyze_process(&process, &image_regions, None); + assert_eq!(result.threat_level, ThreatLevel::Clean); + + // PRIVATE region with RX is suspicious + let private_regions = vec![MemoryRegion { + base_address: 0x10000000, + size: 0x1000, + protection: MemoryProtection::ReadExecute, + region_type: "PRIVATE".to_string(), + }]; + + let result2 = engine.analyze_process(&process, &private_regions, None); + // Private executable regions are suspicious but not as severe as RWX + assert!(result2.confidence > 0.0 || result2.indicators.len() > 0); + } +} + +#[cfg(test)] +mod mitre_tests { + use ghost_core::mitre::{MitreMapping, TechniqueId}; + + #[test] + fn test_technique_id_display() { + let id = TechniqueId::new("T1055", Some("001")); + assert_eq!(format!("{}", id), "T1055.001"); + + let id_no_sub = TechniqueId::new("T1055", None); + assert_eq!(format!("{}", id_no_sub), "T1055"); + } + + #[test] + fn test_mitre_mapping_creation() { + let mapping = MitreMapping::default(); + assert!(mapping.techniques.is_empty()); + } + + #[test] + fn test_technique_lookup() { + let mapping = MitreMapping::default(); + // Default mapping should have no techniques initially + assert!(mapping.get_technique("T1055").is_none()); + } +} + +#[cfg(test)] +mod threat_intel_tests { + use ghost_core::ThreatLevel; + + #[test] + fn test_threat_level_description() { + assert_eq!(ThreatLevel::Clean.description(), "No threats detected"); + assert_eq!( + ThreatLevel::Suspicious.description(), + "Potential security concern" + ); + assert_eq!( + ThreatLevel::Malicious.description(), + "High confidence malicious activity" + ); + } + + #[test] + fn test_threat_level_serialization() { + let level = ThreatLevel::Suspicious; + let serialized = serde_json::to_string(&level).expect("Failed to serialize"); + assert!(serialized.contains("Suspicious")); + + let deserialized: ThreatLevel = + serde_json::from_str(&serialized).expect("Failed to deserialize"); + assert_eq!(deserialized, level); + } +} + +#[cfg(test)] +mod config_tests { + use ghost_core::config::DetectionConfig; + + #[test] + fn test_default_config() { + let config = DetectionConfig::default(); + assert!(config.rwx_detection); + assert!(config.shellcode_detection); + assert!(config.hollowing_detection); + assert!(config.thread_detection); + assert!(config.hook_detection); + } + + #[test] + fn test_config_serialization() { + let config = DetectionConfig::default(); + let json = serde_json::to_string(&config).expect("Failed to serialize"); + let deserialized: DetectionConfig = + serde_json::from_str(&json).expect("Failed to deserialize"); + assert_eq!(config.rwx_detection, deserialized.rwx_detection); + } + + #[test] + fn test_config_toml_format() { + let config = DetectionConfig::default(); + let toml_str = toml::to_string(&config).expect("Failed to serialize to TOML"); + assert!(toml_str.contains("rwx_detection")); + assert!(toml_str.contains("confidence_threshold")); + } } \ No newline at end of file