wraith/manipulation/antidebug/
thread_hide.rs

1//! Thread hiding from debugger
2//!
3//! Uses NtSetInformationThread with ThreadHideFromDebugger to prevent
4//! debuggers from receiving events for the hidden thread.
5
6use crate::error::Result;
7use crate::structures::Teb;
8use std::collections::HashSet;
9use std::sync::{LazyLock, Mutex};
10
11// re-use syscall infrastructure
12#[cfg(feature = "syscalls")]
13use crate::manipulation::syscall::{
14    nt_set_information_thread, CURRENT_THREAD, THREAD_HIDE_FROM_DEBUGGER,
15};
16
17/// track which threads have been hidden
18static HIDDEN_THREADS: LazyLock<Mutex<HashSet<u32>>> =
19    LazyLock::new(|| Mutex::new(HashSet::new()));
20
21/// hide thread from debugger using NtSetInformationThread
22///
23/// once hidden, the debugger will not receive debug events for this thread
24/// and cannot resume it after break. this operation is one-way - cannot be undone.
25#[cfg(feature = "syscalls")]
26pub fn hide_thread(thread_handle: usize) -> Result<()> {
27    nt_set_information_thread(
28        thread_handle,
29        THREAD_HIDE_FROM_DEBUGGER,
30        core::ptr::null(),
31        0,
32    )
33}
34
35/// hide current thread from debugger
36#[cfg(feature = "syscalls")]
37pub fn hide_current_thread() -> Result<()> {
38    hide_thread(CURRENT_THREAD)?;
39
40    // track that this thread is hidden
41    if let Ok(teb) = Teb::current() {
42        let tid = teb.thread_id();
43        if let Ok(mut hidden) = HIDDEN_THREADS.lock() {
44            hidden.insert(tid);
45        }
46    }
47
48    Ok(())
49}
50
51/// fallback when syscalls feature is disabled
52#[cfg(not(feature = "syscalls"))]
53pub fn hide_thread(_thread_handle: usize) -> Result<()> {
54    Err(crate::error::WraithError::SyscallNotFound {
55        name: "NtSetInformationThread (syscalls feature disabled)".into(),
56    })
57}
58
59#[cfg(not(feature = "syscalls"))]
60pub fn hide_current_thread() -> Result<()> {
61    Err(crate::error::WraithError::SyscallNotFound {
62        name: "NtSetInformationThread (syscalls feature disabled)".into(),
63    })
64}
65
66/// check if thread was hidden by us
67pub fn is_thread_hidden(tid: u32) -> bool {
68    HIDDEN_THREADS
69        .lock()
70        .map(|h| h.contains(&tid))
71        .unwrap_or(false)
72}
73
74/// get list of threads hidden by us
75pub fn get_hidden_threads() -> Vec<u32> {
76    HIDDEN_THREADS
77        .lock()
78        .map(|h| h.iter().copied().collect())
79        .unwrap_or_default()
80}
81
82/// number of hidden threads
83pub fn hidden_count() -> usize {
84    HIDDEN_THREADS.lock().map(|h| h.len()).unwrap_or(0)
85}
86
87/// clear tracking (doesn't unhide threads - that's impossible)
88pub fn clear_tracking() {
89    if let Ok(mut hidden) = HIDDEN_THREADS.lock() {
90        hidden.clear();
91    }
92}
93
94#[cfg(test)]
95mod tests {
96    use super::*;
97
98    #[test]
99    fn test_tracking() {
100        clear_tracking();
101
102        // manually add for testing (since hiding requires syscalls)
103        if let Ok(mut hidden) = HIDDEN_THREADS.lock() {
104            hidden.insert(1234);
105        }
106
107        assert!(is_thread_hidden(1234));
108        assert!(!is_thread_hidden(5678));
109
110        let threads = get_hidden_threads();
111        assert!(threads.contains(&1234));
112
113        clear_tracking();
114        assert!(!is_thread_hidden(1234));
115    }
116}