Initial commit — SetecSuite Camera MITM Framework
Original tooling from the Camhak research project (camera teardown of a
rebranded UBIA / Javiscam IP camera). PyQt6 GUI on top of a curses TUI on
top of a service controller; per-service start/stop, intruder detection,
protocol fingerprinting, OAM HMAC signing, CVE verifiers, OTA bucket
probe, firmware fetcher, fuzzer, packet injection.
Tabs: Dashboard, Live Log, Intruders, Cloud API, Fuzzer, Inject, CVEs,
Config, Help. Real-time per-packet protocol detection, conntrack-based
original-destination lookup, log rotation at 1 GiB.
See SECURITY_PAPER.md for the full writeup, site/index.html for the
public report, README.md for usage. Run with:
sudo /usr/bin/python3 gui.py
Co-authored by Setec Labs.
This commit is contained in:
0
utils/__init__.py
Normal file
0
utils/__init__.py
Normal file
99
utils/log.py
Normal file
99
utils/log.py
Normal file
@@ -0,0 +1,99 @@
|
||||
"""Shared logging and hex formatting utilities"""
|
||||
|
||||
import os
|
||||
import threading
|
||||
from datetime import datetime
|
||||
from collections import deque
|
||||
|
||||
lock = threading.Lock()
|
||||
log_lines = deque(maxlen=2000)
|
||||
_logfile = None
|
||||
_logfile_path = None
|
||||
LOG_MAX_BYTES = 1024 * 1024 * 1024 # 1 GiB
|
||||
_log_rotate_lock = threading.Lock()
|
||||
|
||||
# Color codes for TUI
|
||||
C_NONE = 0
|
||||
C_ERROR = 1
|
||||
C_SUCCESS = 2
|
||||
C_INFO = 3
|
||||
C_TRAFFIC = 4
|
||||
C_IMPORTANT = 5
|
||||
|
||||
|
||||
def init_logfile(path):
|
||||
global _logfile, _logfile_path
|
||||
os.makedirs(os.path.dirname(path), exist_ok=True)
|
||||
_logfile_path = path
|
||||
_logfile = open(path, "a")
|
||||
|
||||
|
||||
def close_logfile():
|
||||
global _logfile
|
||||
if _logfile:
|
||||
_logfile.close()
|
||||
_logfile = None
|
||||
|
||||
|
||||
def _maybe_rotate():
|
||||
"""Rotate the active log file if it exceeds LOG_MAX_BYTES."""
|
||||
global _logfile
|
||||
if not _logfile or not _logfile_path:
|
||||
return
|
||||
try:
|
||||
size = os.fstat(_logfile.fileno()).st_size
|
||||
except OSError:
|
||||
return
|
||||
if size < LOG_MAX_BYTES:
|
||||
return
|
||||
with _log_rotate_lock:
|
||||
try:
|
||||
size = os.fstat(_logfile.fileno()).st_size
|
||||
if size < LOG_MAX_BYTES:
|
||||
return
|
||||
_logfile.close()
|
||||
ts = datetime.now().strftime("%Y%m%d_%H%M%S")
|
||||
os.rename(_logfile_path, f"{_logfile_path}.{ts}")
|
||||
_logfile = open(_logfile_path, "a")
|
||||
_logfile.write(f"[{datetime.now().strftime('%H:%M:%S')}] log rotated (>1GB)\n")
|
||||
_logfile.flush()
|
||||
except Exception as e:
|
||||
try:
|
||||
_logfile = open(_logfile_path, "a")
|
||||
except Exception:
|
||||
_logfile = None
|
||||
|
||||
|
||||
def log(msg, color=C_NONE):
|
||||
ts = datetime.now().strftime("%H:%M:%S")
|
||||
line = f"[{ts}] {msg}"
|
||||
with lock:
|
||||
log_lines.append((line, color))
|
||||
if _logfile:
|
||||
try:
|
||||
_logfile.write(line + "\n")
|
||||
_logfile.flush()
|
||||
except Exception:
|
||||
pass
|
||||
_maybe_rotate()
|
||||
|
||||
|
||||
def hexdump(data, max_bytes=128):
|
||||
lines = []
|
||||
for i in range(0, min(len(data), max_bytes), 16):
|
||||
chunk = data[i:i + 16]
|
||||
hx = " ".join(f"{b:02x}" for b in chunk)
|
||||
asc = "".join(chr(b) if 32 <= b < 127 else "." for b in chunk)
|
||||
lines.append(f" {i:04x} {hx:<48} {asc}")
|
||||
if len(data) > max_bytes:
|
||||
lines.append(f" ... ({len(data)} bytes total)")
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
def save_raw(log_dir, name, data):
|
||||
import time
|
||||
os.makedirs(log_dir, exist_ok=True)
|
||||
path = f"{log_dir}/{name}_{int(time.time())}.bin"
|
||||
with open(path, "wb") as f:
|
||||
f.write(data)
|
||||
return path
|
||||
94
utils/proto.py
Normal file
94
utils/proto.py
Normal file
@@ -0,0 +1,94 @@
|
||||
"""
|
||||
Protocol fingerprinting from packet payload first bytes.
|
||||
Returns a short label like 'TLS', 'HTTP', 'IOTC', '?'.
|
||||
"""
|
||||
|
||||
import threading
|
||||
from collections import Counter
|
||||
|
||||
_seen = Counter()
|
||||
_lock = threading.Lock()
|
||||
|
||||
|
||||
def detect(data: bytes) -> str:
|
||||
if not data:
|
||||
return "?"
|
||||
n = len(data)
|
||||
b = data
|
||||
|
||||
# TLS: 0x16 (handshake) 0x03 0x0[0-4]
|
||||
if n >= 3 and b[0] == 0x16 and b[1] == 0x03 and b[2] <= 0x04:
|
||||
return "TLS"
|
||||
# TLS app data / alert / change_cipher_spec
|
||||
if n >= 3 and b[0] in (0x14, 0x15, 0x17) and b[1] == 0x03 and b[2] <= 0x04:
|
||||
return "TLS-DATA"
|
||||
|
||||
# HTTP request methods (ASCII)
|
||||
head = bytes(b[:8])
|
||||
for verb in (b"GET ", b"POST ", b"PUT ", b"HEAD ", b"DELETE ", b"OPTIONS", b"PATCH ", b"CONNECT"):
|
||||
if head.startswith(verb):
|
||||
return "HTTP"
|
||||
if head.startswith(b"HTTP/"):
|
||||
return "HTTP-RESP"
|
||||
|
||||
# RTSP
|
||||
if head.startswith(b"RTSP/") or head.startswith(b"OPTIONS rtsp") or head.startswith(b"DESCRIBE"):
|
||||
return "RTSP"
|
||||
|
||||
# SSH banner
|
||||
if head.startswith(b"SSH-"):
|
||||
return "SSH"
|
||||
|
||||
# FTP banner
|
||||
if head[:3] in (b"220", b"221", b"230"):
|
||||
return "FTP?"
|
||||
|
||||
# DNS — udp payload usually starts with 16-bit ID then flags 0x01 0x00 (query) or 0x81 0x80 (resp)
|
||||
if n >= 4 and b[2] in (0x01, 0x81) and b[3] in (0x00, 0x80, 0x20, 0xa0):
|
||||
return "DNS"
|
||||
|
||||
# NTP — first byte: LI(2)|VN(3)|Mode(3); common values 0x1b (client), 0x24 (server)
|
||||
if n >= 48 and b[0] in (0x1b, 0x23, 0x24, 0xdb, 0xe3):
|
||||
return "NTP?"
|
||||
|
||||
# ThroughTek Kalay IOTC/AVAPI — begins with 0xF1 0xD0 or 0xF1 0xE0 family
|
||||
if n >= 2 and b[0] == 0xF1 and b[1] in (0xD0, 0xE0, 0xF0, 0xC0, 0xA0, 0x10, 0x20, 0x30):
|
||||
return "IOTC"
|
||||
|
||||
# STUN — first byte 0x00 or 0x01, second byte 0x00/0x01/0x11, magic cookie 0x2112A442 at offset 4
|
||||
if n >= 8 and b[0] in (0x00, 0x01) and b[4:8] == b"\x21\x12\xa4\x42":
|
||||
return "STUN"
|
||||
|
||||
# mDNS multicast / SSDP
|
||||
if head.startswith(b"M-SEARCH") or head.startswith(b"NOTIFY *") or head.startswith(b"HTTP/1.1 200 OK"):
|
||||
return "SSDP"
|
||||
|
||||
# MQTT — first byte 0x10 (CONNECT), 0x20 CONNACK, 0x30 PUBLISH...
|
||||
if n >= 2 and (b[0] & 0xF0) in (0x10, 0x20, 0x30, 0x40, 0xC0, 0xD0, 0xE0) and b[0] != 0x00:
|
||||
# weak signal — only if remaining length is sane
|
||||
if 2 <= b[1] <= 200 and (b[0] & 0x0F) == 0:
|
||||
return "MQTT?"
|
||||
|
||||
return "?"
|
||||
|
||||
|
||||
def label_with_hex(data: bytes) -> str:
|
||||
"""Return 'PROTO[hex6]' for log lines."""
|
||||
p = detect(data)
|
||||
h = data[:6].hex() if data else ""
|
||||
return f"{p}[{h}]"
|
||||
|
||||
|
||||
def record(proto: str):
|
||||
with _lock:
|
||||
_seen[proto] += 1
|
||||
|
||||
|
||||
def seen_counts():
|
||||
with _lock:
|
||||
return dict(_seen)
|
||||
|
||||
|
||||
def reset():
|
||||
with _lock:
|
||||
_seen.clear()
|
||||
Reference in New Issue
Block a user