mirror of
https://github.com/infinition/Bjorn.git
synced 2026-03-16 01:01:58 +00:00
Add Loki and Sentinel utility classes for web API endpoints
- Implemented LokiUtils class with GET and POST endpoints for managing scripts, jobs, and payloads. - Added SentinelUtils class with GET and POST endpoints for managing events, rules, devices, and notifications. - Both classes include error handling and JSON response formatting.
This commit is contained in:
375
loki/__init__.py
Normal file
375
loki/__init__.py
Normal file
@@ -0,0 +1,375 @@
|
||||
"""
|
||||
Loki — HID Attack Engine for Bjorn.
|
||||
|
||||
Manages USB HID gadget lifecycle, script execution, and job tracking.
|
||||
Named after the Norse trickster god.
|
||||
|
||||
Loki is the 5th exclusive operation mode (alongside MANUAL, AUTO, AI, BIFROST).
|
||||
When active, the orchestrator stops and the Pi acts as a keyboard/mouse
|
||||
to the connected host via /dev/hidg0 (keyboard) and /dev/hidg1 (mouse).
|
||||
|
||||
HID GADGET STRATEGY:
|
||||
The HID functions (keyboard + mouse) are created ONCE at boot time alongside
|
||||
RNDIS networking by the usb-gadget.sh script. This avoids the impossible task
|
||||
of hot-adding HID functions to a running composite gadget (UDC rebind fails
|
||||
with EIO when RNDIS is active).
|
||||
|
||||
LokiEngine simply opens/closes the /dev/hidg0 and /dev/hidg1 device files.
|
||||
If /dev/hidg0 doesn't exist, the user needs to run the setup once and reboot.
|
||||
"""
|
||||
import os
|
||||
import time
|
||||
import subprocess
|
||||
import logging
|
||||
from threading import Event
|
||||
|
||||
from logger import Logger
|
||||
|
||||
logger = Logger(name="loki", level=logging.DEBUG)
|
||||
|
||||
# USB HID report descriptors — EXACT byte-for-byte copies from P4wnP1_aloa
|
||||
# Source: P4wnP1_aloa-master/service/SubSysUSB.go lines 54-70
|
||||
#
|
||||
# These are written to the gadget at boot time by usb-gadget.sh.
|
||||
# Kept here for reference and for the install_hid_gadget() method.
|
||||
#
|
||||
_KBD_REPORT_DESC = bytes([
|
||||
0x05, 0x01, 0x09, 0x06, 0xa1, 0x01, 0x05, 0x07,
|
||||
0x19, 0xe0, 0x29, 0xe7, 0x15, 0x00, 0x25, 0x01,
|
||||
0x75, 0x01, 0x95, 0x08, 0x81, 0x02, 0x95, 0x01,
|
||||
0x75, 0x08, 0x81, 0x03, 0x95, 0x05, 0x75, 0x01,
|
||||
0x05, 0x08, 0x19, 0x01, 0x29, 0x05, 0x91, 0x02,
|
||||
0x95, 0x01, 0x75, 0x03, 0x91, 0x03, 0x95, 0x06,
|
||||
0x75, 0x08, 0x15, 0x00, 0x25, 0x65, 0x05, 0x07,
|
||||
0x19, 0x00, 0x29, 0x65, 0x81, 0x00, 0xc0,
|
||||
]) # 63 bytes, report_length=8
|
||||
|
||||
_MOUSE_REPORT_DESC = bytes([
|
||||
0x05, 0x01, 0x09, 0x02, 0xa1, 0x01, 0x09, 0x01,
|
||||
0xa1, 0x00, 0x85, 0x01, 0x05, 0x09, 0x19, 0x01,
|
||||
0x29, 0x03, 0x15, 0x00, 0x25, 0x01, 0x95, 0x03,
|
||||
0x75, 0x01, 0x81, 0x02, 0x95, 0x01, 0x75, 0x05,
|
||||
0x81, 0x03, 0x05, 0x01, 0x09, 0x30, 0x09, 0x31,
|
||||
0x15, 0x81, 0x25, 0x7f, 0x75, 0x08, 0x95, 0x02,
|
||||
0x81, 0x06, 0x95, 0x02, 0x75, 0x08, 0x81, 0x01,
|
||||
0xc0, 0xc0, 0x05, 0x01, 0x09, 0x02, 0xa1, 0x01,
|
||||
0x09, 0x01, 0xa1, 0x00, 0x85, 0x02, 0x05, 0x09,
|
||||
0x19, 0x01, 0x29, 0x03, 0x15, 0x00, 0x25, 0x01,
|
||||
0x95, 0x03, 0x75, 0x01, 0x81, 0x02, 0x95, 0x01,
|
||||
0x75, 0x05, 0x81, 0x01, 0x05, 0x01, 0x09, 0x30,
|
||||
0x09, 0x31, 0x15, 0x00, 0x26, 0xff, 0x7f, 0x95,
|
||||
0x02, 0x75, 0x10, 0x81, 0x02, 0xc0, 0xc0,
|
||||
]) # 111 bytes, report_length=6
|
||||
|
||||
# The boot script that creates RNDIS + HID functions at startup.
|
||||
# This replaces /usr/local/bin/usb-gadget.sh
|
||||
_USB_GADGET_SCRIPT = '''#!/bin/bash
|
||||
# usb-gadget.sh — USB composite gadget: RNDIS networking + HID (keyboard/mouse)
|
||||
# Auto-generated by Bjorn Loki. Do not edit manually.
|
||||
|
||||
modprobe libcomposite
|
||||
cd /sys/kernel/config/usb_gadget/
|
||||
mkdir -p g1
|
||||
cd g1
|
||||
|
||||
echo 0x1d6b > idVendor
|
||||
echo 0x0104 > idProduct
|
||||
echo 0x0100 > bcdDevice
|
||||
echo 0x0200 > bcdUSB
|
||||
|
||||
mkdir -p strings/0x409
|
||||
echo "fedcba9876543210" > strings/0x409/serialnumber
|
||||
echo "Raspberry Pi" > strings/0x409/manufacturer
|
||||
echo "Pi Zero USB" > strings/0x409/product
|
||||
|
||||
mkdir -p configs/c.1/strings/0x409
|
||||
echo "Config 1: RNDIS + HID" > configs/c.1/strings/0x409/configuration
|
||||
echo 250 > configs/c.1/MaxPower
|
||||
|
||||
# ── RNDIS networking function ──
|
||||
mkdir -p functions/rndis.usb0
|
||||
[ -L configs/c.1/rndis.usb0 ] && rm configs/c.1/rndis.usb0
|
||||
ln -s functions/rndis.usb0 configs/c.1/
|
||||
|
||||
# ── HID functions (keyboard + mouse) ──
|
||||
# Use python3 to write binary report descriptors (bash printf can't handle null bytes)
|
||||
python3 - <<'PYEOF'
|
||||
import os, sys
|
||||
|
||||
G = "/sys/kernel/config/usb_gadget/g1"
|
||||
|
||||
# Keyboard: P4wnP1 exact boot keyboard descriptor (63 bytes)
|
||||
KBD_DESC = bytes([
|
||||
0x05,0x01,0x09,0x06,0xa1,0x01,0x05,0x07,
|
||||
0x19,0xe0,0x29,0xe7,0x15,0x00,0x25,0x01,
|
||||
0x75,0x01,0x95,0x08,0x81,0x02,0x95,0x01,
|
||||
0x75,0x08,0x81,0x03,0x95,0x05,0x75,0x01,
|
||||
0x05,0x08,0x19,0x01,0x29,0x05,0x91,0x02,
|
||||
0x95,0x01,0x75,0x03,0x91,0x03,0x95,0x06,
|
||||
0x75,0x08,0x15,0x00,0x25,0x65,0x05,0x07,
|
||||
0x19,0x00,0x29,0x65,0x81,0x00,0xc0,
|
||||
])
|
||||
|
||||
# Mouse: P4wnP1 dual-mode (relative + absolute) descriptor (111 bytes)
|
||||
MOUSE_DESC = bytes([
|
||||
0x05,0x01,0x09,0x02,0xa1,0x01,0x09,0x01,
|
||||
0xa1,0x00,0x85,0x01,0x05,0x09,0x19,0x01,
|
||||
0x29,0x03,0x15,0x00,0x25,0x01,0x95,0x03,
|
||||
0x75,0x01,0x81,0x02,0x95,0x01,0x75,0x05,
|
||||
0x81,0x03,0x05,0x01,0x09,0x30,0x09,0x31,
|
||||
0x15,0x81,0x25,0x7f,0x75,0x08,0x95,0x02,
|
||||
0x81,0x06,0x95,0x02,0x75,0x08,0x81,0x01,
|
||||
0xc0,0xc0,0x05,0x01,0x09,0x02,0xa1,0x01,
|
||||
0x09,0x01,0xa1,0x00,0x85,0x02,0x05,0x09,
|
||||
0x19,0x01,0x29,0x03,0x15,0x00,0x25,0x01,
|
||||
0x95,0x03,0x75,0x01,0x81,0x02,0x95,0x01,
|
||||
0x75,0x05,0x81,0x01,0x05,0x01,0x09,0x30,
|
||||
0x09,0x31,0x15,0x00,0x26,0xff,0x7f,0x95,
|
||||
0x02,0x75,0x10,0x81,0x02,0xc0,0xc0,
|
||||
])
|
||||
|
||||
def w(path, content):
|
||||
with open(path, "w") as f:
|
||||
f.write(content)
|
||||
|
||||
def wb(path, data):
|
||||
with open(path, "wb") as f:
|
||||
f.write(data)
|
||||
|
||||
try:
|
||||
# Keyboard (hid.usb0)
|
||||
kbd = G + "/functions/hid.usb0"
|
||||
os.makedirs(kbd, exist_ok=True)
|
||||
w(kbd + "/protocol", "1")
|
||||
w(kbd + "/subclass", "1")
|
||||
w(kbd + "/report_length", "8")
|
||||
wb(kbd + "/report_desc", KBD_DESC)
|
||||
|
||||
# Mouse (hid.usb1)
|
||||
mouse = G + "/functions/hid.usb1"
|
||||
os.makedirs(mouse, exist_ok=True)
|
||||
w(mouse + "/protocol", "2")
|
||||
w(mouse + "/subclass", "1")
|
||||
w(mouse + "/report_length", "6")
|
||||
wb(mouse + "/report_desc", MOUSE_DESC)
|
||||
|
||||
print(f"HID functions created: keyboard ({len(KBD_DESC)}B) + mouse ({len(MOUSE_DESC)}B)")
|
||||
except Exception as e:
|
||||
print(f"WARNING: HID setup failed (non-fatal): {e}", file=sys.stderr)
|
||||
sys.exit(0) # Don't block RNDIS setup
|
||||
PYEOF
|
||||
|
||||
# Symlink HID into config (non-fatal)
|
||||
for func in hid.usb0 hid.usb1; do
|
||||
[ -L "configs/c.1/$func" ] && rm "configs/c.1/$func"
|
||||
if [ -d "functions/$func" ]; then
|
||||
ln -s "functions/$func" "configs/c.1/" 2>/dev/null || true
|
||||
fi
|
||||
done
|
||||
|
||||
# ── Bind UDC ──
|
||||
max_retries=10
|
||||
retry_count=0
|
||||
while ! ls /sys/class/udc > UDC 2>/dev/null; do
|
||||
if [ $retry_count -ge $max_retries ]; then
|
||||
echo "Error: Device or resource busy after $max_retries attempts."
|
||||
exit 1
|
||||
fi
|
||||
retry_count=$((retry_count + 1))
|
||||
sleep 1
|
||||
done
|
||||
|
||||
UDC_NAME=$(ls /sys/class/udc)
|
||||
echo "$UDC_NAME" > UDC
|
||||
echo "Assigned UDC: $UDC_NAME (RNDIS + HID keyboard + HID mouse)"
|
||||
|
||||
# ── Configure network ──
|
||||
if ! ip addr show usb0 2>/dev/null | grep -q "172.20.2.1"; then
|
||||
ifconfig usb0 172.20.2.1 netmask 255.255.255.0 2>/dev/null || true
|
||||
echo "Configured usb0 with IP 172.20.2.1"
|
||||
else
|
||||
echo "Interface usb0 already configured."
|
||||
fi
|
||||
'''
|
||||
|
||||
_GADGET_SCRIPT_PATH = "/usr/local/bin/usb-gadget.sh"
|
||||
|
||||
|
||||
class LokiEngine:
|
||||
"""HID attack engine — manages script execution and job tracking.
|
||||
|
||||
The USB HID gadget (keyboard + mouse) is set up at boot time by
|
||||
usb-gadget.sh. This engine simply opens /dev/hidg0 and /dev/hidg1.
|
||||
"""
|
||||
|
||||
def __init__(self, shared_data):
|
||||
self.shared_data = shared_data
|
||||
self._stop_event = Event()
|
||||
self._running = False
|
||||
self._gadget_ready = False
|
||||
|
||||
# Sub-components (lazy init)
|
||||
self._hid = None
|
||||
self._jobs = None
|
||||
|
||||
# ── Properties ─────────────────────────────────────────────
|
||||
|
||||
@property
|
||||
def hid_controller(self):
|
||||
if self._hid is None:
|
||||
from loki.hid_controller import HIDController
|
||||
self._hid = HIDController()
|
||||
return self._hid
|
||||
|
||||
@property
|
||||
def job_manager(self):
|
||||
if self._jobs is None:
|
||||
from loki.jobs import LokiJobManager
|
||||
self._jobs = LokiJobManager(self)
|
||||
return self._jobs
|
||||
|
||||
# ── Start / Stop ───────────────────────────────────────────
|
||||
|
||||
def start(self):
|
||||
"""Start Loki engine: open HID devices, apply config."""
|
||||
if self._running:
|
||||
logger.warning("Loki already running")
|
||||
return
|
||||
|
||||
logger.info("Starting Loki engine...")
|
||||
self._stop_event.clear()
|
||||
|
||||
# Check if HID gadget is available (set up at boot)
|
||||
if not os.path.exists("/dev/hidg0"):
|
||||
logger.error(
|
||||
"/dev/hidg0 not found — HID gadget not configured at boot. "
|
||||
"Run install_hid_gadget() from the Loki API and reboot."
|
||||
)
|
||||
self._gadget_ready = False
|
||||
return
|
||||
|
||||
self._gadget_ready = True
|
||||
|
||||
# Open HID devices
|
||||
try:
|
||||
self.hid_controller.open()
|
||||
except Exception as e:
|
||||
logger.error("HID device open failed: %s", e)
|
||||
self._gadget_ready = False
|
||||
return
|
||||
|
||||
# Apply config
|
||||
layout = self.shared_data.config.get("loki_default_layout", "us")
|
||||
self.hid_controller.set_layout(layout)
|
||||
speed_min = self.shared_data.config.get("loki_typing_speed_min", 0)
|
||||
speed_max = self.shared_data.config.get("loki_typing_speed_max", 0)
|
||||
self.hid_controller.set_typing_speed(speed_min, speed_max)
|
||||
|
||||
self._running = True
|
||||
logger.info("Loki engine started (HID devices open)")
|
||||
|
||||
# Auto-run script if configured
|
||||
auto_run = self.shared_data.config.get("loki_auto_run", "")
|
||||
if auto_run:
|
||||
self._auto_run_script(auto_run)
|
||||
|
||||
def stop(self):
|
||||
"""Stop Loki engine: cancel jobs, close devices."""
|
||||
if not self._running:
|
||||
return
|
||||
|
||||
logger.info("Stopping Loki engine...")
|
||||
self._stop_event.set()
|
||||
|
||||
# Cancel all running jobs
|
||||
if self._jobs:
|
||||
for job in self._jobs.get_all_jobs():
|
||||
if job["status"] == "running":
|
||||
self._jobs.cancel_job(job["id"])
|
||||
|
||||
# Close HID devices (don't remove gadget — it persists)
|
||||
if self._hid:
|
||||
self._hid.close()
|
||||
|
||||
self._running = False
|
||||
self._gadget_ready = False
|
||||
logger.info("Loki engine stopped")
|
||||
|
||||
def get_status(self) -> dict:
|
||||
"""Return current engine status for the API."""
|
||||
hidg0_exists = os.path.exists("/dev/hidg0")
|
||||
return {
|
||||
"enabled": self.shared_data.config.get("loki_enabled", False),
|
||||
"running": self._running,
|
||||
"gadget_ready": self._gadget_ready,
|
||||
"gadget_installed": hidg0_exists,
|
||||
"layout": self.shared_data.config.get("loki_default_layout", "us"),
|
||||
"jobs_running": self.job_manager.running_count if self._jobs else 0,
|
||||
"jobs_total": len(self._jobs.get_all_jobs()) if self._jobs else 0,
|
||||
}
|
||||
|
||||
# ── Job API (delegated to JobManager) ──────────────────────
|
||||
|
||||
def submit_job(self, script_name: str, script_content: str) -> str:
|
||||
"""Submit a HIDScript for execution. Returns job_id."""
|
||||
if not self._running:
|
||||
raise RuntimeError("Loki engine not running")
|
||||
if not self._gadget_ready:
|
||||
raise RuntimeError("HID gadget not ready")
|
||||
return self.job_manager.create_job(script_name, script_content)
|
||||
|
||||
def cancel_job(self, job_id: str) -> bool:
|
||||
return self.job_manager.cancel_job(job_id)
|
||||
|
||||
def get_jobs(self) -> list:
|
||||
return self.job_manager.get_all_jobs()
|
||||
|
||||
# ── HID Gadget Installation ────────────────────────────────
|
||||
|
||||
@staticmethod
|
||||
def is_gadget_installed() -> bool:
|
||||
"""Check if the HID gadget is available."""
|
||||
return os.path.exists("/dev/hidg0")
|
||||
|
||||
@staticmethod
|
||||
def install_hid_gadget() -> dict:
|
||||
"""Install/update the USB gadget boot script to include HID functions.
|
||||
|
||||
Writes the new usb-gadget.sh that creates RNDIS + HID at boot.
|
||||
Returns status dict. Requires a reboot to take effect.
|
||||
"""
|
||||
try:
|
||||
# Write the new gadget script
|
||||
with open(_GADGET_SCRIPT_PATH, "w") as f:
|
||||
f.write(_USB_GADGET_SCRIPT)
|
||||
os.chmod(_GADGET_SCRIPT_PATH, 0o755)
|
||||
logger.info("USB gadget script updated at %s", _GADGET_SCRIPT_PATH)
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"message": "USB gadget script updated with HID support. Reboot required.",
|
||||
"reboot_required": True,
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.error("Failed to install HID gadget script: %s", e)
|
||||
return {
|
||||
"success": False,
|
||||
"message": f"Installation failed: {e}",
|
||||
"reboot_required": False,
|
||||
}
|
||||
|
||||
# ── Auto-run ───────────────────────────────────────────────
|
||||
|
||||
def _auto_run_script(self, script_name: str):
|
||||
"""Auto-run a script by name from the database."""
|
||||
try:
|
||||
db = self.shared_data.db
|
||||
row = db.query_one(
|
||||
"SELECT content FROM loki_scripts WHERE name = ?", (script_name,)
|
||||
)
|
||||
if row and row.get("content"):
|
||||
self.submit_job(script_name, row["content"])
|
||||
logger.info("Auto-running script: %s", script_name)
|
||||
except Exception as e:
|
||||
logger.error("Auto-run failed for '%s': %s", script_name, e)
|
||||
Reference in New Issue
Block a user