1use memf_core::object_reader::ObjectReader;
17use memf_format::PhysicalMemoryProvider;
18
19use crate::Result;
20
21#[derive(Debug, Clone, serde::Serialize)]
23pub struct SignalHandlerInfo {
24 pub pid: u32,
26 pub comm: String,
28 pub signal: u32,
30 pub signal_name: String,
32 pub handler: u64,
34 pub handler_type: String,
36 pub is_suspicious: bool,
38}
39
40pub fn signal_name(sig: u32) -> &'static str {
45 match sig {
46 1 => "SIGHUP",
47 2 => "SIGINT",
48 3 => "SIGQUIT",
49 6 => "SIGABRT",
50 9 => "SIGKILL",
51 10 => "SIGUSR1",
52 11 => "SIGSEGV",
53 12 => "SIGUSR2",
54 13 => "SIGPIPE",
55 14 => "SIGALRM",
56 15 => "SIGTERM",
57 17 => "SIGCHLD",
58 _ => "UNKNOWN",
59 }
60}
61
62pub fn handler_type(handler: u64) -> String {
68 match handler {
69 0 => "SIG_DFL".to_string(),
70 1 => "SIG_IGN".to_string(),
71 _ => format!("0x{handler:016x}"),
72 }
73}
74
75pub use crate::heuristics::classify_signal_handler;
85
86const MAX_SIGNALS: u32 = 31;
99
100pub fn walk_signal_handlers<P: PhysicalMemoryProvider>(
102 reader: &ObjectReader<P>,
103) -> Result<Vec<SignalHandlerInfo>> {
104 let init_task_addr = match reader.symbols().symbol_address("init_task") {
106 Some(addr) => addr,
107 None => return Ok(Vec::new()),
108 };
109
110 let tasks_offset = match reader.symbols().field_offset("task_struct", "tasks") {
112 Some(off) => off,
113 None => return Ok(Vec::new()),
114 };
115
116 let action_offset = match reader.symbols().field_offset("sighand_struct", "action") {
118 Some(off) => off,
119 None => return Ok(Vec::new()),
120 };
121
122 let k_sigaction_size = match reader.symbols().struct_size("k_sigaction") {
124 Some(s) if s > 0 => s,
125 _ => return Ok(Vec::new()),
126 };
127
128 let head_vaddr = init_task_addr + tasks_offset;
130 let task_addrs = reader.walk_list(head_vaddr, "task_struct", "tasks")?;
131
132 let mut results = Vec::new();
133
134 let all_tasks = std::iter::once(init_task_addr).chain(task_addrs.iter().copied());
136
137 for task_addr in all_tasks {
138 let pid: u32 = match reader.read_field(task_addr, "task_struct", "pid") {
140 Ok(p) => p,
141 Err(_) => continue,
142 };
143 let comm = reader
144 .read_field_string(task_addr, "task_struct", "comm", 16)
145 .unwrap_or_default();
146
147 let sighand_ptr: u64 = match reader.read_field(task_addr, "task_struct", "sighand") {
149 Ok(p) => p,
150 Err(_) => continue,
151 };
152 if sighand_ptr == 0 {
153 continue;
154 }
155
156 let action_base = sighand_ptr + action_offset;
157
158 for sig in 1..=MAX_SIGNALS {
160 let entry_addr = action_base + u64::from(sig - 1) * k_sigaction_size;
164
165 let sa_handler: u64 = reader
166 .read_field(entry_addr, "sigaction", "sa_handler")
167 .unwrap_or(0);
168
169 let suspicious = classify_signal_handler(sig, sa_handler);
170 if suspicious {
171 results.push(SignalHandlerInfo {
172 pid,
173 comm: comm.clone(),
174 signal: sig,
175 signal_name: signal_name(sig).to_string(),
176 handler: sa_handler,
177 handler_type: handler_type(sa_handler),
178 is_suspicious: true,
179 });
180 }
181 }
182 }
183
184 Ok(results)
185}
186
187#[cfg(test)]
188mod tests {
189 use super::*;
190 use memf_core::test_builders::{flags, PageTableBuilder};
191 use memf_core::vas::{TranslationMode, VirtualAddressSpace};
192 use memf_symbols::isf::IsfResolver;
193 use memf_symbols::test_builders::IsfBuilder;
194
195 fn make_reader(
197 isf: &IsfBuilder,
198 builder: PageTableBuilder,
199 ) -> ObjectReader<memf_core::test_builders::SyntheticPhysMem> {
200 let json = isf.build_json();
201 let resolver = IsfResolver::from_value(&json).unwrap();
202 let (cr3, mem) = builder.build();
203 let vas = VirtualAddressSpace::new(mem, cr3, TranslationMode::X86_64FourLevel);
204 ObjectReader::new(vas, Box::new(resolver))
205 }
206
207 #[test]
208 fn signal_name_sigterm() {
209 assert_eq!(signal_name(15), "SIGTERM");
210 }
211
212 #[test]
213 fn signal_name_unknown() {
214 assert_eq!(signal_name(99), "UNKNOWN");
215 }
216
217 #[test]
218 fn handler_default() {
219 assert_eq!(handler_type(0), "SIG_DFL");
220 }
221
222 #[test]
223 fn handler_ignore() {
224 assert_eq!(handler_type(1), "SIG_IGN");
225 }
226
227 #[test]
228 fn classify_sigterm_ignored_suspicious() {
229 assert!(classify_signal_handler(15, 1));
231 assert!(!classify_signal_handler(15, 0));
233 assert!(!classify_signal_handler(15, 0xFFFF_8000_0001_0000));
236 }
237
238 #[test]
239 fn classify_sigsegv_handler_suspicious() {
240 assert!(classify_signal_handler(11, 0xFFFF_8000_0001_0000));
242 assert!(!classify_signal_handler(11, 0));
244 assert!(!classify_signal_handler(11, 1));
246 }
247
248 #[test]
249 fn walk_no_symbol_returns_empty() {
250 let isf = IsfBuilder::new();
252 let ptb = PageTableBuilder::new();
253 let reader = make_reader(&isf, ptb);
254
255 let result = walk_signal_handlers(&reader).unwrap();
256 assert!(result.is_empty());
257 }
258
259 #[test]
264 fn signal_name_all_known() {
265 assert_eq!(signal_name(1), "SIGHUP");
267 assert_eq!(signal_name(2), "SIGINT");
268 assert_eq!(signal_name(3), "SIGQUIT");
269 assert_eq!(signal_name(6), "SIGABRT");
270 assert_eq!(signal_name(9), "SIGKILL");
271 assert_eq!(signal_name(10), "SIGUSR1");
272 assert_eq!(signal_name(11), "SIGSEGV");
273 assert_eq!(signal_name(12), "SIGUSR2");
274 assert_eq!(signal_name(13), "SIGPIPE");
275 assert_eq!(signal_name(14), "SIGALRM");
276 assert_eq!(signal_name(15), "SIGTERM");
277 assert_eq!(signal_name(17), "SIGCHLD");
278 assert_eq!(signal_name(99), "UNKNOWN");
279 }
280
281 #[test]
282 fn handler_type_custom_address() {
283 let addr: u64 = 0xFFFF_8000_DEAD_BEEF;
285 let result = handler_type(addr);
286 assert!(
287 result.starts_with("0x"),
288 "custom handler must be hex-formatted"
289 );
290 assert_eq!(result, format!("0x{addr:016x}"));
291 }
292
293 #[test]
298 fn walk_missing_tasks_field_returns_empty() {
299 let isf = IsfBuilder::new()
301 .add_symbol("init_task", 0xFFFF_8800_0000_0000)
302 .add_struct("task_struct", 128)
303 .add_field("task_struct", "pid", 0u64, "int");
304 let reader = make_reader(&isf, PageTableBuilder::new());
306
307 let result = walk_signal_handlers(&reader).unwrap();
308 assert!(result.is_empty(), "missing tasks field → empty");
309 }
310
311 #[test]
312 fn walk_missing_action_field_returns_empty() {
313 let isf = IsfBuilder::new()
315 .add_symbol("init_task", 0xFFFF_8800_0001_0000)
316 .add_struct("list_head", 16)
317 .add_field("list_head", "next", 0u64, "pointer")
318 .add_field("list_head", "prev", 8u64, "pointer")
319 .add_struct("task_struct", 128)
320 .add_field("task_struct", "pid", 0u64, "int")
321 .add_field("task_struct", "tasks", 16u64, "list_head");
322 let reader = make_reader(&isf, PageTableBuilder::new());
324
325 let result = walk_signal_handlers(&reader).unwrap();
326 assert!(result.is_empty(), "missing sighand_struct.action → empty");
327 }
328
329 #[test]
330 fn walk_missing_k_sigaction_size_returns_empty() {
331 let isf = IsfBuilder::new()
333 .add_symbol("init_task", 0xFFFF_8800_0002_0000)
334 .add_struct("list_head", 16)
335 .add_field("list_head", "next", 0u64, "pointer")
336 .add_field("list_head", "prev", 8u64, "pointer")
337 .add_struct("task_struct", 128)
338 .add_field("task_struct", "pid", 0u64, "int")
339 .add_field("task_struct", "tasks", 16u64, "list_head")
340 .add_struct("sighand_struct", 256)
341 .add_field("sighand_struct", "action", 0u64, "pointer");
342 let reader = make_reader(&isf, PageTableBuilder::new());
344
345 let result = walk_signal_handlers(&reader).unwrap();
346 assert!(result.is_empty(), "missing k_sigaction size → empty");
347 }
348
349 #[test]
350 fn walk_sighand_null_skips_task() {
351 let init_task_vaddr: u64 = 0xFFFF_8800_0003_0000;
353 let init_task_paddr: u64 = 0x0030_0000;
354 let tasks_offset: u64 = 16;
355 let pid_offset: u64 = 0;
356 let sighand_offset: u64 = 48;
357 let action_offset: u64 = 0;
358 let k_sigaction_sz: u64 = 32;
359
360 let mut page = [0u8; 4096];
361 page[pid_offset as usize..pid_offset as usize + 4].copy_from_slice(&42u32.to_le_bytes());
363 let tasks_self = init_task_vaddr + tasks_offset;
365 page[tasks_offset as usize..tasks_offset as usize + 8]
366 .copy_from_slice(&tasks_self.to_le_bytes());
367 page[32..40].copy_from_slice(b"nullhand");
369 page[sighand_offset as usize..sighand_offset as usize + 8]
371 .copy_from_slice(&0u64.to_le_bytes());
372
373 let isf = IsfBuilder::new()
374 .add_symbol("init_task", init_task_vaddr)
375 .add_struct("list_head", 16)
376 .add_field("list_head", "next", 0u64, "pointer")
377 .add_field("list_head", "prev", 8u64, "pointer")
378 .add_struct("task_struct", 128)
379 .add_field("task_struct", "pid", pid_offset, "int")
380 .add_field("task_struct", "tasks", tasks_offset, "list_head")
381 .add_field("task_struct", "comm", 32u64, "char")
382 .add_field("task_struct", "sighand", sighand_offset, "pointer")
383 .add_struct("sighand_struct", 256)
384 .add_field("sighand_struct", "action", action_offset, "pointer")
385 .add_struct("k_sigaction", k_sigaction_sz)
386 .add_struct("sigaction", k_sigaction_sz)
387 .add_field("sigaction", "sa_handler", 0u64, "pointer");
388
389 let ptb = PageTableBuilder::new()
390 .map_4k(init_task_vaddr, init_task_paddr, flags::WRITABLE)
391 .write_phys(init_task_paddr, &page);
392 let reader = make_reader(&isf, ptb);
393
394 let result = walk_signal_handlers(&reader).unwrap();
395 assert!(
396 result.is_empty(),
397 "sighand == 0 → task skipped, no suspicious entries"
398 );
399 }
400
401 #[test]
402 fn walk_sigterm_ignored_detected() {
403 let init_task_vaddr: u64 = 0xFFFF_8800_0000_0000;
405 let init_task_paddr: u64 = 0x0010_0000;
406
407 let tasks_offset: u64 = 776;
409 let pid_offset: u64 = 872;
410 let comm_offset: u64 = 1496;
411 let sighand_field_offset: u64 = 1600;
412
413 let sighand_vaddr: u64 = 0xFFFF_8800_0010_0000;
414 let sighand_paddr: u64 = 0x0020_0000;
415 let action_offset: u64 = 0;
416 let k_sigaction_size: u64 = 152;
417
418 let sigterm_entry_paddr =
420 sighand_paddr + action_offset + u64::from(15u32 - 1) * k_sigaction_size;
421
422 let isf = IsfBuilder::new()
423 .add_symbol("init_task", init_task_vaddr)
424 .add_struct("list_head", 16)
425 .add_field("list_head", "next", 0, "pointer")
426 .add_field("list_head", "prev", 8, "pointer")
427 .add_struct("task_struct", 2048)
428 .add_field("task_struct", "tasks", tasks_offset, "list_head")
429 .add_field("task_struct", "pid", pid_offset, "int")
430 .add_field("task_struct", "comm", comm_offset, "char")
431 .add_field("task_struct", "sighand", sighand_field_offset, "pointer")
432 .add_struct("sighand_struct", 4864)
433 .add_field("sighand_struct", "action", action_offset, "k_sigaction")
434 .add_struct("k_sigaction", k_sigaction_size)
435 .add_struct("sigaction", 152)
436 .add_field("sigaction", "sa_handler", 0, "pointer");
437
438 let tasks_vaddr = init_task_vaddr + tasks_offset;
441
442 let ptb = PageTableBuilder::new()
443 .map_4k(init_task_vaddr, init_task_paddr, flags::WRITABLE)
445 .map_4k(
446 init_task_vaddr + 0x1000,
447 init_task_paddr + 0x1000,
448 flags::WRITABLE,
449 )
450 .map_4k(sighand_vaddr, sighand_paddr, flags::WRITABLE)
452 .map_4k(
453 sighand_vaddr + 0x1000,
454 sighand_paddr + 0x1000,
455 flags::WRITABLE,
456 )
457 .map_4k(
458 sighand_vaddr + 0x2000,
459 sighand_paddr + 0x2000,
460 flags::WRITABLE,
461 )
462 .write_phys_u64(init_task_paddr + tasks_offset, tasks_vaddr)
464 .write_phys_u64(init_task_paddr + pid_offset, 666)
466 .write_phys(init_task_paddr + comm_offset, b"malware\0")
468 .write_phys_u64(init_task_paddr + sighand_field_offset, sighand_vaddr)
470 .write_phys_u64(sigterm_entry_paddr, 1u64);
472
473 let reader = make_reader(&isf, ptb);
474 let result = walk_signal_handlers(&reader).unwrap();
475
476 assert!(!result.is_empty(), "expected at least one suspicious entry");
478
479 let sigterm_entry = result.iter().find(|e| e.signal == 15);
480 assert!(sigterm_entry.is_some(), "expected SIGTERM entry");
481
482 let entry = sigterm_entry.unwrap();
483 assert_eq!(entry.pid, 666);
484 assert_eq!(entry.comm, "malware");
485 assert_eq!(entry.signal_name, "SIGTERM");
486 assert_eq!(entry.handler, 1);
487 assert_eq!(entry.handler_type, "SIG_IGN");
488 assert!(entry.is_suspicious);
489 }
490}