use memf_core::object_reader::ObjectReader;
use memf_format::PhysicalMemoryProvider;
use crate::Result;
#[derive(Debug, Clone, serde::Serialize)]
pub struct SignalHandlerInfo {
pub pid: u32,
pub comm: String,
pub signal: u32,
pub signal_name: String,
pub handler: u64,
pub handler_type: String,
pub is_suspicious: bool,
}
pub fn signal_name(sig: u32) -> &'static str {
match sig {
1 => "SIGHUP",
2 => "SIGINT",
3 => "SIGQUIT",
6 => "SIGABRT",
9 => "SIGKILL",
10 => "SIGUSR1",
11 => "SIGSEGV",
12 => "SIGUSR2",
13 => "SIGPIPE",
14 => "SIGALRM",
15 => "SIGTERM",
17 => "SIGCHLD",
_ => "UNKNOWN",
}
}
pub fn handler_type(handler: u64) -> String {
match handler {
0 => "SIG_DFL".to_string(),
1 => "SIG_IGN".to_string(),
_ => format!("0x{handler:016x}"),
}
}
pub use crate::heuristics::classify_signal_handler;
const MAX_SIGNALS: u32 = 31;
pub fn walk_signal_handlers<P: PhysicalMemoryProvider>(
reader: &ObjectReader<P>,
) -> Result<Vec<SignalHandlerInfo>> {
let init_task_addr = match reader.symbols().symbol_address("init_task") {
Some(addr) => addr,
None => return Ok(Vec::new()),
};
let tasks_offset = match reader.symbols().field_offset("task_struct", "tasks") {
Some(off) => off,
None => return Ok(Vec::new()),
};
let action_offset = match reader.symbols().field_offset("sighand_struct", "action") {
Some(off) => off,
None => return Ok(Vec::new()),
};
let k_sigaction_size = match reader.symbols().struct_size("k_sigaction") {
Some(s) if s > 0 => s,
_ => return Ok(Vec::new()),
};
let head_vaddr = init_task_addr + tasks_offset;
let task_addrs = reader.walk_list(head_vaddr, "task_struct", "tasks")?;
let mut results = Vec::new();
let all_tasks = std::iter::once(init_task_addr).chain(task_addrs.iter().copied());
for task_addr in all_tasks {
let pid: u32 = match reader.read_field(task_addr, "task_struct", "pid") {
Ok(p) => p,
Err(_) => continue,
};
let comm = reader
.read_field_string(task_addr, "task_struct", "comm", 16)
.unwrap_or_default();
let sighand_ptr: u64 = match reader.read_field(task_addr, "task_struct", "sighand") {
Ok(p) => p,
Err(_) => continue,
};
if sighand_ptr == 0 {
continue;
}
let action_base = sighand_ptr + action_offset;
for sig in 1..=MAX_SIGNALS {
let entry_addr = action_base + u64::from(sig - 1) * k_sigaction_size;
let sa_handler: u64 = reader
.read_field(entry_addr, "sigaction", "sa_handler")
.unwrap_or(0);
let suspicious = classify_signal_handler(sig, sa_handler);
if suspicious {
results.push(SignalHandlerInfo {
pid,
comm: comm.clone(),
signal: sig,
signal_name: signal_name(sig).to_string(),
handler: sa_handler,
handler_type: handler_type(sa_handler),
is_suspicious: true,
});
}
}
}
Ok(results)
}
#[cfg(test)]
mod tests {
use super::*;
use memf_core::test_builders::{flags, PageTableBuilder};
use memf_core::vas::{TranslationMode, VirtualAddressSpace};
use memf_symbols::isf::IsfResolver;
use memf_symbols::test_builders::IsfBuilder;
fn make_reader(
isf: &IsfBuilder,
builder: PageTableBuilder,
) -> ObjectReader<memf_core::test_builders::SyntheticPhysMem> {
let json = isf.build_json();
let resolver = IsfResolver::from_value(&json).unwrap();
let (cr3, mem) = builder.build();
let vas = VirtualAddressSpace::new(mem, cr3, TranslationMode::X86_64FourLevel);
ObjectReader::new(vas, Box::new(resolver))
}
#[test]
fn signal_name_sigterm() {
assert_eq!(signal_name(15), "SIGTERM");
}
#[test]
fn signal_name_unknown() {
assert_eq!(signal_name(99), "UNKNOWN");
}
#[test]
fn handler_default() {
assert_eq!(handler_type(0), "SIG_DFL");
}
#[test]
fn handler_ignore() {
assert_eq!(handler_type(1), "SIG_IGN");
}
#[test]
fn classify_sigterm_ignored_suspicious() {
assert!(classify_signal_handler(15, 1));
assert!(!classify_signal_handler(15, 0));
assert!(!classify_signal_handler(15, 0xFFFF_8000_0001_0000));
}
#[test]
fn classify_sigsegv_handler_suspicious() {
assert!(classify_signal_handler(11, 0xFFFF_8000_0001_0000));
assert!(!classify_signal_handler(11, 0));
assert!(!classify_signal_handler(11, 1));
}
#[test]
fn walk_no_symbol_returns_empty() {
let isf = IsfBuilder::new();
let ptb = PageTableBuilder::new();
let reader = make_reader(&isf, ptb);
let result = walk_signal_handlers(&reader).unwrap();
assert!(result.is_empty());
}
#[test]
fn signal_name_all_known() {
assert_eq!(signal_name(1), "SIGHUP");
assert_eq!(signal_name(2), "SIGINT");
assert_eq!(signal_name(3), "SIGQUIT");
assert_eq!(signal_name(6), "SIGABRT");
assert_eq!(signal_name(9), "SIGKILL");
assert_eq!(signal_name(10), "SIGUSR1");
assert_eq!(signal_name(11), "SIGSEGV");
assert_eq!(signal_name(12), "SIGUSR2");
assert_eq!(signal_name(13), "SIGPIPE");
assert_eq!(signal_name(14), "SIGALRM");
assert_eq!(signal_name(15), "SIGTERM");
assert_eq!(signal_name(17), "SIGCHLD");
assert_eq!(signal_name(99), "UNKNOWN");
}
#[test]
fn handler_type_custom_address() {
let addr: u64 = 0xFFFF_8000_DEAD_BEEF;
let result = handler_type(addr);
assert!(
result.starts_with("0x"),
"custom handler must be hex-formatted"
);
assert_eq!(result, format!("0x{addr:016x}"));
}
#[test]
fn walk_missing_tasks_field_returns_empty() {
let isf = IsfBuilder::new()
.add_symbol("init_task", 0xFFFF_8800_0000_0000)
.add_struct("task_struct", 128)
.add_field("task_struct", "pid", 0u64, "int");
let reader = make_reader(&isf, PageTableBuilder::new());
let result = walk_signal_handlers(&reader).unwrap();
assert!(result.is_empty(), "missing tasks field → empty");
}
#[test]
fn walk_missing_action_field_returns_empty() {
let isf = IsfBuilder::new()
.add_symbol("init_task", 0xFFFF_8800_0001_0000)
.add_struct("list_head", 16)
.add_field("list_head", "next", 0u64, "pointer")
.add_field("list_head", "prev", 8u64, "pointer")
.add_struct("task_struct", 128)
.add_field("task_struct", "pid", 0u64, "int")
.add_field("task_struct", "tasks", 16u64, "list_head");
let reader = make_reader(&isf, PageTableBuilder::new());
let result = walk_signal_handlers(&reader).unwrap();
assert!(result.is_empty(), "missing sighand_struct.action → empty");
}
#[test]
fn walk_missing_k_sigaction_size_returns_empty() {
let isf = IsfBuilder::new()
.add_symbol("init_task", 0xFFFF_8800_0002_0000)
.add_struct("list_head", 16)
.add_field("list_head", "next", 0u64, "pointer")
.add_field("list_head", "prev", 8u64, "pointer")
.add_struct("task_struct", 128)
.add_field("task_struct", "pid", 0u64, "int")
.add_field("task_struct", "tasks", 16u64, "list_head")
.add_struct("sighand_struct", 256)
.add_field("sighand_struct", "action", 0u64, "pointer");
let reader = make_reader(&isf, PageTableBuilder::new());
let result = walk_signal_handlers(&reader).unwrap();
assert!(result.is_empty(), "missing k_sigaction size → empty");
}
#[test]
fn walk_sighand_null_skips_task() {
let init_task_vaddr: u64 = 0xFFFF_8800_0003_0000;
let init_task_paddr: u64 = 0x0030_0000;
let tasks_offset: u64 = 16;
let pid_offset: u64 = 0;
let sighand_offset: u64 = 48;
let action_offset: u64 = 0;
let k_sigaction_sz: u64 = 32;
let mut page = [0u8; 4096];
page[pid_offset as usize..pid_offset as usize + 4].copy_from_slice(&42u32.to_le_bytes());
let tasks_self = init_task_vaddr + tasks_offset;
page[tasks_offset as usize..tasks_offset as usize + 8]
.copy_from_slice(&tasks_self.to_le_bytes());
page[32..40].copy_from_slice(b"nullhand");
page[sighand_offset as usize..sighand_offset as usize + 8]
.copy_from_slice(&0u64.to_le_bytes());
let isf = IsfBuilder::new()
.add_symbol("init_task", init_task_vaddr)
.add_struct("list_head", 16)
.add_field("list_head", "next", 0u64, "pointer")
.add_field("list_head", "prev", 8u64, "pointer")
.add_struct("task_struct", 128)
.add_field("task_struct", "pid", pid_offset, "int")
.add_field("task_struct", "tasks", tasks_offset, "list_head")
.add_field("task_struct", "comm", 32u64, "char")
.add_field("task_struct", "sighand", sighand_offset, "pointer")
.add_struct("sighand_struct", 256)
.add_field("sighand_struct", "action", action_offset, "pointer")
.add_struct("k_sigaction", k_sigaction_sz)
.add_struct("sigaction", k_sigaction_sz)
.add_field("sigaction", "sa_handler", 0u64, "pointer");
let ptb = PageTableBuilder::new()
.map_4k(init_task_vaddr, init_task_paddr, flags::WRITABLE)
.write_phys(init_task_paddr, &page);
let reader = make_reader(&isf, ptb);
let result = walk_signal_handlers(&reader).unwrap();
assert!(
result.is_empty(),
"sighand == 0 → task skipped, no suspicious entries"
);
}
#[test]
fn walk_sigterm_ignored_detected() {
let init_task_vaddr: u64 = 0xFFFF_8800_0000_0000;
let init_task_paddr: u64 = 0x0010_0000;
let tasks_offset: u64 = 776;
let pid_offset: u64 = 872;
let comm_offset: u64 = 1496;
let sighand_field_offset: u64 = 1600;
let sighand_vaddr: u64 = 0xFFFF_8800_0010_0000;
let sighand_paddr: u64 = 0x0020_0000;
let action_offset: u64 = 0;
let k_sigaction_size: u64 = 152;
let sigterm_entry_paddr =
sighand_paddr + action_offset + u64::from(15u32 - 1) * k_sigaction_size;
let isf = IsfBuilder::new()
.add_symbol("init_task", init_task_vaddr)
.add_struct("list_head", 16)
.add_field("list_head", "next", 0, "pointer")
.add_field("list_head", "prev", 8, "pointer")
.add_struct("task_struct", 2048)
.add_field("task_struct", "tasks", tasks_offset, "list_head")
.add_field("task_struct", "pid", pid_offset, "int")
.add_field("task_struct", "comm", comm_offset, "char")
.add_field("task_struct", "sighand", sighand_field_offset, "pointer")
.add_struct("sighand_struct", 4864)
.add_field("sighand_struct", "action", action_offset, "k_sigaction")
.add_struct("k_sigaction", k_sigaction_size)
.add_struct("sigaction", 152)
.add_field("sigaction", "sa_handler", 0, "pointer");
let tasks_vaddr = init_task_vaddr + tasks_offset;
let ptb = PageTableBuilder::new()
.map_4k(init_task_vaddr, init_task_paddr, flags::WRITABLE)
.map_4k(
init_task_vaddr + 0x1000,
init_task_paddr + 0x1000,
flags::WRITABLE,
)
.map_4k(sighand_vaddr, sighand_paddr, flags::WRITABLE)
.map_4k(
sighand_vaddr + 0x1000,
sighand_paddr + 0x1000,
flags::WRITABLE,
)
.map_4k(
sighand_vaddr + 0x2000,
sighand_paddr + 0x2000,
flags::WRITABLE,
)
.write_phys_u64(init_task_paddr + tasks_offset, tasks_vaddr)
.write_phys_u64(init_task_paddr + pid_offset, 666)
.write_phys(init_task_paddr + comm_offset, b"malware\0")
.write_phys_u64(init_task_paddr + sighand_field_offset, sighand_vaddr)
.write_phys_u64(sigterm_entry_paddr, 1u64);
let reader = make_reader(&isf, ptb);
let result = walk_signal_handlers(&reader).unwrap();
assert!(!result.is_empty(), "expected at least one suspicious entry");
let sigterm_entry = result.iter().find(|e| e.signal == 15);
assert!(sigterm_entry.is_some(), "expected SIGTERM entry");
let entry = sigterm_entry.unwrap();
assert_eq!(entry.pid, 666);
assert_eq!(entry.comm, "malware");
assert_eq!(entry.signal_name, "SIGTERM");
assert_eq!(entry.handler, 1);
assert_eq!(entry.handler_type, "SIG_IGN");
assert!(entry.is_suspicious);
}
}