feat: Add PE header validation and LD_PRELOAD detection

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

View File

@@ -1,5 +1,8 @@
use crate::{GhostError, MemoryRegion, ProcessInfo, Result};
#[cfg(windows)]
use crate::memory::{validate_pe_header, read_pe_header_info, PEHeaderValidation};
#[derive(Debug, Clone)]
pub struct HollowingDetection {
pub pid: u32,
@@ -14,6 +17,8 @@ pub enum HollowingIndicator {
SuspiciousImageBase,
MemoryLayoutAnomaly { expected_size: usize, actual_size: usize },
MismatchedPEHeader,
InvalidPEHeader { validation: String },
CorruptedPEStructure { address: usize, reason: String },
UnusualEntryPoint { address: usize },
SuspiciousMemoryGaps { gap_count: usize, largest_gap: usize },
}
@@ -27,6 +32,12 @@ impl std::fmt::Display for HollowingIndicator {
write!(f, "Memory layout anomaly: expected {:#x}, found {:#x}", expected_size, actual_size)
}
Self::MismatchedPEHeader => write!(f, "PE header mismatch detected"),
Self::InvalidPEHeader { validation } => {
write!(f, "Invalid PE header: {}", validation)
}
Self::CorruptedPEStructure { address, reason } => {
write!(f, "Corrupted PE structure at {:#x}: {}", address, reason)
}
Self::UnusualEntryPoint { address } => {
write!(f, "Entry point at unusual location: {:#x}", address)
}
@@ -72,12 +83,18 @@ impl HollowingDetector {
confidence += 0.4;
}
// Check for PE header anomalies
// Check for PE header anomalies (heuristic-based)
if let Some(indicator) = self.check_pe_header_anomalies(memory_regions) {
indicators.push(indicator);
confidence += 0.7;
}
// Validate actual PE headers (deep inspection)
if let Some(indicator) = self.validate_pe_headers(process.pid, memory_regions) {
indicators.push(indicator);
confidence += 0.9; // Higher confidence for actual PE validation
}
// Check entry point location
if let Some(indicator) = self.check_entry_point_anomalies(process, memory_regions) {
indicators.push(indicator);
@@ -223,6 +240,71 @@ impl HollowingDetector {
None
}
#[cfg(windows)]
fn validate_pe_headers(&self, pid: u32, regions: &[MemoryRegion]) -> Option<HollowingIndicator> {
// Focus on main executable IMAGE regions
let image_regions: Vec<_> = regions
.iter()
.filter(|r| r.region_type == "IMAGE")
.take(5) // Check first 5 IMAGE regions (main exe + critical DLLs)
.collect();
for region in image_regions {
match validate_pe_header(pid, region.base_address) {
Ok(validation) => {
match validation {
PEHeaderValidation::Valid => continue,
PEHeaderValidation::InvalidDosSignature => {
return Some(HollowingIndicator::InvalidPEHeader {
validation: "Invalid DOS signature (not MZ)".to_string(),
});
}
PEHeaderValidation::InvalidNtSignature => {
return Some(HollowingIndicator::InvalidPEHeader {
validation: "Invalid NT signature (not PE)".to_string(),
});
}
PEHeaderValidation::InvalidHeaderOffset => {
return Some(HollowingIndicator::InvalidPEHeader {
validation: "Invalid PE header offset".to_string(),
});
}
PEHeaderValidation::MismatchedImageBase => {
return Some(HollowingIndicator::CorruptedPEStructure {
address: region.base_address,
reason: "Image base mismatch - possible hollowing".to_string(),
});
}
PEHeaderValidation::SuspiciousEntryPoint => {
return Some(HollowingIndicator::InvalidPEHeader {
validation: "Suspicious entry point location".to_string(),
});
}
PEHeaderValidation::CorruptedHeader => {
return Some(HollowingIndicator::CorruptedPEStructure {
address: region.base_address,
reason: "Corrupted PE header structure".to_string(),
});
}
PEHeaderValidation::NotPE => continue,
}
}
Err(_) => {
// Could not read memory - might be suspicious but don't report
continue;
}
}
}
None
}
#[cfg(not(windows))]
fn validate_pe_headers(&self, _pid: u32, _regions: &[MemoryRegion]) -> Option<HollowingIndicator> {
// PE validation is Windows-specific
None
}
fn check_entry_point_anomalies(
&self,
_process: &ProcessInfo,

View File

@@ -1,130 +1,335 @@
//! Hook detection for identifying SetWindowsHookEx and inline hook-based injection.
//!
//! This module detects Windows message hooks and inline API hooks that are commonly
//! used for process injection (T1055.003, T1055.012).
//! On Linux, it detects LD_PRELOAD and LD_LIBRARY_PATH based injection.
use crate::{GhostError, Result};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
#[derive(Debug, Clone)]
pub struct HookInfo {
pub hook_type: u32,
pub thread_id: u32,
pub hook_proc: usize,
pub module_name: String,
/// Type of hook detected.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum HookType {
/// SetWindowsHookEx hook (message hook).
WindowsHook(u32),
/// Inline/detour hook (JMP patch).
InlineHook,
/// Import Address Table (IAT) hook.
IATHook,
/// Export Address Table (EAT) hook.
EATHook,
/// LD_PRELOAD based library injection (Linux).
LdPreload,
/// LD_LIBRARY_PATH manipulation (Linux).
LdLibraryPath,
/// Ptrace-based injection (Linux).
PtraceInjection,
}
#[derive(Debug, Clone)]
impl std::fmt::Display for HookType {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::WindowsHook(id) => write!(f, "WindowsHook({})", id),
Self::InlineHook => write!(f, "InlineHook"),
Self::IATHook => write!(f, "IATHook"),
Self::EATHook => write!(f, "EATHook"),
Self::LdPreload => write!(f, "LD_PRELOAD"),
Self::LdLibraryPath => write!(f, "LD_LIBRARY_PATH"),
Self::PtraceInjection => write!(f, "PtraceInjection"),
}
}
}
/// Information about a detected hook.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct HookInfo {
/// Type of hook.
pub hook_type: HookType,
/// Thread ID (for message hooks) or 0 for system-wide.
pub thread_id: u32,
/// Address of the hook procedure.
pub hook_proc: usize,
/// Original address (for inline/IAT hooks).
pub original_address: usize,
/// Module containing the hook procedure.
pub module_name: String,
/// Function being hooked (for inline/IAT hooks).
pub hooked_function: String,
}
/// Result of hook detection analysis.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct HookDetectionResult {
/// List of detected hooks.
pub hooks: Vec<HookInfo>,
/// Number of suspicious hooks.
pub suspicious_count: usize,
/// Number of global/system-wide hooks.
pub global_hooks: usize,
/// Number of inline API hooks detected.
pub inline_hooks: usize,
}
#[cfg(windows)]
mod platform {
use super::{HookDetectionResult, HookInfo};
use super::{HookDetectionResult, HookInfo, HookType};
use crate::{GhostError, Result};
use std::collections::HashMap;
use windows::Win32::Foundation::{GetLastError, HWND};
use windows::Win32::System::LibraryLoader::{GetModuleFileNameW, GetModuleHandleW};
use windows::Win32::Foundation::CloseHandle;
use windows::Win32::System::Diagnostics::Debug::ReadProcessMemory;
use windows::Win32::System::LibraryLoader::{
GetModuleHandleW, GetProcAddress, LoadLibraryW,
};
use windows::Win32::System::ProcessStatus::{
EnumProcessModulesEx, GetModuleBaseNameW, GetModuleInformation, LIST_MODULES_ALL,
MODULEINFO,
};
use windows::Win32::System::Threading::{OpenProcess, PROCESS_QUERY_INFORMATION, PROCESS_VM_READ};
use windows::Win32::UI::WindowsAndMessaging::{
EnumWindows, GetWindowThreadProcessId, HC_ACTION, HOOKPROC, WH_CALLWNDPROC,
WH_CALLWNDPROCRET, WH_CBT, WH_DEBUG, WH_FOREGROUNDIDLE, WH_GETMESSAGE, WH_JOURNALPLAYBACK,
WH_JOURNALRECORD, WH_KEYBOARD, WH_KEYBOARD_LL, WH_MOUSE, WH_MOUSE_LL, WH_MSGFILTER,
WH_SHELL, WH_SYSMSGFILTER,
WH_CALLWNDPROC, WH_CALLWNDPROCRET, WH_CBT, WH_DEBUG, WH_FOREGROUNDIDLE, WH_GETMESSAGE,
WH_JOURNALPLAYBACK, WH_JOURNALRECORD, WH_KEYBOARD, WH_KEYBOARD_LL, WH_MOUSE,
WH_MOUSE_LL, WH_MSGFILTER, WH_SHELL, WH_SYSMSGFILTER,
};
/// Detect Windows hook-based injection techniques
/// Critical APIs commonly hooked for injection.
const CRITICAL_APIS: &[(&str, &str)] = &[
("ntdll.dll", "NtCreateThread"),
("ntdll.dll", "NtCreateThreadEx"),
("ntdll.dll", "NtAllocateVirtualMemory"),
("ntdll.dll", "NtWriteVirtualMemory"),
("ntdll.dll", "NtProtectVirtualMemory"),
("ntdll.dll", "NtQueueApcThread"),
("kernel32.dll", "VirtualAllocEx"),
("kernel32.dll", "WriteProcessMemory"),
("kernel32.dll", "CreateRemoteThread"),
("kernel32.dll", "LoadLibraryA"),
("kernel32.dll", "LoadLibraryW"),
("user32.dll", "SetWindowsHookExA"),
("user32.dll", "SetWindowsHookExW"),
];
/// Detect Windows hook-based injection techniques.
pub fn detect_hook_injection(target_pid: u32) -> Result<HookDetectionResult> {
let mut hooks = Vec::new();
let mut suspicious_count = 0;
let mut global_hooks = 0;
let mut inline_hooks = 0;
// This is a simplified implementation - real hook detection requires
// more sophisticated techniques like parsing USER32.dll's hook table
// or using undocumented APIs. For now, we'll detect based on heuristics.
// Check for global hooks that might be used for injection
if let Ok(global_hook_count) = count_global_hooks() {
global_hooks = global_hook_count;
if global_hook_count > 5 {
suspicious_count += 1;
// 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,
})
}

View File

@@ -1,6 +1,120 @@
//! Memory region enumeration and analysis.
//!
//! This module provides cross-platform memory introspection capabilities,
//! allowing analysis of process memory layouts, protection flags, and content.
use serde::{Deserialize, Serialize};
use std::fmt;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
/// PE header constants
#[cfg(windows)]
pub const IMAGE_DOS_SIGNATURE: u16 = 0x5A4D; // "MZ"
#[cfg(windows)]
pub const IMAGE_NT_SIGNATURE: u32 = 0x00004550; // "PE\0\0"
/// DOS header structure (first 64 bytes of a PE file)
#[derive(Debug, Clone, Copy)]
#[repr(C)]
pub struct ImageDosHeader {
pub e_magic: u16, // Magic number ("MZ")
pub e_cblp: u16, // Bytes on last page
pub e_cp: u16, // Pages in file
pub e_crlc: u16, // Relocations
pub e_cparhdr: u16, // Size of header in paragraphs
pub e_minalloc: u16, // Minimum extra paragraphs
pub e_maxalloc: u16, // Maximum extra paragraphs
pub e_ss: u16, // Initial SS value
pub e_sp: u16, // Initial SP value
pub e_csum: u16, // Checksum
pub e_ip: u16, // Initial IP value
pub e_cs: u16, // Initial CS value
pub e_lfarlc: u16, // File address of relocation table
pub e_ovno: u16, // Overlay number
pub e_res: [u16; 4], // Reserved
pub e_oemid: u16, // OEM identifier
pub e_oeminfo: u16, // OEM information
pub e_res2: [u16; 10], // Reserved
pub e_lfanew: i32, // File address of new exe header
}
/// PE file header structure
#[derive(Debug, Clone, Copy)]
#[repr(C)]
pub struct ImageFileHeader {
pub machine: u16,
pub number_of_sections: u16,
pub time_date_stamp: u32,
pub pointer_to_symbol_table: u32,
pub number_of_symbols: u32,
pub size_of_optional_header: u16,
pub characteristics: u16,
}
/// PE optional header structure (64-bit)
#[derive(Debug, Clone, Copy)]
#[repr(C)]
pub struct ImageOptionalHeader64 {
pub magic: u16,
pub major_linker_version: u8,
pub minor_linker_version: u8,
pub size_of_code: u32,
pub size_of_initialized_data: u32,
pub size_of_uninitialized_data: u32,
pub address_of_entry_point: u32,
pub base_of_code: u32,
pub image_base: u64,
pub section_alignment: u32,
pub file_alignment: u32,
pub major_operating_system_version: u16,
pub minor_operating_system_version: u16,
pub major_image_version: u16,
pub minor_image_version: u16,
pub major_subsystem_version: u16,
pub minor_subsystem_version: u16,
pub win32_version_value: u32,
pub size_of_image: u32,
pub size_of_headers: u32,
pub check_sum: u32,
pub subsystem: u16,
pub dll_characteristics: u16,
pub size_of_stack_reserve: u64,
pub size_of_stack_commit: u64,
pub size_of_heap_reserve: u64,
pub size_of_heap_commit: u64,
pub loader_flags: u32,
pub number_of_rva_and_sizes: u32,
}
/// PE header validation result
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum PEHeaderValidation {
Valid,
InvalidDosSignature,
InvalidNtSignature,
InvalidHeaderOffset,
MismatchedImageBase,
SuspiciousEntryPoint,
CorruptedHeader,
NotPE,
}
impl fmt::Display for PEHeaderValidation {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Valid => write!(f, "Valid PE header"),
Self::InvalidDosSignature => write!(f, "Invalid DOS signature"),
Self::InvalidNtSignature => write!(f, "Invalid NT signature"),
Self::InvalidHeaderOffset => write!(f, "Invalid header offset"),
Self::MismatchedImageBase => write!(f, "Image base mismatch"),
Self::SuspiciousEntryPoint => write!(f, "Suspicious entry point"),
Self::CorruptedHeader => write!(f, "Corrupted PE header"),
Self::NotPE => write!(f, "Not a PE file"),
}
}
}
/// Memory protection flags for a memory region.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum MemoryProtection {
NoAccess,
ReadOnly,
@@ -28,11 +142,16 @@ impl fmt::Display for MemoryProtection {
}
}
#[derive(Debug, Clone)]
/// Information about a memory region within a process.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct MemoryRegion {
/// Base address of the memory region.
pub base_address: usize,
/// Size of the region in bytes.
pub size: usize,
/// Memory protection flags.
pub protection: MemoryProtection,
/// Type of memory region (IMAGE, MAPPED, PRIVATE, etc.).
pub region_type: String,
}
@@ -49,18 +168,217 @@ impl fmt::Display for MemoryRegion {
}
}
/// Validates a PE header in process memory
#[cfg(windows)]
pub fn validate_pe_header(pid: u32, base_address: usize) -> anyhow::Result<PEHeaderValidation> {
use std::mem;
// Read DOS header
let dos_header_size = mem::size_of::<ImageDosHeader>();
let dos_header_bytes = read_process_memory(pid, base_address, dos_header_size)?;
if dos_header_bytes.len() < dos_header_size {
return Ok(PEHeaderValidation::CorruptedHeader);
}
let dos_header = unsafe {
std::ptr::read(dos_header_bytes.as_ptr() as *const ImageDosHeader)
};
// Validate DOS signature
if dos_header.e_magic != IMAGE_DOS_SIGNATURE {
return Ok(PEHeaderValidation::InvalidDosSignature);
}
// Validate e_lfanew offset (should be reasonable)
if dos_header.e_lfanew < 0 || dos_header.e_lfanew > 0x1000 {
return Ok(PEHeaderValidation::InvalidHeaderOffset);
}
// Read NT headers
let nt_header_address = base_address.wrapping_add(dos_header.e_lfanew as usize);
// Read NT signature (4 bytes)
let nt_sig_bytes = read_process_memory(pid, nt_header_address, 4)?;
if nt_sig_bytes.len() < 4 {
return Ok(PEHeaderValidation::CorruptedHeader);
}
let nt_signature = u32::from_le_bytes([
nt_sig_bytes[0],
nt_sig_bytes[1],
nt_sig_bytes[2],
nt_sig_bytes[3],
]);
if nt_signature != IMAGE_NT_SIGNATURE {
return Ok(PEHeaderValidation::InvalidNtSignature);
}
// Read file header
let file_header_address = nt_header_address + 4;
let file_header_size = mem::size_of::<ImageFileHeader>();
let file_header_bytes = read_process_memory(pid, file_header_address, file_header_size)?;
if file_header_bytes.len() < file_header_size {
return Ok(PEHeaderValidation::CorruptedHeader);
}
let file_header = unsafe {
std::ptr::read(file_header_bytes.as_ptr() as *const ImageFileHeader)
};
// Read optional header (64-bit)
let optional_header_address = file_header_address + file_header_size;
let optional_header_size = mem::size_of::<ImageOptionalHeader64>();
let optional_header_bytes = read_process_memory(pid, optional_header_address, optional_header_size)?;
if optional_header_bytes.len() < optional_header_size {
return Ok(PEHeaderValidation::CorruptedHeader);
}
let optional_header = unsafe {
std::ptr::read(optional_header_bytes.as_ptr() as *const ImageOptionalHeader64)
};
// Validate image base matches memory address
if optional_header.image_base != base_address as u64 {
return Ok(PEHeaderValidation::MismatchedImageBase);
}
// Validate entry point (should be within the image)
let entry_point_rva = optional_header.address_of_entry_point;
if entry_point_rva == 0 || entry_point_rva >= optional_header.size_of_image {
return Ok(PEHeaderValidation::SuspiciousEntryPoint);
}
// Additional validation: check if sections count is reasonable
if file_header.number_of_sections > 96 {
return Ok(PEHeaderValidation::CorruptedHeader);
}
Ok(PEHeaderValidation::Valid)
}
/// Stub for non-Windows platforms
#[cfg(not(windows))]
pub fn validate_pe_header(_pid: u32, _base_address: usize) -> anyhow::Result<PEHeaderValidation> {
Ok(PEHeaderValidation::NotPE)
}
/// Gets PE header information from process memory
#[cfg(windows)]
pub fn read_pe_header_info(pid: u32, base_address: usize) -> anyhow::Result<Option<PEHeaderInfo>> {
use std::mem;
let dos_header_size = mem::size_of::<ImageDosHeader>();
let dos_header_bytes = read_process_memory(pid, base_address, dos_header_size)?;
if dos_header_bytes.len() < dos_header_size {
return Ok(None);
}
let dos_header = unsafe {
std::ptr::read(dos_header_bytes.as_ptr() as *const ImageDosHeader)
};
if dos_header.e_magic != IMAGE_DOS_SIGNATURE {
return Ok(None);
}
if dos_header.e_lfanew < 0 || dos_header.e_lfanew > 0x1000 {
return Ok(None);
}
let nt_header_address = base_address.wrapping_add(dos_header.e_lfanew as usize);
// Read NT signature
let nt_sig_bytes = read_process_memory(pid, nt_header_address, 4)?;
if nt_sig_bytes.len() < 4 {
return Ok(None);
}
let nt_signature = u32::from_le_bytes([
nt_sig_bytes[0],
nt_sig_bytes[1],
nt_sig_bytes[2],
nt_sig_bytes[3],
]);
if nt_signature != IMAGE_NT_SIGNATURE {
return Ok(None);
}
// Read file header
let file_header_address = nt_header_address + 4;
let file_header_size = mem::size_of::<ImageFileHeader>();
let file_header_bytes = read_process_memory(pid, file_header_address, file_header_size)?;
if file_header_bytes.len() < file_header_size {
return Ok(None);
}
let file_header = unsafe {
std::ptr::read(file_header_bytes.as_ptr() as *const ImageFileHeader)
};
// Read optional header
let optional_header_address = file_header_address + file_header_size;
let optional_header_size = mem::size_of::<ImageOptionalHeader64>();
let optional_header_bytes = read_process_memory(pid, optional_header_address, optional_header_size)?;
if optional_header_bytes.len() < optional_header_size {
return Ok(None);
}
let optional_header = unsafe {
std::ptr::read(optional_header_bytes.as_ptr() as *const ImageOptionalHeader64)
};
Ok(Some(PEHeaderInfo {
dos_signature: dos_header.e_magic,
nt_signature,
machine: file_header.machine,
number_of_sections: file_header.number_of_sections,
image_base: optional_header.image_base,
entry_point_rva: optional_header.address_of_entry_point,
size_of_image: optional_header.size_of_image,
size_of_headers: optional_header.size_of_headers,
}))
}
/// PE header information extracted from process memory
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PEHeaderInfo {
pub dos_signature: u16,
pub nt_signature: u32,
pub machine: u16,
pub number_of_sections: u16,
pub image_base: u64,
pub entry_point_rva: u32,
pub size_of_image: u32,
pub size_of_headers: u32,
}
#[cfg(not(windows))]
pub fn read_pe_header_info(_pid: u32, _base_address: usize) -> anyhow::Result<Option<PEHeaderInfo>> {
Ok(None)
}
#[cfg(windows)]
mod platform {
use super::{MemoryProtection, MemoryRegion};
use anyhow::{Context, Result};
use windows::Win32::Foundation::{CloseHandle, HANDLE};
use windows::Win32::Foundation::CloseHandle;
use windows::Win32::System::Diagnostics::Debug::ReadProcessMemory;
use windows::Win32::System::Memory::{
VirtualQueryEx, MEMORY_BASIC_INFORMATION, MEM_COMMIT, MEM_FREE, MEM_IMAGE, MEM_MAPPED,
MEM_PRIVATE, MEM_RESERVE, PAGE_EXECUTE, PAGE_EXECUTE_READ, PAGE_EXECUTE_READWRITE,
PAGE_EXECUTE_WRITECOPY, PAGE_NOACCESS, PAGE_READONLY, PAGE_READWRITE, PAGE_WRITECOPY,
VirtualQueryEx, MEMORY_BASIC_INFORMATION, MEM_COMMIT, MEM_IMAGE, MEM_MAPPED, MEM_PRIVATE,
PAGE_EXECUTE, PAGE_EXECUTE_READ, PAGE_EXECUTE_READWRITE, PAGE_EXECUTE_WRITECOPY,
PAGE_NOACCESS, PAGE_READONLY, PAGE_READWRITE, PAGE_WRITECOPY,
};
use windows::Win32::System::Threading::{
OpenProcess, PROCESS_QUERY_INFORMATION, PROCESS_VM_READ,
};
use windows::Win32::System::Threading::{OpenProcess, PROCESS_QUERY_INFORMATION, PROCESS_VM_READ};
fn parse_protection(protect: u32) -> MemoryProtection {
match protect & 0xFF {
@@ -80,7 +398,6 @@ mod platform {
pub fn enumerate_memory_regions(pid: u32) -> Result<Vec<MemoryRegion>> {
let mut regions = Vec::new();
// Skip system process
if pid == 0 || pid == 4 {
return Ok(regions);
}
@@ -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)
}

View File

@@ -202,13 +202,109 @@ mod platform {
#[cfg(target_os = "macos")]
mod platform {
use super::ProcessInfo;
use anyhow::Result;
use anyhow::{Context, Result};
pub fn enumerate_processes() -> Result<Vec<ProcessInfo>> {
// macOS implementation would use libproc or sysctl
// For now, return empty to indicate platform support is partial
log::warn!("macOS process enumeration not yet fully implemented");
Ok(Vec::new())
use libc::{c_int, c_void, size_t, sysctl, CTL_KERN, KERN_PROC, KERN_PROC_ALL};
use std::mem;
use std::ptr;
let mut processes = Vec::new();
unsafe {
// First, get the size needed for the buffer
let mut mib: [c_int; 4] = [CTL_KERN, KERN_PROC, KERN_PROC_ALL, 0];
let mut size: size_t = 0;
let result = sysctl(
mib.as_mut_ptr(),
3,
ptr::null_mut(),
&mut size,
ptr::null_mut(),
0,
);
if result != 0 {
return Err(anyhow::anyhow!(
"Failed to get process list size: {}",
std::io::Error::last_os_error()
));
}
// Allocate buffer with some extra space
let count = size / mem::size_of::<libc::kinfo_proc>();
let mut buffer: Vec<libc::kinfo_proc> = Vec::with_capacity(count + 16);
buffer.resize_with(count + 16, || mem::zeroed());
let mut actual_size = buffer.len() * mem::size_of::<libc::kinfo_proc>();
let result = sysctl(
mib.as_mut_ptr(),
3,
buffer.as_mut_ptr() as *mut c_void,
&mut actual_size,
ptr::null_mut(),
0,
);
if result != 0 {
return Err(anyhow::anyhow!(
"Failed to get process list: {}",
std::io::Error::last_os_error()
));
}
let actual_count = actual_size / mem::size_of::<libc::kinfo_proc>();
for i in 0..actual_count {
let proc_info = &buffer[i];
let pid = proc_info.kp_proc.p_pid as u32;
let ppid = proc_info.kp_eproc.e_ppid as u32;
// Get process name from comm field
let comm = &proc_info.kp_proc.p_comm;
let name = std::ffi::CStr::from_ptr(comm.as_ptr())
.to_string_lossy()
.into_owned();
// Get executable path using proc_pidpath
let path = get_process_path(pid as i32);
processes.push(ProcessInfo {
pid,
ppid,
name,
path,
thread_count: 1, // Would need task_info for accurate count
});
}
}
Ok(processes)
}
fn get_process_path(pid: i32) -> Option<String> {
use std::ffi::CStr;
use std::os::raw::c_char;
extern "C" {
fn proc_pidpath(pid: i32, buffer: *mut c_char, buffersize: u32) -> i32;
}
unsafe {
let mut buffer = [0i8; libc::PATH_MAX as usize];
let result = proc_pidpath(pid, buffer.as_mut_ptr(), libc::PATH_MAX as u32);
if result > 0 {
CStr::from_ptr(buffer.as_ptr())
.to_str()
.ok()
.map(|s| s.to_string())
} else {
None
}
}
}
}
@@ -228,7 +324,7 @@ mod platform {
///
/// - **Windows**: Uses the ToolHelp API to enumerate processes.
/// - **Linux**: Reads from the /proc filesystem.
/// - **macOS**: Partial support (not yet implemented).
/// - **macOS**: Uses sysctl KERN_PROC_ALL and proc_pidpath for process enumeration.
///
/// # Errors
///

View File

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

View File

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

View File

@@ -1,31 +1,171 @@
//! Thread enumeration and analysis for process injection detection.
//!
//! This module provides cross-platform thread introspection capabilities,
//! critical for detecting thread hijacking (T1055.003) and similar techniques.
use serde::{Deserialize, Serialize};
use std::fmt;
#[derive(Debug, Clone)]
/// Information about a thread within a process.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ThreadInfo {
/// Thread ID.
pub tid: u32,
/// Process ID that owns this thread.
pub owner_pid: u32,
/// Start address of the thread (entry point).
pub start_address: usize,
/// Thread creation time (platform-specific format).
pub creation_time: u64,
/// Thread state (Running, Waiting, etc.).
pub state: ThreadState,
}
/// Thread execution state.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum ThreadState {
Running,
Waiting,
Suspended,
Terminated,
Unknown,
}
impl fmt::Display for ThreadState {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let s = match self {
Self::Running => "Running",
Self::Waiting => "Waiting",
Self::Suspended => "Suspended",
Self::Terminated => "Terminated",
Self::Unknown => "Unknown",
};
write!(f, "{}", s)
}
}
impl fmt::Display for ThreadInfo {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"TID {} @ {:#x}",
self.tid, self.start_address
"TID {} @ {:#x} [{}]",
self.tid, self.start_address, self.state
)
}
}
#[cfg(windows)]
mod platform {
use super::ThreadInfo;
use super::{ThreadInfo, ThreadState};
use anyhow::{Context, Result};
use windows::Win32::Foundation::CloseHandle;
use windows::Win32::System::Diagnostics::ToolHelp::{
CreateToolhelp32Snapshot, Thread32First, Thread32Next, THREADENTRY32, TH32CS_SNAPTHREAD,
};
use windows::Win32::System::Threading::{
OpenThread, THREAD_QUERY_INFORMATION, THREAD_QUERY_LIMITED_INFORMATION,
};
/// Attempts to get thread start address using NtQueryInformationThread.
///
/// This requires ntdll.dll and uses ThreadQuerySetWin32StartAddress.
fn get_thread_start_address(tid: u32) -> usize {
unsafe {
// Try to open the thread with query permissions
let thread_handle = match OpenThread(THREAD_QUERY_INFORMATION, false, tid) {
Ok(h) => h,
Err(_) => {
// Fall back to limited information access
match OpenThread(THREAD_QUERY_LIMITED_INFORMATION, false, tid) {
Ok(h) => h,
Err(_) => return 0,
}
}
};
// Load NtQueryInformationThread from ntdll
let ntdll = match windows::Win32::System::LibraryLoader::GetModuleHandleW(
windows::core::w!("ntdll.dll"),
) {
Ok(h) => h,
Err(_) => {
let _ = CloseHandle(thread_handle);
return 0;
}
};
let proc_addr = windows::Win32::System::LibraryLoader::GetProcAddress(
ntdll,
windows::core::s!("NtQueryInformationThread"),
);
let start_address = if let Some(func) = proc_addr {
// ThreadQuerySetWin32StartAddress = 9
type NtQueryInformationThreadFn = unsafe extern "system" fn(
thread_handle: windows::Win32::Foundation::HANDLE,
thread_information_class: u32,
thread_information: *mut std::ffi::c_void,
thread_information_length: u32,
return_length: *mut u32,
) -> i32;
let nt_query: NtQueryInformationThreadFn = std::mem::transmute(func);
let mut start_addr: usize = 0;
let mut return_length: u32 = 0;
let status = nt_query(
thread_handle,
9, // ThreadQuerySetWin32StartAddress
&mut start_addr as *mut usize as *mut std::ffi::c_void,
std::mem::size_of::<usize>() as u32,
&mut return_length,
);
if status == 0 {
start_addr
} else {
0
}
} else {
0
};
let _ = CloseHandle(thread_handle);
start_address
}
}
/// Gets thread creation time using GetThreadTimes.
fn get_thread_creation_time(tid: u32) -> u64 {
unsafe {
let thread_handle = match OpenThread(THREAD_QUERY_LIMITED_INFORMATION, false, tid) {
Ok(h) => h,
Err(_) => return 0,
};
let mut creation_time = windows::Win32::Foundation::FILETIME::default();
let mut exit_time = windows::Win32::Foundation::FILETIME::default();
let mut kernel_time = windows::Win32::Foundation::FILETIME::default();
let mut user_time = windows::Win32::Foundation::FILETIME::default();
let result = windows::Win32::System::Threading::GetThreadTimes(
thread_handle,
&mut creation_time,
&mut exit_time,
&mut kernel_time,
&mut user_time,
);
let _ = CloseHandle(thread_handle);
if result.is_ok() {
// Convert FILETIME to u64
((creation_time.dwHighDateTime as u64) << 32) | (creation_time.dwLowDateTime as u64)
} else {
0
}
}
}
pub fn enumerate_threads(pid: u32) -> Result<Vec<ThreadInfo>> {
let mut threads = Vec::new();
@@ -42,11 +182,16 @@ mod platform {
if Thread32First(snapshot, &mut entry).is_ok() {
loop {
if entry.th32OwnerProcessID == pid {
let tid = entry.th32ThreadID;
let start_address = get_thread_start_address(tid);
let creation_time = get_thread_creation_time(tid);
threads.push(ThreadInfo {
tid: entry.th32ThreadID,
tid,
owner_pid: entry.th32OwnerProcessID,
start_address: 0, // TODO: Get actual start address
creation_time: 0, // TODO: Get thread creation time
start_address,
creation_time,
state: ThreadState::Unknown, // Would need NtQueryInformationThread with ThreadBasicInformation
});
}
@@ -63,16 +208,277 @@ mod platform {
}
}
#[cfg(not(windows))]
#[cfg(target_os = "linux")]
mod platform {
use super::{ThreadInfo, ThreadState};
use anyhow::{Context, Result};
use std::fs;
pub fn enumerate_threads(pid: u32) -> Result<Vec<ThreadInfo>> {
let task_dir = format!("/proc/{}/task", pid);
let entries =
fs::read_dir(&task_dir).context(format!("Failed to read {}", task_dir))?;
let mut threads = Vec::new();
for entry in entries.flatten() {
if let Some(tid_str) = entry.file_name().to_str() {
if let Ok(tid) = tid_str.parse::<u32>() {
let thread_info = get_thread_info(pid, tid);
threads.push(thread_info);
}
}
}
Ok(threads)
}
fn get_thread_info(pid: u32, tid: u32) -> ThreadInfo {
let stat_path = format!("/proc/{}/task/{}/stat", pid, tid);
let (state, start_time) = if let Ok(content) = fs::read_to_string(&stat_path) {
parse_thread_stat(&content)
} else {
(ThreadState::Unknown, 0)
};
// Get start address from /proc/[pid]/task/[tid]/syscall
let start_address = get_thread_start_address(pid, tid);
ThreadInfo {
tid,
owner_pid: pid,
start_address,
creation_time: start_time,
state,
}
}
fn parse_thread_stat(stat: &str) -> (ThreadState, u64) {
// Format: pid (comm) state ppid pgrp session tty_nr tpgid flags ...
// Field 22 (1-indexed) is starttime
let close_paren = match stat.rfind(')') {
Some(pos) => pos,
None => return (ThreadState::Unknown, 0),
};
let rest = &stat[close_paren + 2..];
let fields: Vec<&str> = rest.split_whitespace().collect();
let state = if !fields.is_empty() {
match fields[0] {
"R" => ThreadState::Running,
"S" | "D" => ThreadState::Waiting,
"T" | "t" => ThreadState::Suspended,
"Z" | "X" => ThreadState::Terminated,
_ => ThreadState::Unknown,
}
} else {
ThreadState::Unknown
};
// starttime is field 22 (0-indexed: 19 after state)
let start_time = fields.get(19).and_then(|s| s.parse().ok()).unwrap_or(0);
(state, start_time)
}
fn get_thread_start_address(pid: u32, tid: u32) -> usize {
// Try to get the instruction pointer from /proc/[pid]/task/[tid]/syscall
let syscall_path = format!("/proc/{}/task/{}/syscall", pid, tid);
if let Ok(content) = fs::read_to_string(&syscall_path) {
// Format: syscall_number arg0 arg1 ... stack_pointer instruction_pointer
let fields: Vec<&str> = content.split_whitespace().collect();
if fields.len() >= 9 {
// Last field is the instruction pointer
if let Some(ip_str) = fields.last() {
if let Ok(ip) = usize::from_str_radix(ip_str.trim_start_matches("0x"), 16) {
return ip;
}
}
}
}
// Alternative: parse /proc/[pid]/task/[tid]/maps for the first executable region
0
}
}
#[cfg(target_os = "macos")]
mod platform {
use super::{ThreadInfo, ThreadState};
use anyhow::Result;
pub fn enumerate_threads(pid: u32) -> Result<Vec<ThreadInfo>> {
use libc::{mach_port_t, natural_t};
use std::mem;
// Mach thread info structures and constants
const THREAD_BASIC_INFO: i32 = 3;
const TH_STATE_RUNNING: i32 = 1;
const TH_STATE_STOPPED: i32 = 2;
const TH_STATE_WAITING: i32 = 3;
const TH_STATE_UNINTERRUPTIBLE: i32 = 4;
const TH_STATE_HALTED: i32 = 5;
#[repr(C)]
#[derive(Default)]
struct thread_basic_info {
user_time: time_value_t,
system_time: time_value_t,
cpu_usage: i32,
policy: i32,
run_state: i32,
flags: i32,
suspend_count: i32,
sleep_time: i32,
}
#[repr(C)]
#[derive(Default, Copy, Clone)]
struct time_value_t {
seconds: i32,
microseconds: i32,
}
extern "C" {
fn task_for_pid(
target_tport: mach_port_t,
pid: i32,
task: *mut mach_port_t,
) -> i32;
fn mach_task_self() -> mach_port_t;
fn task_threads(
target_task: mach_port_t,
act_list: *mut *mut mach_port_t,
act_list_cnt: *mut u32,
) -> i32;
fn thread_info(
target_act: mach_port_t,
flavor: i32,
thread_info_out: *mut i32,
thread_info_out_cnt: *mut u32,
) -> i32;
fn mach_port_deallocate(task: mach_port_t, name: mach_port_t) -> i32;
fn vm_deallocate(
target_task: mach_port_t,
address: usize,
size: usize,
) -> i32;
}
let mut threads = Vec::new();
unsafe {
let mut task: mach_port_t = 0;
let kr = task_for_pid(mach_task_self(), pid as i32, &mut task);
if kr != 0 {
return Err(anyhow::anyhow!(
"task_for_pid failed with error code {}. Requires root or taskgated entitlement.",
kr
));
}
let mut thread_list: *mut mach_port_t = std::ptr::null_mut();
let mut thread_count: u32 = 0;
let kr = task_threads(task, &mut thread_list, &mut thread_count);
if kr != 0 {
return Err(anyhow::anyhow!(
"task_threads failed with error code {}",
kr
));
}
// Iterate through all threads
for i in 0..thread_count {
let thread_port = *thread_list.add(i as usize);
let tid = thread_port; // On macOS, thread port is often used as TID
// Get thread basic info
let mut info: thread_basic_info = mem::zeroed();
let mut info_count =
(mem::size_of::<thread_basic_info>() / mem::size_of::<natural_t>()) as u32;
let kr = thread_info(
thread_port,
THREAD_BASIC_INFO,
&mut info as *mut _ as *mut i32,
&mut info_count,
);
let state = if kr == 0 {
match info.run_state {
TH_STATE_RUNNING => ThreadState::Running,
TH_STATE_STOPPED | TH_STATE_HALTED => ThreadState::Suspended,
TH_STATE_WAITING | TH_STATE_UNINTERRUPTIBLE => ThreadState::Waiting,
_ => ThreadState::Unknown,
}
} else {
ThreadState::Unknown
};
// Calculate creation time from user_time + system_time (accumulated time)
let creation_time = if kr == 0 {
let total_microseconds = (info.user_time.seconds as u64 * 1_000_000
+ info.user_time.microseconds as u64)
+ (info.system_time.seconds as u64 * 1_000_000
+ info.system_time.microseconds as u64);
total_microseconds
} else {
0
};
threads.push(ThreadInfo {
tid,
owner_pid: pid,
start_address: 0, // macOS doesn't easily expose thread start address
creation_time,
state,
});
// Deallocate the thread port
let _ = mach_port_deallocate(mach_task_self(), thread_port);
}
// Deallocate the thread list
if !thread_list.is_null() && thread_count > 0 {
let _ = vm_deallocate(
mach_task_self(),
thread_list as usize,
(thread_count as usize) * mem::size_of::<mach_port_t>(),
);
}
}
Ok(threads)
}
}
#[cfg(not(any(windows, target_os = "linux", target_os = "macos")))]
mod platform {
use super::ThreadInfo;
use anyhow::Result;
pub fn enumerate_threads(_pid: u32) -> Result<Vec<ThreadInfo>> {
Ok(Vec::new())
Err(anyhow::anyhow!(
"Thread enumeration not supported on this platform"
))
}
}
/// Enumerates all threads for a process.
///
/// # Platform Support
///
/// - **Windows**: Uses CreateToolhelp32Snapshot with NtQueryInformationThread for start addresses.
/// - **Linux**: Parses /proc/[pid]/task/ directory.
/// - **macOS**: Not yet implemented.
///
/// # Returns
///
/// A vector of `ThreadInfo` structs containing thread details.
/// Critical for detecting thread hijacking (T1055.003) attacks.
pub fn enumerate_threads(pid: u32) -> anyhow::Result<Vec<ThreadInfo>> {
platform::enumerate_threads(pid)
}