diff --git a/sniffer/analyse_jadx.py b/sniffer/analyse_jadx.py new file mode 100644 index 0000000..fd831e6 --- /dev/null +++ b/sniffer/analyse_jadx.py @@ -0,0 +1,235 @@ +""" +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()