diff --git a/docs/HUYA_HDID_ALGORITHM_GEN.md b/docs/HUYA_HDID_ALGORITHM_GEN.md index 6893f9d..68005dc 100644 --- a/docs/HUYA_HDID_ALGORITHM_GEN.md +++ b/docs/HUYA_HDID_ALGORITHM_GEN.md @@ -237,3 +237,65 @@ lib 内置大量模拟器检测串(qemu_pipe / mumuvmm / genymotion / windroye b 一旦真注册,`(ANDROID_ID → GUID)` 差分对即可在 unidbg 内批量生成 → MD5/SHA1/AES 对拍。 3. b 的 fnPtr 兜底获取:运行期 dump hydeviceid .data 时同时读 JNINativeMethod 运行期构造表 (spawn+bypass 一次抓全,附 base 校验)。 + +## 十一、静态还原定案(2026-08-29 凌晨,PC 侧完成,无需 SPAWN/真机) + +### 11.1 isNative 判定 = 纯 Java(dex 直读,比 SPAWN 更硬) +- 工具 `tools/dex_probe_native.py`(dex 解析器)扫全部 18 个 classes*.dex: + - `NativeBridge`(classes5.dex):`b(I)L..;`、`a/c/g/h/klog/i1/i2/j` **全部为 Java**(有 code_off), + **无任何 native 方法** —— 10.1 的怪象全部解释:JNI_OnLoad 只 FindClass+NewGlobalRef 是因为 + 没有任何 native 需要注册;.data@0x3e31e4(vaddr 0x3e41e4) 的描述符块(klog/b/c/h/g/getDataDir/getPath + +类名 com/huya/security/hydeviceid/NativeBridge + com/duowan/biz/wup/WupHelper) = + **native 代码运行期 GetStaticMethodID 反向调用 Java 的 name/sig 常量表**。 + - `NativeEntry`:getGUID/getCDID/getHDID/getMID/getSDID/init 全部 native(code_off=0,RN 19 条已实锤)。 +- `NativeBridge.b(I)` = `fj3.c(I)` 分发器(DeviceInfo.java),`b(100)` → `pnc.c()` → 静态 `pnc.a`。 + +### 11.2 GUID 值流完整还原(GUID ≠ 本地指纹公式!) +- **native getGUID = 纯缓存 getter**(unidbg 旧配置金测试实证): + `pref hydeviceid_guid` → 空则 Java `WupHelper.getGuid()` → `NativeBridge.b(100)` → 回写 pref 并返回。 + 即 unidbg 金测试的"GUID 一致"= Java 桩直喂 golden 的闭环,**从未验证过本地公式**。 +- Java 链(每环唯一调用者/写入者,交叉验证): + ``` + YYProtoSdkModule.initHuyaUdbSdk (classes17.dex, code@0x3834d0) + → HyDeviceProxy.j(HY_APPID, WupHelper.getGuid(), mid) (setAppInfoId) + → pnc.k(guid) → pnc.a + → NativeBridge.b(100) → fj3.c(100) → pnc.c() + WupHelper.getGuid() (classes12.dex) = ((IHal)hlf.i(IHal.class)).getGuid() + → HalImpl.getGuid() = sGuidProperty.get() + → sGuidProperty 仅两处写入: initCache() 与 initialInner 网络回调 → + LiveLaunchRsp.sGuid (live-launch/doLaunch 服务端响应, 32hex 硬校验: 非32位触发 mIllegalGuidCallBack) + ``` +- **结论修正(推翻 §9.3"GUID=f(androidId密, config, MID)")**:铸币本体不是本地算法,而是 + **服务端 doLaunch/device_register 按设备指纹签发 sGuid**。doLaunch 请求携带 + `LiveUserbase{tUAEx.sDeviceId(Config 持久化), sIMEI, sMId} + tId(userId.sGuid=当前guid, lUid)}`; + 响应 `sGuid` 即后续所有登录帧 tag0 的 32hex hdid。 +- 与既有认知吻合:旧实验"改 ANDROID_ID/全清数据 GUID 不变"(服务端签发+本地缓存)、 + `tools/` 注记"hdid 服务端硬锚/不可铸造"。 + +### 11.3 新工具(PC 侧,无设备依赖) +- `tools/dex_probe_native.py`:dex 类/方法 access_flags 判定(含 class_data fields 跳过修复)。 +- `tools/dex_scan_calls.py`:跨 dex 指令级扫描指定方法的调用点(找 setAppInfoId→j() 即用此)。 +- 用法:`python3 tools/dex_probe_native.py work/apk_extract` / + `python3 tools/dex_scan_calls.py work/apk_extract Lcom/hy/HyDeviceProxy; j "(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)V"` + +### 11.4 unidbg 金测试回归修复(重要教训) +- 症状:新 harness(加入 libudbauthunify 第二库 + 扩展桩)后 getGUID/getCDID/… 全 null, + init 阶段 Dynarmic "assertion failed" 崩溃(PC 跳 libc++ 零区)。 +- 根因:**C++ locale/facet 路径**(`std::locale::use_facet`、`ios_base::getloc` 等)在 unidbg + 内置 libc++ 上 ABI 不兼容;第二库加载改变了执行路径触达该处。 +- 修复/验证:**14:16 金测试同款 harness(无 udb 第二库,git 70621cf)+ 当前 merged so → + getGUID = 0a7dfaa882938a6ab502511452142c57 MATCH**,并首次实证 native 缓存写回流程 + (WupHelper.getGuid→null → b(100)→golden → prefs-put hydeviceid_guid → return)。 +- dump 真实运行基址 = **0x7814200000**(非 probe 记录的 0x7813211000);merge v3 重定位结果 + 正确(merged = dump - 0x7814200000 = 模块内偏移)。 + +### 11.5 铸币路径重定义(下一步蓝线) +1. **服务端签发复现**:用现有纯 Python WUP 链(tools/huya_wup_encoder.py 等)构造虚拟设备指纹的 + `doLaunch`/`device_register` 请求(deviceId/IMEI/mid 取自虚拟 profile)→ 收 sGuid → + **先验确定性**:同一指纹多次请求 sGuid 是否一致 / sGuid 是否 = f(请求设备字段)。 +2. 若确定性签发:`device_mint.py` = 虚拟指纹 → doLaunch → sGuid →(可选 dfpReport 注册) + → 现有 WUP 登录链验证服务端接受度(用户既有"每账号一套全新虚拟身份"验收口径不变)。 +3. 若 sGuid 随机签发(仅库内下发),则评估"锁定身份"的现实等价物:设备指纹→注册→登录 + 全链路用同一份虚拟 profile 保持一致性,铸币=整套身份模板随机化。 +- 真机/未决项:sGuid 签发是否依赖 qimei16/36、hdid/cdid/sdid 上报(采集器还是把设备信息上报 + 给 dfpReport 侧),这些在 §六 的注册链实测中一并验证。 diff --git a/tools/dex_probe_native.py b/tools/dex_probe_native.py new file mode 100644 index 0000000..e224f4f --- /dev/null +++ b/tools/dex_probe_native.py @@ -0,0 +1,192 @@ +#!/usr/bin/env python3 +"""dex 静态判定: 扫描 APK 全部 classes*.dex, 定位目标类及其方法的 access_flags。 + +用途: 确定 NativeBridge.b / WupHelper.getGuid 等是否声明为 native (ACC_NATIVE=0x100), +完全不碰设备。 +""" +import struct +import sys +from pathlib import Path + +ACC_PUBLIC = 0x1 +ACC_PRIVATE = 0x2 +ACC_PROTECTED = 0x4 +ACC_STATIC = 0x8 +ACC_FINAL = 0x10 +ACC_SYNCHRONIZED = 0x20 +ACC_VOLATILE = 0x40 +ACC_NATIVE = 0x100 +ACC_ABSTRACT = 0x400 +ACC_CONSTRUCTOR = 0x10000 +ACC_DECLARED_SYNCHRONIZED = 0x20000 + +ACC_NAMES = { + ACC_PUBLIC: "public", ACC_PRIVATE: "private", ACC_PROTECTED: "protected", + ACC_STATIC: "static", ACC_FINAL: "final", ACC_SYNCHRONIZED: "synchronized", + ACC_VOLATILE: "volatile", ACC_NATIVE: "native", ACC_ABSTRACT: "abstract", + ACC_CONSTRUCTOR: "constructor", ACC_DECLARED_SYNCHRONIZED: "declared-synchronized", +} + + +def read_uleb128(buf, off): + result = 0 + shift = 0 + while True: + b = buf[off] + off += 1 + result |= (b & 0x7F) << shift + if (b & 0x80) == 0: + break + shift += 7 + return result, off + + +def read_sleb128(buf, off): + result = 0 + shift = 0 + b = 0 + while True: + b = buf[off] + off += 1 + result |= (b & 0x7F) << shift + shift += 7 + if (b & 0x80) == 0: + break + if b & 0x40: + result |= -(1 << shift) + return result, off + + +class Dex: + def __init__(self, data): + self.data = data + assert data[:8] == b"dex\n035\0" or data[:8] == b"dex\n037\0" or data[:8] == b"dex\n038\0" or data[:8] == b"dex\n039\0", data[:8] + self.string_ids_size, self.string_ids_off = struct.unpack_from(" 1 else Path("work/apk_extract") + # 目标: 类名 -> 关注的方法名集合 + targets = { + "Lcom/huya/security/hydeviceid/NativeBridge;": {"b", "klog", "a", "c", "h", "g"}, + "Lcom/huya/security/hydeviceid/NativeEntry;": {"getGUID", "init", "getMID", "getHDID", "getCDID", "getSDID"}, + "Lcom/huya/security/hydeviceid/WupHelper;": {"getGuid", "getGuid1", "buildGuid"}, + "Lcom/huya/security/hydeviceid/HuyaAuthCore;": None, # 全部方法 + } + found = {} + for dex_path in sorted(dex_dir.glob("classes*.dex")): + data = dex_path.read_bytes() + dex = Dex(data) + for cd in dex.class_defs_iter(): + try: + cls = dex.type(cd["class_idx"]) + except struct.error: + continue + if cls not in targets: + continue + wanted = targets[cls] + try: + methods = dex.class_data_methods(cd["class_data_off"]) + except struct.error as e: + print(f" [skip] {cls} in {dex_path.name}: class_data parse error {e}") + continue + entry = found.setdefault(cls, []) + entry.append(str(dex_path.name)) + out = [] + for m_idx, acc, code_off in methods: + if m_idx >= dex.method_ids_size: + continue + mcls, mname, proto_idx = dex.method(m_idx) + if wanted is not None and mname not in wanted and not (wanted is None): + continue + rt, params = dex.proto(proto_idx) + sig = f"({''.join(params)}){rt}" + out.append(f" {dex.flags_str(acc):32s} {mname}{sig} code_off={code_off:#x}") + if out: + print(f"== {cls} class_flags={dex.flags_str(cd['access_flags'])}") + print("\n".join(out)) + if not found: + # 打印包含这些类名串的 dex, 方便定位动态下载的 dex + print("target classes NOT found in any classes*.dex; searching string blobs...") + for cls in targets: + blob = cls.encode() + for dex_path in sorted(dex_dir.glob("classes*.dex")): + data = dex_path.read_bytes() + if blob in data: + print(f" {cls} string present in {dex_path.name}") + + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/tools/dex_scan_calls.py b/tools/dex_scan_calls.py new file mode 100644 index 0000000..6ca8a3a --- /dev/null +++ b/tools/dex_scan_calls.py @@ -0,0 +1,89 @@ +#!/usr/bin/env python3 +"""扫描全部 classes*.dex 的 code, 找调用指定方法的调用点 (调用者类/方法/code_off)。""" +import struct +import sys +from pathlib import Path +import dex_probe_native as d # 复用 Dex 解析 + +INVOKE35 = {0x6e, 0x6f, 0x70, 0x71, 0x72, 0x74, 0x75, 0x76, 0x77, 0x78, + 0xf0, 0xf1, 0xf2, 0xf4, 0xf5, 0xf6, 0xf7} +INVOKE3RC = {0x6b, 0x6c, 0x6d, 0xfa, 0xfb, 0xfc, 0xfd} + +def scan_code(dex, code_off, target_idx): + if code_off == 0: + return [] + off = code_off + regs, ins_, outs, tries, dbg, insns = struct.unpack_from(' len(b): + break + op = b[insns_off + i] + if op in INVOKE35: + method_idx = struct.unpack_from('1, 11x->1, 12x->1, 21x->2, 22x->2, 23x->2, 22c->2, 21c->2, 23c->2, + # 20t->2, 22t->2, 21t->2, 22s->2, 21s->2, 2ss? -> 简单用 opcode 高低位判断 + hi = op >> 4 + lo = op & 0xf + if hi == 0x1: # 12x,11x,10x,0x + i += 2 + elif hi in (0x2,): # 21x,22x,23x,22c,21c,23c,22b,21b,22s,21s,21t,22t,20t,2(hi=2) + i += 4 + elif hi == 0x3: # 30t,32x,31i,31t,31c 或 3rc(op=0x3a-0x3d 但已处理部分) + i += 6 + elif op in (0x0, 0xe): + i += 2 + else: + # 保守: 2 units + i += 2 + return found + +def main(): + dex_dir = Path(sys.argv[1]) if len(sys.argv) > 1 else Path("work/apk_extract") + target_cls = sys.argv[2] if len(sys.argv) > 2 else "Lcom/hy/HyDeviceProxy;" + target_name = sys.argv[3] if len(sys.argv) > 3 else "j" + target_proto = sys.argv[4] if len(sys.argv) > 4 else "V" # 简化: 用 return type 匹配 + + for dex_path in sorted(dex_dir.glob("classes*.dex")): + data = dex_path.read_bytes() + dex = d.Dex(data) + # 找目标 method_idx + t_idx = None + for i in range(dex.method_ids_size): + mcls, mname, pidx = dex.method(i) + if mcls == target_cls and mname == target_name: + rt, params = dex.proto(pidx) + if "".join(params) + rt == target_proto or target_proto == "*": + t_idx = i + print(f"[{dex_path.name}] target method_idx={i} {mcls}.{mname}{tuple(params)}{rt}") + if t_idx is None: + continue + for cd in dex.class_defs_iter(): + try: + cls = dex.type(cd["class_idx"]) + methods = dex.class_data_methods(cd["class_data_off"]) + except Exception: + continue + for m_idx, acc, code_off in methods: + if m_idx >= dex.method_ids_size or code_off == 0: + continue + mcls, mname, pidx = dex.method(m_idx) + f = scan_code(dex, code_off, t_idx) + if f: + rt, params = dex.proto(pidx) + print(f" CALLER {cls}.{mname}{tuple(params)}{rt} code_off={code_off:#x} calls@{[hex(x) for x in f]}") + +if __name__ == "__main__": + main() \ No newline at end of file