docs(huya): 逆向定位 32hex hdid = NativeEntry.getGUID() + 真机注入通道证据

核心结论:
- 32hex hdid (WUP 登录帧 field1.tag0) = com.huya.security.hydeviceid.NativeEntry.getGUID()
- 金样本 ed0db8 为金样本设备 GUID; 当前真机 M2102J2SC getGUID()=0a7dfaa882938a6ab502511452142c57(稳定)
- NativeEntry 全字段: getGUID(32hex)/getCDID(40hex)/getHDID(40hex)/getMID(16hex)/getSDID(base64 safedeviceid)
- 单机不可铸造: 改 serialno/ANDROID_ID/删files/hydevice持久化 均不变(硬锚底层硬件)
- libhydeviceid.so 为 OLLVM+datadiv 壳; 用 dlopen->JNI_OnLoad->RegisterNatives(env表索引215) 绕开混淆

脚本: phone_probe_registernatives2(注册表)/phone_java_call_nativeentry(Java读getGUID)/phone_read_hdid(native hook)
      phone_dump_hydev(dump解密so)/phone_probe_login_frame(登录帧)/arm64_disasm(反汇编)
证据: hdid_regtable/java_nativeentry/hdid_read/login_frames/hdid_probe* + libhydeviceid_{original,dump}.so
This commit is contained in:
yml2213
2026-08-28 12:30:34 +08:00
parent 45b5e9128c
commit 0c6aff6a6d
23 changed files with 86238 additions and 0 deletions
+139
View File
@@ -0,0 +1,139 @@
#!/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='<n>';
try{ s = ret.isNull()? '<null>' : ret.readCString(64)||'<?>'; }catch(e){ s='<err>'; }
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()