//! 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; /// 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, self.state ) } } #[cfg(windows)] mod platform { use super::{ThreadInfo, ThreadState}; use anyhow::{Context, Result}; use windows::Win32::Foundation::CloseHandle; use windows::Win32::System::Diagnostics::ToolHelp::{ CreateToolhelp32Snapshot, Thread32First, Thread32Next, TH32CS_SNAPTHREAD, THREADENTRY32, }; 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::() 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> { let mut threads = Vec::new(); unsafe { let snapshot = CreateToolhelp32Snapshot(TH32CS_SNAPTHREAD, 0) .context("Failed to create thread snapshot")?; let mut entry = THREADENTRY32 { dwSize: std::mem::size_of::() as u32, ..Default::default() }; 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, owner_pid: entry.th32OwnerProcessID, start_address, creation_time, state: ThreadState::Unknown, // Would need NtQueryInformationThread with ThreadBasicInformation }); } if Thread32Next(snapshot, &mut entry).is_err() { break; } } } let _ = CloseHandle(snapshot); } Ok(threads) } } #[cfg(target_os = "linux")] mod platform { use super::{ThreadInfo, ThreadState}; use anyhow::{Context, Result}; use std::fs; pub fn enumerate_threads(pid: u32) -> Result> { 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::() { 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> { 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::() / mem::size_of::()) 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 { (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) } 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::(), ); } } 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> { 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> { platform::enumerate_threads(pid) }