#!/usr/bin/env python3 """真机 v9: 触发一键登录, hook SSL_write 抓 WUP 登录帧 + createWupDeviceInfo(0x2746a0), 从登录帧提取 32hex hdid (t1.t0) 和其来源. 触发: 点击 GameSdkLoginActivity 的"本机号码一键登录" (mBtnLogin, 中心 540,1287), App 会自动发起登录 WUP 请求, 帧内经 JCE 编码含 t1.t0=hdid 字段. 用法: /Users/yml/codes/Reverse-Engineering-Agent-Universal-v3.0/.venv/bin/python \ scripts/phone_probe_login_frame.py [serial] [remote] 输出: evidence/diag_phone/login_frames.json """ from __future__ import annotations import json import re import subprocess import sys import time from pathlib import Path import frida SERIAL = sys.argv[1] if len(sys.argv) > 1 else "5dd8c93f" REMOTE = sys.argv[2] if len(sys.argv) > 2 else "127.0.0.1:31878" PACKAGE = "com.duowan.kiwi" LOGIN_ACT = "com.duowan.kiwi/.loginui.impl.gamesdk.GameSdkLoginActivity" REPO = Path("/Users/yml/codes/douyu_login_py") OUT = REPO / "evidence" / "diag_phone" / "login_frames.json" RE = Path("/Users/yml/codes/Reverse-Engineering-Agent-Universal-v3.0") ART_CALLSITE = RE / "evidence" / "scripts" / "bypass_msaoaid_maps_art_callsite.js" JS = r""" 'use strict'; var sslDone=false, udbHooked=false, cdiHooked=false; function hookSSL(){ if(sslDone) return; try{ var r=new ApiResolver('module'); r.enumerateMatchesSync('exports:*!SSL_write').forEach(function(m){ Interceptor.attach(m.address,{onEnter:function(a){ var len=a[2].toInt32(); if(len<200||len>80000) return; try{ var buf=a[1].readByteArray(len); send({type:'sslfr', len:len, hex:Array.from(new Uint8Array(buf)).map(function(b){return ('0'+b.toString(16)).slice(-2);}).join('')}); }catch(e){} }}); }); send({type:'ssl-hooked'}); }catch(e){ send({type:'ssl-err', e:String(e)}); } sslDone=true; } function hookUdb(){ if(udbHooked) return; var md=Process.findModuleByName('libudbauthunify.so'); if(!md) return; udbHooked=true; send({type:'udb', base:''+md.base}); try{ // BusinessCfg::getHdid Interceptor.attach(md.base.add(0x26a484),{onLeave:function(ret){ var s=''; try{ s = ret.isNull()? '' : ret.readCString(64)||''; }catch(e){ s=''; } var bt=''; try{ bt = Thread.backtrace(this.context, Backtracer.ACCURATE).slice(0,16).map(function(x){return x.toString();}).join(' <- '); }catch(e){} send({type:'getHdid', ret:''+ret, str:s, bt:bt}); }}); }catch(e){} try{ // createWupDeviceInfo(wup::DeviceInfo*) Interceptor.attach(md.base.add(0x2746a0),{onEnter:function(a){ send({type:'createWupDev-enter', tid:Process.getCurrentThreadId()}); }}); }catch(e){} send({type:'udb-hooked'}); } setInterval(hookSSL, 500); setInterval(hookUdb, 60); """ def adb(*a): return subprocess.run(["adb", "-s", SERIAL, *a], capture_output=True, text=True) def tap(x, y): adb("shell", f"input tap {x} {y}") def main(): OUT.parent.mkdir(parents=True, exist_ok=True) adb("shell", "am", "force-stop", PACKAGE) time.sleep(1.5) d = frida.get_device_manager().add_remote_device(REMOTE) pid = d.spawn([PACKAGE]) print(f"[*] spawn pid={pid}", flush=True) session = d.attach(pid) det = [] session.on("detached", lambda r, dd: det.append({"reason": r, "detail": str(dd)}) or print(f"[*] detached {r} {dd}", flush=True)) session.create_script(ART_CALLSITE.read_text()).load() result = {"frames": [], "events": [], "detached": det} def on_message(m, data): if m.get("type") == "error": print("[JS-ERR]", str(m)[:200], flush=True); return p = m.get("payload") or {} t = p.get("type") result["events"].append(p) if t == "sslfr": result["frames"].append(p) b = bytes.fromhex(p.get("hex", "")) # WUP/JCE 帧特征: 找 t1.t0 的 32hex 候选 h32 = list(set(x.decode() for x in re.findall(rb"[0-9a-f]{32}", b))) h40 = list(set(x.decode() for x in re.findall(rb"[0-9a-f]{40}", b))) print(f"[ssl] len={p['len']} 32hex候选={h32[:4]} 40hex候选={h40[:2]}", flush=True) elif t in ("udb","udb-hooked","ssl-hooked"): print(f"[*] {t}", p if t=="udb" else "", flush=True) elif t == "getHdid": print(f"[getHdid] ret={p.get('ret')} str={p.get('str')[:60]!r}", flush=True) if p.get("bt"): print(f" bt: {p['bt']}", flush=True) elif t == "createWupDev-enter": print(f"[createWupDeviceInfo] enter tid={p.get('tid')}", flush=True) sc = session.create_script(JS) sc.on("message", on_message) sc.load() d.resume(pid) print("[*] resumed", flush=True) time.sleep(6) # 打开登录页 (不自动点击, 交给用户手动切账号密码登录) adb("shell", "am", "start", "-n", LOGIN_ACT) time.sleep(3) duration = int(sys.argv[3]) if len(sys.argv) > 3 else 240 print(f"[*] 探针持续抓帧 {duration}s, 请手动登录...", flush=True) time.sleep(duration) result["detached"] = det OUT.write_text(json.dumps(result, indent=1, ensure_ascii=False), encoding="utf-8") print(f"[*] 帧数: {len(result['frames'])} saved {OUT}") if __name__ == "__main__": main()