Skip to content

Latest commit

 

History

History
250 lines (209 loc) · 13 KB

plugins.md

File metadata and controls

250 lines (209 loc) · 13 KB

Penguin Plugins

The following penguin plugins are currently supported. Each is documented below.

  • Coverage: Track block-level coverage of binaries
  • Env: Track usage of boot arguments, environment variable accesses, and environment variable comparisons.
  • Health: Track system health metrics including processes run.
  • Interfaces: Track network interfaces referenced
  • KernelVersion: Change output of uname syscall
  • Lifeguard: Track and block signals
  • Mounts: Track attempts to mount file systems
  • NVRAM2: Tracks accesses to NVRAM
  • Netbinds: Track network listening guest processes
  • Nmap: Network scanning for guest applications that bind to TCP ports
  • Pseudofiles: Model and monitor interactions to devices in /dev /proc and /sys
  • Shell: Track behavior of shell scripts including lines executed
  • VPNguin: Bridge network connections to networked guest processes
  • Zap: Currently disabled Network scanning of guest web applications

Coverage

This plugin tracks the module and offset block level coverage of all binaries in the system. These results are reported in coverage.csv. The file coverage_tree.csv stores this information with parent/child relationships to visualize as a tree. The file coverage_transitions.csv records all context switches between processes.

Crashes

The crashes plugin records fatal signal deliveries to guest processes (userland crashes) in crashes.yaml. Each record includes the process name, pid, signal number and name, the faulting program counter, the time (host wall-clock seconds since emulation start) of the first occurrence, and a count de-duplicating identical (proc, signal, pc) deliveries. By default it records SIGSEGV, SIGBUS, SIGILL, SIGABRT, SIGFPE, and SIGSYS; the set is configurable via the signals argument (use signal names — numbering is resolved per guest architecture). The number of unique crash records is also reported (negated) as the crashes score category.

Note that records are signal deliveries, not confirmed terminations: the driver hook fires when a signal is dequeued for delivery, before the kernel applies its disposition, and the event does not indicate whether the process has a handler installed. A process that catches a fatal-class signal and survives is still recorded. Deliveries that another plugin has already dropped (event.drop, e.g. a SIGILL-bypass consumer) are not recorded.

Env

The env plugin dynamically tracks linux environment variables accessed through /proc/cmdline and calls to getenv. It also tracks accesses to /proc/mtd as well as /dev/mtdX to identify accesses to u-boot environment variables.

If an env value is set to the magic string DYNVALDYNVALDYNVAL a dynamic analysis to detect comparisons between this magic string and any other string will be enabled. The results of this analysis will be stored in env_cmp.txt

In a config file, a user may add key-value pairs into the env filed to set new values into the linux environment. Note that a number of required internal variables (e.g., root=/dev/vda) will added to the system's arguments after any arguments you specify here.

Health

The health plugin tracks the system health over time. health.csv tracks counts of various behaviors of interest over time while health_final.yaml just reports these values at the end of execution.

The plugin also creates health_procs.txt as a sorted list of processes run and health_procs_with_args.txt as a sorted list of processes with their arguments.

HookLogger

"Easy mode" instrumentation: register uprobes and syscall hooks from printf-style action strings (sys_read(%fd, %s:out, %d) = %d) instead of writing callbacks. Drive it from the breakpoint CLI (over the RemoteCtrl plugin's socket) or the Python API. The grammar, format specifiers and filters are in hooklogger.md.

Interfaces

Track network interfaces referenced in executed commands. Results are reported in iface.log.

KernelVersion

Change output of uname syscall. Config Options:

  • sysname: changes operating system name
  • nodename: changtes network node hostname
  • release: changes the kernel release
  • kversion: changes the kernel version
  • machine: changes the machine hardware name
  • domainname: changes NIS or YP domain name

Lifeguard

Track and block signals sent between processes. Results are stored in lifeguard.csv.

Lifeguard suppresses configured signals sent through supported signal-send syscalls by skipping the syscall before the kernel sends the signal. Supported syscalls are defined in one table in the plugin and resolved through the syscall prototype argument helpers supplied by the driver, falling back to fixed indexes only if the prototype names are unavailable. It also consumes signal_monitor for configured signals other than SIGKILL and SIGSTOP, allowing delivery paths to be observed and dropped when the driver hook is effective.

Delivery-time drops are not equivalent to preventing a signal from being sent. The driver hooks the kernel dequeue path, so it can drop catchable signal deliveries after the kernel has selected a target. Default-fatal signals that do not have a userspace handler may start process or thread-group exit before that dequeue path is reached. For process-preserving behavior, the preferred path is still to block the sending syscall before the kernel applies signal semantics. SIGKILL and SIGSTOP cannot be caught or ignored by Linux processes, so Lifeguard treats them as syscall-only and only suppresses instances generated by supported signal-send syscalls.

The syscall path covers the supported signal-send syscalls currently listed by Lifeguard: kill, tkill, tgkill, rt_sigqueueinfo, rt_tgsigqueueinfo, and pidfd_send_signal. Signals sent by other kernel paths or unsupported syscalls may be visible through delivery monitoring when the driver hooks them, but the syscall fallback will not prevent them before send time.

Some signals also require guest state repair when dropped. For example, blindly dropping synchronous fault signals such as SIGILL, SIGSEGV, SIGBUS, SIGFPE, SIGTRAP, or SIGSYS can immediately re-enter the same faulting instruction. Use a purpose-built signal_monitor consumer, such as sigill_bypass.py, when the handler needs to advance the PC, emulate an instruction, or otherwise repair guest state.

Mounts

Track which file systems are mounted (or attempted to be mounted) at which paths. Results stored in mounts.csv. Note this plugin will track some penguin-internal initialization logic with mounts in the /igloo directory.

NVRAM2

This plugin tracks accesses to keys and values stored in NVRAM. Results are stored in nvram.csv

Netbinds

This plugin detects and logs network binds by guest processes. The results are logged into netbinds.csv and include a time column indicating how many seconds after boot until the bind occurred, plus state and closed_time columns describing each socket's final lifecycle state:

  • listening — announced and still bound.
  • transient — released within the announce window; not a working service.
  • closed — listened, then released for good (closed_time set).
  • pending — bound when the run ended but younger than the announce window; never confirmed, so not a working service either.

New binds are announced to downstream plugins (VPN, Nmap, FetchWeb) only after an announce debounce (announce_debounce_s, default 2s), so short-lived binds never trigger VPN bridging or scans. Both the announce and the close/flap debounce windows use host wall-clock time, so the classification of a very short-lived bind can vary with emulation speed. Every open/flap/ transient/close transition is appended to netbind_events.csv, and a per-socket lifecycle summary is written to netbinds_lifecycle.csv at run end.

The socket lifecycle survives a VM snapshot/restore: the record is bundled with the snapshot and rebuilt on restore (a restored guest is already past its binds and would otherwise vanish from netbinds.csv). Already-announced services are rebuilt silently — downstream consumers replay their own state — while a service still within its announce window at snapshot time is announced on restore.

Nmap

This plugin runs nmap scans on all network-listening services. It depends on the VPN plugin to establish network connections to guest services. Logs are written to nmap_{protocol}_{port}.log

Pseudofiles

This plugin tracks accesses and interactions with files in /dev/ and /proc/. In pseudofiles_failures.yaml details of failed interactions are reported, ranked by impact (crashing_callers >> distinct_callers > hits), with the observed calling processes and a suggested starting model per path that can be pasted directly into the pseudofiles config section.

Users can add pseudofiles and configure models for reads, writes, and IOCTLs on these files by adding entries into the pseudofiles config section. See the pseudofile model catalog for every available model (read/write/ioctl/poll/seek/mmap/open/release), copy-paste snippets, and how to add custom models in Python via @register_model.

Shell

This plugin tracks the behavior of shell scripts, capturing coverage in shell_cov.csv, environment variable values in shell_env.csv and a combined trace in shell_cov_trace.csv.

VPNguin

This plugin detects network binds and configures a custom VPN to bridge network connections to guest services. The mappings between guest network services and what port the VPN exposes them on are listed in vpn_bridges.csv For example, if the file contains:

procname,ipvn,domain,guest_ip,guest_port,host_port
lighttpd,ipv4,tcp,127.0.0.1,80,80
lighttpd,ipv4,tcp,192.168.0.1,80,48823

This means lighttpd started listening on port 80 on the loopback interface as well as another IP address. To talk to the service as if you were connecting via loopback, you'd connect to the relevant host_port, here 80. To talk to the service as if you were connecting via the other IP address, you'd connect to the other host_port, here 48823. Note these are ports within your container, not on your host, so you must connect to the appropriate IP address to reach the container.

The plugin also writes two convenience files in the results directory, regenerated as bridges are established: guest_ip (the bare reachable IP, same content as ipaddr.txt) and endpoints.txt (one proto host_ip:host_port -> guest_ip:guest_port (proc) line per bridge).

The published port is not always the guest's port

host_port matches guest_port when it can, but the plugin has to move it when that port is unavailable — already bound in the container, already claimed by an earlier bind in this same run, or privileged (below 1024) when the process cannot bind privileged ports. It then searches deterministically in strides of 1000: guest port 80 becomes 1080, or 2080 if 1080 is taken too, and so on.

So the same guest port can be published on different host ports in different runs of the same container, without anything having changed in the config. That is working as intended, but it means nothing may hardcode a host port. Read it from endpoints.txt (or the host_port column of netbinds.csv), which is the only source of truth for a given run. The run log also states the mapping and the reason for it as each bind happens:

        lighttpd binds tcp 0.0.0.0:80    reach it at container foo:2080   80 is already in use

An explicit mapping is available when you need a fixed port: set plugins.vpn.IGLOO_VPN_PORT_MAPS (or the IGLOO_VPN_PORT_MAPS environment variable) to comma-separated <proto>:<host_port>:<guest_ip>:<guest_port> rules, e.g. tcp:8080:192.168.0.1:80. A fixed mapping is never relocated: if the host port is busy the run fails loudly instead.

connect.sh

Every run also gets an executable results/<n>/connect.sh, generated by penguin (not the VPN plugin) next to runtime.yaml. It is the one-glance "get me into this device" helper and talks to the guest's serial root shell through the container's localhost (docker exec <container> telnet localhost <port>), so it does not depend on the container's per-run IP address:

  • ./connect.sh — attach to the serial console.

  • ./connect.sh <cmd...> — run a command on the serial console, print its output, and exit with the command's own status. Waits up to PENGUIN_CONNECT_TIMEOUT seconds (default 15) for the guest to answer, and exits 1 with a diagnostic if it does not — so it is safe in a script:

    if ./connect.sh "test -f /var/run/httpd.pid"; then echo "httpd is up"; fi

It requires core.root_shell: true (there is no serial console otherwise) and prints how to enable it when it is off.

ZAP

Currently disabled This plugin runs the zap web application scanner to crawl and interact with guest web applications listening on TCP port 80. Logs are written to zap.log and zap_tcp_80.log.