Skip to main content

memf_linux/
signal_handlers.rs

1//! Linux process signal handler inspection for malware detection.
2//!
3//! Inspects signal handlers for each process. Malware sometimes installs
4//! custom signal handlers to prevent termination (ignoring SIGTERM/SIGKILL),
5//! restart on SIGSEGV, or communicate via signals (SIGUSR1/SIGUSR2).
6//! MITRE ATT&CK T1036.
7//!
8//! The kernel stores signal handling state in `task_struct.sighand`, which
9//! points to a `sighand_struct` containing an array of `k_sigaction` entries
10//! (one per signal, 1-31 for standard signals). Each `k_sigaction` contains
11//! a `sigaction` struct with an `sa_handler` field:
12//! - 0 (`SIG_DFL`): default handler
13//! - 1 (`SIG_IGN`): signal is ignored
14//! - other: address of a custom signal handler function
15
16use memf_core::object_reader::ObjectReader;
17use memf_format::PhysicalMemoryProvider;
18
19use crate::Result;
20
21/// Signal handler information extracted from a process's `task_struct`.
22#[derive(Debug, Clone, serde::Serialize)]
23pub struct SignalHandlerInfo {
24    /// Process ID.
25    pub pid: u32,
26    /// Process command name.
27    pub comm: String,
28    /// Signal number (1-31).
29    pub signal: u32,
30    /// Human-readable signal name (e.g., "SIGTERM").
31    pub signal_name: String,
32    /// Raw `sa_handler` value from the kernel.
33    pub handler: u64,
34    /// Handler type description: "SIG_DFL", "SIG_IGN", or hex address.
35    pub handler_type: String,
36    /// Whether this signal handler configuration is suspicious.
37    pub is_suspicious: bool,
38}
39
40/// Map a signal number to its human-readable name.
41///
42/// Covers the standard POSIX signals relevant to forensic analysis.
43/// Returns `"UNKNOWN"` for unrecognised signal numbers.
44pub 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
62/// Describe the handler type based on the raw `sa_handler` value.
63///
64/// - `0` maps to `"SIG_DFL"` (default disposition).
65/// - `1` maps to `"SIG_IGN"` (signal ignored).
66/// - Any other value is formatted as a 16-digit hex address.
67pub 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
75/// Classify whether a signal handler configuration is suspicious.
76///
77/// A handler is considered suspicious if:
78/// - SIGTERM (15) or SIGHUP (1) is set to `SIG_IGN` (1) -- process resists
79///   termination, common in persistent malware.
80/// - SIGSEGV (11) has a custom handler (not `SIG_DFL` or `SIG_IGN`) --
81///   self-healing malware that catches segfaults to restart or re-inject.
82/// - SIGKILL (9) has been tampered with (any non-default handler) --
83///   impossible under normal circumstances, indicates kernel-level rootkit.
84pub use crate::heuristics::classify_signal_handler;
85
86/// Walk signal handlers for all processes found via the `init_task` list.
87///
88/// For each process, reads `task_struct.sighand` to locate the
89/// `sighand_struct`, then iterates over the `action` array (signals 1-31).
90/// For each signal, reads the `sa_handler` field from the `sigaction`
91/// struct embedded in each `k_sigaction` entry. Only entries classified
92/// as suspicious by [`classify_signal_handler`] are included in the
93/// output.
94///
95/// Returns `Ok(Vec::new())` if the required symbols (`init_task`,
96/// `sighand_struct.action`, etc.) are missing from the profile.
97/// Number of standard POSIX signals to inspect (1-31).
98const MAX_SIGNALS: u32 = 31;
99
100/// Walk signal handlers for all processes found via the `init_task` list.
101pub fn walk_signal_handlers<P: PhysicalMemoryProvider>(
102    reader: &ObjectReader<P>,
103) -> Result<Vec<SignalHandlerInfo>> {
104    // Resolve init_task symbol to start walking the task list.
105    let init_task_addr = match reader.symbols().symbol_address("init_task") {
106        Some(addr) => addr,
107        None => return Ok(Vec::new()),
108    };
109
110    // Resolve task_struct.tasks for the linked list walk.
111    let tasks_offset = match reader.symbols().field_offset("task_struct", "tasks") {
112        Some(off) => off,
113        None => return Ok(Vec::new()),
114    };
115
116    // Resolve sighand_struct.action (array of k_sigaction).
117    let action_offset = match reader.symbols().field_offset("sighand_struct", "action") {
118        Some(off) => off,
119        None => return Ok(Vec::new()),
120    };
121
122    // Resolve k_sigaction size for array stride.
123    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    // Walk the task list.
129    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    // Process init_task and all tasks in the list.
135    let all_tasks = std::iter::once(init_task_addr).chain(task_addrs.iter().copied());
136
137    for task_addr in all_tasks {
138        // Read pid and comm for this task.
139        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        // Read the sighand pointer.
148        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        // Iterate over signals 1-31.
159        for sig in 1..=MAX_SIGNALS {
160            // Each k_sigaction entry: action_base + (sig - 1) * k_sigaction_size.
161            // k_sigaction embeds sigaction at offset 0; sa_handler is resolved
162            // by read_field via the "sigaction"/"sa_handler" symbol pair.
163            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    /// Helper: create an ObjectReader from ISF and page table builders.
196    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        // SIGTERM (15) with SIG_IGN (1) should be suspicious.
230        assert!(classify_signal_handler(15, 1));
231        // SIGTERM with SIG_DFL should NOT be suspicious.
232        assert!(!classify_signal_handler(15, 0));
233        // SIGTERM with a custom handler should NOT be suspicious
234        // (only SIG_IGN is flagged for SIGTERM).
235        assert!(!classify_signal_handler(15, 0xFFFF_8000_0001_0000));
236    }
237
238    #[test]
239    fn classify_sigsegv_handler_suspicious() {
240        // SIGSEGV (11) with a custom handler is suspicious (self-healing).
241        assert!(classify_signal_handler(11, 0xFFFF_8000_0001_0000));
242        // SIGSEGV with SIG_DFL is NOT suspicious.
243        assert!(!classify_signal_handler(11, 0));
244        // SIGSEGV with SIG_IGN is NOT suspicious (just ignoring it).
245        assert!(!classify_signal_handler(11, 1));
246    }
247
248    #[test]
249    fn walk_no_symbol_returns_empty() {
250        // When required symbols are missing, walk should return empty Vec.
251        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    // -----------------------------------------------------------------------
260    // signal_name: cover all branches
261    // -----------------------------------------------------------------------
262
263    #[test]
264    fn signal_name_all_known() {
265        // Covers lines 46-57 (all match arms in signal_name)
266        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        // Covers line 71 (custom address branch in handler_type)
284        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    // -----------------------------------------------------------------------
294    // walk_signal_handlers: graceful degradation branches
295    // -----------------------------------------------------------------------
296
297    #[test]
298    fn walk_missing_tasks_field_returns_empty() {
299        // init_task present but task_struct.tasks missing → empty (line 123)
300        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        // tasks intentionally absent
305        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        // init_task + tasks present but sighand_struct.action missing → empty (line 129)
314        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        // sighand_struct.action absent
323        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        // action present but k_sigaction struct absent → empty (line 135)
332        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        // k_sigaction struct absent → struct_size returns None
343        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        // task has sighand == 0 → task is skipped (line 162-163)
352        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        // pid = 42
362        page[pid_offset as usize..pid_offset as usize + 4].copy_from_slice(&42u32.to_le_bytes());
363        // tasks self-pointing
364        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        // comm = "nullhand"
368        page[32..40].copy_from_slice(b"nullhand");
369        // sighand = 0 → skip
370        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        // Set up a single process with SIGTERM handler set to SIG_IGN (1).
404        let init_task_vaddr: u64 = 0xFFFF_8800_0000_0000;
405        let init_task_paddr: u64 = 0x0010_0000;
406
407        // Layout constants.
408        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        // SIGTERM is signal 15 -> action[14] (0-indexed).
419        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        // Build the page tables. The task list is circular: tasks.next points
439        // back to &init_task.tasks so walk_list returns empty (only init_task).
440        let tasks_vaddr = init_task_vaddr + tasks_offset;
441
442        let ptb = PageTableBuilder::new()
443            // Map init_task pages (need enough for comm at offset 1496+)
444            .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 sighand_struct pages (need space for action array)
451            .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            // task_struct.tasks.next -> points back to itself (circular)
463            .write_phys_u64(init_task_paddr + tasks_offset, tasks_vaddr)
464            // task_struct.pid = 666
465            .write_phys_u64(init_task_paddr + pid_offset, 666)
466            // task_struct.comm = "malware\0"
467            .write_phys(init_task_paddr + comm_offset, b"malware\0")
468            // task_struct.sighand = pointer to sighand_struct
469            .write_phys_u64(init_task_paddr + sighand_field_offset, sighand_vaddr)
470            // SIGTERM (signal 15) sa_handler = 1 (SIG_IGN)
471            .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        // Should detect SIGTERM being ignored as suspicious.
477        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}