feat: Add PE header validation and LD_PRELOAD detection
This commit is contained in:
@@ -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)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user