Source code for pyplugins.interventions.lifeguard

"""
Lifeguard: Signal Blocking Plugin
=================================

This module provides a plugin for the Penguin framework to block specified Linux signals
by replacing them with a harmless SIGCONT. It is useful for preventing certain signals
from terminating or interrupting processes during analysis or emulation.

Features
--------

- Block user-specified signals (e.g., SIGKILL, SIGTERM) for target processes.
- Log all signal delivery attempts to a CSV file.
- Optionally enable verbose logging for debugging.

Usage
-----

To use this plugin, specify the signals to block in the configuration:

.. code-block:: json

    {
        "blocked_signals": [9, 15]  # Block SIGKILL and SIGTERM
    }

The plugin will log all signal attempts to lifeguard.csv in the specified output directory.
"""

from penguin import plugins, Plugin

LIFELOG: str = "lifeguard.csv"

signals = {
    "SIGHUP": 1,
    "SIGINT": 2,
    "SIGQUIT": 3,
    "SIGILL": 4,
    "SIGTRAP": 5,
    "SIGABRT": 6,
    "SIGIOT": 6,
    "SIGBUS": 7,
    "SIGFPE": 8,
    "SIGKILL": 9,
    "SIGUSR1": 10,
    "SIGSEGV": 11,
    "SIGUSR2": 12,
    "SIGPIPE": 13,
    "SIGALRM": 14,
    "SIGTERM": 15,
    "SIGSTKFLT": 16,
    "SIGCHLD": 17,
    "SIGCONT": 18,
    "SIGSTOP": 19,
    "SIGTSTP": 20,
    "SIGTTIN": 21,
    "SIGTTOU": 22,
    "SIGURG": 23,
    "SIGXCPU": 24,
    "SIGXFSZ": 25,
    "SIGVTALRM": 26,
    "SIGPROF": 27,
    "SIGWINCH": 28,
    "SIGIO": 29,
    "SIGPWR": 30,
    "SIGSYS": 31,
    "SIGRTMIN": 32
}

# make reversible
for i, v in list(signals.items()):
    signals[v] = i


[docs] class Lifeguard(Plugin): """ Plugin to block specified signals by replacing them with SIGCONT. **Attributes** - `outdir` (`str`): Output directory for logs. - `blocked_signals` (`list[int]`): List of blocked signal numbers. """ outdir: str blocked_signals: list[int] def __init__(self) -> None: """ **Initialize the Lifeguard plugin.** **Args** - `panda` (`object`): The PANDA instance. **Returns** - `None` """ self.outdir = self.get_arg("outdir") if self.get_arg_bool("verbose"): self.logger.setLevel("DEBUG") self.blocked_signals = [] conf = self.get_arg("conf") if "blocked_signals" in conf: self.blocked_signals = [int(x) for x in conf["blocked_signals"]] with open(f"{self.outdir}/{LIFELOG}", "w") as f: f.write("signal,target_process,blocked\n") if len(self.blocked_signals) > 0: self.logger.info(f"Blocking signals: {self.blocked_signals}")
[docs] @plugins.syscalls.syscall( name_or_pattern="sys_kill", on_enter=True, on_return=False, ) def on_sys_kill_enter(self, pt_regs, proto, syscall, *args): """ **Handler for the kill syscall. Blocks signals if configured.** **Args** - `pt_regs` (`object`): The CPU registers at syscall entry. - `proto` (`object`): The syscall prototype. - `syscall` (`object`): The syscall event object. - `args` (`tuple`): The arguments passed to the syscall. **Returns** - `None` """ (pid, sig) = args[0:2] save = sig in self.blocked_signals with open(f"{self.outdir}/{LIFELOG}", "a") as f: f.write(f"{sig},{pid},{1 if save else 0}\n") proc = yield from plugins.osi.get_proc() if proc: ppid = proc.pid else: ppid = "[?]" pname = yield from plugins.osi.get_proc_name() kpname = yield from plugins.osi.get_proc_name(pid) if not kpname: kpname = "[?]" expl = signals.get(sig, "[?]") self.logger.debug(f"{pname}({ppid}) kill({kpname}({pid}), {expl}({sig})) {'blocked' if save else ''}") if save: # Old approach was to change to SIGCONT, but some architectures (e.g., mips64eb/powerpc64) have syscall contexts that cause that to break syscall.skip_syscall = True syscall.retval = 0