From d6eeb9e018822e38a30cc187eebba5ca65f50462 Mon Sep 17 00:00:00 2001 From: Adir Shitrit Date: Fri, 21 Nov 2025 00:50:48 +0200 Subject: [PATCH] Implement thread hijacking detection with context inspection - Added thread context inspection (RIP/EIP register analysis) - Detect threads executing from RWX memory regions - Detect threads in unbacked/private memory - Thread start address vs current IP divergence detection - Suspended thread analysis - Support for both x86 and x64 architectures - Cross-platform stubs for Linux/macOS Detects MITRE ATT&CK T1055.003 (Thread Execution Hijacking). Generated with [Claude Code](https://claude.com/claude-code) --- ghost-core/src/lib.rs | 2 +- ghost-core/src/thread.rs | 226 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 227 insertions(+), 1 deletion(-) diff --git a/ghost-core/src/lib.rs b/ghost-core/src/lib.rs index 179cfba..b462e50 100644 --- a/ghost-core/src/lib.rs +++ b/ghost-core/src/lib.rs @@ -106,7 +106,7 @@ pub use streaming::{ Alert, AlertManager, AlertRule, CorrelationEngine, EventChannel, EventSeverity, EventStreamingSystem, EventType, NotificationSystem, StreamingEvent, }; -pub use thread::ThreadInfo; +pub use thread::{detect_thread_hijacking, HijackedThreadInfo, ThreadHijackingResult, ThreadInfo}; pub use threat_intel::{ Campaign, IndicatorOfCompromise, IocType, SophisticationLevel, ThreatActor as ThreatIntelActor, ThreatContext, ThreatIntelligence, diff --git a/ghost-core/src/thread.rs b/ghost-core/src/thread.rs index ddf7b03..73b6a09 100644 --- a/ghost-core/src/thread.rs +++ b/ghost-core/src/thread.rs @@ -21,6 +21,25 @@ pub struct ThreadInfo { pub state: ThreadState, } +/// Thread hijacking detection result +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ThreadHijackingResult { + pub hijacked_threads: Vec, + pub suspicious_count: usize, +} + +/// Information about a potentially hijacked thread +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct HijackedThreadInfo { + pub tid: u32, + pub start_address: usize, + pub current_ip: usize, + pub is_in_rwx_memory: bool, + pub is_in_unbacked_memory: bool, + pub is_suspended: bool, + pub indicators: Vec, +} + /// Thread execution state. #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] pub enum ThreadState { @@ -206,6 +225,175 @@ mod platform { Ok(threads) } + + /// Detect thread hijacking by analyzing thread contexts and start addresses + pub fn detect_thread_hijacking( + pid: u32, + memory_regions: &[crate::MemoryRegion], + ) -> Result { + use windows::Win32::System::Diagnostics::Debug::ReadProcessMemory; + use windows::Win32::System::Threading::{ + GetThreadContext, OpenProcess, SuspendThread, ResumeThread, + PROCESS_QUERY_INFORMATION, PROCESS_VM_READ, THREAD_GET_CONTEXT, THREAD_SUSPEND_RESUME, + }; + + let threads = enumerate_threads(pid)?; + let mut hijacked_threads = Vec::new(); + + unsafe { + let process_handle = OpenProcess( + PROCESS_QUERY_INFORMATION | PROCESS_VM_READ, + false, + pid, + ) + .context("Failed to open process for thread analysis")?; + + for thread in threads { + let mut indicators = Vec::new(); + let mut is_in_rwx = false; + let mut is_in_unbacked = false; + let mut current_ip = thread.start_address; + + // Open thread for context inspection + let thread_handle = match OpenThread( + THREAD_GET_CONTEXT | THREAD_SUSPEND_RESUME, + false, + thread.tid, + ) { + Ok(h) => h, + Err(_) => continue, + }; + + // Suspend thread to get consistent context + let suspend_count = SuspendThread(thread_handle); + + if suspend_count != u32::MAX { + // Get thread context (registers) + #[cfg(target_arch = "x86_64")] + { + use windows::Win32::System::Diagnostics::Debug::CONTEXT; + use windows::Win32::System::Diagnostics::Debug::CONTEXT_CONTROL; + + let mut context = CONTEXT { + ContextFlags: CONTEXT_CONTROL, + ..Default::default() + }; + + if GetThreadContext(thread_handle, &mut context).is_ok() { + current_ip = context.Rip as usize; + + // Check if RIP points to suspicious memory + let region = memory_regions.iter().find(|r| { + current_ip >= r.base_address + && current_ip < r.base_address + r.size + }); + + if let Some(region) = region { + // Check for RWX memory + if region.protection == crate::MemoryProtection::ReadWriteExecute { + is_in_rwx = true; + indicators.push("Thread executing from RWX memory".to_string()); + } + + // Check for private/unbacked memory + if region.region_type == "PRIVATE" { + is_in_unbacked = true; + indicators.push("Thread executing from unbacked memory".to_string()); + } + + // Check if start address differs significantly from current IP + if thread.start_address != 0 + && (current_ip < thread.start_address.saturating_sub(0x10000) + || current_ip > thread.start_address.saturating_add(0x10000)) + { + indicators.push(format!( + "Thread IP diverged from start address (start: {:#x}, current: {:#x})", + thread.start_address, current_ip + )); + } + } else { + indicators.push("Thread IP points to unmapped memory".to_string()); + } + } + } + + #[cfg(target_arch = "x86")] + { + use windows::Win32::System::Diagnostics::Debug::CONTEXT; + use windows::Win32::System::Diagnostics::Debug::CONTEXT_CONTROL; + + let mut context = CONTEXT { + ContextFlags: CONTEXT_CONTROL, + ..Default::default() + }; + + if GetThreadContext(thread_handle, &mut context).is_ok() { + current_ip = context.Eip as usize; + + let region = memory_regions.iter().find(|r| { + current_ip >= r.base_address + && current_ip < r.base_address + r.size + }); + + if let Some(region) = region { + if region.protection == crate::MemoryProtection::ReadWriteExecute { + is_in_rwx = true; + indicators.push("Thread executing from RWX memory".to_string()); + } + + if region.region_type == "PRIVATE" { + is_in_unbacked = true; + indicators.push("Thread executing from unbacked memory".to_string()); + } + } else { + indicators.push("Thread IP points to unmapped memory".to_string()); + } + } + } + + // Resume thread + let _ = ResumeThread(thread_handle); + } + + let _ = CloseHandle(thread_handle); + + // Check if start address is suspicious + if thread.start_address != 0 { + let start_region = memory_regions.iter().find(|r| { + thread.start_address >= r.base_address + && thread.start_address < r.base_address + r.size + }); + + if let Some(region) = start_region { + if region.region_type == "PRIVATE" && region.protection.is_executable() { + indicators.push("Thread started in private executable memory".to_string()); + } + } + } + + if !indicators.is_empty() { + hijacked_threads.push(super::HijackedThreadInfo { + tid: thread.tid, + start_address: thread.start_address, + current_ip, + is_in_rwx_memory: is_in_rwx, + is_in_unbacked_memory: is_in_unbacked, + is_suspended: suspend_count > 0 && suspend_count != u32::MAX, + indicators, + }); + } + } + + let _ = CloseHandle(process_handle); + } + + let suspicious_count = hijacked_threads.len(); + + Ok(super::ThreadHijackingResult { + hijacked_threads, + suspicious_count, + }) + } } #[cfg(target_os = "linux")] @@ -471,3 +659,41 @@ mod platform { pub fn enumerate_threads(pid: u32) -> anyhow::Result> { platform::enumerate_threads(pid) } + +/// Detects thread hijacking by analyzing thread contexts and execution addresses. +/// +/// # Platform Support +/// +/// - **Windows**: Full support with context inspection (RIP/EIP analysis) +/// - **Linux**: Not yet implemented +/// - **macOS**: Not yet implemented +/// +/// # Detection Methods +/// +/// - Thread context inspection (register analysis) +/// - RIP/EIP points to RWX memory +/// - Thread executing from unbacked/private memory +/// - Start address divergence detection +/// - Suspended thread analysis +/// +/// # Returns +/// +/// A `ThreadHijackingResult` with details of potentially hijacked threads. +#[cfg(windows)] +pub fn detect_thread_hijacking( + pid: u32, + memory_regions: &[crate::MemoryRegion], +) -> anyhow::Result { + platform::detect_thread_hijacking(pid, memory_regions) +} + +#[cfg(not(windows))] +pub fn detect_thread_hijacking( + _pid: u32, + _memory_regions: &[crate::MemoryRegion], +) -> anyhow::Result { + Ok(ThreadHijackingResult { + hijacked_threads: Vec::new(), + suspicious_count: 0, + }) +}