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)
This commit is contained in:
Adir Shitrit
2025-11-21 00:50:48 +02:00
parent 461bc1fb80
commit d6eeb9e018
2 changed files with 227 additions and 1 deletions

View File

@@ -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,

View File

@@ -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<HijackedThreadInfo>,
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<String>,
}
/// 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<super::ThreadHijackingResult> {
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<Vec<ThreadInfo>> {
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<ThreadHijackingResult> {
platform::detect_thread_hijacking(pid, memory_regions)
}
#[cfg(not(windows))]
pub fn detect_thread_hijacking(
_pid: u32,
_memory_regions: &[crate::MemoryRegion],
) -> anyhow::Result<ThreadHijackingResult> {
Ok(ThreadHijackingResult {
hijacked_threads: Vec::new(),
suspicious_count: 0,
})
}