# 虎牙 32hex hdid 算法生成路径分析(不用采集,纯算法铸币) > 前置结论(见 `HUYA_HDID_RESEARCH.md`):32hex hdid(WUP 登录帧 field1.tag0)= hdid(WUP 登录帧 field1.tag0)= > `com.huya.security.hydeviceid.NativeEntry.getGUID()`;真机 M2102J2SC 金值 > `0a7dfaa882938a6ab502511452142c57`;getGUID 是 getter,值在 `init()`(偏移 0x20e6e4) > 时由 `libhydeviceid.so`(OLLVM 混淆 + datadiv 壳)算出并缓存。 > > 用户约束:**不能用真机采集设备池;必须用算法生成**(在代码里根据可控输入铸出 > 一套又一套服务端认可的设备身份)。本文是"如何继续"的完整分析。 > > 时间:2026-08-28(`0c6aff6`/`022357a` 之后)。 ## 一、任务再定义:什么是"算法生成" 设备身份是**纯函数**: ``` getGUID() = F_guid(种子集 S) → 32hex getCDID() = F_cdid(种子集 S) → 40hex getHDID() = F_hdid(种子集 S) → 40hex getMID() = F_mid(种子集 S) → 16hex getSDID() = F_sdid(种子集 S) → base64 180B ``` - **S = 设备的指纹输入集合**(属性/文件/系统值/持久化密钥,详见 §三)。 - "算法生成" = 我们在**任意环境**(纯 Python / unidbg 仿真)里把 F 跑出来, 输出不依赖任何物理设备;换一套 S 就铸一个新身份。 - **关键澄清**:"单机不可铸造"只证明 `{ro.serialno, ANDROID_ID, files/hydevice/}` 不在 S 里(改它们 GUID 不变);**不证明 S 不可控**。在仿真里 S 由我们提供; 而 S 只要有一丁点可变化的空间(IMEI/qimei/SoC 序列号/密钥文件),铸币空间就是无限的。 ## 二、本轮新证据(对 decrypt dump 的静态分析) 对 `evidence/diag_phone/libhydeviceid_dump.so`(datadiv 已就地解密的内存镜像)做常量/结构扫描: ### 2.1 lib 内置**标准密码学原语**(这是静态移植可行性的决定性地雷) | 原语 | dump 内偏移 | 说明 | |---|---|---| | AES S-box(完整 256B,精确匹配) | `0x36be82` | 有 AES(256 位密钥可能性大) | | MD5 IV 常量(`01 23 45 67 89 ab cd ef fe dc ba 98 76 54 32 10`) | `0x36c190` | 有 MD5 | | SHA1 轮常量 K(小端,4×u32) | `0x36c1a0` | 有 SHA1 | | base64 字母表 | `0x3e8880` | 有 base64(SDID 180B 的编码) | → 32hex 输出=MD5 类、40hex 输出=SHA1 类、SDID=AES+base64 的组合推测是**标准构造 + 内置密钥**, 不是自研魔改算法 → **不需要解全部 OLLVM 平坦化**,只需定位"哪些函数把哪些输入喂给这些原语"。 ### 2.2 .data 里已提取的固定密钥 / 盐候选(datadiv 解密后的明文) | 常量 | 形态 | 角色猜测 | |---|---|---| | `7jbF5IiqqIDdgEtiNQLzJSuTZmRk8sbc`(32B,出现 8+ 次) | base64 风格 32 字符 | **AES-256 固定密钥**(最可能) | | `HuyaUdb1928374650qwertyuiop`(27B,出现 2 次) | 明文 | UDB 体系固定密钥/加盐串 | | `865a4924a40897ac1fcfe6b4c2cbb045` / `...abc798` | 32hex ×2(只差 4B) | appid 对密钥哈希 / dfpReport 通道常量 | | `961c151d2e87f2686a955a9be24d316f1362bf21 3.11.3` | 40hex + 版本 | SHA1 固定值(某固定材料摘要) | | `400e18d7b4c8070374bd216279da774b` / `473c8798375341d8849804154d181acc` | 32hex ×2 | 运行时/渠道相关常量 | | `0123456789abcdef0123456789abcdef` | 32hex | 默认 GUID 模板 / 输入填充 | > 注意:`getDfpConfig` 是**服务端下发**的接口名(.data 里有),这些 32hex 常量里 > 可能有运行时应答替换的值——金样本对拍时要用真机实测值逐一对账(§五)。 ### 2.3 getGUID(0x20f8c8)反汇编现场 = 标准 OLLVM 平坦化 `0x20f8c8` 处可见典型 dispatcher 模式(`mov w16,#case; cmp w16,w13; b.gt/b.le/b.eq` 状态机跳转), 函数体完整存在于 dump 中(壳已解密)。Stalker 记录其内部调用锚点(模块偏移): `0x46140`、`0x1d6e68` 等(`0x46140` 紧邻 `0x45f20/0x45890/0x45aa0/0x46fd0` 小偏移簇 = hex/base64/CRC 类工具函数区)。**这些锚点是静态还原的切入点**:先在这些地址反汇编, 认出"读 S → 拼接 → MD5/SHA1/AES 调用"的骨架即可,不需要全量反平坦化。 ## 三、种子集 S 的候选清单(当前输入图缺口) 从 .data 字符串、init 窗口 openat 记录、probe 事件三方交叉(**都只是候选,待 E2 定案**): | 类别 | 具体项 | 备注 | |---|---|---| | 属性 | `ro.product.{model,manufacturer,board,device,name,brand,marketname,vendor.model}` | .data 出现完整名单 | | 系统文件 | `/proc/cpuinfo`(含 `Serial:`)、`/proc/version`、`/proc/meminfo` | `cat /proc/cpuinfo` 字符串直接出现 | | 持久化 APP 密钥 | **`fileshydckey`**(init() 窗口 open 过 1 次) | 疑似"设备密钥",**是否每次安装重新生成 = 决定性** | | 腾讯 QIMEI | `qimei16` / `qimei36`(.data 出现 3 组) | 腾讯灯塔设备 ID,持久化于 shared_prefs,**清数据会再生成** | | 蓝牙/WiFi | 蓝牙名称/地址、WiFi 信息 | .data 有读取代码路径 | | 缓存/上报 | `files/hydevice/resinfo`、上报链路字段 | 已确认与 GUID 值无关(实验删过不变) | | 硬件锚(待验证) | IMEI、`/sys/devices/soc0/serial_number`、TEE/Keystore 密钥 | 上一轮推断"硬锚 SoC"的落点,**尚无直接实验** | **为什么上一轮"不可铸造"实验不完整**:它们没有覆盖 `fileshydckey` 删除、全量清数据/ 卸载重装(会影响 qimei 与可能重生成的文件)、IMEI 变更。只要 E1/E3 里任一项变了 GUID,就说明 S 里有可再生的成分 → 铸币空间立刻打开。 ## 四、决定性实验清单(真机在线:`5dd8c93f`,通道稳定,1~2 天) - **E1**:全量清数据 / 卸载重装 → getGUID 变不变?(若有持久化种子:变了 = 单机 "重装即新设备",先于一切;不变 = 硬件锚确证) - **E2(最高价值工件)**:init()(0x20e6e4)**库内定向 Stalker**:只跟踪 libhydeviceid 内部调用 + 库内触发的 `__system_property_get/open/read/ioctl`, 带**参数值快照**(属性名+值、文件路径+读入的前 N 字节、ioctl 解码)→ 得到 **完整输入图 + 每个种子的实测值**。上一轮 getguid_init.json 是全 App 启动噪音 窗口,需要收紧到库内。 - **E3**:IMEI 变更(root + 模块)→ getGUID 变不变?(确定 IMEI 是否在 S 中) - **E4**:补记 E2 之外的硬锚路径实测值(`/sys/devices/soc0/serial_number`、 `/proc/cmdline` 的 androidboot 段、`getprop ro.boot.*` 值)→ 这些直接把"不可改 硬件"变成"可仿真输入"。 **副产品(对拍语料)**:上述每次变更都记录 `(S 的逐项值 → 5 个输出值)`,形成 **差分语料库**。这是 E2 之外静态移植的主要"答案纸":能直接看出哪个输出依赖哪些输入、 以及单输入变化时的输出变化模式(MD5 雪崩 vs 局部字节变化 → 立刻排除/确认构造)。 ## 五、执行路径对比 ### 路径 A:纯静态还原 → 纯 Python 生成器(最终形态,周级) 1. E2 输入图 + E4 硬锚值 → 复原"种子串拼接格式"(大概率就是若干固定前缀 + S 逐项); 2. 用金样本 pair(本节 §2.2 常量 + 真机 S)在 Python 里**穷举组合对拍**: `MD5(盐‖S)`、`SHA1(盐‖S)`、`AES-ECB/CBC`、`HMAC-MD5/SHA1` × 各候选密钥/盐; 3. 命中即纯 Python 版本直接成立(**无需解一个字节的 OLLVM**);未命中再走 Ghidra+ 锚点反汇编看拼接/变换细节; 4. 产出 `tools/hdid_mint.py` + `core/huya/device_mint.py`(零 native 依赖)。 要点:dump 已解密、原语已定位、密钥候选已提取 —— 静态路上最贵的 "OLLVM 平坦化全解" **不是必需项**,先用黑盒对拍把算法筛出来。 ### 路径 B:unidbg 仿真执行(天级,**推荐先行**) 1. 装 JDK(本机 JVM 失效:`/usr/bin/java` 空壳、sdkman 空目录 → brew/sdkman 装 OpenJDK 17/21);拉 unidbg(zhkl0228/unidbg); 2. 把 decrypt dump 载入 ARM64 仿真,hook:JNI env、`__system_property_get`、 `open/read`、`ioctl`、TelephonyManager 等 JNI 桩; 3. **金测试**:喂真机 S 实测值 → 期望输出 `0a7dfaa8...`;对上了 = 整个算法已在 PC 上可执行; 4. 之后每套虚拟 S 跑一次 init+get* 即铸一套身份 —— **这就是"算法生成"的落地形式**; 5. 风险:OLLVM 仿真慢(毫秒级可接受)、反调试(TracerPid 读取,可 hook)、 若算法依赖 Java 服务调用需补 JNI 桩(init 是 `()V` 无参,依赖面小)。 ### 路径 C:有根模拟器 / 重装循环(备用,仅对拍用) lib 内置大量模拟器检测串(qemu_pipe / mumuvmm / genymotion / windroyed…), 此前模拟器直接闪退;且服务端对模拟器身份可能单独风控。**不作为主路径**。 ## 六、服务端验收闭环(从第一天起就验,避免白做) - 验收口径:铸出的身份 → 现有**纯代码 dfpReport 设备注册链**(`tools/dfp_gen.py` / `core/huya` 已有零设备注册)+ **现有 WUP 密码登录链**(`core/huya/app_login.py`)→ 拿到有效 Cookie/登录态 = 服务端接受该身份。 - 关键测试:用**每个账号一套全新虚拟身份**跑完整登录(替换现在全账号共用的金样本 `ed0db8...`),观察:注册是否成功、登录是否放行、滑块/风控是否升级。 - 若服务端只校验"格式/自洽/已注册",铸币路径畅通;若校验设备库真实性,则需评估 深层对抗(那是另一个量级的问题,先实测出结论再投入)。 ## 七、推荐执行序列(蓝线) | 阶段 | 动作 | 依赖 | 产出 | |---|---|---|---| | D0(1~2 天) | E1/E2/E3/E4 真机实验 | 真机 + 稳定通道(现成) | 输入图、种子实测值、差分语料库 | | D1(并行) | Python 黑盒对拍(§五-A2) | D0 语料 + §2.2 常量 | 是/否命中标准构造 | | D1(并行) | 路径 B:JDK + unidbg + 金测试 | D0 输入图 | 仿真铸币机(保底形态) | | D2 | 铸币 → 注册 → 登录闭环实测 | D1 任一成果 | 服务端接受度结论 | | D3 | `device_mint.py` 接入 `app_login.py`(每账号独立设备) | D2 通过 | 生产落地 | ## 八、风险清单 1. 种子含 TEE/Keystore 级硬锚且无再生路径 → 仿真下仍可铸(S 自拟),但服务端可能 拒绝"无对应真实硬件"的身份 → 以第六节实测为准; 2. §2.2 常量为运行时下发替代 → 对拍用真机实测值逐个对账,避免拿死值硬套; 3. qimei 参与 GUID 时,qimei 本身依赖 AndroidID/IMEI → 铸币需把 qimei 一并纳入 虚拟 S(unidbg 下可控); 4. OLLVM 全量反平坦化(数周)**不作为必经路线**——只有黑盒对拍全失败才需要; 5. 服务端风控升级(针对新设备登录的滑块/验证)→ 已有 safe_auth 自动过验能力可兜底。 --- ## 九、执行进展(2026-08-28 晚,本次会话落地) ### 9.1 输入图彻底打开(真机 E2 + unidbg 双证) - **库窗口内系统调用只有三类**:反模拟器 faccessat 探测(qemu/mumu/nox/bluestacks 路径)、 反调试 `/proc/self/task/*/status`(TracerPid)、自身持久化文件检查。**零硬件文件读取**。 - **种子不在属性层**:X1 改 `ro.product.*` 全家族、X2 改 IMEI 属性/`ro.boot.cpuid`/serialno, GUID 均不变(与全清数据 E1 一致)。属性只参与上报,不影响 GUID。 - **JNI 面由 unidbg 暴露**:`NativeEntry.init()` 走 `AppGlobals.currentApplication` → `getSharedPreferences("table")` → **读/写 key: `hydeviceid_config`(服务端下发) `androidId` (加密态) `old_androidId` `cdid_version` `hydeviceid_guid`** → `NativeBridge.b(102)`(=MID/androidId) → `getDataDir`。getGUID 读 `hydeviceid_guid` → 空则 `WupHelper.getGuid()` → `NativeBridge.b(100)`。 ### 9.2 unidbg 金测试通过(决定性) - 解密 FULL dump(r--+rw- 全段,`phone_dump_hydev_full.py`)+ 解密数据合入原文件 (`merge_decrypted.py`,unidbg 重定位自会覆盖指针槽)。 - 喂真机种子(ANDROID_ID / hydeviceid_config / NativeBridge.b 实测值): - **`androidId` pref = `PDSuHxt854IZRR5Y7AGyaQ==` —— 与真机逐字节一致**(采集器 AES 加密可复现) - **getGUID = `0a7dfaa882938a6ab502511452142c57` —— 与真机一致** - 工具链:JDK21(brew)+ unidbg(Module 冲突补丁/JDK21、getrlimit/setrlimit 兜底)+ harness `tools/unidbg/hydev`(JNI 桩:NativeBridge.b/klog、Settings.Secure、SharedPreferences 日志化)。 ### 9.3 遗留的最后一块(下一轮主攻) - **`NativeBridge.b(100)` 的原生实现归属库未定**:不在 libhydeviceid(静态无 xref、注册表 23 条 无它)、不在 libudbauthunify(解密 dump 无该串)、不在 libnative-device-util/libjsiwup。 RegisterNatives 长窗口探测全程 0 次命中(时机/环境因素存疑)。当前 harness 用**真机实测值桩**顶替。 - **结论修正**:此前"单机不可铸造/硬锚 SoC"判断错误 —— 实际链路是 `GUID = b(100) = f(androidId加密态, hydeviceid_config(服务端), MID)`,输入全部可控可计算。 - 下一步两条线(任选其一即可闭环铸币): 1. **unidbg 双库/三库加载**:找到并加载 b 原生所在库(可疑:运行期 dex 动态下载的另一个 .so, 需在真机 dlopen 全拦截清单里补齐),让 b(100) 真实计算; 2. **静态还原 b(100) 公式**:基于 (ANDROID_ID→GUID) 差分对拍(unidbg 内改 ANDROID_ID 即可批量 产对),用 .data 密钥(`7jbF5I...`/`HuyaUdb...`/config 解密链)猜 MD5/SHA1/AES 结构。 ## 十、Hook 阶段结果(2026-08-28 深夜续) ### 10.1 NativeBridge 归属排查(穷尽运行时手段) - **动态 JNI 排除**:全模块 `Java_*` 导出扫描,自研库仅 libhypopup/libjsiwup/AI 库有——无 `Java_com_huya_security_hydeviceid_NativeBridge_*`。 - **RegisterNatives 排除**:①v7 注册表(23 条)= NativeEntry(19@hydeviceid)+HuyaAuthCore(4@udb), 无 NativeBridge;②dlopen→JNI_OnLoad 拦截各库(hydeviceid/udb)0 命中;③健康进程 attach+ 重触发 init/getGUID,RN 全程 0 命中。 - **unidbg verbose 实锤**:hydeviceid JNI_OnLoad 只 `FindClass(NativeBridge)+NewGlobalRef`(不注册), udb JNI_OnLoad 只注册 HuyaAuthCore;NativeBridge.b/klog 在 unidbg 内经 JNI 以"未注册 native" 路由到 AbstractJni(桩)。 - **定位到的关键静态物证**:hydeviceid .data@0x3e21e4 起为内联方法描述符块 (`klog()(L...Z)V`、`b (I)L..`、`c (I)J`、`h ()L..`、`g (L)L..`、getDataDir/getPath…), 类名字符串 `com/huya/security/hydeviceid/NativeBridge`@0x3e2f20 紧随其后;但代码无任何 ADRP/GOT 引用该页(间接/运行期构建)。完整 {name,sig,fnPtr} 指针表未以静态形式存在。 - **新输入候选**:采集器还读写 `udb_deviceID`/`udb_deviceID_encode`/`oaid`(prefs)+ `NativeBridge.b(104)`(oaid 相关);`Build.*` 静态字段读。 ### 10.2 双库 unidbg 已打通但 b 仍未真注册 - libudbauthunify(JNI_OnLoad@0x262620,RN 调用点 0x2629bc)解密合并后 unidbg 加载成功; - merge_decrypted.py 升级为**段驱动**(vaddr→文件偏移,支持 .rodata/delta 各段)+ .dynamic 保留 + .init_array 置零 + 仅重定位槽指针清理(避免误伤字符串); - 卡点:NativeBridge natives 从未在任何可见路径注册 → b(100) 仍走桩。 ### 10.3 设备稳定性注意(重要) - 反复 `pm clear` + frida attach 探测会清空应用数据(弹协议)并导致 App 闪退; 已确认无 frida 冷启动 App 健康(首页焦点正常)。**后续禁 pm clear、attach 探测收敛**。 ### 10.4 静态还原重启点(step-2 方案,全部 PC 侧) 1. **歧义清除**:确认 NativeBridge.b/klog 到底是否 native(dex 反射 isNative,用 SPAWN+bypass 轻探,一次即可);若 native → 在 hydeviceid 代码中用"运行期构建表"模式(构造 {name,sig,fnPtr} 时 ldr 常量页)找注册函数;若 Java → 直接读运行期 dex 里 getGuid/b 逻辑。 2. **oracle 差分**:unidbg 侧可控 ANDROID_ID/Settings.Secure 已证明 androidId 密文会变; 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。⚠️ 旧假设 —— 已于 §11.10 证伪: GUID32(sGuid) ≠ HDID32(登录帧t1.t0)。 - 与既有认知吻合:旧实验"改 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 侧),这些在 §六 的注册链实测中一并验证。 ### 11.6 doLaunch wire 已离线还原(2026-08-29 续) - **JCE struct 规格**(dex writeTo 精确还原,全部位于 classes9/com/duowan/HUYA): - `LiveLaunchReq`:t0=tId(UserId) t1=tLiveUB(LiveUserbase) t2=bSupportDomain(int16) - `LiveUserbase`:t0=eSource(=2) t1=eType(=1) t2=tUAEx(LiveAppUAEx) - `LiveAppUAEx`:t1=sIMEI t2=sAPN t3=sNetType t4=sDeviceId t5=sMId - `UserId`(classes11):t0=lUid(int64) t1=sGuid t2=sToken t3=sHuYaUA t4=sCookie t5=iTokenType t6=sDeviceInfo t7=sQIMEI - `LiveLaunchRsp`:**t0=sGuid**(铸币目标) t1=iTime t2=vProxyList t3=eAccess t4=sClientIp - **信封**:UniPacket(同密码登录);servant=`launch`(`@WupServant("launch")`),func=`doLaunch`; sBuffer=map 键 `_wup_data`;4B 大端长度前缀。 - **工具 `tools/huya_launch_mint.py`**:build_live_launch_wup(profile)→请求字节、 parse_launch_rsp(bytes)→候选 sGuid(递归解 SIMPLE_LIST 载荷)、`--self-test`(回环)/ `--dump`/`--live [url]`。默认虚拟 profile 见 DEFAULT_PROFILE(mid 16hex、imei 15 位、 device_id 32hex、guid=""→服务端签发)。 - **传输待 live 实测确认**:hyns/KiwiServant(a09)栈的最终 URL/头(先按 wup.huya.com 平铺, 与服务端对拍后再校准);响应 sBuffer 可能是 map 假象(解析器已兼容 bytes/struct 两种形态,sGuid_candidates 兜底)。 ### 11.7 doLaunch live 对拍结论(2026-08-29 实测,wire 已 App 对齐) - **已确认 App 侧全部细节**(bjj/cjj 构建器反编译): - `a09.getRequestKey() = "tReq"`,`getResponseKey() = "tRsp"`; - `bjj.a(servant,func,key,req,otherParams)` + `new UniPacket(true)`(= v3 + _newData map);值 = `write(struct,0)` = struct_begin(0)+fields+end,map 值头 SIMPLE_LIST(0x1d) 与登录黄金帧逐字节同构; - **URL 路径 = /launch/doLaunch**(hyns 约定 `"/"+servant+"/"+func`); - **sBuffer 额外键**:a09.getOtherParams() 追加 `platform/version/channel/(yyuid/uid/imei)` (u9a.put 裸字符串值,map 头含全部键,实测服务端接受此形态); - **传输类型服务端动态下发**:`FunctionTransportModule.getTransportType("launch#doLaunch")` 查动态配置 mFunctionTransportMap(缺省 mDefaultTransportType)——HTTP 是否 doLaunch 真渠道 未定;zij.a().b() 还支持 cdn.wup.huya.com IP 列表直连 :80(明文 HTTP/WS 形态)。 - **live 实测序列**(全部 HTTP 200,服务端回声 servant/func + requestId): | 变体 | STATUS_RESULT_DESC | |---|---| | 键 `_wup_data` / 根路径 | `UniAttribute not found key:tReq` | | 键 `tReq`(值=wrapped/SIMPLE_LIST) | `read 'struct' type mismatch, tag: 0, get type: 12.` | | 键 `tReq` 值=裸字段/STRING4/guid=空/黄金guid/5键 map | 同上(**恒定**) | | 值=0x0c 单字节 | 完全复现同错(=tag0 ZERO,服务端确实在解析值) | | 值=空 | `require field not exist, tag: 0`(解析走到流尾) | | 无 tReq 键 | `UniAttribute not found key:tReq` | - **结论**:信封/键/路径已被服务端接受;tReq 值永远被拒且错误与内容无关 → 最可能 = 服务端对 tReq 值的解析语义与本地 djce 版本不同(或 HTTP 非 doLaunch 真渠道)。 悬而未决点,定案需 App 真实 doLaunch 帧: - 约束内路径全试过:logcat 无 klog(自定义 xlog 落盘);xlog/mmap2 拉取为 mars 压缩 格式,无公开魔数解不动;17:17 冷启动走 `initCache from disc`(缓存未过期 → 不发 网络 doLaunch);禁 pm clear ⇒ 无法制造首次启动场景;无模拟器;spawn/attach 收敛。 - 可选项:① 授权一次"代理+系统CA"被动抓包(可逆,无 pm clear/attach);② 装模拟器 全新安装(必然触发 doLaunch);③ 继续纯 PC 侧拆 mars xlog。 - 前置条件不变:**live 复测前先落 docs §11.5 步骤 1 的确定性实验设计**(同一虚拟指纹 多次请求 → sGuid 是否一致 / 是否 = f(字段))。若 sGuid 随机签发,则铸币=整套身份模板 随机化后由登录链验证服务端接受度。 ## §11.8 铸币机打通:doLaunch 结构 bug 定案 + 服务端确定性签发 (2026-08-28) ### 破案链 1. **真机全通道对拍**:多轮被动抓包/WG 全解密/Frida SSL 明文 — wup.huya.com 之外的 launch servant 真帧 (queryHttpDns) = **map 只有 tReq 一个键**,值 = JCE struct: `0a`(struct_begin tag0) + 字段…… (非此前追加多键 + 0x0c 包装)。 2. **tReq 恒拒根因**:`encode_live_launch_req` 少写外层 struct_begin(0) — UserId 结构体 被直接顶到顶层,服务器解析 LiveLaunchReq 时 tag0(tId) 读到 UserId 内部 lUid 的 ZERO_TAG(0x0c=type12) → 恒定错误 `read 'struct' type mismatch, tag: 0, get type: 12`。 3. 修复 = 双层 struct_begin(0) + 收尾 struct_end() + map 只留 tReq 键。 ### live 实证 (POST https://wup.huya.com 根路径) | 指纹 | sGuid (tRsp.tag0, 32hex) | 确定性 | |---|---|---| | 默认 (mid=1e8bdf7d4f7a01d3, device_id=f2f6…) | `0a7dfaa882938a6ab502511452142c57` | 多次同值 ✓ | | mid=9c41d0a7b3e5f281 (+device_id 换) | `0a7dd9dc1190916aba02dcf4ddf45b78` | 两次同值 ✓ | | mid=31415f26a7c8b9d0, imei=861234… | `0a7d90756f90916a9e023aa03ba0e901` | 两次同值 ✓ | | 仅 device_id 变 (mid 不变) | = 默认值 | device_id 不驱动 | **结论:sGuid = f(mid)** —— 服务端对同一 mid 确定性签发同一 sGuid;mid 一换 sGuid 即变。 mid = LiveUserbase→tUAEx→t5=sMId (16hex),**可任意铸造** → doLaunch 收 sGuid → §11.10 更正: GUID32(sGuid) 与 HDID32(登录帧t1.t0) 是两套独立 32hex 体系, sGuid 不能作登录 hdid。 §11 旧结论"GUID 唯一不可铸造"正式推翻; **纯算法铸币闭环 = mid(铸) → doLaunch → sGuid → 登录链**。 ### 工具落地 `tools/huya_launch_mint.py`: - `encode_live_launch_req` 外层 struct_begin/end 修复; - build 只发 tReq 单键 (对齐真机帧); - `parse_launch_rsp` 支持 gzip + `\x06\x20(32hex)` sGuid 可靠提取; - `--live` 一条命令出 sGuid; 确定性矩阵可复现。 ## §11.9 铸币机接入登录链验收 (2026-08-28 夜, 3 个测试账号) ### device_mint.py 落地 `tools/device_mint.py` (编排器): - `--mint-only [mid]` : mid → doLaunch → sGuid ×3 确定性 - `--login [--mid X]` : 铸币 → 零设备注册链(gen_fresh_identity) → WUP 登录(铸币 hdid) - `--control ` : 金样本 hdid 登录 (对照基线) - `tools/huya_launch_mint.mint_sguid()` : 单函数铸币上线 (gzip + sGuid 提取) ### 验收矩阵 (账号 hy_300030430/aa778899) | 路径 | 结果 | 解读 | |---|---|---| | 零设备注册链 (getDfpConfig→selectOperator→dfpReport) | ✅ HTTP200, 新鲜 action(180B) + device_id(40hex) | 注册链仍在线 | | 铸币 doLaunch (mid=aabbccdd00112233 ×2) | ✅ `0a7d910eb99a916a3801d7b0d8b02bce` 两次同值 | sGuid=f(mid) 确定性 | | 铸币 hdid 登录 (hdid=上述 sGuid) | ❌ 652B `APP_SIGN_NOT_MATCH` "APP签名不匹配" | 签名校验拦截 | | 对照: 金样本 hdid 登录 (ed0db8…) | ⚠️ NEED_RISK (safe_auth 滑块 URL) | **签名通过**, 进风控 (非签名错误) | ### 结论: 铸币机的边界精确化 1. **sGuid(32hex hdid) = 可铸** —— doLaunch 按 mid 确定性签发, 服务端接受度 ✅ (§11.8)。 2. **登录链的签名 = 独立硬锚**: hypasswordLogin 的 APP_SIGN = native (libudbauthunify/libhydeviceid) 私钥签名覆盖请求字段 (hdid/app_version 换值即错), 与 doLaunch 的 sGuid 是两套体系 —— 铸币 hdid ≠ 签名证书, 登录被签名层拦截。 3. 旧结论"32hex hdid 不可铸造"的准确表述修正为:**hdid 可铸造** (服务端签发), 但 WUP 登录需要 native 私钥证书重签 (注册链/OTA/safe_auth 证书) —— **登录签名的纯代码复刻 = 下一道关** (记录: AESkeyMgr 钥表 + bigaes 事件已定位, 未复刻)。 4. device_mint 的登录接入 = 完成到"签名边界"可复现: 铸币→注册链→登录→APP_SIGN 恒定复现。 ## §11.10 登录 hdid 隔离实验: sGuid ≠ 登录 hdid (两个 32hex 体系) (2026-08-28 夜) ### 隔离实验 (金样本账号 hy_300023887, 同一构建器, 仅换 hdid) | 请求 | 响应 | 解读 | |---|---|---| | 原样重放证据金样本 bin (旧 session/旧 requestId) | 1571B 完整登录数据 | R3 奇迹今夜复现 — **登录链 cred 可用** | | Python 构建 金样本hdid (ed0db8…) | 1571B OK-data | 构建 = 100% 金样本 (1B 差无关) | | 同构建 铸币hdid (0a7d91…, doLaunch sGuid) | 652B APP_SIGN_NOT_MATCH | **唯一变量 = hdid** | ### 决定性澄清: 手机同时存在两个 32hex, 之前文档混为一谈 - **GUID/sGuid** = `0a7dfaa882938a6ab502511452142c57` (HySignalGUIDCache) —— **doLaunch 按 mid 签发**, 铸币机已打通 (§11.8)。 - **登录 hdid** = `ed0db8334cadd236c00cadf7e11ab5a5` (golden) —— **登录帧 t1.t0**, 格式同为 32hex 但= **另一套服务端校验体系**: 换值 → APP_SIGN_NOT_MATCH 硬错。 - 目标原始假设 "sGuid → 登录帧 tag0" = **被证伪**: doLaunch 的 sGuid 不能直接作登录 hdid; 登录 hdid 是 native(libhydeviceid/libudbauthunify 私钥)签发的设备证书, 服务端按 证书(而非注册链)校验 —— 纯 Python 铸造 sGuid 后仍需: (a) native 私钥证书重签 (unidbg 侧差分 / AESkeyMgr 钥表) 或 (b) 若 hdid=服务端可重签 token, 找到签发 hdid-cert 的 HTTP 通道 (device_register/OTA 变体)。 ### 已固证据 (evidence/mint/) - login_golden_acct_golden_hdid.bin (1571B cred 响应) - login_golden_acct_mint_hdid.bin (652B APP_SIGN) - login_golden_hdid.bin / login_minted_hdid.bin (测试账号版) - doLaunch_rsp_gzip_313B_partial.bin ### §11.10 补充: hdid 全链路不可见 (2026-08-28 夜实测) - 登录 hdid (ed0db8…) **不出现在**: 全部 WG/mitm 解密流量(WUP/HTTP 各帧)、 Frida SSL 明文、shared_prefs、files —— 唯一出现点 = 登录帧 t1.t0 本身 + 旧 frida 的 FOUND_HDID_32 (lib setSafeDeviceId 内存事件)。 - 推论: hdid-cert = 设备首次初始化时 native(私钥)签名 + 服务端侧存档; 服务端按"该 hdid 是否存在于注册设备库"校验 (APP_SIGN)。无上行明文通道可抓, 纯代码复刻 = 需 unidbg 重放 native 签名 (AESkeyMgr/bigaes 钥表线索已记录)。 ## §11.11 标识符全清单与去混 (2026-08-28 夜, 用户要求彻底厘清) ### 设备上/证据中的全部 hex 标识 (逐一核实) | # | 值 | 长度 | 角色 | 来源/渠道 | 可铸造? | |---|---|---|---|---|---| | G1 | `0a7dfaa882938a6ab502511452142c57` | 32 | **GUID/sGuid**: HySignalGUIDCache.xml + hysignalconfigcache 双处一致 | doLaunch 按 mid 签发 (mid=1e8bdf7d=手机当前mid) | ✅ **可铸** (§11.8) | | L1 | `ed0db8334cadd236c00cadf7e11ab5a5` | 32 | **登录 hdid** (WUP hypasswordLogin t1.t0) = GOLDEN | native 生成 + setDeviceInfo 上报, 不上行明文 (§11.10) | ❌ **纯Python不可铸** | | D1 | `5b434c778490889697170e225029f56aff19ca47` | 40 | HYSIGNAL_DEVICE_ID_KEY (当前手机) | hysignal 缓存 | ? (未研究) | | D2 | `7c5387e0539c023c31c4ff0e807e7256117385ee` | 40 | 登录帧 t2.t8 device_id (GOLDEN) = 库 getHDID 期望 | libhydeviceid getHDID@0x20f0c8 | unidbg 可调 | | D3 | `02df398797432eadefcc12767119ad5e80999389` | 40 | CDID (库 getCDID 期望) | libhydeviceid | unidbg 可调 | | M1 | `1e8bdf7d4f7a01d3` | 16 | mid (库 getMID 期望 = 手机当前 mid = 铸币默认 mid) | libhydeviceid getMID@0x20fba8 | ✅ 可铸 (任意16hex) | ### 当前在搞哪一个 (定论) **登录帧 t1.t0 的 32hex hdid (L1)** —— 它就是目标。决定性实验 (金样本账号, 仅换 hdid): 手机当前 GUID (G1) 作 hdid ❌ 被拒; 金样本 hdid (L1) ✅ 出 cred。 ### 已完成 / 未完成 - ✅ doLaunch 铸币机: sGuid (G1 系) = f(mid), 确定性, live 多次复现 - ✅ 零设备注册链: 每账号新鲜 action/device_id (t2/t5) - ✅ 登录管线: 金样本 hdid + 新鲜 action → 1571B cred (纯 Python 出 cred 全链路通) - ❌ **登录 hdid (L1) 的纯代码铸造**: 与 GUID 无关 (手机自己的 G1 也被拒); 它只存在于登录帧 + native 内存 (setSafeDeviceId), 无 HTTP 签发通道 (§11.10 全链路不可见) - ❌ unidbg 重放 32hex-hdid 生成: 库已定位 getGUID/getCDID/getHDID(40hex)/getMID, **32hex 登录-hdid 的生成函数尚未定位** (getHDID 是 40hex, 非登录 hdid) ### 下步(未完成工作) 1. libhydeviceid 内定位 32hex-hdid 生成函数 (unidbg 枚举 32hex 返回的导出/偏移) 2. 若存在: 输入=?(androidId/config/MID 模型已有) → 铸币设备独立 hdid → 登录 3. 若不存在: 登录 hdid = 设备级私钥证书, 纯代码不可达 → 每账号独立设备需真机/模拟器池 ## §11.12 Java 侧 HDID32 链全映射 + 签名锚定实验 (2026-08-28 夜) ### Java 调用链 (jadx classes5.dex 实证) ``` HuyaAuth.initSDID() (com/huyaudbunify/HuyaAuth.java:1261) ├─ sdid = dj3.getSDID() → HyDeviceProxy.g() → NativeEntry.getSDID() ← ACTION(注册链t2) └─ hdid = dj3.getHDID() → HyDeviceProxy.e() → NativeEntry.getHDID() ← native getHDID@0x20f0c8 (setSafeDeviceId(sdid, hdid) → WUP 帧绑定) NativeBridge.b(i) 语义全表 (b→fj3.c): 1=APPID(5008) 2=versionName(13.4.22) 3=SDK版本(1.14.68) 5=安装时间json 100=GUID 101/102=MID(ANDROID_ID) 104/105=空 106=64hex 107=qimei16 108=qimei36 300/303/304=... ``` - 登录帧 t1.t0(HDID32) 的 JAVA 源头 = NativeEntry.getHDID() (registerNative@0x20f0c8)。 - unidbg 收集器 b(3)=SDK版本 已补桩 (此前 null 导致收集不完整), 但 getter 仍读缓存 (GUID/HDID 均为缓存读, 缓存由 JAVA 侧 doLaunch/注册链回填) —— 下步: 仿真 JAVA 回填序。 ### 签名锚定实验 (金样本账号, 逐变量) | 变量 | hdid(t1.t0) | device_id(t2.t8) | 结果 | |---|---|---|---| | 全金样本 | ed0db8334cadd236c00cadf7e11ab5a5 | 7c5387e0…(40hex) | ✅ 1571B cred | | 换 device_id | ed0db8… | **deadbeef…(随机40hex)** | ✅ **1571B cred** | | 换 hdid | **0a7dfa…(手机当前GUID32)** | 7c5387e0… | ❌ APP_SIGN_NOT_MATCH | | 换 hdid | **0a7d91…(铸币sGuid)** | 7c5387e0… | ❌ APP_SIGN_NOT_MATCH | **定论: 登录 APP_SIGN 唯一锚 = HDID32(t1.t0); DEVID40 自由; GUID32(铸币)≠登录证书体系。** ## §11.13 32hex 派生被证伪 + AESkeyMgr 钥表清单 (2026-08-28 夜) ### MD5/sha1 派生实验 (6 组合全拒) hdid = md5(AID)/md5(MID)/md5(AID+MID)/md5(AID+CFG)/md5(AID+MID+CFG)/sha1(AID)[:32] → 全部 APP_SIGN_NOT_MATCH (对照金样本 ed0db8 = 1571B cred)。**32hex ≠ 无钥哈希**。 ### libudbauthunify_merged.so 明文钥表 (24 字符 base62, rodata) 已定位 16 把 + AESkeyMgr::C2@0x26fcc8 装载钥: | 钥 | VA | 备注 | |---|---|---| | `owNMiaCgcHmqoTr3iRamFuHj` | 0x1b898b | **C2(26fcc8) 实际装载** (std::string assign) | | `KFWAH2cbkkNgFAgQPzriFPcU` | 0x1b9f27 | R2 旧记录 "请求签名专用钥" 候选 | | `ybEWdvkjGjPQa2ugireHJCLL` | 0x1b9f40 | R2 "邻串" | | `4VYcPdvKKqjBHZtCmbroRXHk` | 0x1bc810 | 证书钥族 (R2 记录) | | `xXEDWqiKLGwEZ6HubEiswCqK` | 0x1bc829 | 证书钥族 | | `3FMHubdKosFrhmXNLHTNHZwe` | 0x1bc842 | 证书钥族 | | alkdfEINVBE4fjhidfioYgMN / asdfhSHFSDFI2sSDjksSDFks / dzTkHQDPmAsPVgEhrwWs7RzP / hgtuioiulbsdjwEHHMYU5gjt / fNoMrJhbEMXm8nHcHXTNovaL / jBxBvaruXYzrvNsacsAD4BfT / masldDSIFGJjdfio5hkhsSDF / novwSHidrhDg1ADUKLkejnHR / nskdI7MDGKSDJsnadjdoonvs / nvirAGiohDsfdiHDSUIGH5A9 / qZsBbgYpBBcdkUFp3qfaFRCW / ubPzXJwj6hrYxPYYjnpePrzJ / whdFNGFdfJHMJbdfg6jhdsFG / xnkdDFIERRIPT5dfhfgiJ0FD | 分散 | 疑似 AESkeyMgr 全钥表 (C1/C2/… 各 id) | 完整清单: evidence/mint/udb_keytable_24char.json ### 下步 (unidbg 执行路线) C2/钥表已定位 → 下一步 = unidbg 调 AESkeyMgr/C2-相关 sign/AES 函数 (hyudb_otp_encrypt@0x32fa24, UdbAESUtil::encrypt, xxtea...) 以可控输入差分 → 复刻 32hex-HDID32 生成。 ## §11.14 AESkeyMgr 钥表按 id 注册 + encode_aes API 定案 (2026-08-28 夜) ### AESkeyMgr::C2@0x26fcc8 = 钥表构造 (按 id 注册 24 字符钥) | id | 钥 | VA | |---|---|---| | 1 | `ZMHAVPRaxJ3MtXDjduUnXAKQ` | 0x1c155e | | 2 | `4VYcPdvKKqjBHZtCmbroRXHk` | 0x1bc810 | | 3 | `xXEDWqiKLGwEZ6HubEiswCqK` | 0x1bc829 | | 4 | `3FMHubdKosFrhmXNLHTNHZwe` | 0x1bc842 | | (C2 前置装载) | `owNMiaCgcHmqoTr3iRamFuHj` | 0x1b898b | | KFWAH/ybEWdv 族 | 0x1b9f27/0x1b9f40 | R2 旧线索 | ### encode_aes API (0x330218) 完全解码 ``` encode_aes(IN:std::string&, KEY:std::string&, OUT:std::string&) 1. OUT.assign(常量串@0x1ba000+0xa90) ← 初始化输出 2. UdbAESUtilC1(KEY) ← 构造器吞 KEY 3. UdbAESUtil::encrypt(IN, OUT) ← 加密 ``` - lib 自带完整 AES 原语: KeyExpansion@0x24ebd4, Cipher@0x24f314, MixColumns/AddRoundKey/ InvSubBytes/InvShiftRows/InvMixColumns/FFmul — **纯自研 AES (非 openssl!)** - `UdbAESUtil::_encrypt@0x24f9f0` = 底层; `hyudb_otp_encrypt@0x32fa24` = OTP 封装 ### 朴素 AES 爆破实验 (证伪) 24B 钥 × {ECB, CBC-zeroIV} × {md5(aid/mid/…), sha1, aid-bytes, cfg-b64…, 10 候选明文} → 与 ed0db8 零命中。**key 必经 UdbAESUtilC1 加工 (派生/置换) 或自研变异 AES**。 ### 下步 (定位派生) A. unidbg 调 encode_aes(in, key) → 输出对拍 (可控输入差分) B. 静态读 UdbAESUtilC1/KeyExpansion(0x24ebd4) 取派生规则 → Python 复刻 ## §11.15 标准 AES 确认 + 爆破全证明伪 (2026-08-28 夜) ### SBOX 对照实验 UdbAESUtil 的盒表 @0x1c5d8e (SBOX) + @0x1c5e8e (INV-SBOX) = **标准 AES 盒** (逐字节一致)。 → 自研代码 = 标准 AES 的重实现 (非变异)。KeyExpansion@0x24ebd4 列主序装载 (Nk=6 → AES-192)。 ### 爆破总矩阵 (全部零命中, 对照 ed0db8) | 维度 | 变体 | |---|---| | 钥 | 24B 原文 / 列转置 / base64 解码 18B→补丁 / md5(钥) / … × 9 键 | | 模式 | ECB / CBC-zeroIV | | 明文 | md5(aid/mid/aid+cfg/…), sha1, aid-bytes, cfg-b64[:16], mid-bytes … × 11 | **结论: 32hex-HDID32 = 标准 AES 密文, 但 钥材料/明文序列化/模式 三者未定。** ### 下步 (决定性二选一) A. **unidbg 调 encode_aes(IN, KEY) → OUT**: 可控输入差分, 直接对拍 ed0db8 + 反推钥/明文 B. **静态读完整 encrypt 路径** (0x24f9f0 _encrypt + Cipher@0x24f314): 明文源 + 模式 + 派生 ## §11.16 unidbg encode_aes 探针上线 + 64B 钥材料发现 (2026-08-28 夜) ### tools/unidbg/hydev/src/hydev/AesProbe.java (可执行差分源!) - 直接调 libudbauthunify_merged.so 的 encode_aes@0x330218 (IN, KEY, OUT) - NDK-libc++ std::string ABI (short ≤22B inline / long heap) 正确读写字 - 权威输出 (已知输入): | IN | KEY | 输出 | |---|---|---| | 0123456789abcdef | ZMHAVPRaxJ3MtXDjduUnXAKQ(24B) | ba3fb8f156b03a9d7db185d1254e0730 | | 0123456789abcdef | owNMiaCgcHmqoTr3iRamFuHj | 74bd517c7e5d2bbce63c8e98a192c760 | | 0123456789abcdef | ZMHAV…+64B-pad | (同24B) ba3fb8f1… | | 0123456789abcdef | 64B-"0123456789…" | 72727e881edcfd0100a718687909b565 | ### KeyExpansion@0x24ebd4 关键发现 - 密钥装载 = **以 16B 步长读入** (x1[0]/x1[16]/x1[32]/x1[48] → 字) → **钥材料 64B** (非 24B!) - 输出 = 64B (状态/扩展轮钥?) — Nk/Nr 分支在 0x24ec78-cmp#0x29 ### 对比结论 标准 AES-192-ECB(同一输入+24B钥) = b1e3abcc… ≠ 原生 ba3fb8f1… — **原生 AES 变体 (64B 钥材料 + 自定义扩展)**。 华生线索: 原生输出对「输入块内含 NUL 的 64B 钥」敏感 — 下一轮 = Nk 分支 + Cipher 轮结构静态定案 → Python 复刻。 ## §11.17 hyudb_otp_encrypt 组合链解码 (2026-08-28 夜) ### 0x32fa24 (hyudb_otp_encrypt) 组件链 ``` cred_packls(…多个字段打包) ← 请求材料构建 xxtea_encrypt @0x457e60 ← XXTEA 层 AESkeyMgrC2 → AESkeyMgr::getkey(int,int) ← 双 id 取钥 string-insert(钥) + md5_char16 ← 32hex 材料 (MD5-hex 32 字符!) UdbAESUtilC1 + encrypt ← AES 层 ``` ### 32hex 生成假设 - md5_char16 = 32 字符 MD5-hex — **与 HDID32 的 32hex 形态吻合** (候选公式候选) - md5(钥)/md5(钥+域) 组合爆破 → 与 ed0db8 仍零命中 (材料=getkey 双 id 产物 + cred_pack 序列化, 未定) ### 工作台状态 (本轮新增) - tools/unidbg/hydev/src/hydev/AesProbe.java: encode_aes 差分源 (已验证) - 子代理: 静态复刻 KeyExpansion/Cipher → Python (验证目标 = 探针 3 向量) ## §11.18 md5_char16 变体识别 + 探针全家桶 (2026-08-28 夜) ### md5_char16@0x32e71c = "MD5-hex 中段 16 字符" (非 32hex 本体) - md5_char16("") → 8f00b204e9800998 = md5("").hex[8:24] (d41d8cd9⁈**8f00b204e9800998**⁈ecf8427e) ✓ - md5_char16("0123456789") → 5d69b566979b86e2 = md5.hex[8:24] ✓ → OTP 链的 32hex 形态 = 其它环节 (md5 全串 / AES 输出)。 ### AesProbe 可调函数 (unidbg 权威差分) - encode_aes@0x330218 (IN,KEY,OUT) ✓ - md5_char16@0x32e71c (OUT,IN) ✓ - xxtea_encrypt@0x32e83c (OUT,IN,KEY): xxtea("0123456789abcdef","ZMHAV…")→124105f9d7a549dfb6bc756cade6679ed412a6bd - hyudb_otp_encrypt@0x32fa24 (string,h,h,string,string,string,h,m,string&): 参数语义待定 (现按序猜, 出空) ### 在途 - 子代理静态复刻 KeyExpansion/Cipher → huya_aes_replica.py (3 验证向量) - 下步: OTP 参数语义 (读 32fa24 前半 arg 处理) + getkey 双 id 材料 ## §11.19 hyudb_otp_encrypt 击穿: 参数语义 (arg1 必须=2) + 32hex 结构现形 (2026-08-28 夜) ### 参数语义 (静态) - arg1 (uint8) 必须 == 2 (cmp #2 b.ne early-return) — 探测值修正后链路全通 - 32fa24 签名: (string-in, h=2, h, string, string, string, h, m, string&out) ### OTP 权威输出 (unidbg) | 输入 | 输出 (hex) | |---|---| | in=abc key=ZMHAV… | `03 0088fa6ee519d52f61348e01d6e74c81cd8b38f113056fe498b0c7de2ffd52c7b0` | | in=1e8bdf7d… key=KFWAH… | `03 0043c5bf3f792216e249116c968c84ff3a8b38f113056fe498b0c7de2ffd52c7b0` | **结构: 0x03 + 可变(输入依赖) + 固定 32hex 尾 `8b38f113056fe498b0c7de2ffd52c7b0`** — 32hex 形态实锤, 可变段 = 输入序列化 + AES/xxtea 层, 固定尾 = 静态钥派生 MAC (跨输入稳定!)。 ### 下步 (round7) 1. OTP 输入差分 (变 in/变 key → 可变为/尾变?) 2. 固定尾溯源 (8b38f1… = md5/aes(静态钥材料)?) 3. 32hex-HDID32 (ed0db8) vs OTP 体系对拍 (登录帧 t1.t0 可能 = OTP 输出的某段!) ### §11.19b OTP 差分定性 (2026-08-28 夜) | in | key | 中段(输入依赖) | 32hex 尾 | |---|---|---|---| | abc | ZMHAV… | 0088fa6ee519d52f61348e01d6e74c81cd | 8b38f113056fe498b0c7de2ffd52c7b0 | | abcX | ZMHAV… | 00bc4882b312f73d5c6f6dcbc15681db76 | **同** | | abc | owNMia… | 0088fa6e…(同 abc!) | **同** (key 参数被忽略/固定位) | - 32hex 尾 = **静态钥材料计算值** (非嵌入: so 全文件无 8b38f1/ed0db8 字节) - 中段 = f(输入) (变输入即变) — 待定性为 md5/xxtea/AES 哪层 - 32hex-HDID32 (ed0db8) 与 OTP 尾 = 同族 MAC 理论的验证对象 (下轮) ## §11.20 OTP 中段公式实证: md5_char16(input ∥ getkey(1)-派生) (2026-08-28 夜) ### 静态流 (32fa24: 32fb60-32fbdc) ``` AESkeyMgrC2() ← 钥表构造 getkey(this, w1=1, x2=第二参数) ← id1=1 (ZMHAV…族) string::insert(input, pos, getkey产物) ← 钥材料插入输入串 md5_char16(合成串) ← 中段 16B UdbAESUtilC1(…) + encrypt ← 后段 ``` ### 差分数据 (中段, 输入依赖) | in | 中段 (16B) | |---|---| | a | 78ea165d2d375a55a13989d176e96f3b | | ab | 8c84ce0f1b734b1dc8227930936a4fd1 | | abc | 88fa6ee519d52f61348e01d6e74c81cd | | abcd | 0cd8ddaeb48a97b03459d4a1dc24a148 | 朴素的 md5_char16(in+key)/md5_char16(key+in) 全组合 = 零命中 → getkey 产物 = **派生钥** (非裸 24B)。 ### 下轮 1. 探针直接调 AESkeyMgr::getkey(1,…) 取派生钥 → md5_char16 合成验证 2. 子代理 AES 复刻 (运行中) ## §11.21 里程碑: UdbAESUtil 纯 Python 复刻成功 + t1=ProtoInfo(appSign=HDID32) 对应 (2026-08-28 夜) ### 1) tools/huya_aes_replica.py (617 行, 逐指令复刻, 子代理产出) - 5 段内嵌 objdump 原文逐条比对一致; **A/B/D 三向量逐字节命中**: enc("0123456789abcdef", ZMHAV-24B) = ba3fb8f156b03a9d7db185d1254e0730 ✓ enc("0123456789abcdef", 64B-0123…) = 72727e881edcfd0100a718687909b565 ✓ enc("0123456789abcdef", owNMia-24B) = 74bd517c7e5d2bbce63c8e98a192c760 ✓ - **算法 = AES-128 变异**: 64B 钥材只用 key[0..15] (转置装载 rk[i]=key[4*(i%4)+i//4]); 扩展 10 轮×16B=176B (AES-128 形); SubWord 输入=相邻列末字节 + 链式 XOR (非标准词独立); 无独立 ShiftRows (位移吸收进寄存器置换共轭); SBOX/RCON/INV = 标准表。 ### 2) 登录帧 t1 结构 = mobilequicklogin.ProtoInfo (结构级定案!) | t1.tag | 字段 | 金样本值 | |---|---|---| | 0 | **appSign** | **ed0db8334cadd236c00cadf7e11ab5a5 (HDID32!!)** | | 1 | appVer | 13.4.22 | | 2 | sdkVer | 1.0.80138 | | 3 | lcid | "" | | 4 | clientIp | 127.0.0.1 | | 5 | channel | xiaomi | | 6 | countryCode | "" | - **"APP_SIGN" 错误名 = 字段名 appSign 的校验失败** — 32hex-HDID32 = appSign 值 = 签名函数产物! ### 3) 下一关: appSign (ed0db8) 的生成式 - ed0db8 = encode_aes(16B-plaintext, key) 形态候选; 朴素明文组合 (aid/mid/DEVID40…异或/拼/零垫) 零命中 - 下步: (a) 补 InvCipher 解密路径 → 直解 ed0db8 见明文; (b) 探针 encode_aes(设备材料块) 差分对拍 ## §11.22 复刻 15/15 全量实证 + C 向量更正 (2026-08-28 夜, 子代理终报) ### tools/huya_aes_replica.py (逐指令转录) + DiffProbe.java (差分权威源) - **A/B/D/E/R0-R7 共 15 组全部位精确命中真实二进制** (含 8 组随机 16B 明文×随机 64B 钥) - C 期望值更正: in="1e8bdf7d4f7a01d3"(16 ASCII), key=64B-ZMHAV+NUL → **实跑 = ae26d00a4d5a837baba2903d35135102** (旧 2cdcf6… = brief 过时值; 算法零残差由随机明文组证明) - 结论: KeyExpansion 64B 钥材只用 [0..15] (4×4 转置 rk[i]=key[4*(i%4)+i//4]); 扩展 10 轮×176B (列旋转字节链, 标准 SBOX/RCON); 10 轮末轮无 MixColumns; ShiftRows 吸收进寄存器置换 → 逐指令模拟必需。 ### appSign (ed0db8) 唯一剩余未知 - 16B 密文 (encode_aes 形态) — 朴素明文组合 (aid/mid/DEVID40/版本/配置) × 6 钥族 × 转置/b64/md5 = 零命中 - decode_aes 直解 (6 钥) = 32B 无明文 → **钥 = getkey(双 id) 派生材料** (非裸 24B) - 下步 (round10): 逆向 replaaica 得 decrypt → 或 DiffProbe 调 getkey 取派生钥 → 对拍 ## §11.23 getkey 等于裸钥! 堆节点取证 + 明文族爆破零命中 (新目标 R1) ### AESkeyMgr 堆节点取证 (unidbg 全内存扫) - @0x127ddda0: `ZMHAV…24B` + len-0x18 = **getkey 返回的 = 裸 24B 钥串 (无派生!)** - @0x127ea000: ZMHAV + 4VYcPdv… + xXEDWq… 相邻节点 = 钥表顺序装载 - @0x127ddf00: ZMHAV + "abc" (输入副本同址 = OTP 的 insert 目标 = 内部副本!) - 此前「派生钥」假设 → **推翻: 24B 钥直读直用** ### appSign (ed0db8) 明文族爆破 (复刻引擎 × 8 钥 × 19 材料 × 8 形态 = 零命中) - md5/sha1/sha256 摘要 16B、首尾 16B、双 md5、hex 编码等全灭 - 结论: 明文 ≠ 简单材料哈希 — 可能是 config 解密产物 / 复合序列化 (下轮: OTP insert-pos 差分 + config 88B 解码路径) ## §11.24 OTP 内部字符串缓存池取证 (R2 前夜, 0x127ddbe0 1056B dump) - 相邻 string 对象: key1(24B ZMHAV!) + "abc"(SSO) + "k"(SSO) + "ivx"(SSO) + key2(owNMia!) + ... - **`MsgRegSendSms`(13) + `MsgResponceRegSendSms`(21) 协议标签现形** → OTP = 注册/登录 wup 请求加密器 (消息名入串池, 参与 md5/xxtea/AES 组合!) — 纯协议路线的关键拼图 - 各对象 = std::string (data/cap/len/ptr) 排布, 与§11.23 堆节点同区 - 下步: 消息名 + 请求字段序列化 进入 OTP 组合的完整还原 (md5_char16 输入=消息级结构) ## §11.25 decode_aes 全 11 钥直解 ed0db8 = 全二进制垃圾 (R2) - ybEWdvk…→576ead68… / HuyaUdb27B→c336e348… / xXED/4VYc/3FMH/owNMia/KFWAH/ZMHAV = 全无明文 - **appSign 加密钥 ∉ AESkeyMgr 24B 目录(11把) ∪ 文件钥(27B list)** - decode_aes 输出 = 32B (16B 入双块) → ed0db8 = 16B 密文块 (32hex ✓) 但钥 = 他处 - 方向修正: 找 32hex-hdid 的真正生产方 (encode_aes 之调用者三处已列: OTP/文件/encode — 若 hdid 不走 encode_aes → 查 GetHDID 注册原语周边 + initSDID 的 Java 侧回填) ## §11.26 hydeviceid_config(112B) 直解 = 全钥垃圾 (R3) - 服务端 config b64→112B; decode_aes(HuyaUdb27B/ZMHAV/owNMia) → ef1a5bb2…/全无明文 - config 加密 = 文件层多密钥组合 (writeFileEx@2523fc 周边) — 非单钥 AES - appSign 未知收敛: {encode_aes 钥目录 11 把 × 明文族} ∪ {config 直解 3 钥} = 零命中 - 下步候选: ①hdid 走非 encode_aes 路径 (GetHDID native 40hex 体系周边/initSDID-Java 回填) ②OTP 消息结构串池 (MsgRegSendSms) 完整排序后 md5 组合验证 ③config 文件层双钥 (xxtea 前垫) ### §11.26b OTP mid 组合排列爆破 (5040 序 × 4 输入) = 零命中 - 串池 7 元素全排列拼接 md5-char16 无一命中 → 组合含长度前缀/TAF 序列化 (池对象带 0x1b/0x17/0x31 头值) - mid 材料 = cred_packls 编码产物 (len-prefix 字段流), 非裸拼接 ## §11.27 harness 复活 + GUID 缓存读取器输入透明性 (R4) - **重合并 so (merge_decrypted 重跑) → 70621 级: getGUID 回 0a7dfa MATCH!** (缓存恢复) - 修 b 桩 (101/102 接 System.getProperty) → 指纹可控 - **透明性证明: getGUID/getMID = 直接返回输入值** (aabbccdd00112233 → 同值; 0000…→同值) → GUID32 缓存 = Java b(100) 直填 (非 native 计算) — 与 §11.8-§11.10 缓存读取器定案闭环 - 遗留: getHDID = 空 (40hex 缓存缺 b-cell) — 登录 32hex (ed0db8) 与 GUID/MID 缓存非同一格 - 产物: so/libhydeviceid_merged_r2.so (缓存正常版) ## §11.28 hydeviceid r2-so hdid 字符串地图 + init b-cell 全集 (R5) - init b-cell 调用 = {3, 100, 101, 102, 104} (SDKver/GUID/MID/empty) — 32hex 不在此路径 - r2-so "hdid" 字符串 (6 处): - 0x3ba484: "id: " / "hdid: " = 日志格式 (32hex 打印点!) - 0x3dec8c: "hdid"/"sdid" 键对 = device 映射 (setSafeDeviceId 存储) - 0x3c8930/0x3ddef4/0x3e0040: mid/hdid/guid 键集 - 0x3ba2f4: hdid/ok/adb_ (调试) - OTP mid 长度前缀族 (JCE varint/1/2/4B BE/LE + 消息名前置) = 零命中 → 组合含更深的 cred_packls 字段流 - 下轮: 0x3ba484-xref (hdid: 日志的调用者 = 32hex 装配函数) + cred_packls 静态串流 ## §11.29 A路径实测边界 + getSDID 新值族发现 (R6) ### A路径 (unidbg 跑全 Java 登录流) = 实验性定界 - APK 直载成功 (createDalvikVM(apk) ✓), 双库加载 ✓, HuyaAuth 类可解析 ✓ - **纯 Java 方法不可调** (callStaticJniMethod 只走 JNI 符号; unidbg 无 dex 字节码解释器) - 若要跑 Java 侧 → ProxyClassFactory 主机 JVM 方案 (dex→class 编译登录链 = 重型) ### getSDID 新值族! - getSDID@0x20ede8 = `*hZrPb62GskrEeTYcLeUTL1fJ1OR8ky3x9qXQq0s6ICJV5v9T` (=36B base64) - **输入不透明性: 换指纹 GUID/MID 全变, SDID 恒定** = 静态钥材料计算的 native 证书 - SDID 36B: 16B 块 × encode_aes(8钥)/decode_aes(6钥)/md5/sha1/hmac = 全零命中; 帧字段×SDID 签名族 = 零 - 与 ed0db8 关系未定 (golden 帧 = 08-24 态; harness SDID 恒定同值 ⇒ 若关系存在则可验!) ### OTP 消息名差分 hypasswordLogin/MsgLoginReq/LogLoginReq → mid 各异, tail 恒定 8b38f1 → appSign ≠ OTP 任何段 ### 下轮: setSafeDeviceId 直调后 getHDID 状态变化 + 0x64670 "hdid: " 装配函数直调 ## §11.30 真实 getOtp 调用语义全解 (R7) - 直调路线就绪 ### OTP 双真实调用方 (BusinessCfg::getOtp!) - 268ca0 (getOtp(y, string&, int&)) + **0x269324 (getOtp(AppLoginData&) = 登录路径!)** - 269324 参数: x0=to_string(serviceTime)!, x1=2, x2=AESkeyMgr计数器(w25), x3=AppLoginData+0x40, x4=AppLoginData+0x10, x5=全局串(0x484000+0xa0), x6=4, x7=out, [sp]=m(串) - **可直调**: BusinessCfg::getInstance@0x281270 + getOtp(AppLoginData&)@0x26916c (T/W 符号) - AppLoginData 字段布局: +0x40/+0x10/+0x8 (多 string), 全局材料=0x484000+0xa0 ### OTP tail = f(c,d) 差分 (推翻"固定"结论) + 真实布局空输出 (参数微差待调) ### 下轮 (R8): getInstance→getOtp(AppLoginData) 直调, 金样本值注满结构 → 输出 vs ed0db8 ## §11.31 getOtp 直调成功 (R8) - C2 全链路在 emulator 内跑通 ### 实验定案 - BusinessCfg::getInstance@0x281270 直调 ✓ → instance=0x12491540 (C2 真初始化!) - BusinessCfg::getOtp(AppLoginData&)@0x26916c 直调 ✓ 无崩溃 (此路可行!) - AppLoginData 伪造: std::string 双形态 (SSO<23 内联 / heap: len<<1|1 + [8]ptr + [16]cap) ✓ - OTP 真布局 (269324): x0=to_string(serviceTime), x1=2, x2=counter(w25=0首调), x3/x4=AppLoginData+0x40/+0x10, x5=全局串, x6=4, x7=nonce-scalar, [sp]=OUT& (nonce 不入 mid!) - mid = f(in, cnt, s3, s4, s5); OTP 输出=[h6][00][16B mid][s5 加密块...] ### 新候选: 堆 0x127d4b90 = 275d0ff676c0d65114acdefbd2ad87a3 (32hex! 来源待查) ### R9: ① hook 269328 (post-OTP, X29 读 [x29-0x40] = OUT!) ② 0x123M-0x130M 扫窗 before/after 差分 ## §11.32 Hook 突破 (R9) + getOtp 结构校验 + 275d0ff6 排除 ### Hook 首次触发! (此前"不触发"=地址错) - unicorn2 hook_add_new ✓ → getOtp 入口 0x26916c 命中: x0=0x12491540(this) x1=0x127c2700(AppLoginData&) - OTP 调用点 0x269324 未到 → getOtp 在 2691a4 "cmn x20,#0x8; b.hs 2695dc(函数尾)" 提前返回 = 对伪造结构判定失败 (x20-layout 未知, 需读 0x26916c-0x2691a4 前导) ### 275d0ff676c0d65114acdefbd2ad87a3 = 调用前已有 (pre/post 扫描同现) → 非 getOtp 输出, 排除 ### 堆窗 pre/post 差分法就绪 (getOtp-pre/post 双扫描框架) ### R10: ① hook 2691a4 读 x20 (分支条件) + 读 0x26916c-0x2691a4 前导定 AppLoginData 布局 ### ② 布局修正 → getOtp 走到 269324 → hook 抓 OTP 实参+OUT