#!/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()