Skip to main content

memf_linux/
kthread.rs

1//! Linux kernel thread enumeration and anomaly detection.
2//!
3//! Enumerates kernel threads and flags suspicious ones. Rootkits commonly
4//! create kernel threads to maintain persistence. Kernel threads have
5//! specific characteristics: their `mm` pointer is NULL (meaning `cr3` is
6//! `None` in `ProcessInfo`) and their parent is typically `kthreadd` (pid 2).
7
8use memf_core::object_reader::ObjectReader;
9use memf_format::PhysicalMemoryProvider;
10
11use crate::{ProcessInfo, Result};
12
13/// Information about a kernel thread extracted from memory.
14#[derive(Debug, Clone, serde::Serialize)]
15pub struct KernelThreadInfo {
16    /// Process ID of the kernel thread.
17    pub pid: u32,
18    /// Thread name from `task_struct.comm`.
19    pub name: String,
20    /// Thread function pointer (`threadfn`) -- where the thread started.
21    pub start_fn_addr: u64,
22    /// Whether heuristic analysis flagged this thread as suspicious.
23    pub is_suspicious: bool,
24    /// Human-readable reason for the suspicious flag.
25    pub reason: Option<String>,
26}
27
28/// Walk the given process list and extract kernel thread information.
29///
30/// Kernel threads are identified by having `cr3 == None` (mm pointer is
31/// NULL). For each kernel thread, the thread function pointer is read
32/// from memory when available, and the thread is classified for anomalies.
33///
34/// Returns `Ok(Vec::new())` when required symbols are missing.
35pub fn walk_kernel_threads<P: PhysicalMemoryProvider>(
36    reader: &ObjectReader<P>,
37    processes: &[ProcessInfo],
38) -> Result<Vec<KernelThreadInfo>> {
39    let mut kthreads = Vec::new();
40
41    for proc in processes {
42        // Kernel threads have mm == NULL, which means cr3 is None.
43        if proc.cr3.is_some() {
44            continue;
45        }
46
47        let pid = proc.pid as u32;
48        let name = proc.comm.clone();
49
50        // Try to read the thread function pointer from the kthread struct.
51        // In Linux, kernel threads store their function pointer in
52        // `task_struct -> set_child_tid` (overloaded for kthreads) or via
53        // the kthread struct. We attempt to read it; if the symbol/field
54        // is missing we fall back to 0.
55        let start_fn_addr: u64 = reader
56            .read_field(proc.vaddr, "task_struct", "set_child_tid")
57            .unwrap_or(0);
58
59        let (is_suspicious, reason) = classify_kthread(&name, start_fn_addr);
60
61        kthreads.push(KernelThreadInfo {
62            pid,
63            name,
64            start_fn_addr,
65            is_suspicious,
66            reason,
67        });
68    }
69
70    Ok(kthreads)
71}
72
73/// Classify a kernel thread as benign or suspicious.
74///
75/// Returns `(is_suspicious, reason)`. A thread is considered suspicious if:
76/// - Its name is empty (unnamed kernel thread)
77/// - Its name contains sequences of hex characters (random-looking names)
78/// - Its start function address is in userspace range (below `KERNEL_SPACE_MIN`)
79pub use crate::heuristics::classify_kthread;
80
81#[cfg(test)]
82mod tests {
83    use super::*;
84
85    /// Check whether a name looks like random hex characters.
86    fn looks_like_hex_name(name: &str) -> bool {
87        let mut run = 0u32;
88        for ch in name.chars() {
89            if ch.is_ascii_hexdigit() {
90                run += 1;
91                if run >= 8 {
92                    return true;
93                }
94            } else {
95                run = 0;
96            }
97        }
98        false
99    }
100
101    // ---------------------------------------------------------------
102    // classify_kthread tests (pure function, no mock memory needed)
103    // ---------------------------------------------------------------
104
105    #[test]
106    fn classify_kthread_benign() {
107        // A well-known kernel worker thread at a kernel address is benign.
108        let (suspicious, reason) = classify_kthread("kworker/0:0", 0xFFFF_FFFF_8100_0000);
109        assert!(!suspicious, "kworker should not be suspicious");
110        assert!(reason.is_none());
111    }
112
113    #[test]
114    fn classify_kthread_suspicious_unnamed() {
115        // An empty name is suspicious -- legitimate kernel threads always
116        // have a name set via kthread_create / kthread_run.
117        let (suspicious, reason) = classify_kthread("", 0xFFFF_FFFF_8100_0000);
118        assert!(suspicious, "unnamed thread should be suspicious");
119        assert!(reason.is_some());
120        let r = reason.unwrap();
121        assert!(
122            r.to_lowercase().contains("unnamed") || r.to_lowercase().contains("empty"),
123            "reason should mention unnamed/empty, got: {r}"
124        );
125    }
126
127    #[test]
128    fn classify_kthread_suspicious_userspace_fn() {
129        // A kernel thread whose start function is in userspace range is
130        // highly suspicious -- indicates possible rootkit manipulation.
131        let (suspicious, reason) = classify_kthread("worker", 0x0000_7F00_0000_0000);
132        assert!(suspicious, "userspace fn addr should be suspicious");
133        assert!(reason.is_some());
134        let r = reason.unwrap();
135        assert!(
136            r.to_lowercase().contains("userspace") || r.to_lowercase().contains("user"),
137            "reason should mention userspace, got: {r}"
138        );
139    }
140
141    #[test]
142    fn classify_kthread_suspicious_hex_name() {
143        // A name that looks like random hex is suspicious.
144        let (suspicious, reason) = classify_kthread("a1b2c3d4e5f6", 0xFFFF_FFFF_8100_0000);
145        assert!(suspicious, "hex-looking name should be suspicious");
146        assert!(reason.is_some());
147    }
148
149    #[test]
150    fn classify_kthread_benign_short_hex() {
151        // Short names that happen to be hex-ish but are common (e.g. "md")
152        // should not trigger the hex heuristic.
153        let (suspicious, _) = classify_kthread("md", 0xFFFF_FFFF_8100_0000);
154        assert!(!suspicious, "short common name should not be suspicious");
155    }
156
157    // ---------------------------------------------------------------
158    // walk_kernel_threads tests
159    // ---------------------------------------------------------------
160
161    #[test]
162    fn walk_kthreads_empty() {
163        // Empty process list should produce empty result.
164        use memf_core::object_reader::ObjectReader;
165        use memf_core::test_builders::PageTableBuilder;
166        use memf_core::vas::{TranslationMode, VirtualAddressSpace};
167        use memf_symbols::isf::IsfResolver;
168        use memf_symbols::test_builders::IsfBuilder;
169
170        let isf = IsfBuilder::new()
171            .add_struct("task_struct", 128)
172            .add_field("task_struct", "pid", 0, "int")
173            .add_field("task_struct", "comm", 32, "char")
174            .add_struct("list_head", 16)
175            .add_field("list_head", "next", 0, "pointer")
176            .add_field("list_head", "prev", 8, "pointer")
177            .build_json();
178        let resolver = IsfResolver::from_value(&isf).unwrap();
179        let (cr3, mem) = PageTableBuilder::new().build();
180        let vas = VirtualAddressSpace::new(mem, cr3, TranslationMode::X86_64FourLevel);
181        let reader = ObjectReader::new(vas, Box::new(resolver));
182
183        let result = walk_kernel_threads(&reader, &[]).unwrap();
184        assert!(
185            result.is_empty(),
186            "empty process list should give empty kthread list"
187        );
188    }
189
190    #[test]
191    fn walk_kthreads_filters_userspace() {
192        // Processes with cr3 = Some(_) are userspace and should be excluded.
193        use memf_core::object_reader::ObjectReader;
194        use memf_core::test_builders::PageTableBuilder;
195        use memf_core::vas::{TranslationMode, VirtualAddressSpace};
196        use memf_symbols::isf::IsfResolver;
197        use memf_symbols::test_builders::IsfBuilder;
198
199        let isf = IsfBuilder::new()
200            .add_struct("task_struct", 128)
201            .add_field("task_struct", "pid", 0, "int")
202            .add_field("task_struct", "comm", 32, "char")
203            .add_struct("list_head", 16)
204            .add_field("list_head", "next", 0, "pointer")
205            .add_field("list_head", "prev", 8, "pointer")
206            .build_json();
207        let resolver = IsfResolver::from_value(&isf).unwrap();
208        let (cr3, mem) = PageTableBuilder::new().build();
209        let vas = VirtualAddressSpace::new(mem, cr3, TranslationMode::X86_64FourLevel);
210        let reader = ObjectReader::new(vas, Box::new(resolver));
211
212        let processes = vec![ProcessInfo {
213            pid: 100,
214            ppid: 1,
215            comm: "bash".into(),
216            state: crate::ProcessState::Running,
217            vaddr: 0xFFFF_8000_0010_0000,
218            cr3: Some(0x1000),
219            start_time: 0,
220        }];
221
222        let result = walk_kernel_threads(&reader, &processes).unwrap();
223        assert!(
224            result.is_empty(),
225            "userspace process should not appear in kthread list"
226        );
227    }
228
229    #[test]
230    fn walk_kthreads_includes_kernel_thread() {
231        // A process with cr3 = None is a kernel thread and should be included.
232        use memf_core::object_reader::ObjectReader;
233        use memf_core::test_builders::PageTableBuilder;
234        use memf_core::vas::{TranslationMode, VirtualAddressSpace};
235        use memf_symbols::isf::IsfResolver;
236        use memf_symbols::test_builders::IsfBuilder;
237
238        let isf = IsfBuilder::new()
239            .add_struct("task_struct", 128)
240            .add_field("task_struct", "pid", 0, "int")
241            .add_field("task_struct", "comm", 32, "char")
242            .add_struct("list_head", 16)
243            .add_field("list_head", "next", 0, "pointer")
244            .add_field("list_head", "prev", 8, "pointer")
245            .build_json();
246        let resolver = IsfResolver::from_value(&isf).unwrap();
247        let (cr3, mem) = PageTableBuilder::new().build();
248        let vas = VirtualAddressSpace::new(mem, cr3, TranslationMode::X86_64FourLevel);
249        let reader = ObjectReader::new(vas, Box::new(resolver));
250
251        let processes = vec![ProcessInfo {
252            pid: 2,
253            ppid: 0,
254            comm: "kthreadd".into(),
255            state: crate::ProcessState::Sleeping,
256            vaddr: 0xFFFF_8000_0010_0000,
257            cr3: None,
258            start_time: 0,
259        }];
260
261        let result = walk_kernel_threads(&reader, &processes).unwrap();
262        assert_eq!(result.len(), 1);
263        assert_eq!(result[0].pid, 2);
264        assert_eq!(result[0].name, "kthreadd");
265        assert!(
266            !result[0].is_suspicious,
267            "kthreadd should not be suspicious"
268        );
269    }
270
271    // walk_kernel_threads: kernel thread with set_child_tid field readable
272    // Exercises line 61: read_field("set_child_tid") returns actual value.
273    #[test]
274    fn walk_kthreads_reads_start_fn_addr_from_set_child_tid() {
275        use memf_core::object_reader::ObjectReader;
276        use memf_core::test_builders::{flags as ptf, PageTableBuilder, SyntheticPhysMem};
277        use memf_core::vas::{TranslationMode, VirtualAddressSpace};
278        use memf_symbols::isf::IsfResolver;
279        use memf_symbols::test_builders::IsfBuilder;
280
281        let task_vaddr: u64 = 0xFFFF_8000_0050_0000;
282        let task_paddr: u64 = 0x0050_0000;
283
284        // set_child_tid at offset 0x80, value = kernel address 0xFFFF_FFFF_8100_0042
285        let start_fn: u64 = 0xFFFF_FFFF_8100_0042;
286        let mut task_page = [0u8; 4096];
287        task_page[0x80..0x88].copy_from_slice(&start_fn.to_le_bytes());
288
289        let isf = IsfBuilder::new()
290            .add_struct("task_struct", 256)
291            .add_field("task_struct", "pid", 0, "int")
292            .add_field("task_struct", "comm", 32, "char")
293            .add_field("task_struct", "set_child_tid", 0x80, "pointer")
294            .build_json();
295        let resolver = IsfResolver::from_value(&isf).unwrap();
296
297        let (cr3, mem) = PageTableBuilder::new()
298            .map_4k(task_vaddr, task_paddr, ptf::WRITABLE)
299            .write_phys(task_paddr, &task_page)
300            .build();
301        let vas = VirtualAddressSpace::new(mem, cr3, TranslationMode::X86_64FourLevel);
302        let reader: ObjectReader<SyntheticPhysMem> = ObjectReader::new(vas, Box::new(resolver));
303
304        let processes = vec![ProcessInfo {
305            pid: 99,
306            ppid: 2,
307            comm: "kworker/0:1".into(),
308            state: crate::ProcessState::Sleeping,
309            vaddr: task_vaddr,
310            cr3: None, // kernel thread
311            start_time: 0,
312        }];
313
314        let result = walk_kernel_threads(&reader, &processes).unwrap();
315        assert_eq!(result.len(), 1);
316        assert_eq!(
317            result[0].start_fn_addr, start_fn,
318            "start_fn_addr must be read from set_child_tid"
319        );
320        assert!(!result[0].is_suspicious, "kernel-space fn addr → benign");
321    }
322
323    // walk_kernel_threads: suspicious kernel thread with userspace start fn
324    #[test]
325    fn walk_kthreads_suspicious_userspace_start_fn() {
326        use memf_core::object_reader::ObjectReader;
327        use memf_core::test_builders::{flags as ptf, PageTableBuilder, SyntheticPhysMem};
328        use memf_core::vas::{TranslationMode, VirtualAddressSpace};
329        use memf_symbols::isf::IsfResolver;
330        use memf_symbols::test_builders::IsfBuilder;
331
332        let task_vaddr: u64 = 0xFFFF_8000_0051_0000;
333        let task_paddr: u64 = 0x0051_0000;
334
335        // start_fn in userspace (suspicious)
336        let start_fn: u64 = 0x0000_7F00_0000_1234;
337        let mut task_page = [0u8; 4096];
338        task_page[0x80..0x88].copy_from_slice(&start_fn.to_le_bytes());
339
340        let isf = IsfBuilder::new()
341            .add_struct("task_struct", 256)
342            .add_field("task_struct", "pid", 0, "int")
343            .add_field("task_struct", "comm", 32, "char")
344            .add_field("task_struct", "set_child_tid", 0x80, "pointer")
345            .build_json();
346        let resolver = IsfResolver::from_value(&isf).unwrap();
347
348        let (cr3, mem) = PageTableBuilder::new()
349            .map_4k(task_vaddr, task_paddr, ptf::WRITABLE)
350            .write_phys(task_paddr, &task_page)
351            .build();
352        let vas = VirtualAddressSpace::new(mem, cr3, TranslationMode::X86_64FourLevel);
353        let reader: ObjectReader<SyntheticPhysMem> = ObjectReader::new(vas, Box::new(resolver));
354
355        let processes = vec![ProcessInfo {
356            pid: 5555,
357            ppid: 2,
358            comm: "backdoor".into(),
359            state: crate::ProcessState::Running,
360            vaddr: task_vaddr,
361            cr3: None,
362            start_time: 0,
363        }];
364
365        let result = walk_kernel_threads(&reader, &processes).unwrap();
366        assert_eq!(result.len(), 1);
367        assert!(result[0].is_suspicious, "userspace start_fn → suspicious");
368        assert!(result[0].reason.is_some());
369    }
370
371    // walk_kernel_threads: suspicious kernel thread with hex name
372    #[test]
373    fn walk_kthreads_suspicious_hex_name() {
374        use memf_core::object_reader::ObjectReader;
375        use memf_core::test_builders::PageTableBuilder;
376        use memf_core::vas::{TranslationMode, VirtualAddressSpace};
377        use memf_symbols::isf::IsfResolver;
378        use memf_symbols::test_builders::IsfBuilder;
379
380        let isf = IsfBuilder::new()
381            .add_struct("task_struct", 128)
382            .build_json();
383        let resolver = IsfResolver::from_value(&isf).unwrap();
384        let (cr3, mem) = PageTableBuilder::new().build();
385        let vas = VirtualAddressSpace::new(mem, cr3, TranslationMode::X86_64FourLevel);
386        let reader = ObjectReader::new(vas, Box::new(resolver));
387
388        let processes = vec![ProcessInfo {
389            pid: 1337,
390            ppid: 2,
391            comm: "a1b2c3d4e5f6".into(), // hex-looking name
392            state: crate::ProcessState::Sleeping,
393            vaddr: 0xFFFF_8000_0010_0000,
394            cr3: None,
395            start_time: 0,
396        }];
397
398        let result = walk_kernel_threads(&reader, &processes).unwrap();
399        assert_eq!(result.len(), 1);
400        assert!(result[0].is_suspicious, "hex-looking name → suspicious");
401        assert!(!result[0].reason.as_deref().unwrap_or("").is_empty());
402    }
403
404    // KernelThreadInfo: Clone + Serialize coverage.
405    #[test]
406    fn kernel_thread_info_clone_serialize() {
407        let info = KernelThreadInfo {
408            pid: 2,
409            name: "kthreadd".to_string(),
410            start_fn_addr: 0xFFFF_FFFF_8100_0000,
411            is_suspicious: false,
412            reason: None,
413        };
414        let cloned = info.clone();
415        assert_eq!(cloned.pid, 2);
416        let json = serde_json::to_string(&cloned).unwrap();
417        assert!(json.contains("\"pid\":2"));
418        assert!(json.contains("\"is_suspicious\":false"));
419    }
420
421    // ---------------------------------------------------------------
422    // looks_like_hex_name tests
423    // ---------------------------------------------------------------
424
425    #[test]
426    fn hex_name_detection() {
427        assert!(looks_like_hex_name("a1b2c3d4e5f6"));
428        assert!(looks_like_hex_name("deadbeef01234567"));
429        assert!(!looks_like_hex_name("kworker/0:0"));
430        assert!(!looks_like_hex_name("ksoftirqd/0"));
431        assert!(!looks_like_hex_name("md"));
432        assert!(!looks_like_hex_name(""));
433    }
434}