Add APK protocol extractor - pulls and analyses KAIYU APK from device
This commit is contained in:
@@ -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()
|
||||||
Reference in New Issue
Block a user