Add APK protocol extractor - pulls and analyses KAIYU APK from device

This commit is contained in:
2026-07-01 22:18:14 +10:00
parent 7781000d1a
commit 686bc7dcea
+217
View File
@@ -0,0 +1,217 @@
"""
extract_apk_protocol.py - Extract BLE UUIDs and protocol from KAIYU APK
Pulls the APK directly from the connected Android device via ADB, then
searches the DEX bytecode for UUID patterns and BLE write calls.
No jadx or external decompiler needed — pure Python + adb.
Usage:
python sniffer/extract_apk_protocol.py
"""
import subprocess
import zipfile
import re
import os
import sys
import json
ADB = r"D:\platform-tools\adb.exe"
PKG = "com.qunyu.kaiyu"
OUT_DIR = r"D:\Claude\bugreport\apk_extract"
APK_LOCAL = os.path.join(OUT_DIR, "kaiyu.apk")
RESULTS = os.path.join(OUT_DIR, "protocol_hints.json")
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}'
)
KNOWN_UUIDS = {
"0000ffe1-0000-1000-8000-00805f9b34fb": "LEDBLE/MagicLight write",
"0000fff3-0000-1000-8000-00805f9b34fb": "ELK-BLEDOM write",
"0000ffd9-0000-1000-8000-00805f9b34fb": "LED controller write",
"0000ffe9-0000-1000-8000-00805f9b34fb": "MagicLight older write",
"0000ffb2-0000-1000-8000-00805f9b34fb": "Colorific write",
"0000ffe0-0000-1000-8000-00805f9b34fb": "LEDBLE service",
"0000fff0-0000-1000-8000-00805f9b34fb": "ELK-BLEDOM service",
}
BLE_KEYWORDS = [
"writecharacteristic", "writegattchar", "blewrite", "writevalue",
"setcolor", "set_color", "setrgb", "rgb", "brightness",
"ffe1", "fff3", "ffd9", "ffe0", "fff0", "ffe9",
"7e00", "cc23", "cc24", "56ff",
"service_uuid", "characteristic_uuid", "gatt",
"BluetoothGattCharacteristic", "BluetoothLeService",
]
def run(cmd):
result = subprocess.run(cmd, capture_output=True, text=True)
return result.stdout.strip(), result.returncode
def pull_apk() -> bool:
os.makedirs(OUT_DIR, exist_ok=True)
if os.path.exists(APK_LOCAL) and os.path.getsize(APK_LOCAL) > 100_000:
print(f"APK already cached: {APK_LOCAL} ({os.path.getsize(APK_LOCAL):,} bytes)")
return True
print(f"Finding APK on device for package: {PKG}")
out, rc = run([ADB, "shell", f"pm path {PKG}"])
if rc != 0 or "package:" not in out:
print(f"[ERROR] Package not found on device: {out}")
print("Make sure the KAIYU app is installed and the phone is connected via USB.")
return False
apk_paths = [line.replace("package:", "").strip() for line in out.splitlines()]
apk_path = next((p for p in apk_paths if "base" in p.lower()), apk_paths[0])
print(f"APK on device: {apk_path}")
print(f"Pulling APK... (this may take 10-20 seconds)")
_, rc = run([ADB, "pull", apk_path, APK_LOCAL])
if rc != 0 or not os.path.exists(APK_LOCAL):
# Try copy to sdcard first (some devices block direct pull)
print("Direct pull failed, trying via sdcard...")
run([ADB, "shell", f"cp {apk_path} /sdcard/kaiyu_tmp.apk"])
_, rc = run([ADB, "pull", "/sdcard/kaiyu_tmp.apk", APK_LOCAL])
run([ADB, "shell", "rm /sdcard/kaiyu_tmp.apk"])
if not os.path.exists(APK_LOCAL) or os.path.getsize(APK_LOCAL) < 1000:
print("[ERROR] APK pull failed.")
return False
print(f"APK pulled: {os.path.getsize(APK_LOCAL):,} bytes")
return True
def extract_strings_from_dex(apk_path: str) -> list[str]:
"""Extract all printable ASCII strings >= 6 chars from DEX bytecode."""
strings = []
with zipfile.ZipFile(apk_path, 'r') as zf:
dex_files = [n for n in zf.namelist() if n.endswith('.dex')]
print(f"DEX files in APK: {dex_files}")
for dex_name in dex_files:
raw = zf.read(dex_name)
current = []
for byte in raw:
if 32 <= byte < 127:
current.append(chr(byte))
else:
if len(current) >= 6:
strings.append("".join(current))
current = []
if len(current) >= 6:
strings.append("".join(current))
print(f"Total strings extracted: {len(strings):,}")
return strings
def analyse(strings: list[str]) -> dict:
results = {
"full_uuids": {},
"known_uuid_matches": [],
"ble_strings": [],
"resource_uuids": [],
}
for s in strings:
for uuid in UUID_RE.findall(s):
ul = uuid.lower()
results["full_uuids"][ul] = results["full_uuids"].get(ul, 0) + 1
if ul in KNOWN_UUIDS:
hit = f"{ul} -> {KNOWN_UUIDS[ul]}"
if hit not in results["known_uuid_matches"]:
results["known_uuid_matches"].append(hit)
slower = s.lower()
if any(kw in slower for kw in BLE_KEYWORDS) and len(s) < 300:
if s not in results["ble_strings"]:
results["ble_strings"].append(s)
return results
def check_resources(apk_path: str) -> list[str]:
found = []
with zipfile.ZipFile(apk_path, 'r') as zf:
for name in zf.namelist():
if name.startswith("res/") and name.endswith(".xml"):
try:
content = zf.read(name).decode("utf-8", errors="ignore")
for uuid in UUID_RE.findall(content):
found.append(f"{name}: {uuid}")
except Exception:
pass
return found
def main():
print("=" * 60)
print(" KAIYU APK Protocol Extractor")
print("=" * 60)
print()
if not pull_apk():
sys.exit(1)
print("\nExtracting strings from DEX bytecode...")
strings = extract_strings_from_dex(APK_LOCAL)
print("Analysing for BLE UUIDs and protocol hints...")
results = analyse(strings)
print("\nChecking XML resources...")
results["resource_uuids"] = check_resources(APK_LOCAL)
# ── Print results ──────────────────────────────────────────────────────
print(f"\n{'='*60}")
print(f" RESULTS")
print(f"{'='*60}")
print(f"\n--- Known BLE LED controller UUIDs ({len(results['known_uuid_matches'])}) ---")
if results["known_uuid_matches"]:
for m in results["known_uuid_matches"]:
print(f" *** {m}")
else:
print(" None of the well-known LED UUIDs found as plain text in DEX")
print(f"\n--- All UUIDs found ({len(results['full_uuids'])}) ---")
for uuid, count in sorted(results["full_uuids"].items(), key=lambda x: -x[1]):
known = f" <-- {KNOWN_UUIDS[uuid]}" if uuid in KNOWN_UUIDS else ""
print(f" {uuid} (x{count}){known}")
print(f"\n--- BLE-related strings ({len(results['ble_strings'])}) ---")
for s in results["ble_strings"][:60]:
print(f" {repr(s)}")
if results["resource_uuids"]:
print(f"\n--- Resource file UUIDs ({len(results['resource_uuids'])}) ---")
for r in results["resource_uuids"]:
print(f" {r}")
# Save full results
with open(RESULTS, "w") as f:
json.dump(results, f, indent=2)
print(f"\nFull results saved: {RESULTS}")
# ── Recommendation ─────────────────────────────────────────────────────
print(f"\n{'='*60}")
print(f" NEXT STEP")
print(f"{'='*60}")
if results["known_uuid_matches"]:
print(" Known UUID found! Update controller/config.py with the write UUID.")
print(" Then run: python scanner/ble_scanner.py --probe <device_address>")
elif results["full_uuids"]:
print(" Custom UUIDs found. Try each as WRITE_CHARACTERISTIC in config.py")
print(" and test: python scanner/ble_scanner.py --probe <device_address>")
else:
print(" No UUIDs in DEX (may be obfuscated or in native lib).")
print(" Run the interactive probe instead:")
print(" python scanner/ble_scanner.py --probe <device_address>")
if __name__ == "__main__":
main()