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
.claude/
.claude-plugins/
.claude-embeddings/
.claude*.cache/
.claude*/

View File

@@ -5,7 +5,7 @@
### Windows
- Rust toolchain (MSVC target)
- Visual Studio Build Tools with C++ workload
- Windows SDK
- Windows SDK (for windows crate bindings)
Install via:
```powershell
@@ -15,22 +15,76 @@ rustup default stable-msvc
### Linux
- Rust toolchain
- GCC/Clang
- libelf-dev (for eBPF)
- libc development headers
```bash
# Debian/Ubuntu
sudo apt install build-essential
# RHEL/Fedora
sudo dnf groupinstall "Development Tools"
```
### macOS
- Rust toolchain
- Xcode Command Line Tools
- Xcode Command Line Tools (for libc bindings)
```bash
xcode-select --install
```
## Building
```bash
# Release build (recommended for performance)
cargo build --release
# Debug build
cargo build
# Check for compilation errors without full build
cargo check
```
## Running
```bash
# CLI interface
cargo run --bin ghost-cli
# Terminal UI
cargo run --bin ghost-tui
# With arguments
cargo run --bin ghost-cli -- --pid 1234
cargo run --bin ghost-cli -- --config examples/ghost.toml
```
Note: Requires elevated privileges for full process memory access.
## Testing
```bash
# Run all tests
cargo test
# Run specific test module
cargo test --package ghost-core detection_tests
# Run with output
cargo test -- --nocapture
```
## Documentation
```bash
# Generate and open documentation
cargo doc --open
# Generate without dependencies
cargo doc --no-deps --open
```
## Platform Notes
- **Windows**: Requires elevated privileges (Administrator) for full process memory access
- **Linux**: Requires appropriate permissions to read /proc/[pid]/mem (root or ptrace capability)
- **macOS**: Some features require System Integrity Protection (SIP) to be adjusted for full functionality

View File

@@ -4,62 +4,64 @@ Cross-platform process injection detection framework written in Rust.
## Overview
Ghost is a comprehensive security framework for detecting process injection, memory manipulation, and advanced evasion techniques in running processes. It combines kernel-level monitoring with behavioral analysis, machine learning, and threat intelligence to provide enterprise-grade detection capabilities.
Ghost is a security framework for detecting process injection, memory manipulation, and suspicious process behavior. It provides memory analysis, behavioral monitoring, and MITRE ATT&CK technique mapping for security research and defensive purposes.
## Features
- **Multi-layer detection**: Memory analysis, behavioral patterns, and ML-based anomaly detection
- **MITRE ATT&CK mapping**: Automatic technique classification using the ATT&CK framework
- **Threat intelligence**: Integration with threat feeds for IOC correlation and attribution
- **Cross-platform**: Windows (full support), Linux (with eBPF), macOS (planned)
- **Real-time monitoring**: Continuous scanning with configurable intervals
- **Low overhead**: Performance-optimized for production environments
- **Memory Analysis**: RWX region detection, shellcode pattern scanning, memory protection analysis
- **MITRE ATT&CK Mapping**: Technique identification using the ATT&CK framework
- **Cross-platform Support**:
- **Windows**: Process enumeration, memory reading (ReadProcessMemory), thread analysis (NtQueryInformationThread), inline hook detection, PE header validation
- **Linux**: Process enumeration via procfs, memory region analysis (/proc/[pid]/maps), thread state monitoring, LD_PRELOAD detection, ptrace detection
- **macOS**: Process enumeration via sysctl/KERN_PROC_ALL
- **Real-time Monitoring**: Continuous scanning with configurable intervals
- **Threat Intelligence**: IOC storage and correlation framework
## Architecture
```
ghost/
├── ghost-core/ # Core detection engine (21 modules)
├── ghost-core/ # Core detection engine and platform abstractions
├── ghost-cli/ # Command-line interface
├── ghost-tui/ # Interactive terminal UI
├── ghost-tui/ # Interactive terminal UI (Ratatui-based)
├── examples/ # Configuration examples
└── docs/ # Technical documentation
```
### Core Modules
- **Detection Engine**: Orchestrates all analysis components
- **Memory Analysis**: RWX region detection, shellcode patterns
- **Process Hollowing**: PE header validation, memory gap analysis
- **Thread Analysis**: Start address validation, behavioral patterns
- **Evasion Detection**: Anti-debugging, VM detection, obfuscation
- **MITRE ATT&CK Engine**: Technique mapping and threat actor profiling
- **Threat Intelligence**: IOC matching and campaign correlation
- **Detection Engine** ([detection.rs](ghost-core/src/detection.rs)): Orchestrates analysis and threat scoring
- **Memory Analysis** ([memory.rs](ghost-core/src/memory.rs)): Platform-specific memory enumeration and reading
- **Process Enumeration** ([process.rs](ghost-core/src/process.rs)): Cross-platform process listing
- **Thread Analysis** ([thread.rs](ghost-core/src/thread.rs)): Thread enumeration with start address and creation time
- **Hook Detection** ([hooks.rs](ghost-core/src/hooks.rs)): Inline hook detection via JMP pattern analysis
- **MITRE ATT&CK** ([mitre.rs](ghost-core/src/mitre.rs)): Technique mapping and categorization
- **Configuration** ([config.rs](ghost-core/src/config.rs)): TOML-based configuration with validation
## Supported Detection Techniques
### Process Injection (T1055)
- RWX memory region detection
- Private executable memory analysis
- Remote thread creation monitoring
- SetWindowsHookEx injection (T1055.001)
- Thread hijacking (T1055.003)
- APC injection patterns (T1055.004)
- Process hollowing (T1055.012)
- Reflective DLL injection
- Thread count anomaly detection
- Inline hook detection (JMP patches on ntdll.dll, kernel32.dll, user32.dll)
- LD_PRELOAD and LD_LIBRARY_PATH detection (Linux)
- Ptrace injection detection (Linux)
- SetWindowsHookEx hook enumeration
- Thread hijacking indicators (T1055.003)
- Process hollowing detection with PE header validation (T1055.012)
### Evasion Techniques
- Anti-debugging detection
- Virtual machine detection attempts
- Code obfuscation analysis
- Timing-based analysis evasion
- Environment fingerprinting
### Memory Analysis
- Memory protection flags (R/W/X combinations)
- Region type classification (IMAGE, PRIVATE, MAPPED, HEAP, STACK)
- Small executable region detection (shellcode indicators)
- Memory region size anomalies
### Behavioral Anomalies
- Thread count deviations
- Memory allocation patterns
- API call sequences
- Process relationship analysis
### Behavioral Monitoring
- Thread count changes from baseline
- New thread creation detection
- Process parent-child relationships
- System process identification
## Installation
@@ -192,6 +194,27 @@ Please review [SECURITY.md](SECURITY.md) for:
- Security considerations
- Threat model
## Platform Support Matrix
| Feature | Windows | Linux | macOS |
|---------|---------|-------|-------|
| Process Enumeration | CreateToolhelp32Snapshot | /proc filesystem | sysctl KERN_PROC_ALL |
| Memory Enumeration | VirtualQueryEx | /proc/[pid]/maps | Not implemented |
| Memory Reading | ReadProcessMemory | /proc/[pid]/mem | Not implemented |
| Thread Enumeration | Thread32First/Next | /proc/[pid]/task | Not implemented |
| Thread Start Address | NtQueryInformationThread | /proc/[pid]/task/[tid]/syscall | Not implemented |
| Thread Creation Time | GetThreadTimes | /proc/[pid]/task/[tid]/stat | Not implemented |
| Hook Detection | Inline JMP pattern scanning | LD_PRELOAD/ptrace detection | Not applicable |
| PE Header Validation | Full PE validation | Not applicable | Not applicable |
| Process Path | GetProcessImageFileNameW | /proc/[pid]/exe | proc_pidpath |
## Status
Active development. Core detection engine stable. Windows support complete. Linux eBPF support in progress. macOS Endpoint Security framework planned.
Active development. Core detection engine functional with cross-platform abstractions. Windows support most complete. Linux support via procfs. macOS has process enumeration but limited memory/thread analysis.
### Known Limitations
- macOS memory enumeration and reading not yet implemented (requires vm_read and mach_vm_region)
- Windows SetWindowsHookEx chain enumeration requires parsing undocumented USER32.dll structures
- Shellcode pattern matching currently uses heuristics (no actual signature database)
- No kernel-level monitoring (all userspace APIs)

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.
**Detection Logic** (Planned):
- Compare thread creator PID with owner PID
- Check thread start addresses against known modules
**Detection Logic**:
- Enumerate threads using CreateToolhelp32Snapshot (Windows) or /proc/[pid]/task (Linux)
- Get thread start addresses via NtQueryInformationThread (Windows) or /proc syscall file (Linux)
- Get thread creation times via GetThreadTimes (Windows) or stat parsing (Linux)
- Track thread state (Running, Waiting, Suspended, Terminated)
- Flag threads starting in private memory regions
## Hook Detection
### Inline API Hooks
**MITRE ATT&CK**: T1055.003
Detects JMP patches at the start of critical API functions.
**Detection Logic**:
- Enumerate loaded modules in target process (EnumProcessModulesEx)
- Check entry points of critical APIs (ntdll, kernel32, user32)
- Detect common hook patterns:
- JMP rel32 (E9 xx xx xx xx)
- JMP [rip+disp32] (FF 25 xx xx xx xx)
- MOV RAX, imm64; JMP RAX (48 B8 ... FF E0)
- PUSH imm32; RET (68 xx xx xx xx C3)
**Critical APIs Monitored**:
- NtCreateThread, NtCreateThreadEx
- NtAllocateVirtualMemory, NtWriteVirtualMemory, NtProtectVirtualMemory
- VirtualAllocEx, WriteProcessMemory, CreateRemoteThread
- LoadLibraryA, LoadLibraryW
- SetWindowsHookExA, SetWindowsHookExW
## Heuristic Analysis
### Confidence Scoring
@@ -74,23 +100,37 @@ Ghost uses weighted confidence scoring:
### Windows
- [x] Classic DLL injection detection
- [x] Memory region analysis
- [x] Thread enumeration
- [x] Memory region analysis (VirtualQueryEx)
- [x] Memory reading (ReadProcessMemory)
- [x] Thread enumeration (CreateToolhelp32Snapshot)
- [x] Thread start addresses (NtQueryInformationThread)
- [x] Thread creation times (GetThreadTimes)
- [x] Inline hook detection (JMP pattern scanning)
- [x] Process hollowing heuristics
- [ ] APC injection detection
- [ ] Process hollowing detection
- [ ] Hook detection (IAT/EAT)
- [ ] Reflective DLL injection
- [ ] SetWindowsHookEx chain enumeration
- [ ] Reflective DLL injection signature matching
### Linux
- [ ] ptrace injection
- [ ] LD_PRELOAD detection
- [x] Process enumeration (/proc filesystem)
- [x] Memory region analysis (/proc/[pid]/maps)
- [x] Memory reading (/proc/[pid]/mem)
- [x] Thread enumeration (/proc/[pid]/task)
- [x] Thread state detection (stat parsing)
- [x] ptrace injection detection
- [x] LD_PRELOAD detection
- [ ] process_vm_writev monitoring
- [ ] Shared memory inspection
### macOS
- [ ] DYLD_INSERT_LIBRARIES
- [x] Process enumeration (sysctl KERN_PROC_ALL)
- [x] Process path retrieval (proc_pidpath)
- [ ] Memory enumeration (vm_region)
- [ ] Memory reading (vm_read)
- [ ] Thread enumeration (task_threads)
- [ ] DYLD_INSERT_LIBRARIES detection
- [ ] task_for_pid monitoring
- [ ] Mach port analysis

View File

@@ -46,7 +46,9 @@ Ghost detection engine coverage mapped to MITRE ATT&CK framework techniques.
- **Indicators**:
- Unmapped main executable image
- Suspicious memory gaps (>16MB)
- PE header mismatches
- PE header validation (DOS/NT signatures)
- Image base mismatches
- Corrupted PE structures
- Unusual entry point locations
- Memory layout anomalies
- **Confidence**: Very High (0.8-1.0)
@@ -121,35 +123,82 @@ Ghost detection engine coverage mapped to MITRE ATT&CK framework techniques.
| Technique | Detection Module | Implementation Status | Test Coverage |
|-----------|------------------|----------------------|---------------|
| T1055.001 | hooks.rs | ✅ Complete | ✅ Tested |
| T1055.002 | shellcode.rs | ✅ Complete | ✅ Tested |
| T1055.003 | thread.rs | ✅ Complete | ✅ Tested |
| T1055.004 | detection.rs | ⚠️ Partial | ✅ Tested |
| T1055.012 | hollowing.rs | ✅ Complete | ✅ Tested |
| T1027 | shellcode.rs | ✅ Complete | ✅ Tested |
| T1036 | process.rs | ⚠️ Partial | ❌ Pending |
| T1106 | detection.rs | ⚠️ Basic | ❌ Pending |
| T1055.001 | hooks.rs | ✅ Inline hooks + Linux LD_PRELOAD | ❌ Basic |
| T1055.002 | shellcode.rs | ⚠️ Heuristic only | ✅ Basic |
| T1055.003 | thread.rs | ✅ Thread enumeration | ✅ Unit tests |
| T1055.004 | detection.rs | ⚠️ Heuristic only | ✅ Basic |
| T1055.012 | hollowing.rs | ✅ PE header validation | ❌ Pending |
| T1027 | shellcode.rs | ⚠️ Basic patterns | ❌ Pending |
| T1036 | process.rs | ❌ Not implemented | ❌ Pending |
| T1106 | detection.rs | ❌ Not implemented | ❌ Pending |
**Implementation Status Legend**:
- ✅ Complete: Full implementation with actual API calls
- ⚠️ Partial: Heuristic-based or incomplete implementation
- ❌ Not implemented: Placeholder or missing
## Current Implementation Details
### What's Actually Implemented
1. **Memory Analysis** (memory.rs)
- Windows: VirtualQueryEx, ReadProcessMemory
- Linux: /proc/[pid]/maps parsing, /proc/[pid]/mem reading
- macOS: Not implemented
2. **Thread Analysis** (thread.rs)
- Windows: Thread32First/Next, NtQueryInformationThread, GetThreadTimes
- Linux: /proc/[pid]/task enumeration, stat parsing
- macOS: Not implemented
3. **Hook Detection** (hooks.rs)
- Windows: Inline hook detection via JMP pattern scanning
- Linux: LD_PRELOAD detection, LD_LIBRARY_PATH monitoring, ptrace detection
- Detects suspicious library loading from /tmp/, /dev/shm/, etc.
- Does NOT enumerate SetWindowsHookEx chains on Windows
- No IAT/EAT hook scanning (pattern detection only)
4. **Process Hollowing Detection** (hollowing.rs)
- Windows: Full PE header validation (DOS/NT signatures, image base)
- Detects corrupted PE structures
- Detects image base mismatches
- Memory layout anomaly detection
- Memory gap analysis
5. **Process Enumeration** (process.rs)
- Windows: CreateToolhelp32Snapshot
- Linux: /proc filesystem
- macOS: sysctl KERN_PROC_ALL
### What's NOT Implemented
- Actual shellcode signature database
- Entropy analysis for obfuscation detection
- SetWindowsHookEx chain parsing (Windows)
- APC injection detection
- MITRE ATT&CK technique attribution (framework only)
- process_vm_writev monitoring (Linux)
## Future Enhancements
### High Priority
- **T1055.008** - Ptrace System Calls (Linux)
- **T1055.009** - Proc Memory (Linux)
- **T1055.008** - Ptrace System Calls (Linux) - ✅ Basic detection implemented
- **T1055.013** - Process Doppelgänging
- **T1055.014** - VDSO Hijacking (Linux)
- Shellcode signature database
### Medium Priority
- **T1134** - Access Token Manipulation
- **T1548.002** - Bypass User Account Control
- **T1562.001** - Disable or Modify Tools
- SetWindowsHookEx chain enumeration
- IAT/EAT hook scanning
- LD_PRELOAD detection (Linux) - ✅ Implemented
### Research Areas
- Machine learning-based anomaly detection
- Graph analysis of process relationships
- Timeline analysis for attack progression
- Behavioral analysis over time
- Process relationship analysis
- Integration with threat intelligence feeds
## References

View File

@@ -2,298 +2,197 @@
## Overview
Ghost is designed for high-performance real-time detection with minimal system impact. This guide covers optimization strategies and performance monitoring.
Ghost is designed for process injection detection with configurable performance characteristics. This guide covers actual optimization strategies and expected performance.
## Performance Characteristics
### Detection Engine Performance
### Expected Detection Engine Performance
- **Scan Speed**: 500-1000 processes/second on modern hardware
- **Memory Usage**: 50-100MB base footprint
- **CPU Impact**: <2% during active monitoring
- **Latency**: <10ms detection response time
- **Process Enumeration**: 10-50ms for all system processes
- **Memory Region Analysis**: 1-5ms per process (platform-dependent)
- **Thread Enumeration**: 1-10ms per process
- **Detection Heuristics**: <1ms per process
- **Memory Usage**: ~10-20MB for core engine
### Optimization Techniques
**Note**: Actual performance varies significantly by:
- Number of processes (100-1000+ typical)
- Memory region count per process
- Thread count per process
- Platform (Windows APIs vs Linux procfs)
#### 1. Selective Scanning
### Configuration Options
#### 1. Selective Detection
```rust
// Configure detection modules based on threat landscape
let mut config = DetectionConfig::new();
config.enable_shellcode_detection(true);
config.enable_hook_detection(false); // Disable if not needed
config.enable_anomaly_detection(true);
use ghost_core::config::DetectionConfig;
// Disable expensive detections for performance
let mut config = DetectionConfig::default();
config.rwx_detection = true; // Fast: O(n) memory regions
config.shellcode_detection = false; // Skip pattern matching
config.hook_detection = false; // Skip module enumeration
config.thread_detection = true; // Moderate: thread enum
config.hollowing_detection = false; // Skip heuristics
```
#### 2. Batch Processing
#### 2. Preset Modes
```rust
// Process multiple items in batches for efficiency
let processes = enumerate_processes()?;
let results: Vec<DetectionResult> = processes
.chunks(10)
.flat_map(|chunk| engine.analyze_batch(chunk))
.collect();
// Fast scanning mode
let config = DetectionConfig::performance_mode();
// Thorough scanning mode
let config = DetectionConfig::thorough_mode();
```
#### 3. Memory Pool Management
#### 3. Process Filtering
```rust
// Pre-allocate memory pools to reduce allocations
pub struct MemoryPool {
process_buffers: Vec<ProcessBuffer>,
detection_results: Vec<DetectionResult>,
}
// Skip system processes
config.skip_system_processes = true;
// Limit memory scan size
config.max_memory_scan_size = 10 * 1024 * 1024; // 10MB per process
```
## Performance Monitoring
## Performance Considerations
### Built-in Metrics
### Platform-Specific Performance
```rust
use ghost_core::metrics::PerformanceMonitor;
**Windows**:
- CreateToolhelp32Snapshot: Single syscall, fast
- VirtualQueryEx: Iterative, slower for processes with many regions
- ReadProcessMemory: Cross-process, requires proper handles
- NtQueryInformationThread: Undocumented API call per thread
let monitor = PerformanceMonitor::new();
monitor.start_collection();
**Linux**:
- /proc enumeration: Directory reads, fast
- /proc/[pid]/maps parsing: File I/O, moderate
- /proc/[pid]/mem reading: Requires ptrace or same user
- /proc/[pid]/task parsing: Per-thread file I/O
// Detection operations...
**macOS**:
- sysctl KERN_PROC_ALL: Single syscall, fast
- Memory/thread analysis: Not yet implemented
let stats = monitor.get_statistics();
println!("Avg scan time: {:.2}ms", stats.avg_scan_time);
println!("Memory usage: {}MB", stats.memory_usage_mb);
```
### Custom Benchmarks
### Running Tests
```bash
# Run comprehensive benchmarks
cargo bench
# Run all tests including performance assertions
cargo test
# Profile specific operations
cargo bench -- shellcode_detection
cargo bench -- process_enumeration
# Run tests with timing output
cargo test -- --nocapture
```
## Tuning Guidelines
### For High-Volume Environments
### For Continuous Monitoring
1. **Increase batch sizes**: Process 20-50 items per batch
2. **Reduce scan frequency**: 2-5 second intervals
3. **Enable result caching**: Cache stable process states
4. **Use filtered scanning**: Skip known-good processes
1. **Adjust scan interval**: Configure `scan_interval_ms` in DetectionConfig
2. **Skip system processes**: Set `skip_system_processes = true`
3. **Limit memory scans**: Reduce `max_memory_scan_size`
4. **Disable heavy detections**: Turn off hook_detection and shellcode_detection
### For Low-Latency Requirements
### For One-Time Analysis
1. **Decrease batch sizes**: Process 1-5 items per batch
2. **Increase scan frequency**: Sub-second intervals
3. **Disable heavy detections**: Skip complex ML analysis
4. **Use memory-mapped scanning**: Direct memory access
### Memory Optimization
```rust
// Configure memory limits
let config = DetectionConfig {
max_memory_usage_mb: 200,
enable_result_compression: true,
cache_size_limit: 1000,
..Default::default()
};
```
1. **Enable all detections**: Use `DetectionConfig::thorough_mode()`
2. **Full memory scanning**: Increase `max_memory_scan_size`
3. **Include system processes**: Set `skip_system_processes = false`
## Platform-Specific Optimizations
### Windows
- Use `SetProcessWorkingSetSize` to limit memory
- Enable `SE_INCREASE_QUOTA_NAME` privilege for better access
- Leverage Windows Performance Toolkit (WPT) for profiling
- Run as Administrator for full process access
- Use `PROCESS_QUERY_LIMITED_INFORMATION` when `PROCESS_QUERY_INFORMATION` fails
- Handle access denied errors gracefully (system processes)
### Linux
- Use `cgroups` for resource isolation
- Enable `CAP_SYS_PTRACE` for enhanced process access
- Leverage `perf` for detailed performance analysis
- Run with appropriate privileges (root or CAP_SYS_PTRACE)
- Handle permission denied for /proc/[pid]/mem gracefully
- Consider using process groups for batch access
### macOS
- Limited functionality (process enumeration only)
- Most detection features require kernel extensions or Endpoint Security framework
## Troubleshooting Performance Issues
### High CPU Usage
1. Check scan frequency settings
2. Verify filter effectiveness
3. Profile detection module performance
4. Consider disabling expensive detections
1. Reduce scan frequency (`scan_interval_ms`)
2. Disable thread analysis for each scan
3. Skip memory region enumeration
4. Filter out known-good processes
### High Memory Usage
1. Monitor result cache sizes
2. Check for memory leaks in custom modules
3. Verify proper cleanup of process handles
4. Consider reducing batch sizes
1. Reduce baseline cache size (limited processes tracked)
2. Clear detection history periodically
3. Limit memory reading buffer sizes
### Slow Detection Response
1. Profile individual detection modules
2. Check system resource availability
3. Verify network latency (if applicable)
4. Consider async processing optimization
1. Disable hook detection (expensive module enumeration)
2. Skip shellcode pattern matching
3. Use performance preset mode
## Benchmarking Results
## Current Implementation Limits
### Baseline Performance (Intel i7-9700K, 32GB RAM)
**What's NOT implemented**:
- No performance metrics collection system
- No Prometheus/monitoring integration
- No SIMD-accelerated pattern matching
- No parallel/async process scanning (single-threaded)
- No LRU caching of results
- No batch processing APIs
```
Process Enumeration: 2.3ms (avg)
Shellcode Detection: 0.8ms per process
Hook Detection: 1.2ms per process
Anomaly Analysis: 3.5ms per process
Full Scan (100 proc): 847ms total
```
**Current architecture**:
- Sequential process scanning
- Simple HashMap for baseline tracking
- Basic confidence scoring
- Manual timer-based intervals (TUI)
### Memory Usage
```
Base Engine: 45MB
+ Shellcode Patterns: +12MB
+ ML Models: +23MB
+ Result Cache: +15MB (1000 entries)
Total Runtime: 95MB typical
```
## Advanced Optimizations
### SIMD Acceleration
## Testing Performance
```rust
// Enable SIMD for pattern matching
#[cfg(target_feature = "avx2")]
use std::arch::x86_64::*;
#[test]
fn test_detection_performance() {
use std::time::Instant;
// Vectorized shellcode scanning
unsafe fn simd_pattern_search(data: &[u8], pattern: &[u8]) -> bool {
// AVX2 accelerated pattern matching
}
```
let mut engine = DetectionEngine::new().unwrap();
let process = ProcessInfo::new(1234, 4, "test.exe".to_string());
let regions = vec![/* test regions */];
### Multi-threading
```rust
use rayon::prelude::*;
// Parallel process analysis
let results: Vec<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 start = Instant::now();
for _ in 0..100 {
engine.analyze_process(&process, &regions, None);
}
let duration = start.elapsed();
// Should complete 100 analyses in under 100ms
assert!(duration.as_millis() < 100);
}
```
## Best Practices
1. **Profile First**: Always benchmark before optimizing
2. **Measure Impact**: Quantify optimization effectiveness
3. **Monitor Production**: Continuous performance monitoring
4. **Gradual Tuning**: Make incremental adjustments
5. **Document Changes**: Track optimization history
1. **Start with defaults**: Use `DetectionConfig::default()` initially
2. **Profile specific modules**: Identify which detection is slow
3. **Adjust based on needs**: Disable features you don't need
4. **Handle errors gracefully**: Processes may exit during scan
5. **Test on target hardware**: Performance varies by system
## Performance Testing Framework
## Future Performance Improvements
```rust
#[cfg(test)]
mod performance_tests {
use super::*;
use std::time::Instant;
#[test]
fn benchmark_full_system_scan() {
let engine = DetectionEngine::new().unwrap();
let start = Instant::now();
let results = engine.scan_all_processes().unwrap();
let duration = start.elapsed();
assert!(duration.as_millis() < 5000, "Scan took too long");
assert!(results.len() > 0, "No processes detected");
}
#[test]
fn memory_usage_benchmark() {
let initial = get_memory_usage();
let engine = DetectionEngine::new().unwrap();
// Perform operations
for _ in 0..1000 {
engine.analyze_dummy_process();
}
let final_usage = get_memory_usage();
let growth = final_usage - initial;
assert!(growth < 50_000_000, "Memory usage grew too much: {}MB",
growth / 1_000_000);
}
}
```
## Conclusion
Ghost's performance can be fine-tuned for various deployment scenarios. Regular monitoring and benchmarking ensure optimal operation while maintaining security effectiveness.
For additional performance support, see:
- [Profiling Guide](PROFILING.md)
- [Deployment Strategies](DEPLOYMENT.md)
- [Scaling Recommendations](SCALING.md)
Potential enhancements (not yet implemented):
- Parallel process analysis using rayon
- Async I/O for file system operations (Linux)
- Result caching with TTL
- Incremental scanning (only changed processes)
- Memory-mapped file parsing
- SIMD pattern matching for shellcode

View File

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

View File

@@ -1,5 +1,8 @@
use crate::{GhostError, MemoryRegion, ProcessInfo, Result};
#[cfg(windows)]
use crate::memory::{validate_pe_header, read_pe_header_info, PEHeaderValidation};
#[derive(Debug, Clone)]
pub struct HollowingDetection {
pub pid: u32,
@@ -14,6 +17,8 @@ pub enum HollowingIndicator {
SuspiciousImageBase,
MemoryLayoutAnomaly { expected_size: usize, actual_size: usize },
MismatchedPEHeader,
InvalidPEHeader { validation: String },
CorruptedPEStructure { address: usize, reason: String },
UnusualEntryPoint { address: usize },
SuspiciousMemoryGaps { gap_count: usize, largest_gap: usize },
}
@@ -27,6 +32,12 @@ impl std::fmt::Display for HollowingIndicator {
write!(f, "Memory layout anomaly: expected {:#x}, found {:#x}", expected_size, actual_size)
}
Self::MismatchedPEHeader => write!(f, "PE header mismatch detected"),
Self::InvalidPEHeader { validation } => {
write!(f, "Invalid PE header: {}", validation)
}
Self::CorruptedPEStructure { address, reason } => {
write!(f, "Corrupted PE structure at {:#x}: {}", address, reason)
}
Self::UnusualEntryPoint { address } => {
write!(f, "Entry point at unusual location: {:#x}", address)
}
@@ -72,12 +83,18 @@ impl HollowingDetector {
confidence += 0.4;
}
// Check for PE header anomalies
// Check for PE header anomalies (heuristic-based)
if let Some(indicator) = self.check_pe_header_anomalies(memory_regions) {
indicators.push(indicator);
confidence += 0.7;
}
// Validate actual PE headers (deep inspection)
if let Some(indicator) = self.validate_pe_headers(process.pid, memory_regions) {
indicators.push(indicator);
confidence += 0.9; // Higher confidence for actual PE validation
}
// Check entry point location
if let Some(indicator) = self.check_entry_point_anomalies(process, memory_regions) {
indicators.push(indicator);
@@ -223,6 +240,71 @@ impl HollowingDetector {
None
}
#[cfg(windows)]
fn validate_pe_headers(&self, pid: u32, regions: &[MemoryRegion]) -> Option<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(
&self,
_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 serde::{Deserialize, Serialize};
use std::collections::HashMap;
#[derive(Debug, Clone)]
pub struct HookInfo {
pub hook_type: u32,
pub thread_id: u32,
pub hook_proc: usize,
pub module_name: String,
/// Type of hook detected.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum HookType {
/// SetWindowsHookEx hook (message hook).
WindowsHook(u32),
/// Inline/detour hook (JMP patch).
InlineHook,
/// Import Address Table (IAT) hook.
IATHook,
/// Export Address Table (EAT) hook.
EATHook,
/// LD_PRELOAD based library injection (Linux).
LdPreload,
/// LD_LIBRARY_PATH manipulation (Linux).
LdLibraryPath,
/// Ptrace-based injection (Linux).
PtraceInjection,
}
#[derive(Debug, Clone)]
impl std::fmt::Display for HookType {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::WindowsHook(id) => write!(f, "WindowsHook({})", id),
Self::InlineHook => write!(f, "InlineHook"),
Self::IATHook => write!(f, "IATHook"),
Self::EATHook => write!(f, "EATHook"),
Self::LdPreload => write!(f, "LD_PRELOAD"),
Self::LdLibraryPath => write!(f, "LD_LIBRARY_PATH"),
Self::PtraceInjection => write!(f, "PtraceInjection"),
}
}
}
/// Information about a detected hook.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct HookInfo {
/// Type of hook.
pub hook_type: HookType,
/// Thread ID (for message hooks) or 0 for system-wide.
pub thread_id: u32,
/// Address of the hook procedure.
pub hook_proc: usize,
/// Original address (for inline/IAT hooks).
pub original_address: usize,
/// Module containing the hook procedure.
pub module_name: String,
/// Function being hooked (for inline/IAT hooks).
pub hooked_function: String,
}
/// Result of hook detection analysis.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct HookDetectionResult {
/// List of detected hooks.
pub hooks: Vec<HookInfo>,
/// Number of suspicious hooks.
pub suspicious_count: usize,
/// Number of global/system-wide hooks.
pub global_hooks: usize,
/// Number of inline API hooks detected.
pub inline_hooks: usize,
}
#[cfg(windows)]
mod platform {
use super::{HookDetectionResult, HookInfo};
use super::{HookDetectionResult, HookInfo, HookType};
use crate::{GhostError, Result};
use std::collections::HashMap;
use windows::Win32::Foundation::{GetLastError, HWND};
use windows::Win32::System::LibraryLoader::{GetModuleFileNameW, GetModuleHandleW};
use windows::Win32::Foundation::CloseHandle;
use windows::Win32::System::Diagnostics::Debug::ReadProcessMemory;
use windows::Win32::System::LibraryLoader::{
GetModuleHandleW, GetProcAddress, LoadLibraryW,
};
use windows::Win32::System::ProcessStatus::{
EnumProcessModulesEx, GetModuleBaseNameW, GetModuleInformation, LIST_MODULES_ALL,
MODULEINFO,
};
use windows::Win32::System::Threading::{OpenProcess, PROCESS_QUERY_INFORMATION, PROCESS_VM_READ};
use windows::Win32::UI::WindowsAndMessaging::{
EnumWindows, GetWindowThreadProcessId, HC_ACTION, HOOKPROC, WH_CALLWNDPROC,
WH_CALLWNDPROCRET, WH_CBT, WH_DEBUG, WH_FOREGROUNDIDLE, WH_GETMESSAGE, WH_JOURNALPLAYBACK,
WH_JOURNALRECORD, WH_KEYBOARD, WH_KEYBOARD_LL, WH_MOUSE, WH_MOUSE_LL, WH_MSGFILTER,
WH_SHELL, WH_SYSMSGFILTER,
WH_CALLWNDPROC, WH_CALLWNDPROCRET, WH_CBT, WH_DEBUG, WH_FOREGROUNDIDLE, WH_GETMESSAGE,
WH_JOURNALPLAYBACK, WH_JOURNALRECORD, WH_KEYBOARD, WH_KEYBOARD_LL, WH_MOUSE,
WH_MOUSE_LL, WH_MSGFILTER, WH_SHELL, WH_SYSMSGFILTER,
};
/// Detect Windows hook-based injection techniques
/// Critical APIs commonly hooked for injection.
const CRITICAL_APIS: &[(&str, &str)] = &[
("ntdll.dll", "NtCreateThread"),
("ntdll.dll", "NtCreateThreadEx"),
("ntdll.dll", "NtAllocateVirtualMemory"),
("ntdll.dll", "NtWriteVirtualMemory"),
("ntdll.dll", "NtProtectVirtualMemory"),
("ntdll.dll", "NtQueueApcThread"),
("kernel32.dll", "VirtualAllocEx"),
("kernel32.dll", "WriteProcessMemory"),
("kernel32.dll", "CreateRemoteThread"),
("kernel32.dll", "LoadLibraryA"),
("kernel32.dll", "LoadLibraryW"),
("user32.dll", "SetWindowsHookExA"),
("user32.dll", "SetWindowsHookExW"),
];
/// Detect Windows hook-based injection techniques.
pub fn detect_hook_injection(target_pid: u32) -> Result<HookDetectionResult> {
let mut hooks = Vec::new();
let mut suspicious_count = 0;
let mut global_hooks = 0;
let mut inline_hooks = 0;
// This is a simplified implementation - real hook detection requires
// more sophisticated techniques like parsing USER32.dll's hook table
// or using undocumented APIs. For now, we'll detect based on heuristics.
// Check for global hooks that might be used for injection
if let Ok(global_hook_count) = count_global_hooks() {
global_hooks = global_hook_count;
if global_hook_count > 5 {
suspicious_count += 1;
}
}
// 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) {
// Detect inline hooks in critical APIs
match detect_inline_hooks(target_pid) {
Ok(inline) => {
inline_hooks = inline.len();
for hook in inline {
if is_suspicious_inline_hook(&hook) {
suspicious_count += 1;
}
hooks.push(hook);
}
}
Err(e) => {
log::debug!("Failed to detect inline hooks: {}", e);
}
}
// Estimate global hooks based on system state
global_hooks = estimate_global_hooks();
if global_hooks > 10 {
suspicious_count += 1;
}
Ok(HookDetectionResult {
hooks,
suspicious_count,
global_hooks,
inline_hooks,
})
}
fn count_global_hooks() -> Result<usize> {
// In a real implementation, this would examine the global hook chain
// by parsing USER32.dll internal structures or using WinAPIOverride
// For now, return a realistic count based on typical system state
Ok(3) // Typical Windows system has 2-4 global hooks
}
fn enumerate_process_hooks(pid: u32) -> Result<Vec<HookInfo>> {
/// Detect inline (detour) hooks by checking for JMP instructions at API entry points.
fn detect_inline_hooks(target_pid: u32) -> Result<Vec<HookInfo>> {
let mut hooks = Vec::new();
// Real implementation would:
// 1. Enumerate all threads in the process
// 2. Check each thread's hook chain
// 3. Validate hook procedures and their locations
// 4. Cross-reference with loaded modules
unsafe {
let handle = OpenProcess(PROCESS_QUERY_INFORMATION | PROCESS_VM_READ, false, target_pid)
.map_err(|e| GhostError::Process {
message: format!("Failed to open process: {}", e),
})?;
// Simplified detection: check for common hook types that might indicate injection
let common_injection_hooks = vec![
(WH_CALLWNDPROC.0, "WH_CALLWNDPROC"),
(WH_GETMESSAGE.0, "WH_GETMESSAGE"),
(WH_CBT.0, "WH_CBT"),
(WH_KEYBOARD_LL.0, "WH_KEYBOARD_LL"),
(WH_MOUSE_LL.0, "WH_MOUSE_LL"),
];
// Get loaded modules in target process
let mut modules = [windows::Win32::Foundation::HMODULE::default(); 1024];
let mut cb_needed = 0u32;
// This is a placeholder - real hook enumeration requires low-level API calls
// or kernel debugging interfaces
for (hook_type, _name) in common_injection_hooks {
if might_have_hook(pid, hook_type) {
hooks.push(HookInfo {
hook_type,
thread_id: 0, // Would get actual thread ID
hook_proc: 0, // Would get actual procedure address
module_name: "unknown".to_string(),
let result = EnumProcessModulesEx(
handle,
modules.as_mut_ptr(),
(modules.len() * std::mem::size_of::<windows::Win32::Foundation::HMODULE>()) as u32,
&mut cb_needed,
LIST_MODULES_ALL,
);
if result.is_err() {
let _ = CloseHandle(handle);
return Err(GhostError::Process {
message: "Failed to enumerate process modules".to_string(),
});
}
let module_count =
(cb_needed as usize) / std::mem::size_of::<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)
}
fn might_have_hook(pid: u32, hook_type: u32) -> bool {
// Heuristic: certain processes are more likely to have hooks
// This is a simplified check - real implementation would examine memory
hook_type == WH_KEYBOARD_LL.0 || hook_type == WH_MOUSE_LL.0
/// Detect common hook patterns in function prologue.
fn detect_hook_pattern(bytes: &[u8], base_addr: usize) -> Option<usize> {
if bytes.len() < 5 {
return None;
}
fn is_suspicious_hook(hook: &HookInfo) -> bool {
// Check for hooks with suspicious characteristics
match hook.hook_type {
t if t == WH_CALLWNDPROC.0 => true, // Often used for injection
t if t == WH_GETMESSAGE.0 => true, // Common injection vector
t if t == WH_CBT.0 => true, // Can be used maliciously
t if t == WH_DEBUG.0 => true, // Debugging hooks are suspicious
_ => false,
}
// JMP rel32 (E9 xx xx xx xx)
if bytes[0] == 0xE9 {
let offset = i32::from_le_bytes([bytes[1], bytes[2], bytes[3], bytes[4]]);
let target = (base_addr as i64 + 5 + offset as i64) as usize;
return Some(target);
}
/// 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 {
match hook_type {
t if t == WH_CALLWNDPROC.0 => "WH_CALLWNDPROC",
@@ -147,14 +352,259 @@ mod platform {
}
}
#[cfg(not(windows))]
#[cfg(target_os = "linux")]
mod platform {
use super::{HookDetectionResult, HookInfo, HookType};
use crate::{GhostError, Result};
use std::fs;
use std::path::Path;
/// Detect hook injection on Linux (LD_PRELOAD, LD_LIBRARY_PATH, ptrace).
pub fn detect_hook_injection(target_pid: u32) -> Result<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 {
use super::HookDetectionResult;
use crate::{GhostError, Result};
pub fn detect_hook_injection(_target_pid: u32) -> Result<HookDetectionResult> {
Err(GhostError::Detection {
message: "Hook detection not implemented for this platform".to_string(),
// Hook detection is not implemented for this platform
Ok(HookDetectionResult {
hooks: Vec::new(),
suspicious_count: 0,
global_hooks: 0,
inline_hooks: 0,
})
}

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;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
/// PE header constants
#[cfg(windows)]
pub const IMAGE_DOS_SIGNATURE: u16 = 0x5A4D; // "MZ"
#[cfg(windows)]
pub const IMAGE_NT_SIGNATURE: u32 = 0x00004550; // "PE\0\0"
/// DOS header structure (first 64 bytes of a PE file)
#[derive(Debug, Clone, Copy)]
#[repr(C)]
pub struct ImageDosHeader {
pub e_magic: u16, // Magic number ("MZ")
pub e_cblp: u16, // Bytes on last page
pub e_cp: u16, // Pages in file
pub e_crlc: u16, // Relocations
pub e_cparhdr: u16, // Size of header in paragraphs
pub e_minalloc: u16, // Minimum extra paragraphs
pub e_maxalloc: u16, // Maximum extra paragraphs
pub e_ss: u16, // Initial SS value
pub e_sp: u16, // Initial SP value
pub e_csum: u16, // Checksum
pub e_ip: u16, // Initial IP value
pub e_cs: u16, // Initial CS value
pub e_lfarlc: u16, // File address of relocation table
pub e_ovno: u16, // Overlay number
pub e_res: [u16; 4], // Reserved
pub e_oemid: u16, // OEM identifier
pub e_oeminfo: u16, // OEM information
pub e_res2: [u16; 10], // Reserved
pub e_lfanew: i32, // File address of new exe header
}
/// PE file header structure
#[derive(Debug, Clone, Copy)]
#[repr(C)]
pub struct ImageFileHeader {
pub machine: u16,
pub number_of_sections: u16,
pub time_date_stamp: u32,
pub pointer_to_symbol_table: u32,
pub number_of_symbols: u32,
pub size_of_optional_header: u16,
pub characteristics: u16,
}
/// PE optional header structure (64-bit)
#[derive(Debug, Clone, Copy)]
#[repr(C)]
pub struct ImageOptionalHeader64 {
pub magic: u16,
pub major_linker_version: u8,
pub minor_linker_version: u8,
pub size_of_code: u32,
pub size_of_initialized_data: u32,
pub size_of_uninitialized_data: u32,
pub address_of_entry_point: u32,
pub base_of_code: u32,
pub image_base: u64,
pub section_alignment: u32,
pub file_alignment: u32,
pub major_operating_system_version: u16,
pub minor_operating_system_version: u16,
pub major_image_version: u16,
pub minor_image_version: u16,
pub major_subsystem_version: u16,
pub minor_subsystem_version: u16,
pub win32_version_value: u32,
pub size_of_image: u32,
pub size_of_headers: u32,
pub check_sum: u32,
pub subsystem: u16,
pub dll_characteristics: u16,
pub size_of_stack_reserve: u64,
pub size_of_stack_commit: u64,
pub size_of_heap_reserve: u64,
pub size_of_heap_commit: u64,
pub loader_flags: u32,
pub number_of_rva_and_sizes: u32,
}
/// PE header validation result
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum PEHeaderValidation {
Valid,
InvalidDosSignature,
InvalidNtSignature,
InvalidHeaderOffset,
MismatchedImageBase,
SuspiciousEntryPoint,
CorruptedHeader,
NotPE,
}
impl fmt::Display for PEHeaderValidation {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Valid => write!(f, "Valid PE header"),
Self::InvalidDosSignature => write!(f, "Invalid DOS signature"),
Self::InvalidNtSignature => write!(f, "Invalid NT signature"),
Self::InvalidHeaderOffset => write!(f, "Invalid header offset"),
Self::MismatchedImageBase => write!(f, "Image base mismatch"),
Self::SuspiciousEntryPoint => write!(f, "Suspicious entry point"),
Self::CorruptedHeader => write!(f, "Corrupted PE header"),
Self::NotPE => write!(f, "Not a PE file"),
}
}
}
/// Memory protection flags for a memory region.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum MemoryProtection {
NoAccess,
ReadOnly,
@@ -28,11 +142,16 @@ impl fmt::Display for MemoryProtection {
}
}
#[derive(Debug, Clone)]
/// Information about a memory region within a process.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct MemoryRegion {
/// Base address of the memory region.
pub base_address: usize,
/// Size of the region in bytes.
pub size: usize,
/// Memory protection flags.
pub protection: MemoryProtection,
/// Type of memory region (IMAGE, MAPPED, PRIVATE, etc.).
pub region_type: String,
}
@@ -49,18 +168,217 @@ impl fmt::Display for MemoryRegion {
}
}
/// Validates a PE header in process memory
#[cfg(windows)]
pub fn validate_pe_header(pid: u32, base_address: usize) -> anyhow::Result<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)]
mod platform {
use super::{MemoryProtection, MemoryRegion};
use anyhow::{Context, Result};
use windows::Win32::Foundation::{CloseHandle, HANDLE};
use windows::Win32::Foundation::CloseHandle;
use windows::Win32::System::Diagnostics::Debug::ReadProcessMemory;
use windows::Win32::System::Memory::{
VirtualQueryEx, MEMORY_BASIC_INFORMATION, MEM_COMMIT, MEM_FREE, MEM_IMAGE, MEM_MAPPED,
MEM_PRIVATE, MEM_RESERVE, PAGE_EXECUTE, PAGE_EXECUTE_READ, PAGE_EXECUTE_READWRITE,
PAGE_EXECUTE_WRITECOPY, PAGE_NOACCESS, PAGE_READONLY, PAGE_READWRITE, PAGE_WRITECOPY,
VirtualQueryEx, MEMORY_BASIC_INFORMATION, MEM_COMMIT, MEM_IMAGE, MEM_MAPPED, MEM_PRIVATE,
PAGE_EXECUTE, PAGE_EXECUTE_READ, PAGE_EXECUTE_READWRITE, PAGE_EXECUTE_WRITECOPY,
PAGE_NOACCESS, PAGE_READONLY, PAGE_READWRITE, PAGE_WRITECOPY,
};
use windows::Win32::System::Threading::{
OpenProcess, PROCESS_QUERY_INFORMATION, PROCESS_VM_READ,
};
use windows::Win32::System::Threading::{OpenProcess, PROCESS_QUERY_INFORMATION, PROCESS_VM_READ};
fn parse_protection(protect: u32) -> MemoryProtection {
match protect & 0xFF {
@@ -80,7 +398,6 @@ mod platform {
pub fn enumerate_memory_regions(pid: u32) -> Result<Vec<MemoryRegion>> {
let mut regions = Vec::new();
// Skip system process
if pid == 0 || pid == 4 {
return Ok(regions);
}
@@ -138,19 +455,358 @@ mod platform {
Ok(regions)
}
/// Reads memory from a process at the specified address.
///
/// # Safety
///
/// This function reads arbitrary process memory. The caller must ensure
/// the address and size are valid for the target process.
pub fn read_process_memory(pid: u32, address: usize, size: usize) -> Result<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 {
use super::MemoryRegion;
use anyhow::Result;
pub fn enumerate_memory_regions(_pid: u32) -> Result<Vec<MemoryRegion>> {
// TODO: Implement Linux/macOS memory enumeration
Ok(Vec::new())
Err(anyhow::anyhow!(
"Memory enumeration not supported on this platform"
))
}
pub fn read_process_memory(_pid: u32, _address: usize, _size: usize) -> Result<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>> {
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")]
mod platform {
use super::ProcessInfo;
use anyhow::Result;
use anyhow::{Context, Result};
pub fn enumerate_processes() -> Result<Vec<ProcessInfo>> {
// macOS implementation would use libproc or sysctl
// For now, return empty to indicate platform support is partial
log::warn!("macOS process enumeration not yet fully implemented");
Ok(Vec::new())
use libc::{c_int, c_void, size_t, sysctl, CTL_KERN, KERN_PROC, KERN_PROC_ALL};
use std::mem;
use std::ptr;
let mut processes = Vec::new();
unsafe {
// First, get the size needed for the buffer
let mut mib: [c_int; 4] = [CTL_KERN, KERN_PROC, KERN_PROC_ALL, 0];
let mut size: size_t = 0;
let result = sysctl(
mib.as_mut_ptr(),
3,
ptr::null_mut(),
&mut size,
ptr::null_mut(),
0,
);
if result != 0 {
return Err(anyhow::anyhow!(
"Failed to get process list size: {}",
std::io::Error::last_os_error()
));
}
// Allocate buffer with some extra space
let count = size / mem::size_of::<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.
/// - **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
///

View File

@@ -31,77 +31,309 @@ impl ShellcodeDetector {
}
fn initialize_signatures(&mut self) {
// GetProcAddress hash resolution pattern (common in position-independent code)
// ===== PEB/TEB Access Patterns (Windows) =====
// x86 PEB Access via FS segment
self.signatures.push(ShellcodeSignature {
pattern: vec![0x64, 0x8B, 0x25, 0x30, 0x00, 0x00, 0x00], // mov esp, fs:[0x30]
pattern: vec![0x64, 0x8B, 0x15, 0x30, 0x00, 0x00, 0x00], // mov edx, fs:[0x30]
mask: vec![0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0x00, 0x00],
name: "PEB Access Pattern",
confidence: 0.7,
name: "x86 PEB Access (fs:[0x30])",
confidence: 0.85,
});
// x86 PEB Access variant
self.signatures.push(ShellcodeSignature {
pattern: vec![0x64, 0xA1, 0x30, 0x00, 0x00, 0x00], // mov eax, fs:[0x30]
mask: vec![0xFF, 0xFF, 0xFF, 0x00, 0x00, 0x00],
name: "x86 PEB Access (fs:[0x30] via eax)",
confidence: 0.85,
});
// x64 PEB Access via GS segment
self.signatures.push(ShellcodeSignature {
pattern: vec![0x65, 0x48, 0x8B, 0x04, 0x25, 0x60, 0x00, 0x00, 0x00], // mov rax, gs:[0x60]
mask: vec![0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0x00, 0x00],
name: "x64 PEB Access (gs:[0x60])",
confidence: 0.9,
});
// x64 TEB Access
self.signatures.push(ShellcodeSignature {
pattern: vec![0x65, 0x48, 0x8B, 0x04, 0x25, 0x30, 0x00, 0x00, 0x00], // mov rax, gs:[0x30]
mask: vec![0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0x00, 0x00],
name: "x64 TEB Access (gs:[0x30])",
confidence: 0.8,
});
// ===== API Hashing Patterns =====
// ROR 13 hash (Metasploit style)
self.signatures.push(ShellcodeSignature {
pattern: vec![0xC1, 0xCF, 0x0D, 0x01, 0xC7], // ror edi, 0xD; add edi, eax
mask: vec![0xFF, 0xFF, 0xFF, 0xFF, 0xFF],
name: "ROR13 API Hash (Metasploit)",
confidence: 0.95,
});
// ROR 13 hash variant
self.signatures.push(ShellcodeSignature {
pattern: vec![0xC1, 0xCA, 0x0D, 0x01, 0xC2], // ror edx, 0xD; add edx, eax
mask: vec![0xFF, 0xFF, 0xFF, 0xFF, 0xFF],
name: "ROR13 API Hash Variant",
confidence: 0.95,
});
// x64 ROR 13 hash
self.signatures.push(ShellcodeSignature {
pattern: vec![0x48, 0xC1, 0xC9, 0x0D], // ror rcx, 0xD
mask: vec![0xFF, 0xFF, 0xFF, 0xFF],
name: "x64 ROR13 API Hash",
confidence: 0.9,
});
// FNV-1a hash pattern
self.signatures.push(ShellcodeSignature {
pattern: vec![0x69, 0xC0, 0x01, 0x00, 0x01, 0x00], // imul eax, eax, 0x01000193
mask: vec![0xFF, 0xFF, 0x00, 0x00, 0x00, 0x00],
name: "FNV-1a Hash Pattern",
confidence: 0.85,
});
// ===== Shellcode Prologues =====
// Metasploit x64 staged reverse TCP
self.signatures.push(ShellcodeSignature {
pattern: vec![0xFC, 0x48, 0x83, 0xE4, 0xF0, 0xE8], // CLD; and rsp, -16; call
mask: vec![0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF],
name: "Metasploit x64 Reverse TCP",
confidence: 0.98,
});
// Metasploit x86 staged reverse TCP
self.signatures.push(ShellcodeSignature {
pattern: vec![0xFC, 0xE8, 0x82, 0x00, 0x00, 0x00], // CLD; call $+0x82
mask: vec![0xFF, 0xFF, 0x00, 0x00, 0x00, 0x00],
name: "Metasploit x86 Reverse TCP",
confidence: 0.95,
});
// Cobalt Strike beacon
self.signatures.push(ShellcodeSignature {
pattern: vec![0xFC, 0x48, 0x83, 0xE4, 0xF0, 0xE8, 0xC8, 0x00, 0x00, 0x00],
mask: vec![0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0x00],
name: "Cobalt Strike Beacon Prologue",
confidence: 0.98,
});
// Common x64 shellcode prologue
self.signatures.push(ShellcodeSignature {
pattern: vec![0x48, 0x83, 0xEC, 0x00, 0x48, 0x89], // sub rsp, XX; mov
mask: vec![0xFF, 0xFF, 0xFF, 0x00, 0xFF, 0xFF],
name: "x64 Stack Setup",
confidence: 0.6,
pattern: vec![0x48, 0x83, 0xEC, 0x28, 0x48, 0x83, 0xE4, 0xF0], // sub rsp, 0x28; and rsp, -16
mask: vec![0xFF, 0xFF, 0xFF, 0x00, 0xFF, 0xFF, 0xFF, 0xFF],
name: "x64 Stack Setup Pattern",
confidence: 0.7,
});
// Egg hunter pattern (searches for specific marker in memory)
self.signatures.push(ShellcodeSignature {
pattern: vec![0x66, 0x81, 0x3F], // cmp word ptr [edi], XXXX
mask: vec![0xFF, 0xFF, 0xFF],
name: "Egg Hunter Pattern",
confidence: 0.8,
});
// ===== Position-Independent Code Patterns =====
// API hashing pattern (djb2 hash commonly used)
// Call-pop technique (get current EIP/RIP)
self.signatures.push(ShellcodeSignature {
pattern: vec![0xC1, 0xCF, 0x0D, 0x01, 0xC7], // ror edi, 0xD; add edi, eax
mask: vec![0xFF, 0xFF, 0xFF, 0xFF, 0xFF],
name: "DJB2 Hash Algorithm",
pattern: vec![0xE8, 0x00, 0x00, 0x00, 0x00, 0x58], // call $+5; pop eax
mask: vec![0xFF, 0x00, 0x00, 0x00, 0x00, 0xFF],
name: "Call-Pop GetPC (eax)",
confidence: 0.9,
});
// Common Windows API call pattern
self.signatures.push(ShellcodeSignature {
pattern: vec![0xE8, 0x00, 0x00, 0x00, 0x00, 0x5B], // call $+5; pop ebx
mask: vec![0xFF, 0x00, 0x00, 0x00, 0x00, 0xFF],
name: "Call-Pop GetPC (ebx)",
confidence: 0.9,
});
self.signatures.push(ShellcodeSignature {
pattern: vec![0xE8, 0x00, 0x00, 0x00, 0x00, 0x5D], // call $+5; pop ebp
mask: vec![0xFF, 0x00, 0x00, 0x00, 0x00, 0xFF],
name: "Call-Pop GetPC (ebp)",
confidence: 0.9,
});
self.signatures.push(ShellcodeSignature {
pattern: vec![0xE8, 0x00, 0x00, 0x00, 0x00, 0x5E], // call $+5; pop esi
mask: vec![0xFF, 0x00, 0x00, 0x00, 0x00, 0xFF],
name: "Call-Pop GetPC (esi)",
confidence: 0.9,
});
// FPU-based GetPC (classic technique)
self.signatures.push(ShellcodeSignature {
pattern: vec![0xD9, 0xEE, 0xD9, 0x74, 0x24, 0xF4], // fldz; fnstenv [esp-12]
mask: vec![0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF],
name: "FPU GetPC Technique",
confidence: 0.95,
});
// ===== Egg Hunter Patterns =====
self.signatures.push(ShellcodeSignature {
pattern: vec![0x66, 0x81, 0xCA, 0xFF, 0x0F], // or dx, 0x0FFF (page alignment)
mask: vec![0xFF, 0xFF, 0xFF, 0xFF, 0xFF],
name: "Egg Hunter Page Scan",
confidence: 0.9,
});
self.signatures.push(ShellcodeSignature {
pattern: vec![0x6A, 0x02, 0x58, 0xCD, 0x2E], // push 2; pop eax; int 0x2E
mask: vec![0xFF, 0xFF, 0xFF, 0xFF, 0xFF],
name: "Egg Hunter NtAccessCheckAndAuditAlarm",
confidence: 0.95,
});
// ===== Windows API Function Resolution =====
// Walking InMemoryOrderModuleList
self.signatures.push(ShellcodeSignature {
pattern: vec![0x8B, 0x52, 0x0C, 0x8B, 0x52, 0x14], // mov edx, [edx+0x0C]; mov edx, [edx+0x14]
mask: vec![0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF],
name: "PEB_LDR_DATA Walk (x86)",
confidence: 0.92,
});
// x64 LDR walk
self.signatures.push(ShellcodeSignature {
pattern: vec![0x48, 0x8B, 0x52, 0x18, 0x48, 0x8B, 0x52, 0x20], // mov rdx, [rdx+0x18]; mov rdx, [rdx+0x20]
mask: vec![0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF],
name: "PEB_LDR_DATA Walk (x64)",
confidence: 0.92,
});
// ===== Syscall Patterns =====
// Direct syscall (x64 Windows)
self.signatures.push(ShellcodeSignature {
pattern: vec![0x4C, 0x8B, 0xD1, 0xB8], // mov r10, rcx; mov eax, <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 {
pattern: vec![0xFF, 0x15], // call [address]
mask: vec![0xFF, 0xFF],
name: "Indirect API Call",
confidence: 0.4,
});
// NOP sled detection (common in exploits)
self.signatures.push(ShellcodeSignature {
pattern: vec![0x90, 0x90, 0x90, 0x90, 0x90, 0x90], // Multiple NOPs
mask: vec![0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF],
name: "NOP Sled",
confidence: 0.5,
});
// String loading pattern (common in shellcode)
self.signatures.push(ShellcodeSignature {
pattern: vec![0xE8, 0x00, 0x00, 0x00, 0x00, 0x5E], // call $+5; pop esi
mask: vec![0xFF, 0x00, 0x00, 0x00, 0x00, 0xFF],
name: "String Loading Technique",
confidence: 0.8,
});
// PE header in memory (process hollowing indicator)
self.signatures.push(ShellcodeSignature {
pattern: vec![0x4D, 0x5A], // MZ header
mask: vec![0xFF, 0xFF],
name: "PE Header in Memory",
confidence: 0.6,
});
// Common metasploit meterpreter pattern
self.signatures.push(ShellcodeSignature {
pattern: vec![0xFC, 0x48, 0x83, 0xE4, 0xF0, 0xE8], // CLD; and rsp, -16; call
mask: vec![0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF],
name: "Meterpreter Payload Pattern",
confidence: 0.95,
});
}
/// Scan memory region for shellcode patterns

View File

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

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;
#[derive(Debug, Clone)]
/// Information about a thread within a process.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ThreadInfo {
/// Thread ID.
pub tid: u32,
/// Process ID that owns this thread.
pub owner_pid: u32,
/// Start address of the thread (entry point).
pub start_address: usize,
/// Thread creation time (platform-specific format).
pub creation_time: u64,
/// Thread state (Running, Waiting, etc.).
pub state: ThreadState,
}
/// Thread execution state.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum ThreadState {
Running,
Waiting,
Suspended,
Terminated,
Unknown,
}
impl fmt::Display for ThreadState {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let s = match self {
Self::Running => "Running",
Self::Waiting => "Waiting",
Self::Suspended => "Suspended",
Self::Terminated => "Terminated",
Self::Unknown => "Unknown",
};
write!(f, "{}", s)
}
}
impl fmt::Display for ThreadInfo {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"TID {} @ {:#x}",
self.tid, self.start_address
"TID {} @ {:#x} [{}]",
self.tid, self.start_address, self.state
)
}
}
#[cfg(windows)]
mod platform {
use super::ThreadInfo;
use super::{ThreadInfo, ThreadState};
use anyhow::{Context, Result};
use windows::Win32::Foundation::CloseHandle;
use windows::Win32::System::Diagnostics::ToolHelp::{
CreateToolhelp32Snapshot, Thread32First, Thread32Next, THREADENTRY32, TH32CS_SNAPTHREAD,
};
use windows::Win32::System::Threading::{
OpenThread, THREAD_QUERY_INFORMATION, THREAD_QUERY_LIMITED_INFORMATION,
};
/// Attempts to get thread start address using NtQueryInformationThread.
///
/// This requires ntdll.dll and uses ThreadQuerySetWin32StartAddress.
fn get_thread_start_address(tid: u32) -> usize {
unsafe {
// Try to open the thread with query permissions
let thread_handle = match OpenThread(THREAD_QUERY_INFORMATION, false, tid) {
Ok(h) => h,
Err(_) => {
// Fall back to limited information access
match OpenThread(THREAD_QUERY_LIMITED_INFORMATION, false, tid) {
Ok(h) => h,
Err(_) => return 0,
}
}
};
// Load NtQueryInformationThread from ntdll
let ntdll = match windows::Win32::System::LibraryLoader::GetModuleHandleW(
windows::core::w!("ntdll.dll"),
) {
Ok(h) => h,
Err(_) => {
let _ = CloseHandle(thread_handle);
return 0;
}
};
let proc_addr = windows::Win32::System::LibraryLoader::GetProcAddress(
ntdll,
windows::core::s!("NtQueryInformationThread"),
);
let start_address = if let Some(func) = proc_addr {
// ThreadQuerySetWin32StartAddress = 9
type NtQueryInformationThreadFn = unsafe extern "system" fn(
thread_handle: windows::Win32::Foundation::HANDLE,
thread_information_class: u32,
thread_information: *mut std::ffi::c_void,
thread_information_length: u32,
return_length: *mut u32,
) -> i32;
let nt_query: NtQueryInformationThreadFn = std::mem::transmute(func);
let mut start_addr: usize = 0;
let mut return_length: u32 = 0;
let status = nt_query(
thread_handle,
9, // ThreadQuerySetWin32StartAddress
&mut start_addr as *mut usize as *mut std::ffi::c_void,
std::mem::size_of::<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>> {
let mut threads = Vec::new();
@@ -42,11 +182,16 @@ mod platform {
if Thread32First(snapshot, &mut entry).is_ok() {
loop {
if entry.th32OwnerProcessID == pid {
let tid = entry.th32ThreadID;
let start_address = get_thread_start_address(tid);
let creation_time = get_thread_creation_time(tid);
threads.push(ThreadInfo {
tid: entry.th32ThreadID,
tid,
owner_pid: entry.th32OwnerProcessID,
start_address: 0, // TODO: Get actual start address
creation_time: 0, // TODO: Get thread creation time
start_address,
creation_time,
state: ThreadState::Unknown, // Would need NtQueryInformationThread with ThreadBasicInformation
});
}
@@ -63,16 +208,277 @@ mod platform {
}
}
#[cfg(not(windows))]
#[cfg(target_os = "linux")]
mod platform {
use super::{ThreadInfo, ThreadState};
use anyhow::{Context, Result};
use std::fs;
pub fn enumerate_threads(pid: u32) -> Result<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 {
use super::ThreadInfo;
use anyhow::Result;
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>> {
platform::enumerate_threads(pid)
}

View File

@@ -1,6 +1,11 @@
//! Integration tests for Ghost detection engine.
#[cfg(test)]
mod tests {
use ghost_core::{DetectionEngine, MemoryProtection, MemoryRegion, ProcessInfo, ThreatLevel};
use ghost_core::{
config::DetectionConfig, DetectionEngine, MemoryProtection, MemoryRegion, ProcessInfo,
ThreatLevel,
};
fn create_test_process() -> ProcessInfo {
ProcessInfo {
@@ -23,7 +28,7 @@ mod tests {
#[test]
fn test_clean_process_detection() {
let mut engine = DetectionEngine::new();
let mut engine = DetectionEngine::new().expect("Failed to create engine");
let process = create_test_process();
let regions = vec![MemoryRegion {
base_address: 0x400000,
@@ -39,7 +44,7 @@ mod tests {
#[test]
fn test_rwx_region_detection() {
let mut engine = DetectionEngine::new();
let mut engine = DetectionEngine::new().expect("Failed to create engine");
let process = create_test_process();
let regions = vec![create_rwx_region()];
@@ -51,7 +56,7 @@ mod tests {
#[test]
fn test_multiple_small_executable_regions() {
let mut engine = DetectionEngine::new();
let mut engine = DetectionEngine::new().expect("Failed to create engine");
let process = create_test_process();
let regions = vec![
MemoryRegion {
@@ -84,7 +89,7 @@ mod tests {
#[test]
fn test_baseline_tracking() {
let mut engine = DetectionEngine::new();
let mut engine = DetectionEngine::new().expect("Failed to create engine");
let mut process = create_test_process();
let regions = vec![];
@@ -100,4 +105,252 @@ mod tests {
.iter()
.any(|i| i.contains("new threads")));
}
#[test]
fn test_multiple_rwx_regions_high_severity() {
let mut engine = DetectionEngine::new().expect("Failed to create engine");
let process = create_test_process();
let regions = vec![
MemoryRegion {
base_address: 0x10000000,
size: 0x1000,
protection: MemoryProtection::ReadWriteExecute,
region_type: "PRIVATE".to_string(),
},
MemoryRegion {
base_address: 0x20000000,
size: 0x2000,
protection: MemoryProtection::ReadWriteExecute,
region_type: "PRIVATE".to_string(),
},
MemoryRegion {
base_address: 0x30000000,
size: 0x3000,
protection: MemoryProtection::ReadWriteExecute,
region_type: "PRIVATE".to_string(),
},
];
let result = engine.analyze_process(&process, &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"));
}
}