feat: Add PE header validation and LD_PRELOAD detection
This commit is contained in:
5
.gitignore
vendored
5
.gitignore
vendored
@@ -32,3 +32,8 @@ config.local.toml
|
||||
|
||||
#Ai stuff
|
||||
.claude/
|
||||
.claude-plugins/
|
||||
.claude-embeddings/
|
||||
.claude*.cache/
|
||||
.claude*/
|
||||
|
||||
|
||||
62
BUILD.md
62
BUILD.md
@@ -5,7 +5,7 @@
|
||||
### Windows
|
||||
- Rust toolchain (MSVC target)
|
||||
- Visual Studio Build Tools with C++ workload
|
||||
- Windows SDK
|
||||
- Windows SDK (for windows crate bindings)
|
||||
|
||||
Install via:
|
||||
```powershell
|
||||
@@ -15,22 +15,76 @@ rustup default stable-msvc
|
||||
### Linux
|
||||
- Rust toolchain
|
||||
- GCC/Clang
|
||||
- libelf-dev (for eBPF)
|
||||
- libc development headers
|
||||
|
||||
```bash
|
||||
# Debian/Ubuntu
|
||||
sudo apt install build-essential
|
||||
|
||||
# RHEL/Fedora
|
||||
sudo dnf groupinstall "Development Tools"
|
||||
```
|
||||
|
||||
### macOS
|
||||
- Rust toolchain
|
||||
- Xcode Command Line Tools
|
||||
- Xcode Command Line Tools (for libc bindings)
|
||||
|
||||
```bash
|
||||
xcode-select --install
|
||||
```
|
||||
|
||||
## Building
|
||||
|
||||
```bash
|
||||
# Release build (recommended for performance)
|
||||
cargo build --release
|
||||
|
||||
# Debug build
|
||||
cargo build
|
||||
|
||||
# Check for compilation errors without full build
|
||||
cargo check
|
||||
```
|
||||
|
||||
## Running
|
||||
|
||||
```bash
|
||||
# CLI interface
|
||||
cargo run --bin ghost-cli
|
||||
|
||||
# Terminal UI
|
||||
cargo run --bin ghost-tui
|
||||
|
||||
# With arguments
|
||||
cargo run --bin ghost-cli -- --pid 1234
|
||||
cargo run --bin ghost-cli -- --config examples/ghost.toml
|
||||
```
|
||||
|
||||
Note: Requires elevated privileges for full process memory access.
|
||||
## Testing
|
||||
|
||||
```bash
|
||||
# Run all tests
|
||||
cargo test
|
||||
|
||||
# Run specific test module
|
||||
cargo test --package ghost-core detection_tests
|
||||
|
||||
# Run with output
|
||||
cargo test -- --nocapture
|
||||
```
|
||||
|
||||
## Documentation
|
||||
|
||||
```bash
|
||||
# Generate and open documentation
|
||||
cargo doc --open
|
||||
|
||||
# Generate without dependencies
|
||||
cargo doc --no-deps --open
|
||||
```
|
||||
|
||||
## Platform Notes
|
||||
|
||||
- **Windows**: Requires elevated privileges (Administrator) for full process memory access
|
||||
- **Linux**: Requires appropriate permissions to read /proc/[pid]/mem (root or ptrace capability)
|
||||
- **macOS**: Some features require System Integrity Protection (SIP) to be adjusted for full functionality
|
||||
|
||||
91
README.md
91
README.md
@@ -4,62 +4,64 @@ Cross-platform process injection detection framework written in Rust.
|
||||
|
||||
## Overview
|
||||
|
||||
Ghost is a comprehensive security framework for detecting process injection, memory manipulation, and advanced evasion techniques in running processes. It combines kernel-level monitoring with behavioral analysis, machine learning, and threat intelligence to provide enterprise-grade detection capabilities.
|
||||
Ghost is a security framework for detecting process injection, memory manipulation, and suspicious process behavior. It provides memory analysis, behavioral monitoring, and MITRE ATT&CK technique mapping for security research and defensive purposes.
|
||||
|
||||
## Features
|
||||
|
||||
- **Multi-layer detection**: Memory analysis, behavioral patterns, and ML-based anomaly detection
|
||||
- **MITRE ATT&CK mapping**: Automatic technique classification using the ATT&CK framework
|
||||
- **Threat intelligence**: Integration with threat feeds for IOC correlation and attribution
|
||||
- **Cross-platform**: Windows (full support), Linux (with eBPF), macOS (planned)
|
||||
- **Real-time monitoring**: Continuous scanning with configurable intervals
|
||||
- **Low overhead**: Performance-optimized for production environments
|
||||
- **Memory Analysis**: RWX region detection, shellcode pattern scanning, memory protection analysis
|
||||
- **MITRE ATT&CK Mapping**: Technique identification using the ATT&CK framework
|
||||
- **Cross-platform Support**:
|
||||
- **Windows**: Process enumeration, memory reading (ReadProcessMemory), thread analysis (NtQueryInformationThread), inline hook detection, PE header validation
|
||||
- **Linux**: Process enumeration via procfs, memory region analysis (/proc/[pid]/maps), thread state monitoring, LD_PRELOAD detection, ptrace detection
|
||||
- **macOS**: Process enumeration via sysctl/KERN_PROC_ALL
|
||||
- **Real-time Monitoring**: Continuous scanning with configurable intervals
|
||||
- **Threat Intelligence**: IOC storage and correlation framework
|
||||
|
||||
## Architecture
|
||||
|
||||
```
|
||||
ghost/
|
||||
├── ghost-core/ # Core detection engine (21 modules)
|
||||
├── ghost-core/ # Core detection engine and platform abstractions
|
||||
├── ghost-cli/ # Command-line interface
|
||||
├── ghost-tui/ # Interactive terminal UI
|
||||
├── ghost-tui/ # Interactive terminal UI (Ratatui-based)
|
||||
├── examples/ # Configuration examples
|
||||
└── docs/ # Technical documentation
|
||||
```
|
||||
|
||||
### Core Modules
|
||||
|
||||
- **Detection Engine**: Orchestrates all analysis components
|
||||
- **Memory Analysis**: RWX region detection, shellcode patterns
|
||||
- **Process Hollowing**: PE header validation, memory gap analysis
|
||||
- **Thread Analysis**: Start address validation, behavioral patterns
|
||||
- **Evasion Detection**: Anti-debugging, VM detection, obfuscation
|
||||
- **MITRE ATT&CK Engine**: Technique mapping and threat actor profiling
|
||||
- **Threat Intelligence**: IOC matching and campaign correlation
|
||||
- **Detection Engine** ([detection.rs](ghost-core/src/detection.rs)): Orchestrates analysis and threat scoring
|
||||
- **Memory Analysis** ([memory.rs](ghost-core/src/memory.rs)): Platform-specific memory enumeration and reading
|
||||
- **Process Enumeration** ([process.rs](ghost-core/src/process.rs)): Cross-platform process listing
|
||||
- **Thread Analysis** ([thread.rs](ghost-core/src/thread.rs)): Thread enumeration with start address and creation time
|
||||
- **Hook Detection** ([hooks.rs](ghost-core/src/hooks.rs)): Inline hook detection via JMP pattern analysis
|
||||
- **MITRE ATT&CK** ([mitre.rs](ghost-core/src/mitre.rs)): Technique mapping and categorization
|
||||
- **Configuration** ([config.rs](ghost-core/src/config.rs)): TOML-based configuration with validation
|
||||
|
||||
## Supported Detection Techniques
|
||||
|
||||
### Process Injection (T1055)
|
||||
- RWX memory region detection
|
||||
- Private executable memory analysis
|
||||
- Remote thread creation monitoring
|
||||
- SetWindowsHookEx injection (T1055.001)
|
||||
- Thread hijacking (T1055.003)
|
||||
- APC injection patterns (T1055.004)
|
||||
- Process hollowing (T1055.012)
|
||||
- Reflective DLL injection
|
||||
- Thread count anomaly detection
|
||||
- Inline hook detection (JMP patches on ntdll.dll, kernel32.dll, user32.dll)
|
||||
- LD_PRELOAD and LD_LIBRARY_PATH detection (Linux)
|
||||
- Ptrace injection detection (Linux)
|
||||
- SetWindowsHookEx hook enumeration
|
||||
- Thread hijacking indicators (T1055.003)
|
||||
- Process hollowing detection with PE header validation (T1055.012)
|
||||
|
||||
### Evasion Techniques
|
||||
- Anti-debugging detection
|
||||
- Virtual machine detection attempts
|
||||
- Code obfuscation analysis
|
||||
- Timing-based analysis evasion
|
||||
- Environment fingerprinting
|
||||
### Memory Analysis
|
||||
- Memory protection flags (R/W/X combinations)
|
||||
- Region type classification (IMAGE, PRIVATE, MAPPED, HEAP, STACK)
|
||||
- Small executable region detection (shellcode indicators)
|
||||
- Memory region size anomalies
|
||||
|
||||
### Behavioral Anomalies
|
||||
- Thread count deviations
|
||||
- Memory allocation patterns
|
||||
- API call sequences
|
||||
- Process relationship analysis
|
||||
### Behavioral Monitoring
|
||||
- Thread count changes from baseline
|
||||
- New thread creation detection
|
||||
- Process parent-child relationships
|
||||
- System process identification
|
||||
|
||||
## Installation
|
||||
|
||||
@@ -192,6 +194,27 @@ Please review [SECURITY.md](SECURITY.md) for:
|
||||
- Security considerations
|
||||
- Threat model
|
||||
|
||||
## Platform Support Matrix
|
||||
|
||||
| Feature | Windows | Linux | macOS |
|
||||
|---------|---------|-------|-------|
|
||||
| Process Enumeration | CreateToolhelp32Snapshot | /proc filesystem | sysctl KERN_PROC_ALL |
|
||||
| Memory Enumeration | VirtualQueryEx | /proc/[pid]/maps | Not implemented |
|
||||
| Memory Reading | ReadProcessMemory | /proc/[pid]/mem | Not implemented |
|
||||
| Thread Enumeration | Thread32First/Next | /proc/[pid]/task | Not implemented |
|
||||
| Thread Start Address | NtQueryInformationThread | /proc/[pid]/task/[tid]/syscall | Not implemented |
|
||||
| Thread Creation Time | GetThreadTimes | /proc/[pid]/task/[tid]/stat | Not implemented |
|
||||
| Hook Detection | Inline JMP pattern scanning | LD_PRELOAD/ptrace detection | Not applicable |
|
||||
| PE Header Validation | Full PE validation | Not applicable | Not applicable |
|
||||
| Process Path | GetProcessImageFileNameW | /proc/[pid]/exe | proc_pidpath |
|
||||
|
||||
## Status
|
||||
|
||||
Active development. Core detection engine stable. Windows support complete. Linux eBPF support in progress. macOS Endpoint Security framework planned.
|
||||
Active development. Core detection engine functional with cross-platform abstractions. Windows support most complete. Linux support via procfs. macOS has process enumeration but limited memory/thread analysis.
|
||||
|
||||
### Known Limitations
|
||||
|
||||
- macOS memory enumeration and reading not yet implemented (requires vm_read and mach_vm_region)
|
||||
- Windows SetWindowsHookEx chain enumeration requires parsing undocumented USER32.dll structures
|
||||
- Shellcode pattern matching currently uses heuristics (no actual signature database)
|
||||
- No kernel-level monitoring (all userspace APIs)
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -46,7 +46,9 @@ Ghost detection engine coverage mapped to MITRE ATT&CK framework techniques.
|
||||
- **Indicators**:
|
||||
- Unmapped main executable image
|
||||
- Suspicious memory gaps (>16MB)
|
||||
- PE header mismatches
|
||||
- PE header validation (DOS/NT signatures)
|
||||
- Image base mismatches
|
||||
- Corrupted PE structures
|
||||
- Unusual entry point locations
|
||||
- Memory layout anomalies
|
||||
- **Confidence**: Very High (0.8-1.0)
|
||||
@@ -121,35 +123,82 @@ Ghost detection engine coverage mapped to MITRE ATT&CK framework techniques.
|
||||
|
||||
| Technique | Detection Module | Implementation Status | Test Coverage |
|
||||
|-----------|------------------|----------------------|---------------|
|
||||
| T1055.001 | hooks.rs | ✅ Complete | ✅ Tested |
|
||||
| T1055.002 | shellcode.rs | ✅ Complete | ✅ Tested |
|
||||
| T1055.003 | thread.rs | ✅ Complete | ✅ Tested |
|
||||
| T1055.004 | detection.rs | ⚠️ Partial | ✅ Tested |
|
||||
| T1055.012 | hollowing.rs | ✅ Complete | ✅ Tested |
|
||||
| T1027 | shellcode.rs | ✅ Complete | ✅ Tested |
|
||||
| T1036 | process.rs | ⚠️ Partial | ❌ Pending |
|
||||
| T1106 | detection.rs | ⚠️ Basic | ❌ Pending |
|
||||
| T1055.001 | hooks.rs | ✅ Inline hooks + Linux LD_PRELOAD | ❌ Basic |
|
||||
| T1055.002 | shellcode.rs | ⚠️ Heuristic only | ✅ Basic |
|
||||
| T1055.003 | thread.rs | ✅ Thread enumeration | ✅ Unit tests |
|
||||
| T1055.004 | detection.rs | ⚠️ Heuristic only | ✅ Basic |
|
||||
| T1055.012 | hollowing.rs | ✅ PE header validation | ❌ Pending |
|
||||
| T1027 | shellcode.rs | ⚠️ Basic patterns | ❌ Pending |
|
||||
| T1036 | process.rs | ❌ Not implemented | ❌ Pending |
|
||||
| T1106 | detection.rs | ❌ Not implemented | ❌ Pending |
|
||||
|
||||
**Implementation Status Legend**:
|
||||
- ✅ Complete: Full implementation with actual API calls
|
||||
- ⚠️ Partial: Heuristic-based or incomplete implementation
|
||||
- ❌ Not implemented: Placeholder or missing
|
||||
|
||||
## Current Implementation Details
|
||||
|
||||
### What's Actually Implemented
|
||||
|
||||
1. **Memory Analysis** (memory.rs)
|
||||
- Windows: VirtualQueryEx, ReadProcessMemory
|
||||
- Linux: /proc/[pid]/maps parsing, /proc/[pid]/mem reading
|
||||
- macOS: Not implemented
|
||||
|
||||
2. **Thread Analysis** (thread.rs)
|
||||
- Windows: Thread32First/Next, NtQueryInformationThread, GetThreadTimes
|
||||
- Linux: /proc/[pid]/task enumeration, stat parsing
|
||||
- macOS: Not implemented
|
||||
|
||||
3. **Hook Detection** (hooks.rs)
|
||||
- Windows: Inline hook detection via JMP pattern scanning
|
||||
- Linux: LD_PRELOAD detection, LD_LIBRARY_PATH monitoring, ptrace detection
|
||||
- Detects suspicious library loading from /tmp/, /dev/shm/, etc.
|
||||
- Does NOT enumerate SetWindowsHookEx chains on Windows
|
||||
- No IAT/EAT hook scanning (pattern detection only)
|
||||
|
||||
4. **Process Hollowing Detection** (hollowing.rs)
|
||||
- Windows: Full PE header validation (DOS/NT signatures, image base)
|
||||
- Detects corrupted PE structures
|
||||
- Detects image base mismatches
|
||||
- Memory layout anomaly detection
|
||||
- Memory gap analysis
|
||||
|
||||
5. **Process Enumeration** (process.rs)
|
||||
- Windows: CreateToolhelp32Snapshot
|
||||
- Linux: /proc filesystem
|
||||
- macOS: sysctl KERN_PROC_ALL
|
||||
|
||||
### What's NOT Implemented
|
||||
|
||||
- Actual shellcode signature database
|
||||
- Entropy analysis for obfuscation detection
|
||||
- SetWindowsHookEx chain parsing (Windows)
|
||||
- APC injection detection
|
||||
- MITRE ATT&CK technique attribution (framework only)
|
||||
- process_vm_writev monitoring (Linux)
|
||||
|
||||
## Future Enhancements
|
||||
|
||||
### High Priority
|
||||
|
||||
- **T1055.008** - Ptrace System Calls (Linux)
|
||||
- **T1055.009** - Proc Memory (Linux)
|
||||
- **T1055.008** - Ptrace System Calls (Linux) - ✅ Basic detection implemented
|
||||
- **T1055.013** - Process Doppelgänging
|
||||
- **T1055.014** - VDSO Hijacking (Linux)
|
||||
- Shellcode signature database
|
||||
|
||||
### Medium Priority
|
||||
### Medium Priority
|
||||
|
||||
- **T1134** - Access Token Manipulation
|
||||
- **T1548.002** - Bypass User Account Control
|
||||
- **T1562.001** - Disable or Modify Tools
|
||||
- SetWindowsHookEx chain enumeration
|
||||
- IAT/EAT hook scanning
|
||||
- LD_PRELOAD detection (Linux) - ✅ Implemented
|
||||
|
||||
### Research Areas
|
||||
|
||||
- Machine learning-based anomaly detection
|
||||
- Graph analysis of process relationships
|
||||
- Timeline analysis for attack progression
|
||||
- Behavioral analysis over time
|
||||
- Process relationship analysis
|
||||
- Integration with threat intelligence feeds
|
||||
|
||||
## References
|
||||
|
||||
@@ -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, ®ions, None);
|
||||
}
|
||||
let duration = start.elapsed();
|
||||
|
||||
// Should complete 100 analyses in under 100ms
|
||||
assert!(duration.as_millis() < 100);
|
||||
}
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
1. **Profile First**: Always benchmark before optimizing
|
||||
2. **Measure Impact**: Quantify optimization effectiveness
|
||||
3. **Monitor Production**: Continuous performance monitoring
|
||||
4. **Gradual Tuning**: Make incremental adjustments
|
||||
5. **Document Changes**: Track optimization history
|
||||
1. **Start with defaults**: Use `DetectionConfig::default()` initially
|
||||
2. **Profile specific modules**: Identify which detection is slow
|
||||
3. **Adjust based on needs**: Disable features you don't need
|
||||
4. **Handle errors gracefully**: Processes may exit during scan
|
||||
5. **Test on target hardware**: Performance varies by system
|
||||
|
||||
## Performance Testing Framework
|
||||
## Future Performance Improvements
|
||||
|
||||
```rust
|
||||
#[cfg(test)]
|
||||
mod performance_tests {
|
||||
use super::*;
|
||||
use std::time::Instant;
|
||||
|
||||
#[test]
|
||||
fn benchmark_full_system_scan() {
|
||||
let engine = DetectionEngine::new().unwrap();
|
||||
let start = Instant::now();
|
||||
|
||||
let results = engine.scan_all_processes().unwrap();
|
||||
let duration = start.elapsed();
|
||||
|
||||
assert!(duration.as_millis() < 5000, "Scan took too long");
|
||||
assert!(results.len() > 0, "No processes detected");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn memory_usage_benchmark() {
|
||||
let initial = get_memory_usage();
|
||||
let engine = DetectionEngine::new().unwrap();
|
||||
|
||||
// Perform operations
|
||||
for _ in 0..1000 {
|
||||
engine.analyze_dummy_process();
|
||||
}
|
||||
|
||||
let final_usage = get_memory_usage();
|
||||
let growth = final_usage - initial;
|
||||
|
||||
assert!(growth < 50_000_000, "Memory usage grew too much: {}MB",
|
||||
growth / 1_000_000);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Conclusion
|
||||
|
||||
Ghost's performance can be fine-tuned for various deployment scenarios. Regular monitoring and benchmarking ensure optimal operation while maintaining security effectiveness.
|
||||
|
||||
For additional performance support, see:
|
||||
|
||||
- [Profiling Guide](PROFILING.md)
|
||||
- [Deployment Strategies](DEPLOYMENT.md)
|
||||
- [Scaling Recommendations](SCALING.md)
|
||||
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
|
||||
@@ -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",
|
||||
] }
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
// Detect inline hooks in critical APIs
|
||||
match detect_inline_hooks(target_pid) {
|
||||
Ok(inline) => {
|
||||
inline_hooks = inline.len();
|
||||
for hook in inline {
|
||||
if is_suspicious_inline_hook(&hook) {
|
||||
suspicious_count += 1;
|
||||
}
|
||||
hooks.push(hook);
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
log::debug!("Failed to detect inline hooks: {}", e);
|
||||
}
|
||||
}
|
||||
|
||||
// Check for hooks targeting specific process
|
||||
if let Ok(process_hooks) = enumerate_process_hooks(target_pid) {
|
||||
for hook in process_hooks {
|
||||
// Check if hook procedure is in suspicious location
|
||||
if is_suspicious_hook(&hook) {
|
||||
suspicious_count += 1;
|
||||
}
|
||||
hooks.push(hook);
|
||||
}
|
||||
// Estimate global hooks based on system state
|
||||
global_hooks = estimate_global_hooks();
|
||||
if global_hooks > 10 {
|
||||
suspicious_count += 1;
|
||||
}
|
||||
|
||||
Ok(HookDetectionResult {
|
||||
hooks,
|
||||
suspicious_count,
|
||||
global_hooks,
|
||||
inline_hooks,
|
||||
})
|
||||
}
|
||||
|
||||
fn count_global_hooks() -> Result<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
|
||||
}
|
||||
|
||||
fn is_suspicious_hook(hook: &HookInfo) -> bool {
|
||||
// Check for hooks with suspicious characteristics
|
||||
match hook.hook_type {
|
||||
t if t == WH_CALLWNDPROC.0 => true, // Often used for injection
|
||||
t if t == WH_GETMESSAGE.0 => true, // Common injection vector
|
||||
t if t == WH_CBT.0 => true, // Can be used maliciously
|
||||
t if t == WH_DEBUG.0 => true, // Debugging hooks are suspicious
|
||||
_ => false,
|
||||
/// Detect common hook patterns in function prologue.
|
||||
fn detect_hook_pattern(bytes: &[u8], base_addr: usize) -> Option<usize> {
|
||||
if bytes.len() < 5 {
|
||||
return None;
|
||||
}
|
||||
|
||||
// JMP rel32 (E9 xx xx xx xx)
|
||||
if bytes[0] == 0xE9 {
|
||||
let offset = i32::from_le_bytes([bytes[1], bytes[2], bytes[3], bytes[4]]);
|
||||
let target = (base_addr as i64 + 5 + offset as i64) as usize;
|
||||
return Some(target);
|
||||
}
|
||||
|
||||
// JMP [rip+disp32] (FF 25 xx xx xx xx) - 64-bit
|
||||
if bytes.len() >= 6 && bytes[0] == 0xFF && bytes[1] == 0x25 {
|
||||
// This is an indirect jump, would need to read the target address
|
||||
return Some(0xFFFFFFFF); // Indicate hook detected but target unknown
|
||||
}
|
||||
|
||||
// MOV RAX, imm64; JMP RAX (48 B8 ... FF E0)
|
||||
if bytes.len() >= 12
|
||||
&& bytes[0] == 0x48
|
||||
&& bytes[1] == 0xB8
|
||||
&& bytes[10] == 0xFF
|
||||
&& bytes[11] == 0xE0
|
||||
{
|
||||
let target = u64::from_le_bytes([
|
||||
bytes[2], bytes[3], bytes[4], bytes[5], bytes[6], bytes[7], bytes[8], bytes[9],
|
||||
]) as usize;
|
||||
return Some(target);
|
||||
}
|
||||
|
||||
// PUSH imm32; RET (68 xx xx xx xx C3) - 32-bit style
|
||||
if bytes.len() >= 6 && bytes[0] == 0x68 && bytes[5] == 0xC3 {
|
||||
let target = u32::from_le_bytes([bytes[1], bytes[2], bytes[3], bytes[4]]) as usize;
|
||||
return Some(target);
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
/// Get hook type name for display
|
||||
fn is_suspicious_inline_hook(hook: &HookInfo) -> bool {
|
||||
// All inline hooks are suspicious in security context
|
||||
matches!(hook.hook_type, HookType::InlineHook | HookType::IATHook)
|
||||
}
|
||||
|
||||
fn estimate_global_hooks() -> usize {
|
||||
// In a full implementation, this would enumerate the global hook chain
|
||||
// by parsing USER32.dll's internal structures.
|
||||
// Return typical value for now.
|
||||
3
|
||||
}
|
||||
|
||||
/// Get hook type name for display.
|
||||
pub fn get_hook_type_name(hook_type: u32) -> &'static str {
|
||||
match hook_type {
|
||||
t if t == WH_CALLWNDPROC.0 => "WH_CALLWNDPROC",
|
||||
@@ -147,14 +352,259 @@ mod platform {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(windows))]
|
||||
#[cfg(target_os = "linux")]
|
||||
mod platform {
|
||||
use super::{HookDetectionResult, HookInfo, HookType};
|
||||
use crate::{GhostError, Result};
|
||||
use std::fs;
|
||||
use std::path::Path;
|
||||
|
||||
/// Detect hook injection on Linux (LD_PRELOAD, LD_LIBRARY_PATH, ptrace).
|
||||
pub fn detect_hook_injection(target_pid: u32) -> Result<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,
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -108,7 +425,7 @@ mod platform {
|
||||
let region_type = if mbi.Type == MEM_IMAGE {
|
||||
"IMAGE"
|
||||
} else if mbi.Type == MEM_MAPPED {
|
||||
"MAPPED"
|
||||
"MAPPED"
|
||||
} else if mbi.Type == MEM_PRIVATE {
|
||||
"PRIVATE"
|
||||
} else {
|
||||
@@ -138,19 +455,358 @@ mod platform {
|
||||
|
||||
Ok(regions)
|
||||
}
|
||||
|
||||
/// Reads memory from a process at the specified address.
|
||||
///
|
||||
/// # Safety
|
||||
///
|
||||
/// This function reads arbitrary process memory. The caller must ensure
|
||||
/// the address and size are valid for the target process.
|
||||
pub fn read_process_memory(pid: u32, address: usize, size: usize) -> Result<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)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
///
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
//! Integration tests for Ghost detection engine.
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use ghost_core::{DetectionEngine, MemoryProtection, MemoryRegion, ProcessInfo, ThreatLevel};
|
||||
use ghost_core::{
|
||||
config::DetectionConfig, DetectionEngine, MemoryProtection, MemoryRegion, ProcessInfo,
|
||||
ThreatLevel,
|
||||
};
|
||||
|
||||
fn create_test_process() -> ProcessInfo {
|
||||
ProcessInfo {
|
||||
@@ -23,7 +28,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn test_clean_process_detection() {
|
||||
let mut engine = DetectionEngine::new();
|
||||
let mut engine = DetectionEngine::new().expect("Failed to create engine");
|
||||
let process = create_test_process();
|
||||
let regions = vec![MemoryRegion {
|
||||
base_address: 0x400000,
|
||||
@@ -39,7 +44,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn test_rwx_region_detection() {
|
||||
let mut engine = DetectionEngine::new();
|
||||
let mut engine = DetectionEngine::new().expect("Failed to create engine");
|
||||
let process = create_test_process();
|
||||
let regions = vec![create_rwx_region()];
|
||||
|
||||
@@ -51,7 +56,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn test_multiple_small_executable_regions() {
|
||||
let mut engine = DetectionEngine::new();
|
||||
let mut engine = DetectionEngine::new().expect("Failed to create engine");
|
||||
let process = create_test_process();
|
||||
let regions = vec![
|
||||
MemoryRegion {
|
||||
@@ -84,7 +89,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn test_baseline_tracking() {
|
||||
let mut engine = DetectionEngine::new();
|
||||
let mut engine = DetectionEngine::new().expect("Failed to create engine");
|
||||
let mut process = create_test_process();
|
||||
let regions = vec![];
|
||||
|
||||
@@ -100,4 +105,252 @@ mod tests {
|
||||
.iter()
|
||||
.any(|i| i.contains("new threads")));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_multiple_rwx_regions_high_severity() {
|
||||
let mut engine = DetectionEngine::new().expect("Failed to create engine");
|
||||
let process = create_test_process();
|
||||
let regions = vec![
|
||||
MemoryRegion {
|
||||
base_address: 0x10000000,
|
||||
size: 0x1000,
|
||||
protection: MemoryProtection::ReadWriteExecute,
|
||||
region_type: "PRIVATE".to_string(),
|
||||
},
|
||||
MemoryRegion {
|
||||
base_address: 0x20000000,
|
||||
size: 0x2000,
|
||||
protection: MemoryProtection::ReadWriteExecute,
|
||||
region_type: "PRIVATE".to_string(),
|
||||
},
|
||||
MemoryRegion {
|
||||
base_address: 0x30000000,
|
||||
size: 0x3000,
|
||||
protection: MemoryProtection::ReadWriteExecute,
|
||||
region_type: "PRIVATE".to_string(),
|
||||
},
|
||||
];
|
||||
|
||||
let result = engine.analyze_process(&process, ®ions, None);
|
||||
// Multiple RWX regions should be highly suspicious
|
||||
assert_eq!(result.threat_level, ThreatLevel::Malicious);
|
||||
assert!(result.confidence >= 0.5);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_memory_protection_display() {
|
||||
assert_eq!(format!("{}", MemoryProtection::NoAccess), "---");
|
||||
assert_eq!(format!("{}", MemoryProtection::ReadOnly), "R--");
|
||||
assert_eq!(format!("{}", MemoryProtection::ReadWrite), "RW-");
|
||||
assert_eq!(format!("{}", MemoryProtection::ReadExecute), "R-X");
|
||||
assert_eq!(format!("{}", MemoryProtection::ReadWriteExecute), "RWX");
|
||||
assert_eq!(format!("{}", MemoryProtection::Execute), "--X");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_process_info_display() {
|
||||
let process = create_test_process();
|
||||
let display = format!("{}", process);
|
||||
assert!(display.contains("1234"));
|
||||
assert!(display.contains("test.exe"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_memory_region_display() {
|
||||
let region = create_rwx_region();
|
||||
let display = format!("{}", region);
|
||||
assert!(display.contains("RWX"));
|
||||
assert!(display.contains("PRIVATE"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_threat_level_ordering() {
|
||||
assert!(ThreatLevel::Clean < ThreatLevel::Suspicious);
|
||||
assert!(ThreatLevel::Suspicious < ThreatLevel::Malicious);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_detection_config_validation() {
|
||||
let config = DetectionConfig::default();
|
||||
assert!(config.validate().is_ok());
|
||||
|
||||
let mut invalid_config = DetectionConfig::default();
|
||||
invalid_config.confidence_threshold = 1.5; // Invalid
|
||||
assert!(invalid_config.validate().is_err());
|
||||
|
||||
invalid_config.confidence_threshold = -0.1; // Invalid
|
||||
assert!(invalid_config.validate().is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_detection_config_presets() {
|
||||
let perf_config = DetectionConfig::performance_mode();
|
||||
let thorough_config = DetectionConfig::thorough_mode();
|
||||
|
||||
// Performance mode should have lower thresholds for faster scanning
|
||||
assert!(perf_config.confidence_threshold <= thorough_config.confidence_threshold);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_process_is_system_process() {
|
||||
let mut process = create_test_process();
|
||||
assert!(!process.is_system_process());
|
||||
|
||||
process.pid = 0;
|
||||
assert!(process.is_system_process());
|
||||
|
||||
process.pid = 4;
|
||||
assert!(process.is_system_process());
|
||||
|
||||
process.pid = 100;
|
||||
process.name = "System".to_string();
|
||||
assert!(process.is_system_process());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_engine_with_custom_config() {
|
||||
let mut config = DetectionConfig::default();
|
||||
config.rwx_detection = false;
|
||||
|
||||
let mut engine = DetectionEngine::with_config(config).expect("Failed to create engine");
|
||||
let process = create_test_process();
|
||||
let regions = vec![create_rwx_region()];
|
||||
|
||||
// With RWX detection disabled, should not flag the region
|
||||
let result = engine.analyze_process(&process, ®ions, None);
|
||||
// Might still detect based on other heuristics, but confidence should be lower
|
||||
assert!(result.confidence < 0.5);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_large_memory_region() {
|
||||
let mut engine = DetectionEngine::new().expect("Failed to create engine");
|
||||
let process = create_test_process();
|
||||
let regions = vec![MemoryRegion {
|
||||
base_address: 0x10000000,
|
||||
size: 100 * 1024 * 1024, // 100MB region
|
||||
protection: MemoryProtection::ReadWriteExecute,
|
||||
region_type: "PRIVATE".to_string(),
|
||||
}];
|
||||
|
||||
let result = engine.analyze_process(&process, ®ions, None);
|
||||
assert_ne!(result.threat_level, ThreatLevel::Clean);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_image_vs_private_region() {
|
||||
let mut engine = DetectionEngine::new().expect("Failed to create engine");
|
||||
let process = create_test_process();
|
||||
|
||||
// IMAGE region with RX is normal
|
||||
let image_regions = vec![MemoryRegion {
|
||||
base_address: 0x400000,
|
||||
size: 0x100000,
|
||||
protection: MemoryProtection::ReadExecute,
|
||||
region_type: "IMAGE".to_string(),
|
||||
}];
|
||||
|
||||
let result = engine.analyze_process(&process, &image_regions, None);
|
||||
assert_eq!(result.threat_level, ThreatLevel::Clean);
|
||||
|
||||
// PRIVATE region with RX is suspicious
|
||||
let private_regions = vec![MemoryRegion {
|
||||
base_address: 0x10000000,
|
||||
size: 0x1000,
|
||||
protection: MemoryProtection::ReadExecute,
|
||||
region_type: "PRIVATE".to_string(),
|
||||
}];
|
||||
|
||||
let result2 = engine.analyze_process(&process, &private_regions, None);
|
||||
// Private executable regions are suspicious but not as severe as RWX
|
||||
assert!(result2.confidence > 0.0 || result2.indicators.len() > 0);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod mitre_tests {
|
||||
use ghost_core::mitre::{MitreMapping, TechniqueId};
|
||||
|
||||
#[test]
|
||||
fn test_technique_id_display() {
|
||||
let id = TechniqueId::new("T1055", Some("001"));
|
||||
assert_eq!(format!("{}", id), "T1055.001");
|
||||
|
||||
let id_no_sub = TechniqueId::new("T1055", None);
|
||||
assert_eq!(format!("{}", id_no_sub), "T1055");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_mitre_mapping_creation() {
|
||||
let mapping = MitreMapping::default();
|
||||
assert!(mapping.techniques.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_technique_lookup() {
|
||||
let mapping = MitreMapping::default();
|
||||
// Default mapping should have no techniques initially
|
||||
assert!(mapping.get_technique("T1055").is_none());
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod threat_intel_tests {
|
||||
use ghost_core::ThreatLevel;
|
||||
|
||||
#[test]
|
||||
fn test_threat_level_description() {
|
||||
assert_eq!(ThreatLevel::Clean.description(), "No threats detected");
|
||||
assert_eq!(
|
||||
ThreatLevel::Suspicious.description(),
|
||||
"Potential security concern"
|
||||
);
|
||||
assert_eq!(
|
||||
ThreatLevel::Malicious.description(),
|
||||
"High confidence malicious activity"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_threat_level_serialization() {
|
||||
let level = ThreatLevel::Suspicious;
|
||||
let serialized = serde_json::to_string(&level).expect("Failed to serialize");
|
||||
assert!(serialized.contains("Suspicious"));
|
||||
|
||||
let deserialized: ThreatLevel =
|
||||
serde_json::from_str(&serialized).expect("Failed to deserialize");
|
||||
assert_eq!(deserialized, level);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod config_tests {
|
||||
use ghost_core::config::DetectionConfig;
|
||||
|
||||
#[test]
|
||||
fn test_default_config() {
|
||||
let config = DetectionConfig::default();
|
||||
assert!(config.rwx_detection);
|
||||
assert!(config.shellcode_detection);
|
||||
assert!(config.hollowing_detection);
|
||||
assert!(config.thread_detection);
|
||||
assert!(config.hook_detection);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_config_serialization() {
|
||||
let config = DetectionConfig::default();
|
||||
let json = serde_json::to_string(&config).expect("Failed to serialize");
|
||||
let deserialized: DetectionConfig =
|
||||
serde_json::from_str(&json).expect("Failed to deserialize");
|
||||
assert_eq!(config.rwx_detection, deserialized.rwx_detection);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_config_toml_format() {
|
||||
let config = DetectionConfig::default();
|
||||
let toml_str = toml::to_string(&config).expect("Failed to serialize to TOML");
|
||||
assert!(toml_str.contains("rwx_detection"));
|
||||
assert!(toml_str.contains("confidence_threshold"));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user