Add jadx output analyser - searches decompiled Java for BLE UUIDs and protocol
This commit is contained in:
@@ -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()
|
||||
Reference in New Issue
Block a user