# 虎牙 32hex hdid 算法生成路径分析(不用采集,纯算法铸币) > 前置结论(见 `HUYA_HDID_RESEARCH.md`):32hex 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 结构。