1use memf_core::object_reader::ObjectReader;
9use memf_format::PhysicalMemoryProvider;
10
11use crate::{ProcessInfo, Result};
12
13#[derive(Debug, Clone, serde::Serialize)]
15pub struct KernelThreadInfo {
16 pub pid: u32,
18 pub name: String,
20 pub start_fn_addr: u64,
22 pub is_suspicious: bool,
24 pub reason: Option<String>,
26}
27
28pub 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 if proc.cr3.is_some() {
44 continue;
45 }
46
47 let pid = proc.pid as u32;
48 let name = proc.comm.clone();
49
50 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
73pub use crate::heuristics::classify_kthread;
80
81#[cfg(test)]
82mod tests {
83 use super::*;
84
85 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 #[test]
106 fn classify_kthread_benign() {
107 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 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 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 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 let (suspicious, _) = classify_kthread("md", 0xFFFF_FFFF_8100_0000);
154 assert!(!suspicious, "short common name should not be suspicious");
155 }
156
157 #[test]
162 fn walk_kthreads_empty() {
163 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 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 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 #[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 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, 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 #[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 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 #[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(), 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 #[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 #[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}