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,447 @@
|
||||
#!/usr/bin/env python3
|
||||
"""BLE emergency module — GATT server for local emergency access.
|
||||
|
||||
Provides a Bluetooth Low Energy GATT server for last-resort local access
|
||||
when all network connectivity is lost. PSK-authenticated commands over
|
||||
a custom GATT service allow the operator within ~10m to check status,
|
||||
issue kill commands, reboot, or retrieve the current IP.
|
||||
|
||||
Disabled by default — must be explicitly enabled in config.
|
||||
"""
|
||||
|
||||
import hashlib
|
||||
import hmac
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import secrets
|
||||
import subprocess
|
||||
import threading
|
||||
import time
|
||||
from typing import Optional
|
||||
|
||||
from modules.base import BaseModule
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# Custom BLE service/characteristic UUIDs (random, non-standard)
|
||||
SERVICE_UUID = "bb000001-1337-4000-8000-000000000001"
|
||||
CMD_CHAR_UUID = "bb000002-1337-4000-8000-000000000002" # Write: send command
|
||||
RESP_CHAR_UUID = "bb000003-1337-4000-8000-000000000003" # Read/Notify: response
|
||||
|
||||
# Max BLE MTU for response
|
||||
BLE_MTU = 512
|
||||
ADV_TIMEOUT = 0 # 0 = advertise indefinitely
|
||||
|
||||
|
||||
class BLEEmergency(BaseModule):
|
||||
"""BLE GATT server for emergency local access with PSK auth."""
|
||||
|
||||
name = "ble_emergency"
|
||||
module_type = "connectivity"
|
||||
priority = -50
|
||||
requires_root = True
|
||||
|
||||
def __init__(self, bus, state, config, engine=None):
|
||||
super().__init__(bus, state, config, engine)
|
||||
self._psk: Optional[str] = None
|
||||
self._advertising = False
|
||||
self._gatt_thread: Optional[threading.Thread] = None
|
||||
self._hci_device = "hci0"
|
||||
self._device_name: Optional[str] = None
|
||||
self._nonce: Optional[str] = None
|
||||
self._authenticated_sessions: set = set()
|
||||
self._last_response: str = ""
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# BaseModule interface
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
def start(self) -> None:
|
||||
if self._running:
|
||||
return
|
||||
|
||||
ble_cfg = self.config.get("connectivity", {}).get("ble_emergency", {})
|
||||
|
||||
# Disabled by default
|
||||
if not ble_cfg.get("enabled", False):
|
||||
logger.info("BLE emergency module disabled (set ble_emergency.enabled=true)")
|
||||
self.state.set_module_status(self.name, "disabled",
|
||||
extra={"reason": "not_enabled"})
|
||||
return
|
||||
|
||||
self._psk = ble_cfg.get("psk")
|
||||
if not self._psk:
|
||||
logger.error("BLE emergency PSK not configured")
|
||||
self.state.set_module_status(self.name, "error")
|
||||
return
|
||||
|
||||
self._hci_device = ble_cfg.get("hci_device", "hci0")
|
||||
self._device_name = ble_cfg.get("device_name")
|
||||
|
||||
# If no device name, try to match MAC profile for consistency
|
||||
if not self._device_name:
|
||||
mac_hostname = self.state.get(
|
||||
"mac_manager", "dhcp_hostname", default=None
|
||||
)
|
||||
self._device_name = mac_hostname if mac_hostname else "LE-Device"
|
||||
|
||||
if not self._check_bluetooth_available():
|
||||
logger.error("Bluetooth hardware not available")
|
||||
self.state.set_module_status(self.name, "error")
|
||||
return
|
||||
|
||||
if not self.start_gatt():
|
||||
self.state.set_module_status(self.name, "error")
|
||||
return
|
||||
|
||||
self._running = True
|
||||
self._pid = os.getpid()
|
||||
self._start_time = time.time()
|
||||
|
||||
self.state.set_module_status(self.name, "running", pid=self._pid)
|
||||
logger.info("BLE emergency GATT server active (device: %s)", self._device_name)
|
||||
|
||||
def stop(self) -> None:
|
||||
if not self._running:
|
||||
return
|
||||
|
||||
self.stop_gatt()
|
||||
self._running = False
|
||||
self._advertising = False
|
||||
|
||||
self.state.set_module_status(self.name, "stopped")
|
||||
logger.info("BLE emergency stopped")
|
||||
|
||||
def status(self) -> dict:
|
||||
return {
|
||||
"running": self._running,
|
||||
"pid": self._pid,
|
||||
"uptime": time.time() - self._start_time if self._start_time else 0,
|
||||
"advertising": self._advertising,
|
||||
"hci_device": self._hci_device,
|
||||
"device_name": self._device_name,
|
||||
"authenticated_sessions": len(self._authenticated_sessions),
|
||||
}
|
||||
|
||||
def configure(self, config: dict) -> None:
|
||||
self.config.update(config)
|
||||
|
||||
def health_check(self) -> bool:
|
||||
if not self._running:
|
||||
return False
|
||||
return self.is_advertising()
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# GATT server operations
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
def start_gatt(self) -> bool:
|
||||
"""Start BLE advertising and GATT server."""
|
||||
# Enable BLE adapter
|
||||
if not self._enable_adapter():
|
||||
return False
|
||||
|
||||
# Set device name
|
||||
if not self._set_device_name():
|
||||
logger.warning("Could not set BLE device name")
|
||||
|
||||
# Start advertising
|
||||
if not self._start_advertising():
|
||||
return False
|
||||
|
||||
# Start GATT command listener in background thread
|
||||
self._nonce = secrets.token_hex(16)
|
||||
self._gatt_thread = threading.Thread(
|
||||
target=self._gatt_listen_loop, daemon=True, name="sensor-ble-gatt",
|
||||
)
|
||||
self._gatt_thread.start()
|
||||
|
||||
self._advertising = True
|
||||
return True
|
||||
|
||||
def stop_gatt(self) -> None:
|
||||
"""Stop GATT server and BLE advertising."""
|
||||
self._advertising = False
|
||||
self._stop_advertising()
|
||||
|
||||
if self._gatt_thread and self._gatt_thread.is_alive():
|
||||
self._gatt_thread.join(timeout=3.0)
|
||||
|
||||
self._authenticated_sessions.clear()
|
||||
|
||||
def is_advertising(self) -> bool:
|
||||
"""Check if BLE advertising is active."""
|
||||
try:
|
||||
result = subprocess.run(
|
||||
["hciconfig", self._hci_device],
|
||||
capture_output=True, text=True, timeout=5,
|
||||
)
|
||||
return "UP RUNNING" in result.stdout
|
||||
except (subprocess.TimeoutExpired, FileNotFoundError):
|
||||
return False
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# BLE adapter management
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
def _check_bluetooth_available(self) -> bool:
|
||||
"""Check if Bluetooth hardware is present."""
|
||||
try:
|
||||
result = subprocess.run(
|
||||
["hciconfig", self._hci_device],
|
||||
capture_output=True, timeout=5,
|
||||
)
|
||||
return result.returncode == 0
|
||||
except (subprocess.TimeoutExpired, FileNotFoundError):
|
||||
return False
|
||||
|
||||
def _enable_adapter(self) -> bool:
|
||||
"""Power on the BLE adapter."""
|
||||
try:
|
||||
subprocess.run(
|
||||
["hciconfig", self._hci_device, "up"],
|
||||
check=True, capture_output=True, timeout=10,
|
||||
)
|
||||
# Enable LE mode
|
||||
subprocess.run(
|
||||
["hciconfig", self._hci_device, "leadv", "0"],
|
||||
capture_output=True, timeout=5,
|
||||
)
|
||||
return True
|
||||
except (subprocess.CalledProcessError, subprocess.TimeoutExpired, FileNotFoundError) as exc:
|
||||
logger.error("Failed to enable BLE adapter: %s", exc)
|
||||
return False
|
||||
|
||||
def _set_device_name(self) -> bool:
|
||||
"""Set the BLE device name via bluetoothctl."""
|
||||
try:
|
||||
proc = subprocess.run(
|
||||
["bluetoothctl", "--", "system-alias", self._device_name],
|
||||
capture_output=True, timeout=5,
|
||||
)
|
||||
return proc.returncode == 0
|
||||
except (subprocess.TimeoutExpired, FileNotFoundError):
|
||||
return False
|
||||
|
||||
def _start_advertising(self) -> bool:
|
||||
"""Start BLE advertising."""
|
||||
try:
|
||||
# Set advertising data
|
||||
subprocess.run(
|
||||
["hciconfig", self._hci_device, "leadv", "0"],
|
||||
capture_output=True, timeout=5,
|
||||
)
|
||||
# Also use btmgmt for more control if available
|
||||
subprocess.run(
|
||||
["btmgmt", "--index", self._hci_device.replace("hci", ""),
|
||||
"power", "on"],
|
||||
capture_output=True, timeout=5,
|
||||
)
|
||||
subprocess.run(
|
||||
["btmgmt", "--index", self._hci_device.replace("hci", ""),
|
||||
"le", "on"],
|
||||
capture_output=True, timeout=5,
|
||||
)
|
||||
subprocess.run(
|
||||
["btmgmt", "--index", self._hci_device.replace("hci", ""),
|
||||
"connectable", "on"],
|
||||
capture_output=True, timeout=5,
|
||||
)
|
||||
subprocess.run(
|
||||
["btmgmt", "--index", self._hci_device.replace("hci", ""),
|
||||
"advertising", "on"],
|
||||
capture_output=True, timeout=5,
|
||||
)
|
||||
return True
|
||||
except (subprocess.CalledProcessError, subprocess.TimeoutExpired, FileNotFoundError):
|
||||
return False
|
||||
|
||||
def _stop_advertising(self) -> None:
|
||||
"""Stop BLE advertising."""
|
||||
try:
|
||||
subprocess.run(
|
||||
["hciconfig", self._hci_device, "noleadv"],
|
||||
capture_output=True, timeout=5,
|
||||
)
|
||||
except (subprocess.CalledProcessError, subprocess.TimeoutExpired, FileNotFoundError):
|
||||
pass
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# GATT command processing
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
def _gatt_listen_loop(self) -> None:
|
||||
"""Listen for GATT characteristic writes via bluetoothctl/gatttool.
|
||||
|
||||
This loop uses a simple file-based IPC approach: commands are written
|
||||
to a well-known path by the BLE GATT callback, and this loop processes
|
||||
them. In production, this would use dbus-python to register a proper
|
||||
GATT application.
|
||||
"""
|
||||
cmd_fifo = "/tmp/.bb_ble_cmd"
|
||||
resp_fifo = "/tmp/.bb_ble_resp"
|
||||
|
||||
# Create FIFOs for IPC
|
||||
for fifo in (cmd_fifo, resp_fifo):
|
||||
try:
|
||||
if os.path.exists(fifo):
|
||||
os.unlink(fifo)
|
||||
os.mkfifo(fifo, 0o600)
|
||||
except OSError as exc:
|
||||
logger.error("Cannot create BLE FIFO %s: %s", fifo, exc)
|
||||
return
|
||||
|
||||
logger.debug("BLE GATT listener ready on FIFOs")
|
||||
|
||||
while self._advertising and self._running:
|
||||
try:
|
||||
# Non-blocking read from command FIFO
|
||||
fd = os.open(cmd_fifo, os.O_RDONLY | os.O_NONBLOCK)
|
||||
try:
|
||||
data = os.read(fd, 4096)
|
||||
finally:
|
||||
os.close(fd)
|
||||
|
||||
if data:
|
||||
cmd_str = data.decode(errors='replace').strip()
|
||||
response = self._process_command(cmd_str)
|
||||
self._last_response = response
|
||||
|
||||
# Write response to response FIFO
|
||||
try:
|
||||
fd = os.open(resp_fifo, os.O_WRONLY | os.O_NONBLOCK)
|
||||
try:
|
||||
os.write(fd, response.encode()[:BLE_MTU])
|
||||
finally:
|
||||
os.close(fd)
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
except OSError:
|
||||
pass # No data ready
|
||||
|
||||
time.sleep(0.5)
|
||||
|
||||
# Cleanup FIFOs
|
||||
for fifo in (cmd_fifo, resp_fifo):
|
||||
try:
|
||||
os.unlink(fifo)
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
def _process_command(self, raw_cmd: str) -> str:
|
||||
"""Process an authenticated BLE command.
|
||||
|
||||
Command format: <hmac_hex>:<command>
|
||||
HMAC is computed over: nonce + command using the PSK.
|
||||
"""
|
||||
if ":" not in raw_cmd:
|
||||
return '{"error": "invalid format"}'
|
||||
|
||||
provided_hmac, command = raw_cmd.split(":", 1)
|
||||
command = command.strip().lower()
|
||||
|
||||
# Verify HMAC
|
||||
if not self._verify_hmac(provided_hmac, command):
|
||||
logger.warning("BLE auth failed for command: %s", command)
|
||||
return '{"error": "auth_failed"}'
|
||||
|
||||
# Rotate nonce after successful auth
|
||||
old_nonce = self._nonce
|
||||
self._nonce = secrets.token_hex(16)
|
||||
|
||||
# Execute command
|
||||
if command == "status":
|
||||
return self._cmd_status()
|
||||
elif command == "kill":
|
||||
return self._cmd_kill()
|
||||
elif command == "reboot":
|
||||
return self._cmd_reboot()
|
||||
elif command == "get_ip":
|
||||
return self._cmd_get_ip()
|
||||
elif command == "get_nonce":
|
||||
# Return new nonce for next command (unauthenticated)
|
||||
return json.dumps({"nonce": self._nonce})
|
||||
else:
|
||||
return json.dumps({"error": "unknown_command", "commands": [
|
||||
"status", "kill", "reboot", "get_ip", "get_nonce"
|
||||
]})
|
||||
|
||||
def _verify_hmac(self, provided_hmac: str, command: str) -> bool:
|
||||
"""Verify HMAC authentication for a BLE command."""
|
||||
if not self._psk or not self._nonce:
|
||||
return False
|
||||
|
||||
message = f"{self._nonce}:{command}".encode()
|
||||
expected = hmac.new(
|
||||
self._psk.encode(), message, hashlib.sha256
|
||||
).hexdigest()
|
||||
|
||||
return hmac.compare_digest(provided_hmac.lower(), expected.lower())
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# BLE command handlers
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
def _cmd_status(self) -> str:
|
||||
"""Return implant status summary."""
|
||||
status_data = {
|
||||
"ok": True,
|
||||
"uptime": time.time() - self._start_time if self._start_time else 0,
|
||||
"nonce": self._nonce,
|
||||
}
|
||||
|
||||
# Get connectivity status from state
|
||||
all_status = self.state.get_all_module_status()
|
||||
connectivity_modules = {}
|
||||
for name, info in all_status.items():
|
||||
if name in ("wireguard", "tailscale", "cellular_backup"):
|
||||
connectivity_modules[name] = info.get("status", "unknown")
|
||||
status_data["connectivity"] = connectivity_modules
|
||||
|
||||
return json.dumps(status_data)
|
||||
|
||||
def _cmd_kill(self) -> str:
|
||||
"""Trigger kill switch via event bus."""
|
||||
logger.warning("Kill switch triggered via BLE emergency")
|
||||
self.bus.emit("KILL_SWITCH",
|
||||
{"source": "ble_emergency", "reason": "operator_ble_command"},
|
||||
source_module=self.name)
|
||||
return json.dumps({"ok": True, "action": "kill_switch_triggered"})
|
||||
|
||||
def _cmd_reboot(self) -> str:
|
||||
"""Trigger system reboot."""
|
||||
logger.warning("Reboot triggered via BLE emergency")
|
||||
|
||||
def _delayed_reboot():
|
||||
time.sleep(2.0)
|
||||
try:
|
||||
subprocess.run(["reboot"], timeout=5)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
threading.Thread(target=_delayed_reboot, daemon=True).start()
|
||||
return json.dumps({"ok": True, "action": "reboot_in_2s"})
|
||||
|
||||
def _cmd_get_ip(self) -> str:
|
||||
"""Return all IP addresses on the system."""
|
||||
ips = {}
|
||||
try:
|
||||
result = subprocess.run(
|
||||
["ip", "-4", "-o", "addr", "show"],
|
||||
capture_output=True, text=True, timeout=5,
|
||||
)
|
||||
if result.returncode == 0:
|
||||
import re
|
||||
for line in result.stdout.strip().splitlines():
|
||||
match = re.search(r'(\S+)\s+inet\s+(\d+\.\d+\.\d+\.\d+)', line)
|
||||
if match:
|
||||
iface = match.group(1)
|
||||
ip = match.group(2)
|
||||
if iface != "lo":
|
||||
ips[iface] = ip
|
||||
except (subprocess.TimeoutExpired, FileNotFoundError):
|
||||
pass
|
||||
|
||||
return json.dumps({"ips": ips, "nonce": self._nonce})
|
||||
Reference in New Issue
Block a user