feat: Add PE header validation and LD_PRELOAD detection

This commit is contained in:
pandaadir05
2025-11-17 22:02:41 +02:00
parent 96b0d12099
commit b1f098571d
15 changed files with 2708 additions and 459 deletions

5
.gitignore vendored
View File

@@ -32,3 +32,8 @@ config.local.toml
#Ai stuff #Ai stuff
.claude/ .claude/
.claude-plugins/
.claude-embeddings/
.claude*.cache/
.claude*/

View File

@@ -5,7 +5,7 @@
### Windows ### Windows
- Rust toolchain (MSVC target) - Rust toolchain (MSVC target)
- Visual Studio Build Tools with C++ workload - Visual Studio Build Tools with C++ workload
- Windows SDK - Windows SDK (for windows crate bindings)
Install via: Install via:
```powershell ```powershell
@@ -15,22 +15,76 @@ rustup default stable-msvc
### Linux ### Linux
- Rust toolchain - Rust toolchain
- GCC/Clang - 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 ### macOS
- Rust toolchain - Rust toolchain
- Xcode Command Line Tools - Xcode Command Line Tools (for libc bindings)
```bash
xcode-select --install
```
## Building ## Building
```bash ```bash
# Release build (recommended for performance)
cargo build --release cargo build --release
# Debug build
cargo build
# Check for compilation errors without full build
cargo check
``` ```
## Running ## Running
```bash ```bash
# CLI interface
cargo run --bin ghost-cli 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

View File

@@ -4,62 +4,64 @@ Cross-platform process injection detection framework written in Rust.
## Overview ## 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 ## Features
- **Multi-layer detection**: Memory analysis, behavioral patterns, and ML-based anomaly detection - **Memory Analysis**: RWX region detection, shellcode pattern scanning, memory protection analysis
- **MITRE ATT&CK mapping**: Automatic technique classification using the ATT&CK framework - **MITRE ATT&CK Mapping**: Technique identification using the ATT&CK framework
- **Threat intelligence**: Integration with threat feeds for IOC correlation and attribution - **Cross-platform Support**:
- **Cross-platform**: Windows (full support), Linux (with eBPF), macOS (planned) - **Windows**: Process enumeration, memory reading (ReadProcessMemory), thread analysis (NtQueryInformationThread), inline hook detection, PE header validation
- **Real-time monitoring**: Continuous scanning with configurable intervals - **Linux**: Process enumeration via procfs, memory region analysis (/proc/[pid]/maps), thread state monitoring, LD_PRELOAD detection, ptrace detection
- **Low overhead**: Performance-optimized for production environments - **macOS**: Process enumeration via sysctl/KERN_PROC_ALL
- **Real-time Monitoring**: Continuous scanning with configurable intervals
- **Threat Intelligence**: IOC storage and correlation framework
## Architecture ## Architecture
``` ```
ghost/ ghost/
├── ghost-core/ # Core detection engine (21 modules) ├── ghost-core/ # Core detection engine and platform abstractions
├── ghost-cli/ # Command-line interface ├── ghost-cli/ # Command-line interface
├── ghost-tui/ # Interactive terminal UI ├── ghost-tui/ # Interactive terminal UI (Ratatui-based)
├── examples/ # Configuration examples ├── examples/ # Configuration examples
└── docs/ # Technical documentation └── docs/ # Technical documentation
``` ```
### Core Modules ### Core Modules
- **Detection Engine**: Orchestrates all analysis components - **Detection Engine** ([detection.rs](ghost-core/src/detection.rs)): Orchestrates analysis and threat scoring
- **Memory Analysis**: RWX region detection, shellcode patterns - **Memory Analysis** ([memory.rs](ghost-core/src/memory.rs)): Platform-specific memory enumeration and reading
- **Process Hollowing**: PE header validation, memory gap analysis - **Process Enumeration** ([process.rs](ghost-core/src/process.rs)): Cross-platform process listing
- **Thread Analysis**: Start address validation, behavioral patterns - **Thread Analysis** ([thread.rs](ghost-core/src/thread.rs)): Thread enumeration with start address and creation time
- **Evasion Detection**: Anti-debugging, VM detection, obfuscation - **Hook Detection** ([hooks.rs](ghost-core/src/hooks.rs)): Inline hook detection via JMP pattern analysis
- **MITRE ATT&CK Engine**: Technique mapping and threat actor profiling - **MITRE ATT&CK** ([mitre.rs](ghost-core/src/mitre.rs)): Technique mapping and categorization
- **Threat Intelligence**: IOC matching and campaign correlation - **Configuration** ([config.rs](ghost-core/src/config.rs)): TOML-based configuration with validation
## Supported Detection Techniques ## Supported Detection Techniques
### Process Injection (T1055) ### Process Injection (T1055)
- RWX memory region detection - RWX memory region detection
- Private executable memory analysis - Private executable memory analysis
- Remote thread creation monitoring - Thread count anomaly detection
- SetWindowsHookEx injection (T1055.001) - Inline hook detection (JMP patches on ntdll.dll, kernel32.dll, user32.dll)
- Thread hijacking (T1055.003) - LD_PRELOAD and LD_LIBRARY_PATH detection (Linux)
- APC injection patterns (T1055.004) - Ptrace injection detection (Linux)
- Process hollowing (T1055.012) - SetWindowsHookEx hook enumeration
- Reflective DLL injection - Thread hijacking indicators (T1055.003)
- Process hollowing detection with PE header validation (T1055.012)
### Evasion Techniques ### Memory Analysis
- Anti-debugging detection - Memory protection flags (R/W/X combinations)
- Virtual machine detection attempts - Region type classification (IMAGE, PRIVATE, MAPPED, HEAP, STACK)
- Code obfuscation analysis - Small executable region detection (shellcode indicators)
- Timing-based analysis evasion - Memory region size anomalies
- Environment fingerprinting
### Behavioral Anomalies ### Behavioral Monitoring
- Thread count deviations - Thread count changes from baseline
- Memory allocation patterns - New thread creation detection
- API call sequences - Process parent-child relationships
- Process relationship analysis - System process identification
## Installation ## Installation
@@ -192,6 +194,27 @@ Please review [SECURITY.md](SECURITY.md) for:
- Security considerations - Security considerations
- Threat model - 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 ## 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)

View File

@@ -46,11 +46,37 @@ Monitors thread count changes over time. Sudden increases may indicate CreateRem
Threads created by external processes via CreateRemoteThread or NtCreateThreadEx. Threads created by external processes via CreateRemoteThread or NtCreateThreadEx.
**Detection Logic** (Planned): **Detection Logic**:
- Compare thread creator PID with owner PID - Enumerate threads using CreateToolhelp32Snapshot (Windows) or /proc/[pid]/task (Linux)
- Check thread start addresses against known modules - 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 - 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 ## Heuristic Analysis
### Confidence Scoring ### Confidence Scoring
@@ -74,23 +100,37 @@ Ghost uses weighted confidence scoring:
### Windows ### Windows
- [x] Classic DLL injection detection - [x] Classic DLL injection detection
- [x] Memory region analysis - [x] Memory region analysis (VirtualQueryEx)
- [x] Thread enumeration - [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 - [ ] APC injection detection
- [ ] Process hollowing detection - [ ] SetWindowsHookEx chain enumeration
- [ ] Hook detection (IAT/EAT) - [ ] Reflective DLL injection signature matching
- [ ] Reflective DLL injection
### Linux ### Linux
- [ ] ptrace injection - [x] Process enumeration (/proc filesystem)
- [ ] LD_PRELOAD detection - [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 - [ ] process_vm_writev monitoring
- [ ] Shared memory inspection - [ ] Shared memory inspection
### macOS ### 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 - [ ] task_for_pid monitoring
- [ ] Mach port analysis - [ ] Mach port analysis

View File

@@ -46,7 +46,9 @@ Ghost detection engine coverage mapped to MITRE ATT&CK framework techniques.
- **Indicators**: - **Indicators**:
- Unmapped main executable image - Unmapped main executable image
- Suspicious memory gaps (>16MB) - Suspicious memory gaps (>16MB)
- PE header mismatches - PE header validation (DOS/NT signatures)
- Image base mismatches
- Corrupted PE structures
- Unusual entry point locations - Unusual entry point locations
- Memory layout anomalies - Memory layout anomalies
- **Confidence**: Very High (0.8-1.0) - **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 | | Technique | Detection Module | Implementation Status | Test Coverage |
|-----------|------------------|----------------------|---------------| |-----------|------------------|----------------------|---------------|
| T1055.001 | hooks.rs | ✅ Complete | ✅ Tested | | T1055.001 | hooks.rs | ✅ Inline hooks + Linux LD_PRELOAD | ❌ Basic |
| T1055.002 | shellcode.rs | ✅ Complete | ✅ Tested | | T1055.002 | shellcode.rs | ⚠️ Heuristic only | ✅ Basic |
| T1055.003 | thread.rs | ✅ Complete | ✅ Tested | | T1055.003 | thread.rs | ✅ Thread enumeration | ✅ Unit tests |
| T1055.004 | detection.rs | ⚠️ Partial | ✅ Tested | | T1055.004 | detection.rs | ⚠️ Heuristic only | ✅ Basic |
| T1055.012 | hollowing.rs | ✅ Complete | ✅ Tested | | T1055.012 | hollowing.rs | ✅ PE header validation | ❌ Pending |
| T1027 | shellcode.rs | ✅ Complete | ✅ Tested | | T1027 | shellcode.rs | ⚠️ Basic patterns | ❌ Pending |
| T1036 | process.rs | ⚠️ Partial | ❌ Pending | | T1036 | process.rs | ❌ Not implemented | ❌ Pending |
| T1106 | detection.rs | ⚠️ Basic | ❌ 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 ## Future Enhancements
### High Priority ### High Priority
- **T1055.008** - Ptrace System Calls (Linux) - **T1055.008** - Ptrace System Calls (Linux) - ✅ Basic detection implemented
- **T1055.009** - Proc Memory (Linux)
- **T1055.013** - Process Doppelgänging - **T1055.013** - Process Doppelgänging
- **T1055.014** - VDSO Hijacking (Linux) - **T1055.014** - VDSO Hijacking (Linux)
- Shellcode signature database
### Medium Priority ### Medium Priority
- **T1134** - Access Token Manipulation - **T1134** - Access Token Manipulation
- **T1548.002** - Bypass User Account Control - SetWindowsHookEx chain enumeration
- **T1562.001** - Disable or Modify Tools - IAT/EAT hook scanning
- LD_PRELOAD detection (Linux) - ✅ Implemented
### Research Areas ### Research Areas
- Machine learning-based anomaly detection - Behavioral analysis over time
- Graph analysis of process relationships - Process relationship analysis
- Timeline analysis for attack progression
- Integration with threat intelligence feeds - Integration with threat intelligence feeds
## References ## References

View File

@@ -2,298 +2,197 @@
## Overview ## 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 ## Performance Characteristics
### Detection Engine Performance ### Expected Detection Engine Performance
- **Scan Speed**: 500-1000 processes/second on modern hardware - **Process Enumeration**: 10-50ms for all system processes
- **Memory Usage**: 50-100MB base footprint - **Memory Region Analysis**: 1-5ms per process (platform-dependent)
- **CPU Impact**: <2% during active monitoring - **Thread Enumeration**: 1-10ms per process
- **Latency**: <10ms detection response time - **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 ```rust
// Configure detection modules based on threat landscape use ghost_core::config::DetectionConfig;
let mut config = DetectionConfig::new();
config.enable_shellcode_detection(true); // Disable expensive detections for performance
config.enable_hook_detection(false); // Disable if not needed let mut config = DetectionConfig::default();
config.enable_anomaly_detection(true); 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 ```rust
// Process multiple items in batches for efficiency // Fast scanning mode
let processes = enumerate_processes()?; let config = DetectionConfig::performance_mode();
let results: Vec<DetectionResult> = processes
.chunks(10) // Thorough scanning mode
.flat_map(|chunk| engine.analyze_batch(chunk)) let config = DetectionConfig::thorough_mode();
.collect();
``` ```
#### 3. Memory Pool Management #### 3. Process Filtering
```rust ```rust
// Pre-allocate memory pools to reduce allocations // Skip system processes
pub struct MemoryPool { config.skip_system_processes = true;
process_buffers: Vec<ProcessBuffer>,
detection_results: Vec<DetectionResult>, // 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 **Windows**:
use ghost_core::metrics::PerformanceMonitor; - 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(); **Linux**:
monitor.start_collection(); - /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(); ### Running Tests
println!("Avg scan time: {:.2}ms", stats.avg_scan_time);
println!("Memory usage: {}MB", stats.memory_usage_mb);
```
### Custom Benchmarks
```bash ```bash
# Run comprehensive benchmarks # Run all tests including performance assertions
cargo bench cargo test
# Profile specific operations # Run tests with timing output
cargo bench -- shellcode_detection cargo test -- --nocapture
cargo bench -- process_enumeration
``` ```
## Tuning Guidelines ## Tuning Guidelines
### For High-Volume Environments ### For Continuous Monitoring
1. **Increase batch sizes**: Process 20-50 items per batch 1. **Adjust scan interval**: Configure `scan_interval_ms` in DetectionConfig
2. **Reduce scan frequency**: 2-5 second intervals 2. **Skip system processes**: Set `skip_system_processes = true`
3. **Enable result caching**: Cache stable process states 3. **Limit memory scans**: Reduce `max_memory_scan_size`
4. **Use filtered scanning**: Skip known-good processes 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 1. **Enable all detections**: Use `DetectionConfig::thorough_mode()`
2. **Increase scan frequency**: Sub-second intervals 2. **Full memory scanning**: Increase `max_memory_scan_size`
3. **Disable heavy detections**: Skip complex ML analysis 3. **Include system processes**: Set `skip_system_processes = false`
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()
};
```
## Platform-Specific Optimizations ## Platform-Specific Optimizations
### Windows ### Windows
- Use `SetProcessWorkingSetSize` to limit memory - Run as Administrator for full process access
- Enable `SE_INCREASE_QUOTA_NAME` privilege for better access - Use `PROCESS_QUERY_LIMITED_INFORMATION` when `PROCESS_QUERY_INFORMATION` fails
- Leverage Windows Performance Toolkit (WPT) for profiling - Handle access denied errors gracefully (system processes)
### Linux ### Linux
- Use `cgroups` for resource isolation - Run with appropriate privileges (root or CAP_SYS_PTRACE)
- Enable `CAP_SYS_PTRACE` for enhanced process access - Handle permission denied for /proc/[pid]/mem gracefully
- Leverage `perf` for detailed performance analysis - 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 ## Troubleshooting Performance Issues
### High CPU Usage ### High CPU Usage
1. Check scan frequency settings 1. Reduce scan frequency (`scan_interval_ms`)
2. Verify filter effectiveness 2. Disable thread analysis for each scan
3. Profile detection module performance 3. Skip memory region enumeration
4. Consider disabling expensive detections 4. Filter out known-good processes
### High Memory Usage ### High Memory Usage
1. Monitor result cache sizes 1. Reduce baseline cache size (limited processes tracked)
2. Check for memory leaks in custom modules 2. Clear detection history periodically
3. Verify proper cleanup of process handles 3. Limit memory reading buffer sizes
4. Consider reducing batch sizes
### Slow Detection Response ### Slow Detection Response
1. Profile individual detection modules 1. Disable hook detection (expensive module enumeration)
2. Check system resource availability 2. Skip shellcode pattern matching
3. Verify network latency (if applicable) 3. Use performance preset mode
4. Consider async processing optimization
## 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
``` **Current architecture**:
Process Enumeration: 2.3ms (avg) - Sequential process scanning
Shellcode Detection: 0.8ms per process - Simple HashMap for baseline tracking
Hook Detection: 1.2ms per process - Basic confidence scoring
Anomaly Analysis: 3.5ms per process - Manual timer-based intervals (TUI)
Full Scan (100 proc): 847ms total
```
### Memory Usage ## Testing Performance
```
Base Engine: 45MB
+ Shellcode Patterns: +12MB
+ ML Models: +23MB
+ Result Cache: +15MB (1000 entries)
Total Runtime: 95MB typical
```
## Advanced Optimizations
### SIMD Acceleration
```rust ```rust
// Enable SIMD for pattern matching #[test]
#[cfg(target_feature = "avx2")] fn test_detection_performance() {
use std::arch::x86_64::*; use std::time::Instant;
// Vectorized shellcode scanning let mut engine = DetectionEngine::new().unwrap();
unsafe fn simd_pattern_search(data: &[u8], pattern: &[u8]) -> bool { let process = ProcessInfo::new(1234, 4, "test.exe".to_string());
// AVX2 accelerated pattern matching let regions = vec![/* test regions */];
}
```
### Multi-threading let start = Instant::now();
for _ in 0..100 {
```rust engine.analyze_process(&process, &regions, None);
use rayon::prelude::*;
// Parallel process analysis
let results: Vec<DetectionResult> = processes
.par_iter()
.map(|process| engine.analyze_process(process))
.collect();
```
### Caching Strategies
```rust
use lru::LruCache;
pub struct DetectionCache {
process_hashes: LruCache<u32, u64>,
shellcode_results: LruCache<u64, bool>,
anomaly_profiles: LruCache<u32, ProcessProfile>,
}
```
## 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<WebSocket>,
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 duration = start.elapsed();
// Should complete 100 analyses in under 100ms
assert!(duration.as_millis() < 100);
} }
``` ```
## Best Practices ## Best Practices
1. **Profile First**: Always benchmark before optimizing 1. **Start with defaults**: Use `DetectionConfig::default()` initially
2. **Measure Impact**: Quantify optimization effectiveness 2. **Profile specific modules**: Identify which detection is slow
3. **Monitor Production**: Continuous performance monitoring 3. **Adjust based on needs**: Disable features you don't need
4. **Gradual Tuning**: Make incremental adjustments 4. **Handle errors gracefully**: Processes may exit during scan
5. **Document Changes**: Track optimization history 5. **Test on target hardware**: Performance varies by system
## Performance Testing Framework ## Future Performance Improvements
```rust Potential enhancements (not yet implemented):
#[cfg(test)] - Parallel process analysis using rayon
mod performance_tests { - Async I/O for file system operations (Linux)
use super::*; - Result caching with TTL
use std::time::Instant; - Incremental scanning (only changed processes)
- Memory-mapped file parsing
#[test] - SIMD pattern matching for shellcode
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)

View File

@@ -19,9 +19,11 @@ toml = "0.8"
windows = { version = "0.58", features = [ windows = { version = "0.58", features = [
"Win32_Foundation", "Win32_Foundation",
"Win32_System_Diagnostics_ToolHelp", "Win32_System_Diagnostics_ToolHelp",
"Win32_System_Diagnostics_Debug",
"Win32_System_Threading", "Win32_System_Threading",
"Win32_System_ProcessStatus", "Win32_System_ProcessStatus",
"Win32_System_Memory", "Win32_System_Memory",
"Win32_System_LibraryLoader",
"Win32_Security", "Win32_Security",
] } ] }

View File

@@ -1,5 +1,8 @@
use crate::{GhostError, MemoryRegion, ProcessInfo, Result}; use crate::{GhostError, MemoryRegion, ProcessInfo, Result};
#[cfg(windows)]
use crate::memory::{validate_pe_header, read_pe_header_info, PEHeaderValidation};
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct HollowingDetection { pub struct HollowingDetection {
pub pid: u32, pub pid: u32,
@@ -14,6 +17,8 @@ pub enum HollowingIndicator {
SuspiciousImageBase, SuspiciousImageBase,
MemoryLayoutAnomaly { expected_size: usize, actual_size: usize }, MemoryLayoutAnomaly { expected_size: usize, actual_size: usize },
MismatchedPEHeader, MismatchedPEHeader,
InvalidPEHeader { validation: String },
CorruptedPEStructure { address: usize, reason: String },
UnusualEntryPoint { address: usize }, UnusualEntryPoint { address: usize },
SuspiciousMemoryGaps { gap_count: usize, largest_gap: 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) write!(f, "Memory layout anomaly: expected {:#x}, found {:#x}", expected_size, actual_size)
} }
Self::MismatchedPEHeader => write!(f, "PE header mismatch detected"), 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 } => { Self::UnusualEntryPoint { address } => {
write!(f, "Entry point at unusual location: {:#x}", address) write!(f, "Entry point at unusual location: {:#x}", address)
} }
@@ -72,12 +83,18 @@ impl HollowingDetector {
confidence += 0.4; 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) { if let Some(indicator) = self.check_pe_header_anomalies(memory_regions) {
indicators.push(indicator); indicators.push(indicator);
confidence += 0.7; 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 // Check entry point location
if let Some(indicator) = self.check_entry_point_anomalies(process, memory_regions) { if let Some(indicator) = self.check_entry_point_anomalies(process, memory_regions) {
indicators.push(indicator); indicators.push(indicator);
@@ -223,6 +240,71 @@ impl HollowingDetector {
None None
} }
#[cfg(windows)]
fn validate_pe_headers(&self, pid: u32, regions: &[MemoryRegion]) -> Option<HollowingIndicator> {
// 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<HollowingIndicator> {
// PE validation is Windows-specific
None
}
fn check_entry_point_anomalies( fn check_entry_point_anomalies(
&self, &self,
_process: &ProcessInfo, _process: &ProcessInfo,

View File

@@ -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 crate::{GhostError, Result};
use serde::{Deserialize, Serialize};
use std::collections::HashMap; use std::collections::HashMap;
#[derive(Debug, Clone)] /// Type of hook detected.
pub struct HookInfo { #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub hook_type: u32, pub enum HookType {
pub thread_id: u32, /// SetWindowsHookEx hook (message hook).
pub hook_proc: usize, WindowsHook(u32),
pub module_name: String, /// 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 { pub struct HookDetectionResult {
/// List of detected hooks.
pub hooks: Vec<HookInfo>, pub hooks: Vec<HookInfo>,
/// Number of suspicious hooks.
pub suspicious_count: usize, pub suspicious_count: usize,
/// Number of global/system-wide hooks.
pub global_hooks: usize, pub global_hooks: usize,
/// Number of inline API hooks detected.
pub inline_hooks: usize,
} }
#[cfg(windows)] #[cfg(windows)]
mod platform { mod platform {
use super::{HookDetectionResult, HookInfo}; use super::{HookDetectionResult, HookInfo, HookType};
use crate::{GhostError, Result}; use crate::{GhostError, Result};
use std::collections::HashMap; use std::collections::HashMap;
use windows::Win32::Foundation::{GetLastError, HWND}; use windows::Win32::Foundation::CloseHandle;
use windows::Win32::System::LibraryLoader::{GetModuleFileNameW, GetModuleHandleW}; 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::{ use windows::Win32::UI::WindowsAndMessaging::{
EnumWindows, GetWindowThreadProcessId, HC_ACTION, HOOKPROC, WH_CALLWNDPROC, WH_CALLWNDPROC, WH_CALLWNDPROCRET, WH_CBT, WH_DEBUG, WH_FOREGROUNDIDLE, WH_GETMESSAGE,
WH_CALLWNDPROCRET, WH_CBT, WH_DEBUG, WH_FOREGROUNDIDLE, WH_GETMESSAGE, WH_JOURNALPLAYBACK, WH_JOURNALPLAYBACK, WH_JOURNALRECORD, WH_KEYBOARD, WH_KEYBOARD_LL, WH_MOUSE,
WH_JOURNALRECORD, WH_KEYBOARD, WH_KEYBOARD_LL, WH_MOUSE, WH_MOUSE_LL, WH_MSGFILTER, WH_MOUSE_LL, WH_MSGFILTER, WH_SHELL, WH_SYSMSGFILTER,
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<HookDetectionResult> { pub fn detect_hook_injection(target_pid: u32) -> Result<HookDetectionResult> {
let mut hooks = Vec::new(); let mut hooks = Vec::new();
let mut suspicious_count = 0; let mut suspicious_count = 0;
let mut global_hooks = 0; let mut global_hooks = 0;
let mut inline_hooks = 0;
// This is a simplified implementation - real hook detection requires // Detect inline hooks in critical APIs
// more sophisticated techniques like parsing USER32.dll's hook table match detect_inline_hooks(target_pid) {
// or using undocumented APIs. For now, we'll detect based on heuristics. Ok(inline) => {
inline_hooks = inline.len();
// Check for global hooks that might be used for injection for hook in inline {
if let Ok(global_hook_count) = count_global_hooks() { if is_suspicious_inline_hook(&hook) {
global_hooks = global_hook_count;
if global_hook_count > 5 {
suspicious_count += 1;
}
}
// 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; suspicious_count += 1;
} }
hooks.push(hook); hooks.push(hook);
} }
} }
Err(e) => {
log::debug!("Failed to detect inline hooks: {}", e);
}
}
// Estimate global hooks based on system state
global_hooks = estimate_global_hooks();
if global_hooks > 10 {
suspicious_count += 1;
}
Ok(HookDetectionResult { Ok(HookDetectionResult {
hooks, hooks,
suspicious_count, suspicious_count,
global_hooks, global_hooks,
inline_hooks,
}) })
} }
fn count_global_hooks() -> Result<usize> { /// Detect inline (detour) hooks by checking for JMP instructions at API entry points.
// In a real implementation, this would examine the global hook chain fn detect_inline_hooks(target_pid: u32) -> Result<Vec<HookInfo>> {
// 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<Vec<HookInfo>> {
let mut hooks = Vec::new(); let mut hooks = Vec::new();
// Real implementation would: unsafe {
// 1. Enumerate all threads in the process let handle = OpenProcess(PROCESS_QUERY_INFORMATION | PROCESS_VM_READ, false, target_pid)
// 2. Check each thread's hook chain .map_err(|e| GhostError::Process {
// 3. Validate hook procedures and their locations message: format!("Failed to open process: {}", e),
// 4. Cross-reference with loaded modules })?;
// Simplified detection: check for common hook types that might indicate injection // Get loaded modules in target process
let common_injection_hooks = vec![ let mut modules = [windows::Win32::Foundation::HMODULE::default(); 1024];
(WH_CALLWNDPROC.0, "WH_CALLWNDPROC"), let mut cb_needed = 0u32;
(WH_GETMESSAGE.0, "WH_GETMESSAGE"),
(WH_CBT.0, "WH_CBT"),
(WH_KEYBOARD_LL.0, "WH_KEYBOARD_LL"),
(WH_MOUSE_LL.0, "WH_MOUSE_LL"),
];
// This is a placeholder - real hook enumeration requires low-level API calls let result = EnumProcessModulesEx(
// or kernel debugging interfaces handle,
for (hook_type, _name) in common_injection_hooks { modules.as_mut_ptr(),
if might_have_hook(pid, hook_type) { (modules.len() * std::mem::size_of::<windows::Win32::Foundation::HMODULE>()) as u32,
hooks.push(HookInfo { &mut cb_needed,
hook_type, LIST_MODULES_ALL,
thread_id: 0, // Would get actual thread ID );
hook_proc: 0, // Would get actual procedure address
module_name: "unknown".to_string(), 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::<windows::Win32::Foundation::HMODULE>();
// 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::<MODULEINFO>() 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::<Vec<_>>()
.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) Ok(hooks)
} }
fn might_have_hook(pid: u32, hook_type: u32) -> bool { /// Detect common hook patterns in function prologue.
// Heuristic: certain processes are more likely to have hooks fn detect_hook_pattern(bytes: &[u8], base_addr: usize) -> Option<usize> {
// This is a simplified check - real implementation would examine memory if bytes.len() < 5 {
hook_type == WH_KEYBOARD_LL.0 || hook_type == WH_MOUSE_LL.0 return None;
} }
fn is_suspicious_hook(hook: &HookInfo) -> bool { // JMP rel32 (E9 xx xx xx xx)
// Check for hooks with suspicious characteristics if bytes[0] == 0xE9 {
match hook.hook_type { let offset = i32::from_le_bytes([bytes[1], bytes[2], bytes[3], bytes[4]]);
t if t == WH_CALLWNDPROC.0 => true, // Often used for injection let target = (base_addr as i64 + 5 + offset as i64) as usize;
t if t == WH_GETMESSAGE.0 => true, // Common injection vector return Some(target);
t if t == WH_CBT.0 => true, // Can be used maliciously
t if t == WH_DEBUG.0 => true, // Debugging hooks are suspicious
_ => false,
}
} }
/// Get hook type name for display // 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
}
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 { pub fn get_hook_type_name(hook_type: u32) -> &'static str {
match hook_type { match hook_type {
t if t == WH_CALLWNDPROC.0 => "WH_CALLWNDPROC", 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<HookDetectionResult> {
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<Vec<HookInfo>> {
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<Vec<HookInfo>> {
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<bool> {
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::<u32>().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<Vec<HookInfo>> {
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 { mod platform {
use super::HookDetectionResult; use super::HookDetectionResult;
use crate::{GhostError, Result}; use crate::{GhostError, Result};
pub fn detect_hook_injection(_target_pid: u32) -> Result<HookDetectionResult> { pub fn detect_hook_injection(_target_pid: u32) -> Result<HookDetectionResult> {
Err(GhostError::Detection { // Hook detection is not implemented for this platform
message: "Hook detection not implemented for this platform".to_string(), Ok(HookDetectionResult {
hooks: Vec::new(),
suspicious_count: 0,
global_hooks: 0,
inline_hooks: 0,
}) })
} }

View File

@@ -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; 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 { pub enum MemoryProtection {
NoAccess, NoAccess,
ReadOnly, 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 { pub struct MemoryRegion {
/// Base address of the memory region.
pub base_address: usize, pub base_address: usize,
/// Size of the region in bytes.
pub size: usize, pub size: usize,
/// Memory protection flags.
pub protection: MemoryProtection, pub protection: MemoryProtection,
/// Type of memory region (IMAGE, MAPPED, PRIVATE, etc.).
pub region_type: String, 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<PEHeaderValidation> {
use std::mem;
// Read DOS header
let dos_header_size = mem::size_of::<ImageDosHeader>();
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::<ImageFileHeader>();
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::<ImageOptionalHeader64>();
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<PEHeaderValidation> {
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<Option<PEHeaderInfo>> {
use std::mem;
let dos_header_size = mem::size_of::<ImageDosHeader>();
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::<ImageFileHeader>();
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::<ImageOptionalHeader64>();
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<Option<PEHeaderInfo>> {
Ok(None)
}
#[cfg(windows)] #[cfg(windows)]
mod platform { mod platform {
use super::{MemoryProtection, MemoryRegion}; use super::{MemoryProtection, MemoryRegion};
use anyhow::{Context, Result}; 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::Diagnostics::Debug::ReadProcessMemory;
use windows::Win32::System::Memory::{ use windows::Win32::System::Memory::{
VirtualQueryEx, MEMORY_BASIC_INFORMATION, MEM_COMMIT, MEM_FREE, MEM_IMAGE, MEM_MAPPED, VirtualQueryEx, MEMORY_BASIC_INFORMATION, MEM_COMMIT, MEM_IMAGE, MEM_MAPPED, MEM_PRIVATE,
MEM_PRIVATE, MEM_RESERVE, PAGE_EXECUTE, PAGE_EXECUTE_READ, PAGE_EXECUTE_READWRITE, PAGE_EXECUTE, PAGE_EXECUTE_READ, PAGE_EXECUTE_READWRITE, PAGE_EXECUTE_WRITECOPY,
PAGE_EXECUTE_WRITECOPY, PAGE_NOACCESS, PAGE_READONLY, PAGE_READWRITE, PAGE_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 { fn parse_protection(protect: u32) -> MemoryProtection {
match protect & 0xFF { match protect & 0xFF {
@@ -80,7 +398,6 @@ mod platform {
pub fn enumerate_memory_regions(pid: u32) -> Result<Vec<MemoryRegion>> { pub fn enumerate_memory_regions(pid: u32) -> Result<Vec<MemoryRegion>> {
let mut regions = Vec::new(); let mut regions = Vec::new();
// Skip system process
if pid == 0 || pid == 4 { if pid == 0 || pid == 4 {
return Ok(regions); return Ok(regions);
} }
@@ -138,19 +455,358 @@ mod platform {
Ok(regions) 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<Vec<u8>> {
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<Vec<MemoryRegion>> {
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<MemoryRegion> {
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<Vec<u8>> {
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<Vec<MemoryRegion>> {
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::<vm_region_basic_info_64>()
/ mem::size_of::<natural_t>()) 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<Vec<u8>> {
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 { mod platform {
use super::MemoryRegion; use super::MemoryRegion;
use anyhow::Result; use anyhow::Result;
pub fn enumerate_memory_regions(_pid: u32) -> Result<Vec<MemoryRegion>> { pub fn enumerate_memory_regions(_pid: u32) -> Result<Vec<MemoryRegion>> {
// TODO: Implement Linux/macOS memory enumeration Err(anyhow::anyhow!(
Ok(Vec::new()) "Memory enumeration not supported on this platform"
))
}
pub fn read_process_memory(_pid: u32, _address: usize, _size: usize) -> Result<Vec<u8>> {
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<Vec<MemoryRegion>> { pub fn enumerate_memory_regions(pid: u32) -> anyhow::Result<Vec<MemoryRegion>> {
platform::enumerate_memory_regions(pid) 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<Vec<u8>> {
platform::read_process_memory(pid, address, size)
}

View File

@@ -202,13 +202,109 @@ mod platform {
#[cfg(target_os = "macos")] #[cfg(target_os = "macos")]
mod platform { mod platform {
use super::ProcessInfo; use super::ProcessInfo;
use anyhow::Result; use anyhow::{Context, Result};
pub fn enumerate_processes() -> Result<Vec<ProcessInfo>> { pub fn enumerate_processes() -> Result<Vec<ProcessInfo>> {
// macOS implementation would use libproc or sysctl use libc::{c_int, c_void, size_t, sysctl, CTL_KERN, KERN_PROC, KERN_PROC_ALL};
// For now, return empty to indicate platform support is partial use std::mem;
log::warn!("macOS process enumeration not yet fully implemented"); use std::ptr;
Ok(Vec::new())
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::<libc::kinfo_proc>();
let mut buffer: Vec<libc::kinfo_proc> = Vec::with_capacity(count + 16);
buffer.resize_with(count + 16, || mem::zeroed());
let mut actual_size = buffer.len() * mem::size_of::<libc::kinfo_proc>();
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::<libc::kinfo_proc>();
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<String> {
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. /// - **Windows**: Uses the ToolHelp API to enumerate processes.
/// - **Linux**: Reads from the /proc filesystem. /// - **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 /// # Errors
/// ///

View File

@@ -31,77 +31,309 @@ impl ShellcodeDetector {
} }
fn initialize_signatures(&mut self) { 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 { 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], mask: vec![0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0x00, 0x00],
name: "PEB Access Pattern", name: "x86 PEB Access (fs:[0x30])",
confidence: 0.7, 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 // Common x64 shellcode prologue
self.signatures.push(ShellcodeSignature { self.signatures.push(ShellcodeSignature {
pattern: vec![0x48, 0x83, 0xEC, 0x00, 0x48, 0x89], // sub rsp, XX; mov pattern: vec![0x48, 0x83, 0xEC, 0x28, 0x48, 0x83, 0xE4, 0xF0], // sub rsp, 0x28; and rsp, -16
mask: vec![0xFF, 0xFF, 0xFF, 0x00, 0xFF, 0xFF], mask: vec![0xFF, 0xFF, 0xFF, 0x00, 0xFF, 0xFF, 0xFF, 0xFF],
name: "x64 Stack Setup", name: "x64 Stack Setup Pattern",
confidence: 0.6, confidence: 0.7,
}); });
// Egg hunter pattern (searches for specific marker in memory) // ===== Position-Independent Code Patterns =====
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,
});
// API hashing pattern (djb2 hash commonly used) // Call-pop technique (get current EIP/RIP)
self.signatures.push(ShellcodeSignature { self.signatures.push(ShellcodeSignature {
pattern: vec![0xC1, 0xCF, 0x0D, 0x01, 0xC7], // ror edi, 0xD; add edi, eax pattern: vec![0xE8, 0x00, 0x00, 0x00, 0x00, 0x58], // call $+5; pop eax
mask: vec![0xFF, 0xFF, 0xFF, 0xFF, 0xFF], mask: vec![0xFF, 0x00, 0x00, 0x00, 0x00, 0xFF],
name: "DJB2 Hash Algorithm", name: "Call-Pop GetPC (eax)",
confidence: 0.9, 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, <syscall_num>
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 { self.signatures.push(ShellcodeSignature {
pattern: vec![0xFF, 0x15], // call [address] pattern: vec![0xFF, 0x15], // call [address]
mask: vec![0xFF, 0xFF], mask: vec![0xFF, 0xFF],
name: "Indirect API Call", name: "Indirect API Call",
confidence: 0.4, 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 /// Scan memory region for shellcode patterns

View File

@@ -817,8 +817,10 @@ impl TestFramework {
let threads: Vec<ThreadInfo> = params.thread_data.iter().map(|thread| { let threads: Vec<ThreadInfo> = params.thread_data.iter().map(|thread| {
ThreadInfo { ThreadInfo {
tid: thread.tid, tid: thread.tid,
owner_pid: process_info.pid,
start_address: thread.entry_point, start_address: thread.entry_point,
creation_time: 0, creation_time: 0,
state: crate::thread::ThreadState::Running,
} }
}).collect(); }).collect();

View File

@@ -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; use std::fmt;
#[derive(Debug, Clone)] /// Information about a thread within a process.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ThreadInfo { pub struct ThreadInfo {
/// Thread ID.
pub tid: u32, pub tid: u32,
/// Process ID that owns this thread.
pub owner_pid: u32, pub owner_pid: u32,
/// Start address of the thread (entry point).
pub start_address: usize, pub start_address: usize,
/// Thread creation time (platform-specific format).
pub creation_time: u64, 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 { impl fmt::Display for ThreadInfo {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!( write!(
f, f,
"TID {} @ {:#x}", "TID {} @ {:#x} [{}]",
self.tid, self.start_address self.tid, self.start_address, self.state
) )
} }
} }
#[cfg(windows)] #[cfg(windows)]
mod platform { mod platform {
use super::ThreadInfo; use super::{ThreadInfo, ThreadState};
use anyhow::{Context, Result}; use anyhow::{Context, Result};
use windows::Win32::Foundation::CloseHandle; use windows::Win32::Foundation::CloseHandle;
use windows::Win32::System::Diagnostics::ToolHelp::{ use windows::Win32::System::Diagnostics::ToolHelp::{
CreateToolhelp32Snapshot, Thread32First, Thread32Next, THREADENTRY32, TH32CS_SNAPTHREAD, 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::<usize>() 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<Vec<ThreadInfo>> { pub fn enumerate_threads(pid: u32) -> Result<Vec<ThreadInfo>> {
let mut threads = Vec::new(); let mut threads = Vec::new();
@@ -42,11 +182,16 @@ mod platform {
if Thread32First(snapshot, &mut entry).is_ok() { if Thread32First(snapshot, &mut entry).is_ok() {
loop { loop {
if entry.th32OwnerProcessID == pid { 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 { threads.push(ThreadInfo {
tid: entry.th32ThreadID, tid,
owner_pid: entry.th32OwnerProcessID, owner_pid: entry.th32OwnerProcessID,
start_address: 0, // TODO: Get actual start address start_address,
creation_time: 0, // TODO: Get thread creation time 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<Vec<ThreadInfo>> {
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::<u32>() {
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<Vec<ThreadInfo>> {
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::<thread_basic_info>() / mem::size_of::<natural_t>()) 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::<mach_port_t>(),
);
}
}
Ok(threads)
}
}
#[cfg(not(any(windows, target_os = "linux", target_os = "macos")))]
mod platform { mod platform {
use super::ThreadInfo; use super::ThreadInfo;
use anyhow::Result; use anyhow::Result;
pub fn enumerate_threads(_pid: u32) -> Result<Vec<ThreadInfo>> { pub fn enumerate_threads(_pid: u32) -> Result<Vec<ThreadInfo>> {
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<Vec<ThreadInfo>> { pub fn enumerate_threads(pid: u32) -> anyhow::Result<Vec<ThreadInfo>> {
platform::enumerate_threads(pid) platform::enumerate_threads(pid)
} }

View File

@@ -1,6 +1,11 @@
//! Integration tests for Ghost detection engine.
#[cfg(test)] #[cfg(test)]
mod tests { 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 { fn create_test_process() -> ProcessInfo {
ProcessInfo { ProcessInfo {
@@ -23,7 +28,7 @@ mod tests {
#[test] #[test]
fn test_clean_process_detection() { 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 process = create_test_process();
let regions = vec![MemoryRegion { let regions = vec![MemoryRegion {
base_address: 0x400000, base_address: 0x400000,
@@ -39,7 +44,7 @@ mod tests {
#[test] #[test]
fn test_rwx_region_detection() { 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 process = create_test_process();
let regions = vec![create_rwx_region()]; let regions = vec![create_rwx_region()];
@@ -51,7 +56,7 @@ mod tests {
#[test] #[test]
fn test_multiple_small_executable_regions() { 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 process = create_test_process();
let regions = vec![ let regions = vec![
MemoryRegion { MemoryRegion {
@@ -84,7 +89,7 @@ mod tests {
#[test] #[test]
fn test_baseline_tracking() { 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 mut process = create_test_process();
let regions = vec![]; let regions = vec![];
@@ -100,4 +105,252 @@ mod tests {
.iter() .iter()
.any(|i| i.contains("new threads"))); .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, &regions, 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, &regions, 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, &regions, 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"));
}
} }