Files
hh-bt-controller/sniffer/analyse_jadx.py
T

236 lines
9.2 KiB
Python

"""
analyse_jadx.py - Search jadx-decompiled Java source for KAIYU BLE protocol.
After running decompile_kaiyu.ps1, this searches the Java output for:
- UUID strings and integer constants
- BluetoothGatt write calls with context
- Byte arrays that look like LED commands (7E / 56 families)
- Service/characteristic class definitions
Usage:
python sniffer/analyse_jadx.py --src D:\\Claude\\HH-BT-Controller\\decompile\\kaiyu_src
python sniffer/analyse_jadx.py (auto-finds output dir)
python sniffer/analyse_jadx.py --out results.json
"""
import os
import re
import sys
import json
import argparse
from pathlib import Path
UUID_RE = re.compile(r'["\']([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12})["\']')
FROM_STR_RE = re.compile(r'fromString\s*\(\s*["\']([0-9a-fA-F\-]{36})["\']')
SHORT_UUID_RE = re.compile(r'0x([Ff]{2}[0-9a-fA-F]{2})|"(0000[0-9a-fA-F]{4})')
BYTE_ARR_RE = re.compile(r'(?:new\s+byte\s*\[\s*\]\s*\{|=\s*\{)\s*((?:\(byte\)\s*)?(?:0x[0-9a-fA-F]{2}|-?\d+)(?:\s*,\s*(?:\(byte\)\s*)?(?:0x[0-9a-fA-F]{2}|-?\d+)){3,})\s*\}')
WRITE_RE = re.compile(r'.{0,60}(?:writeCharacteristic|writeGattChar|setValue|write)\s*\(.{0,100}', re.IGNORECASE)
BLE_CLASS_RE = re.compile(r'(setColor|setRGB|setBrightness|setEffect|turnOn|turnOff|sendCommand|writeCmd|sendData|controlLight|BluetoothGatt|BluetoothLeService|BleManager|GattCallback)', re.IGNORECASE)
KNOWN_UUIDS = {
"0000ffe0-0000-1000-8000-00805f9b34fb": "LEDBLE service",
"0000ffe1-0000-1000-8000-00805f9b34fb": "LEDBLE WRITE CHARACTERISTIC",
"0000fff0-0000-1000-8000-00805f9b34fb": "ELK-BLEDOM service",
"0000fff3-0000-1000-8000-00805f9b34fb": "ELK-BLEDOM WRITE CHARACTERISTIC",
"0000ffd5-0000-1000-8000-00805f9b34fb": "LED service",
"0000ffd9-0000-1000-8000-00805f9b34fb": "LED WRITE CHARACTERISTIC",
"0000ffb0-0000-1000-8000-00805f9b34fb": "LED service (alt)",
"0000ffb2-0000-1000-8000-00805f9b34fb": "LED WRITE (alt)",
}
CMD_PREFIXES = {
bytes.fromhex("7e0005"): "7E RGB colour",
bytes.fromhex("7e0004"): "7E power",
bytes.fromhex("7e0001"): "7E brightness",
bytes.fromhex("cc2333"): "56 power ON",
bytes.fromhex("cc2433"): "56 power OFF",
bytes.fromhex("56ff00"): "56 RGB",
}
def decode_bytes(data: bytes) -> str:
for prefix, label in CMD_PREFIXES.items():
if data[:len(prefix)] == prefix:
return label
if data and data[0] == 0x7e and len(data) > 1 and data[-1] == 0xef:
return f"7E protocol cmd=0x{data[2]:02x}"
return ""
def scan_file(path: Path, base: Path):
rel = str(path.relative_to(base))
try:
text = path.read_text(encoding="utf-8", errors="ignore")
except Exception:
return []
findings = []
for i, line in enumerate(text.splitlines(), 1):
s = line.strip()
if not s or s.startswith("//"):
continue
for m in UUID_RE.finditer(line):
findings.append(("uuid", rel, i, m.group(1).lower(), s[:120]))
for m in FROM_STR_RE.finditer(line):
findings.append(("uuid", rel, i, m.group(1).lower(), s[:120]))
for m in SHORT_UUID_RE.finditer(line):
val = (m.group(1) or m.group(2) or "").lower()
if val:
findings.append(("short_uuid", rel, i, val, s[:120]))
for m in BYTE_ARR_RE.finditer(line):
tokens = re.findall(r'0x[0-9a-fA-F]+|-?\d+', m.group(1))
bvals = []
for t in tokens:
try:
bvals.append(int(t, 16) if t.startswith("0x") else int(t))
except ValueError:
pass
if bvals:
bdata = bytes(b & 0xff for b in bvals)
decoded = decode_bytes(bdata)
hex_str = bdata.hex()
findings.append(("byte_array", rel, i, hex_str, decoded or s[:80]))
if WRITE_RE.search(line):
findings.append(("write_call", rel, i, "", s[:120]))
m2 = BLE_CLASS_RE.search(line)
if m2:
findings.append(("ble_ref", rel, i, m2.group(1), s[:120]))
return findings
def main():
parser = argparse.ArgumentParser()
parser.add_argument("--src", help="jadx output directory")
parser.add_argument("--out", help="Save JSON results")
args = parser.parse_args()
candidates = [
Path(r"D:\Claude\HH-BT-Controller\decompile\kaiyu_src"),
Path(r"D:\HereMyHope\hh-bt-controller\decompile\kaiyu_src"),
Path("decompile/kaiyu_src"),
]
src = Path(args.src) if args.src else next((c for c in candidates if c.exists()), None)
if not src or not src.exists():
print("[ERROR] No decompiled source found.")
print(" Run: powershell -ExecutionPolicy Bypass -File sniffer/decompile_kaiyu.ps1")
sys.exit(1)
java_files = list(src.rglob("*.java"))
print(f"Source : {src}")
print(f"Files : {len(java_files)} Java files")
print()
all_findings = []
for jf in java_files:
all_findings.extend(scan_file(jf, src))
uuids = [f for f in all_findings if f[0] == "uuid"]
short_uuids = [f for f in all_findings if f[0] == "short_uuid"]
byte_arrays = [f for f in all_findings if f[0] == "byte_array"]
write_calls = [f for f in all_findings if f[0] == "write_call"]
ble_refs = [f for f in all_findings if f[0] == "ble_ref"]
# UUIDs
print("=" * 70)
print(f" FULL UUIDs ({len(uuids)} hits)")
print("=" * 70)
seen_uuids: dict[str, list] = {}
for f in uuids:
seen_uuids.setdefault(f[3], []).append(f)
if seen_uuids:
for uuid, hits in sorted(seen_uuids.items(), key=lambda x: -len(x[1])):
label = KNOWN_UUIDS.get(uuid, "")
tag = " <-- *** " + label if label else ""
print(f" {uuid} (x{len(hits)}){tag}")
for h in hits[:2]:
print(f" {h[1]}:{h[2]} {h[4][:80]}")
else:
print(" None found as string literals.")
# Short UUIDs
print(f"\n{'─'*70}")
print(f" SHORT UUID constants ({len(short_uuids)} hits)")
print(f"{'─'*70}")
seen_short: dict[str, list] = {}
for f in short_uuids:
seen_short.setdefault(f[3], []).append(f)
for val, hits in sorted(seen_short.items(), key=lambda x: -len(x[1])):
full = f"0000{val}-0000-1000-8000-00805f9b34fb"
label = KNOWN_UUIDS.get(full, "")
tag = " <-- " + label if label else ""
print(f" 0x{val} -> {full}{tag} (x{len(hits)})")
for h in hits[:2]:
print(f" {h[1]}:{h[2]} {h[4][:80]}")
# Byte arrays
print(f"\n{'─'*70}")
print(f" BYTE ARRAYS ({len(byte_arrays)} hits — LED commands may be here)")
print(f"{'─'*70}")
for f in byte_arrays[:25]:
tag = f" -> {f[4]}" if f[4] and not f[4].startswith("0") else ""
print(f" {f[1]}:{f[2]} {f[3][:60]}{tag}")
# Write calls
print(f"\n{'─'*70}")
print(f" GATT WRITE CALLS ({len(write_calls)} hits)")
print(f"{'─'*70}")
write_files: dict[str, list] = {}
for f in write_calls:
write_files.setdefault(f[1], []).append(f)
for fname, hits in list(write_files.items())[:8]:
print(f" {fname} ({len(hits)} calls)")
for h in hits[:3]:
print(f" line {h[2]}: {h[4][:90]}")
# BLE class refs
print(f"\n{'─'*70}")
print(f" BLE CLASS/METHOD REFS ({len(ble_refs)} hits)")
print(f"{'─'*70}")
ref_files: dict[str, set] = {}
for f in ble_refs:
ref_files.setdefault(f[3], set()).add(f[1])
for method, files in sorted(ref_files.items()):
print(f" {method} ({len(files)} files)")
for fn in list(files)[:3]:
print(f" {fn}")
# Final recommendation
print(f"\n{'='*70}")
print(f" RESULT")
print(f"{'='*70}")
if seen_uuids:
print("\n UUIDs found! Check the ones marked <-- above.")
print(" Update WRITE_CHARACTERISTIC in controller/config.py.")
elif seen_short:
print("\n Short UUID constants found. Likely write characteristic candidates:")
for val in seen_short:
print(f" 0000{val}-0000-1000-8000-00805f9b34fb")
print("\n Try each as WRITE_CHARACTERISTIC and run:")
print(" python scanner/ble_scanner.py --probe")
elif write_files:
print(f"\n No UUIDs but found GATT write calls in {len(write_files)} file(s).")
print(" Open these in VS Code and look for the characteristic being written to.")
print(f" code \"{src}\"")
else:
print("\n Nothing found in Java source — UUIDs are in native .so libraries.")
print(" Run: python sniffer/search_so_libs.py")
print(" Or try direct probe: python scanner/ble_scanner.py --probe")
if args.out:
with open(args.out, "w") as f_out:
json.dump({
"uuids": {u: [{"file": h[1], "line": h[2]} for h in hits]
for u, hits in seen_uuids.items()},
"short_uuids": list(seen_short.keys()),
"byte_arrays": [{"file": f[1], "line": f[2], "hex": f[3], "decoded": f[4]}
for f in byte_arrays],
"write_call_files": list(write_files.keys()),
}, f_out, indent=2)
print(f"\n Saved: {args.out}")
if __name__ == "__main__":
main()