Source code for pyplugins.analysis.interfaces

"""
Interfaces Plugin (interfaces.py) for Penguin
=============================================

This module provides the Interfaces plugin, which tracks and logs missing network interfaces and failing ioctl operations in the guest environment. It is intended for use with the Penguin analysis framework and is implemented as a plugin.

Features
--------

- Detects and logs missing network interfaces accessed by the guest.
- Tracks and logs failing ioctl operations related to network interfaces.
- Integrates with syscall and exec event hooks for comprehensive coverage.
- Supports filtering and validation of interface names.

Usage
-----

The plugin is loaded by the Penguin framework and responds to relevant syscall and exec events.

Arguments
---------

- outdir: Output directory for logs.
- conf: Configuration dictionary.
- verbose: Enables debug logging.

Output
------

- Logs missing interfaces to `iface.log`.
- Logs failing ioctl operations to `iface_ioctl.log`.

Example
-------

.. code-block:: python

    from penguin import plugins
    plugins.load("analysis.interfaces", outdir="/tmp", verbose=True)

"""

import re
from penguin import plugins, Plugin
from apis.syscalls import ValueFilter

iface_log = "iface.log"
ioctl_log = "iface_ioctl.log"

# Regex pattern for a valid Linux interface name
intf_pattern = r'^[a-zA-Z][a-zA-Z0-9._/-]{0,15}$'
ENODEV = 19

ignored_interfaces = ["lo"]


[docs] class Interfaces(Plugin): def __init__(self): self.outdir = self.get_arg("outdir") self.conf = self.get_arg("conf") if self.get_arg_bool("verbose"): self.logger.setLevel("DEBUG") open(f"{self.outdir}/{iface_log}", "w").close() open(f"{self.outdir}/{ioctl_log}", "w").close() self.added_ifaces = self.conf.get("netdevs", []) self.missing_ifaces = set() self.failed_ioctls = set()
[docs] def handle_interface(self, iface): if iface is None or not len(iface): return if iface in self.added_ifaces or iface in self.missing_ifaces \ or iface in ignored_interfaces: return if not re.match(intf_pattern, iface): self.logger.debug(f"Invalid interface name {iface}") return self.missing_ifaces.add(iface) with open(f"{self.outdir}/{iface_log}", "a") as f: f.write(f"{iface}\n") self.logger.debug(f"Detected new missing interface {iface}")
[docs] def failing_ioctl(self, ioctl, iface, rv): if iface and not re.match(intf_pattern, iface): self.logger.debug(f"Invalid interface name {iface}") iface = None if (ioctl, iface) in self.failed_ioctls: return self.failed_ioctls.add((ioctl, iface)) self.logger.debug( f"Detected new failing ioctl {hex(ioctl)} for {iface or '[?]'}") with open(f"{self.outdir}/{ioctl_log}", "a") as f: f.write(f"{hex(ioctl)},{iface or '[?]'},{rv}\n")
[docs] @plugins.syscalls.syscall("on_sys_ioctl_return", arg_filters=[ None, ValueFilter.range(0x8000, 0x9000 - 1), None ], retval_filter=ValueFilter.error() ) def after_ioctl(self, regs, proto, syscall, fd, request, arg): iface = yield from plugins.mem.read_str(arg) rv = syscall.retval # try to catch missing interfaces if rv == -ENODEV: self.handle_interface(iface) # try to catch failing ioctls self.failing_ioctl(request, iface, rv)
[docs] @plugins.subscribe(plugins.Execs, "exec_event") def iface_on_exec(self, event): argv = event.get('argv', []) fname = event.get('procname', None) # note argv[0] is the binary name, similar to fname if argv is None or len(argv) == 0: return if fname and fname.startswith("/igloo/utils"): # This is us adding interfaces in /igloo_init return iface = None if fname and (fname.endswith("/ip") or (argv and argv[0] == "ip")): # (ip .* dev \K[a-zA-Z0-9.]+(?=))' for idx, arg in enumerate(argv): if not arg: continue if "dev" in arg and idx < len(argv) - 1: iface = argv[idx + 1] if fname and (fname.endswith("/ifconfig") or (argv and argv[0] == "ifconfig")): # device is the first argument if len(argv) > 1: iface = argv[1] self.handle_interface(iface)