Files
ghost/ghost-core/src/thread.rs
2025-11-20 14:25:44 +02:00

474 lines
16 KiB
Rust

//! 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::<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();
unsafe {
let snapshot = CreateToolhelp32Snapshot(TH32CS_SNAPTHREAD, 0)
.context("Failed to create thread snapshot")?;
let mut entry = THREADENTRY32 {
dwSize: std::mem::size_of::<THREADENTRY32>() 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<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 {
(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::<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>> {
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)
}