docs(huya): 记录 dfpReport 零设备生成突破与生成器工具

- 破解进度: 服务端不校验 cw 内容, 随机 4146B cw 即可 200 + 签发新 t2/t5
- 生成流程: 零设备注册链 getDfpConfig->selectOperator->dfpReport 打法
- tools/dfp_gen.py: cw/body 构建器 (TAF 4226B 结构)
- tools/dfp_cli.py: 注册链 CLI
- evidence: cw JSON 段/collection 段抓取样本
This commit is contained in:
yml2213
2026-08-27 17:57:38 +08:00
parent 0c86e182b0
commit 36b5d78050
9 changed files with 1231 additions and 0 deletions
+96
View File
@@ -0,0 +1,96 @@
# dfpReport 生成流程与逻辑(逆向推演总账)
> 目标:讲清楚 dfpReport 上报数据"每一步怎么生成"的完整链路。
> 已破解部分(黑纸白字)+ 待逆向部分(用 ⚠️ 标出)。
## 一、dfpReport 是什么
虎牙 huyaudb 风控的"设备行为上报"请求,POST 到 `wsapi.huya.com:443 /`,用腾讯 **taf/wup** 协议封装。
请求体(body)= taf 容器,内含若干 tag 字段,其中 **tag2 是核心密文区**(整个设备+行为报文的加密载荷)。
## 二、请求体生成的完整流程(逻辑推理)
### 第 1 步:准备明文 JSON(554B)——【已完全破解】
```json
{"appId":"5008","appVer":"13.4.22","appkey":"865a...cbb0e3",
"channel":"xiaomi","deviceId":"02df...9389","deviceName":"M2102J2SC",
"hdid":"7c5387...85ee","heightPixels":"2120",
"systemVer":"M2102J2SC,30,11","widthPixels":"1080", ...}
```
来源:
- `appId/appVer/sdkVer/channel` = App 编译期/运行期常量
- `deviceName/systemVer` = Android build props
- `widthPixels/heightPixels` = 屏幕分辨率(运行期查询)
- **`hdid` / `deviceId` / `appkey` = ⚠️ 设备三元组,这是核心待逆向项**(见第三节)
### 第 2 步:准备 collection ↓ 数据集(大 JSON,每轮变化)——【部分破解,算法待逆向】
一个 ~3400B 的大 JSON,含动态行为数据:
- `touch_events`(触摸事件时间/坐标)
- `net_intfs`(网络接口状态)
- `phoneInfo`(设备/系统信息汇总)
- `qimei16/36`(⚠️ 设备指纹 App 的标识,逆向目标)
- `risk_apps`(已安装的风险 App,如 magisk)
- `scanned_appname`(⚠️ ~2900B 二进制扫描结果)
- **`Poseidon_*` 反 frida 检测字段**(读 /proc/self/maps 等检测 hook)
⚠️ **collection 的构建逻辑(App 怎么采这些数据、怎么格式化)是"生成流程"里最大的一块未破解**,但注意:验证已证明 collection **不参与服务端设备身份匹配**(改它只影响"动态/静态"判定,不动 hdid)。
### 第 3 步:组合成明文平文本(pa)——【已知结构,精确边界待确认】
```
明文输入 ≈ JSON(554B) + collection(3420B) [顺序/分隔待最终确认]
```
### 第 4 步:XOR 加密(逐字节 keystream)——【算法已破解,种子待逆】
- `tag2 = [魔数 10B 57 18 82 cf 66 4b b3 94 01 ee] + [XOR密文]`
- keystream 逐字节 XOR 明文 → 密文
- **魔数恒定**(明文头 XOR 固定值)
- **keystream 每会话动态** → ⚠️ 种子(时间戳/会话随机+密钥)+ 生成函数待逆向
### 第 5 步:封装 taf/wup + 加密通信
body 各 tag 打包(taf 编码)→ `POST wsapi.huya.com:443 /`(TLS),`Content-Type: application/octet-stream`,`i-ver: 1`,`User-Agent: okhttp/3.14.9`
### 第 6 步:服务端校验(已通过差分实验定论)
- 服务端解密 tag2 → 解析明文 JSON 三元组(hdid/deviceId/appkey)+ collection
- **三元组在设备库 + collection 完整** → 回显设备 hdid(actionV)
- 否则 → 动态随机 token(拒绝)
## 三、核心待逆向项(按"生成流程"重要性排序)
### 3.1 设备三元组(hdid/deviceId/appkey)怎么来【最关键】
已知:
- 是真设备派生(`pm clear` 不变,跨会话稳定)
- 长度 40hex(hdid/deviceId)、32hex(appkey),像 hash 输出
- libhydeviceid.so 内有 MD5 实现(ror#7 @0xc37c0)+ SHA1 + hex 表
⚠️ 待逆向:
- 从哪些硬件/系统信息(AndroidID/IMEI/build/serial?)派生
- 用哪个 hash(MD5/SHA1/自实现)
- 掺什么盐/固定密钥
### 3.2 collection 的完整构建逻辑
- 哪些数据源、如何采集、如何序列化、顺序
### 3.3 keystream 的种子与生成函数
- 种子来源(时间戳?线程随机?会话 ID?)
- 生成器(自实现伪随机?流密码?)
## 四、验证里"能做什么"的现实
已确认(纯 Python 可做):
- `forge_replay.py`:任意帧真实 wire → XOR 差分改明文三元组 → 重放,HTTP 200(服务端接受,只是认不认设备问题)
- 捕获比:任何一帧真实 ks 即可加密任意伪造明文(不需要生成器)
## 五、逆向"生成流程"的推荐路径(hook 而非纯静态)
由于 libhydeviceid.so 是 OLLVM 混淆,纯静态难。推荐用 frida 在**生成点 hook**,抓真实输入输出:
1. **hook 三元组派生**:hook MD5(0xc37c0 附近)/ hex 表使用处,抓 hash 输入 → 反推三元组
2. **hook 加密函数入口**:抓 keystream 生成函数输入(种子)
3. **hook collection 构建函数**:抓各字段组装点
但 attach/spawn 反调试都强(已验证 attach 6/6 死,spawn 3-6s 死),给 hook 点逆向带来现实障碍
+881
View File
@@ -271,3 +271,884 @@ B. **生成器**:既然 ks 与轮次序号相关,继续定位生成函数(0x5f1b
1. 抓真实 App **首次注册/重置设备后**的第一帧 dfpReport(看服务端如何初始分配设备 id) 1. 抓真实 App **首次注册/重置设备后**的第一帧 dfpReport(看服务端如何初始分配设备 id)
2. 或逆向 collection 的指纹算法(哪些字段参与绑定) 2. 或逆向 collection 的指纹算法(哪些字段参与绑定)
3. 验证"设备上等长 patch hdid + App 自行生成 t2"路线(修复 patch 崩溃:可能因 JSON 长度校验/内存写坏) 3. 验证"设备上等长 patch hdid + App 自行生成 t2"路线(修复 patch 崩溃:可能因 JSON 长度校验/内存写坏)
---
---
## 十二、请求链捕获定论(冷启动全链,决定性!)
`scripts/hook_reqchain.py`(全请求链捕获器)冷启动一次,抓全 SSL_write/SSL_read:
### 1. dfpReport 是冷启动自动触发,非登录专属
- 启动后 ~7.6s / 14.7s / 17.6s **自动连发 3 次** dfpReport(无需任何用户操作)
- 每次 dfpReport 后紧接 **dckey/check**(744B 请求,响应 665-667B),两者强关联
- 三次 dfpReport 密文均不同(len 4010/4137/4187/4262),但响应 actionV **全部同一** `7c5387...`
### 2. dfpReport 响应 = 344B 稳定 taf(关键!)
- 三次上报,每次返回 **606B HTTP(200 OK) + 344B body**,actionV 恒 = 真 hdid
- 与 wsapi 重放实验完全一致:body 就是 actionV(40hex) + t1 + base64 会话字段
### 3. 服务端识别机制最终定论
- **服务端识别设备 = 只靠明文三元组 (hdid, deviceId, appkey) 是否在设备库**
- 三次上报 collection/keystream 全不同,但 actionV 恒同 → **collection 不参与设备识别**
- 三元组在库 → 回显真 hdid;不在库 → 动态随机 token(拒绝)
- **这也推翻了文档第十一节 "collection 参与校验" 的猜测**:实际是三元组唯一主导
### 4. 对铸造的真正阻碍
- **不是加密**(XOR 已破解,任意帧 ks 可加密伪造明文)
- **不是 collection**(不参与识别)
- **是服务端设备库**:只有 (hdid, deviceId, appkey) 已在库的组合才会被认可为设备
- 光改明文三元组 → 不在库 → 拒绝。铸造需要伪造"库中已存在"的三元组,或走设备首次注册流程
- **行动**:抓"全新设备首次 dfpReport"(换新设备/清 huyaudb 数据后第一帧),看服务端是否为 unknown 三元组分配 hdid、分配后该 hdid 是否入库可复用 → 判断铸造是否可行
### 5. 清数据后的冷启动验证(新结论!)
- `adb shell pm clear com.duowan.kiwi` 后冷启动:**dfpReport 与 dckey 均未自动触发**
- 14 个请求全是杂项(cfg/日志/广-告/exapp/audid 等),无任何虎牙身份链请求
- 结论:设备身份链(dfpReport→dckey)只在 **App 数据已含身份 或 用户触发登录** 时才发,清数据后启动不会自动重建身份
- 完整看清全新设备首次 dfpReport 需 **用户触发登录**(clear 数据 → 登录 → 抓 dfpReport+登录链)
### 6. 再次验证:清数据后 dfpReport 仍自动触发且 actionV 恒同(重大修正!)
- **推翻上一节结论**:清数据后冷启动, dfpReport 在 t=1708/8695/11126/12274 **仍自动连发 4 次**
-**4 次 actionV 全部 = `7c5387...`(真 hdid)!**
- 真相: 清数据只是清 App 本地缓存, **明文三元组 (hdid/deviceId/appkey) 由固定密钥/算法派生,不受清数据影响**
- 服务端 4 次都识别为同一设备 → 设备身份不是"注册分配",而是**固定派生 + 服务端查库匹配**
- 铸造含义: 三元组无法通过"重新注册/清数据"刷新; 服务端认的是已入库的真正设备的新三元组变化 → 铸造难度进一步确认(见节十一/十二)
### 7. 决定性结论: 设备三元组 = 固定派生, 铸造的生死线
- **证据**: 三次清数据(pm clear)后的冷启动, dfpReport actionV **全部恒 = `7c5387...`**(跨 3 个独立 session)
- **含义**:
1. 三元组 (hdid=`7c5387...`, deviceId=`02df...`, appkey=`865a...e3`) **完全固定**,与本地数据无关
2. 服务端把"这三元组"当已知设备 → 回显 hdid; 任何与这三元组不同 → 动态随机 token(拒绝)
3. **铸造新设备 = 需要一组服务端已入库的 (hdid, deviceId, appkey)**; 或逆向三元组派生算法, 但即便派生成功, 服务端也不认(不在库)
- **因此铸造可行性取决于**:能否获得/伪造一组"服务端库中已存在"的三元组。若不能, 纯改明文铸造不可行
- **可验证方向**:
A. 抓服务端对**从未见过的新三元组**的初始动作(需真·全新设备——如换一台真机/模拟器首次启动), 看是否为 unknown 三元组分配 hdid 并入-库
B. 逆向三元组派生产生逻辑(hdid/deviceId/appkey 从何而来), 看是否能构造"合法派生的新三元组"
- 备注: "登录超时"现象可能与捕获/清数据导致的会话状态有关, 与三元组恒定无直接关系
---
---
## 十三、三元组派生逆向 (静态, OLLVM 混淆)
### 1. 静态线索
- libhydeviceid.so 内 .rodata @0x36c190 有明文 **MD5 初始消息种子** + A/B/C/D 常量
- MD5 特征移位 `ror #7` @0xc37c0(全 SO 唯一), 附近为大量循环左移/加法/状态机(`mov w#; cmp; b.eq` 状态分发)
- 0x36c230 有 hex 小写表 `0123456789abcdef` + 大写表 `0123456789ABCDEF`(hash 输出转 hex)
- **结论**:三元组派生用**自实现散列(MD5 系)+ OLLVM 扁平化混淆**,非标准库,常量被魔改/内联
- 三元组已知: hdid=40hex, deviceId=40hex, appkey=32hex, 见第 7 节
### 2. 静态快速尝试
- 常见设备因子组合(M2102J2SC/30/11/xiaomi/包名/等)拼接 MD5/SHA1 均不匹配 hdid/deviceId
- → 派生混入随机盐或复杂组合, 纯猜测难破
### 3. 效率判断
- 纯静态解 OLLVM 混淆成本高, 性价比低
- **更优: 运行时 hook 派生点** → 直接抓 (输入, hash输出) 真实对应关系, 避开解混淆
- 待办: hook MD5 实现(0xc37c0 附近) 的入参/出参, 抓三元组派生的真实输入
---
---
## 十四、模拟器探索(多号路线)
### 目标
- 用户想"每个号一个独立设备身份"避免风控, 尝试用模拟器作为"全新设备"环境
### 已就绪
- 模拟器(型号 PHU110, Android 12 API32, arm64, 已 root)
- 装了 huya apk (`apks/huya-13.4.22-115315-live.apk`)
- frida-server 15.2.2 (14.2.18 在模拟器上 spawn 超时; 15.2.2 spawn 正常)
- python frida 升级到 15.2.2 (uv 装到 venv)
- adb forward `tcp:31878 -> 31878` (模拟器 frida 独立端口, 不与真机 31877 冲突)
### 关键坑
1. **spawn 导致 EGL 崩溃**: App `RenderThread``Failed to create context, error = EGL_NOT_INITIALIZED` SIGABRT
- 这是 spawn 挂起导致渲染时序问题, **非反调试/frida 检测**
- 非 spawn 正常启动 App 则稳定存活
- → 需用 attach 而非 spawn (但 attach 慢会错过冷启动 dfpReport 窗口)
2. **反调试**: attach + SSL_write hook 未触发崩溃(与真机不同, 模拟器检测较弱)
3. 模拟器 adb/frida 连接可能因模拟器窗口关闭而中断 (端口探测全失败)
### 当前状态 (被模拟器连接中断卡住)
- 待模拟器重新连接后: 用 attach 抓冷启动 dfpReport, 验证"全新模拟器"服务端给的 actionV 是否为新 hdid
---
---
## 十五、模拟器全新设备验证 —— 决定性突破!
### 结论: 全新模拟器 = 独立设备身份 (服务端认可!)
- 模拟器(未登录过的全新环境)spawn+frida bypass 后, 冷启动自动发 dfpReport
- **dfpReport 响应 actionV = `e97dcfc3e46b0b4278d8cab1044c5fb01198c7fd`**
- 与真机 hdid `7c5387e0539c023c31c4ff0e807e7256117385ee` **完全不同**
-**服务端把模拟器识别为一台独立设备, 返回了独立稳定的 hdid**
- → "一个运行环境 = 一个设备身份" 成立
### 多号方案的技术基础确认
- 每个干净模拟器实例 = 一个独立三元组 → 独立 hdid → 设备身份隔离就不同
- 用户"很多号"可分配在不同模拟器/环境, 每个号用独立设备身份
- 不再共用真机的一个固定三元组 → 风控关联大幅降低
### 技术要点 (模拟器 frida)
- USB真机已拔, 只用模拟器 (PHU110, Android12, arm64, root, adb 127.0.0.1:5555)
- frida-server 15.2.2 (`/data/local/tmp/fs152`, 端口 31878); python frida 15.2.2 (uv 装)
- **spawn 必须【立即 resume】**, 挂起太久 App 的 RenderThread EGL 崩溃 (`EGL_NOT_INITIALIZED`)
- 挂起加载 bypass 后**要快速** resume, 加载本身没问题(用户提示 "不要自己制造困难")
- bypass 三件套 (bypass_msaoaid + mask_frida + patch_guard) 可直接复用 (lib 内偏移, 同 apk 版本)
- 脚本: scripts/hook_emu_spawn_bypass.py (OUT=evidence/emu_spawn_bypass_dfp.json)
### 待办
- [ ] 提取模拟器三元组 (hdid/deviceId/appkey): 用 XOR 差分或 hook 加密前明文
- [ ] 验证 e97d 身份是否多次稳定 (非动态 token)
- [ ] forge_replay 用模拟器三元组纯 Python 重放验证 (跨机器可重放性)
---
---
## 十六、多环境独立设备身份验证(核心成果!)
### 验证: 每台(重置)模拟器 = 独立设备身份, 且可纯 Python 重放
| 环境 | hdid (actionV) | 说明 |
|---|---|---|
| 真机 M2102J2SC | `7c5387e0539c023c31c4ff0e807e7256117385ee` | 基准 |
| 模拟器1 PHU110 (Android12) | `e97dcfc3e46b0b4278d8cab1044c5fb01198c7fd` | 已验证纯Python重放 |
| 模拟器2 SM_G998B (Android13) | `3a13955c404c1eea620c75f59b5c6de211863f2f` | 已验证纯Python重放 |
### 关键验证(决定性)
- 三台环境, **actionV 完全不同** → 服务端按环境分离设备身份
- 每台 spawn 抓一帧 dfpReport wire → **纯 Python socket+TLS 原样重放 → 服务端返回相同 actionV**
- **证明"每台干净模拟器 = 一个纯 Python 可重放的独立设备身份"成立**
### 模拟器2 (Android13 SM-G998B) 技术要点
- spawn + **纯 OpenGL(Vulkan)渲染** 解决 EGL_NOT_INITIALIZED 崩溃(src: 用户在模拟器设置选"只有 OpenGL")
- Android 12 那台靠"3-6s窗口"抓; Android13 这台纯OpenGL后 App存活久, 第一次尝试就抓到
- frida 15.2.2 spawn + bypass_msaoaid_maps_skip_cleanup.js(frida反调试全过) → 抓 frame
- frame 文件: evidence/frame_221833.json (dfp_wire + actionV=3a13955c...)
### 多号方案落地路径(成立)
1. 每台(重置)模拟器 → spawn 抓一帧 dfpReport → 获得该环境设备身份(actionV)
2. 之后纯 Python 用该身份与虎牙交互(可重放,已验证)
3. 每台配不同账号 → 各设备独立身份 ∈ 不同 hdid → 风控关联大降
## 十七、设备身份派生因子 排查(克隆模拟器)
### 测试: 改各种设备标识, 观察 hdid(actionV)是否变化
| 变更 | 方法 | hdid 变化? |
|---|---|---|
| IMEI | 用户手动改 `869390046034281``866084040768472` | ❌ 不变 (e97d) |
| Android ID/SSAID | `settings put secure android_id` 改 | ❌ 不变 (e97d) |
| ro.serialno | magisk resetprop 改 | ❌ 不变 (e97d) |
- 结论: clone 继承源身份(必然); 改常规标识无效 → 身份派生源是更底层字段(待 frida hook 定位)
- 延申: 真机/Android12/Android13 三环境 actionV 全不同 → 环境级标识决定身份.
## 十八、完整设备身份登记表(全部经纯Python重放验证)
| 环境 | hdid (actionV) | 状态 |
|---|---|---|
| 真机 M2102J2SC | 7c5387e0539c023c31c4ff0e807e7256117385ee | ✓重放 |
| 模拟器 PHU110 (Android12) 原机+clone | e97dcfc3e46b0b4278d8cab1044c5fb01198c7fd | ✓重放, clone继承相同 |
| 模拟器 SM_G998B (Android13) | 3a13955c404c1eea620c75f59b5c6de211863f2f | ✓重放 |
| **模拟器 GC3VE (Android12, 全新实例)** | **234548246275beedf8d93a242ef50e8f1178c3b2** | ✓重放 |
### 已验证的确定性规律
- **全新(非clone)模拟器实例 = 独立新 hdid**(GC3VE 产生第4个唯一身份)
- **clone 复制 = 继承源实例身份**(共享 e97d)
- 纯 Python 重放可稳定代表各环境身份(不依赖模拟器存活)
### 换身份(在单台上)已穷尽失败
- 改 IMEI / Android ID / ro.serialno / 整机型号(fingerprint) / 删App身份文件 / pm clear
- 全部不影响 hdid → 身份钉在模拟器底层硬件标识(qemu实例), 上层改动无效
## 十九、DeviceInfo(wup) 结构逆向 —— libudbauthunify.so (真机运行时确认)
### 关键: libudbauthunify.so 符号未混淆, 是正确逆向载体
libhydeviceid.so 是其下层的"采集+加密"黑盒; libudbauthunify.so 负责 wup 打包,符号清晰可逆。
### createWupDeviceInfo(0x2746a0) 输出结构 (真机 M2102J2SC 运行时 dump)
| DeviceInfo偏移 | 值(真机) | 含义 |
|---|---|---|
| +0 (dword) | 1 | DeviceInfo type |
| +8 std::string | M2102J2SC | deviceName/model (来自全局 4915C8) |
| +32 std::string | (heap, xxTeaAndBase64) | 加密设备字段(deviceId等, byte_491998==0时) |
| +56 std::string | android | systemInfo (来自全局 491610) |
| +80 std::string | M2102J2SC,30,11 | systemVer (来自全局 491628) |
| +104 | 2120 | heightPixels (来自全局 4915E0) |
| +128 | 1080 | widthPixels (来自全局 4915F8) |
| +152 std::string | BusinessCfg::getHdid() | **hdid** |
### 分支逻辑 (byte_491998)
- byte_491998==1: DeviceInfo[8/32/56/80] = 静态全局字符串(运行时datadiv解密后)
- else: 走 UdbUserFilterUtils::xxTeaAndBase64 编码 (加密设备字段, 可能是deviceId/appkey加密)
### 运行时解密全局(真机确认值)
- 4915C8='M2102J2SC' 491610='android' 491628='M2102J2SC,30,11'
- 4915E0='2120' 4915F8='1080' 491720=1
### getHdid(0x26a484) / getSafeDeviceId(0x26a3c4) / setSafeDeviceId(0x26a2e0)
- 三者都在 BusinessCfg 单例(getInstance)上操作
- setSafeDeviceId: 写 this+1008=safeDeviceId, this+1088=hdid
- getHdid: 读 this+1088 (锁保护)
- hdid 是提前经 setSafeDeviceId 存进 BusinessCfg, 再由 createWupDeviceInfo 取出填 DeviceInfo+152
- setSafeDeviceId 调用者 none(动态注册) → 真正 hdid 派生在 libhydeviceid(混淆区)
### 确认: keystream 每帧独立随机(同设备连续多帧密文仅27/4031字节巧合相同)
→ 帧间差分不可行; 恢复明文只能靠已知明文锚点+值推断
### hook 经验(真机 frida 15.2.2)
- 真机 spawn+G2-0055 bypass 稳定(nont crash), DeviceInfo 可反复抓到
- setSafeDeviceId 在 spawn窗口不触发(更早或另一路径)
- getHdid __usercall(a2@X8) 的 x8 在 frida onLeave 难取, 改读 this+1088 更实用
## 二十、真机完整明文 JSON 恢复成功 (libudbauthunify 逆向成果)
### 确认: 明文 JSON 完整模板 (真机 M2102J2SC, 双重验证)
- 前34B ({"appId":"5008","appVer":"13.4.22") XOR 恢复的 ks 前缀与模板完全一致
- hdid = 7c5387e0539c023c31c4ff0e807e7256117385ee 与文档真机 actionV 一致 (双重验证)
### 真机完整明文 JSON (586B):
{"appId":"5008","appVer":"13.4.22","appkey":"865a4924a40897ac1fcfe6b4c2cbb0e3","channel":"xiaomi","deviceId":"02df398797432eadefcc12767119ad5e80999389","deviceName":"M2102J2SC","hdid":"7c5387e0539c023c31c4ff0e807e7256117385ee","heightPixels":"2120","isCloud":0,"isForbidLog":1,"isHome":0,"isPre":0,"openAppId":"","savePath":"/data/user/0/com.duowan.kiwi/files/huyaudb","sdkVer":"1.0.80138","servantName":"huyaudbwebui","shareAppDataPath":"/data/user/0/com.duowan.kiwi/files/huyaudb","systemInfo":"android","systemVer":"M2102J2SC,30,11","terminalType":1,"testEnv":0,"widthPixels":"1080"}
### 三元组 (真机):
- appkey = 865a4924a40897ac1fcfe6b4c2cbb0e3 (32hex)
- deviceId = 02df398797432eadefcc12767119ad5e80999389 (40hex)
- hdid = 7c5387e0539c023c31c4ff0e807e7256117385ee (40hex)
### JSON 结构推导 (字段偏移, 真机模板):
appkey 值区间 [44,77] 32hex; channel [89,96]; deviceId [109,150] 40hex;
deviceName [165,175] 变长; hdid [184,225] 40hex; systemVer [518,534] 变长
其余字段值固定(savePath/sdkVer/servantName/systemInfo='android'/heightPixels='2120'/widthPixels='1080' 等)
### 重要结论:
- 所有设备 JSON 结构/固定值一致, 仅 hdid/deviceId/appkey/channel/deviceName/systemVer 不同
- 因此"python 零设备自生成"的 JSON 部分: 已完全确定结构, 只需能生成三元组+(deviceName/systemVer 可读自系统)
- ⚠️ 真正的唯一障碍 = **XOR keystream 生成器** (tag2 的 571882cf664bb39401ee 魔数后那段密文)
### dfp 加密路径归属判定:
- createWupRequestData/createWupPackage → 标准 taf/wup + base64 (通用业务请求)
- dfpReport 的 tag2 加密(XOR keystream) → 在 libhydeviceid.so (OLLVM混淆), 与 wup 路径不同
- getOtp/hyudb_otp_encrypt → 登录 OTP (AES + nonce), 非 dfp 路径
### createWupPackage 事务流 (37B020, FindPassword示例):
createWupReqHeader + createWupDeviceInfo + createWupProtoInfo → UniPacket → createWupPackage
→ put<Req> → doEncode → CBase64::Encode (标准wup二进制+base64)
### hook 经验补充:
- setSafeDeviceId 在 spawn 极早期一次性调用(dlopen后立即), 轮询命中不到
- 内存扫描找明文JSON太重(SSL_write触发时scan来不及), 弃用
- 真机 frida15.2.2 spawn+G2-0055 稳定抓 dfp wire (4298-4316B 每帧)
## 二十一、dfpReport 完整 wire 格式确认 (重要, 修订此前的"MAGIC后全XOR密文"理解)
### 真实 wire 结构 (真机抓到, SSL_write是明文!):
```
POST / HTTP/1.1
i-ver: 1
Content-Type: application/octet-stream
Host: wsapi.huya.com
User-Agent: okhttp/3.14.9
(blank)
body = taf/wup 二进制包 (4111B)
```
**SSL_write 抓到的就是明文 HTTP** (dfp 用 OkHttp Java TLS 发送, 调用栈 libjavacrypto.so)
### body (taf/wup TLV) 结构:
```
00 00 10 0f 4B 大端总长
0c "huyaudbwebui" servantName
0f "dfpReport" functionName
0d "tReq"→ "android" 字段/参数
2d 00 01 0f ...
[ M MAGIC: 57 18 82 cf 66 4b b3 94 01 ee ] (0x57=tag2类型标记)
[ cw: 明文JSON^keystream(586B) + collection^keystream(3445B) ]
```
### cw 分两段:
- cw[0:586] = 明文JSON XOR keystream (JSON结构已全解, 见第二十节)
- cw[586:] = collection XOR keystream (3445B, 动态: 每帧不同, 长度也变3445/3460)
### triple in 明文JSON (真机):
hdid=7c5387e0539c023c31c4ff0e807e7256117385ee
deviceId=02df398797432eadefcc12767119ad5e80999389
appkey=865a4924a40897ac1fcfe6b4c2cbb0e3
channel=xiaomi deviceName=M2102J2SC systemVer=M2102J2SC,30,11
### 唯一黑盒剩余:
- XOR keystream 生成器(JSON可解586B, collection明文未知不可解) 在 libhydeviceid.so(混淆)
- collection 的内容/构建(动态随机, 含appkey? deviceId? 时间戳?)
- 三元组派生(在 libhydeviceid 混淆区)
### 模型脚本: scripts/dfp_model.py (wire解析+重建验证)
## 二十二、★★ XOR keystream 生成器定位 (重大静态突破) ★★
### 在 libhydeviceid.so (OLLVM混淆) 找到了 keystream 生成链:
sub_1D1A68 (caller 0x1d3ab0) 是 keystream 种子生成器, 其解码后关键调用:
```
sub_8ECCC(buf, a1, 0); // 用a1初始化随机状态
v9 = time(nullptr); srand(v9); // ★ 种子 = time(NULL) (秒级时间戳!)
v58 = rand(); // rand() 取首随机值
sub_1D0BB8(v58, keystream_state) // 用 rand() 扩展成keystream
```
### 链: srand(time) → rand() → sub_1D0BB8 → sub_1D14C4 (ostream格式化) → 输出keystream字节
### 关键意义:
- keystream 种子 = time(NULL) 秒级时间戳 → **理论上若能复现 sub_1D0BB8 对 rand() 输出的扩展, 即可预测 keystream**
- libhydeviceid 内置 `.srand`(0x464d0)/`.rand`(0x46060) — 非 libc, 自有实现
- sub_1D0BB8: rand()初始值 经 sub_1D14C4(ostringstream, num_put格式化) + 大量C++ std, 生成流
- 动态验证受挫: libhydeviceid 延迟加载, spawn窗口 hook不到 base; 需 wait 模块 + 对内部 .srand/.rand 地址 hook
### 待逆:
- sub_1D0BB8 具体如何把 rand() 一个 u32 扩展成 4031B 字节流 (核心)
- keystream 是否 = rand()连续值 的某种字节拼接, 或经 ostream 格式化的十进制/hex文本
## 二十三、确认 keystream 使用 libc bionic 的 srand/rand (决定性)
### objdump 证实: 0x46060=rand@plt, 0x464d0=srand@plt
- libhydeviceid.so 内 `.rand`/`.srand` 是标准 PLT thunk, br 跳转到 GOT → 实际解析到 bionic libc 的 rand/srand
- 因此 sub_1D1A68 里 "srand(time); rand()" 调用的是 **libc 标准 rand** (glibc/bionic 兼容, 可预测)
### 完整 keystream 生成链 (已确认):
sub_1D1A68 (由 0x1d3ab0 调用):
sub_8ECCC(buf, a1); // 预初始化
srand(time(NULL)); // w9 = time(nullptr) 秒级
v58 = rand(); // 第一个 rand()
sub_1D0BB8(v58); // 扩展成 keystream: sub_1D14C4(ostringstream) + ostream输出
### 意义:
- 因为 rand() 输入是 time(秒), 而 bionic rand() 序列是确定性的
- 若能逆出 sub_1D0BB8 如何把 rand() 序列(或首值)变成 4031B 字节流, keystream 可完全复现
- 关键: 需知道 keystream 每个字节来自 rand() 哪个输出/如何取低字节
### 运行时 hook 受阻(hook libc srand/rand 的 libhydeviceid 调用过滤):
- libc srand export attach "unable to intercept" (地址被当作不可拦截对象)
- 改用编译期 + 在真机调 libmsaoaidsec 时机对齐难
## 二十四、本轮(round 5)成果: keystream 生成链 + rand/srand 归属确认
### 1. sub_1D1A68 是 libhydeviceid 里的"种子/RNG 初始化函数"
- 链: sub_1D1A68 ← sub_1D3AB0 ← sub_1D5844 递归调用
- 内部: time(nullptr) -> srand(v9=time 秒) -> v58=rand() -> sub_1D0BB8(v58)
- sub_1D0BB8: rand()种子v58 经 C++ iostream(num_put/ostringstream/sub_1D14C4十进制格式化)+sub_8F66C 展开成字节流
- nm 确认: rand/srand 都是 `U rand@LIBC` (undefined) → 解析到 Bionic libc 标准 rand/srand (可预测)
### 2. 运行时 hook 观察 (真机):
- libc srand export 无法 Intercept ("unable to intercept at 0x791a282e78", 页面边界问题)
- libc rand 可 hook(rd_ok),但 dfp 期间未捕获到来自 libhydeviceid 的 rand() 调用
- 结论: sub_1D1A68 可能跑在 spawn 极早期(非每帧), 或 dfp 的 keystream 不直接=该路径
- 待 subagent 静态还原 sub_1D0BB8/sub_1D14C4 确定 keystream 是否 = rand() 流的某种变换
### 3. 离线 LCG 测试:
- keystream 每4字节LE组 不匹配 glibc rand LCG → keystream 经变换(非裸rand输出)
- 无法离线验证 rand-seed(缺秒级时间戳) → 需真机抓 seed+同帧
### 4. 完整 wire 格式(最终):
HTTP POST(okhttp) + taf(body: huyaudbwebui/dfpReport/tReq/android)
+ MAGIC(5718..) + [JSON^ks(586)] + [collection^ks(3445, 动态)]
### 5. 结论:
- 明文JSON/taf结构/加密模型 已全解(多轮双向验证)
- 唯一黑盒 = XOR keystream 生成器的精确算法 + collection 内容
- subagent 正在静态还原 sub_1D0BB8(rand->keystream展开) 的精确字节生成
## 二十五、重要修正: sub_1D1A68 (srand(time)/rand) 不是 dfp keystream 路径
### 运行时证据 (真机 hook sub_1D1A68@0x1d1a68):
- 真机反复 spawn 抓 dfpKS_ENT(入口) 一次都没触发
- 但 dfp SSL_write 每次都触发 (tid 21165/22283/22627/23944)
- libc rand/time 频繁被调(其他SDK组件), 但都不是来自 dfp keystream 上下文
**sub_1D1A68 是其他功能(RNG初始化), 不参与 dfp 每帧 keystream**
### 修正结论:
- 之前的 "srand(time)->rand()->sub_1D0BB8 生成 keystream" 假设**不适用于 dfp**
- dfp 的 XOR keystream 每帧独立随机的真正生成器在别处(libhydeviceid 混淆区, 未定位)
- MAGIC(571882cf664bb39401ee) 在二进制里**无连续字面量** → 全部运行时从.datadiv解密构造
- collection 段(3445B) 完全随机, 无明文头结构; 是纯 XORK(明文未知)
### 对 subagent 深挖的校准:
- 需确认 sub_1D0BB8 是否真是 dfp 相关; 若是则要 hook 真实入口
- 或重新定位 dfp 加密函数(通过 hook 构造 tag2/MAGIC 的内存写入)
## 二十六(round6) 进展: 逆向路径重要修正 + 新切入点
### 1. sub_1D1A68 (srand(time)->rand) 确认不是 dfp keystream 路径
- 真机反复 hook, KS_ENT 从不触发, 但 dfp 每次都发
- 该函数是 SDK 其他 RNG 功能, 排除
### 2. dfp 上报入口: HandlerReport::_report (libudbauthunify@0x3cbba0)
- 签名: _report(this=a1, string& a2=payload) — payload 在 a[1]
- 内部: UdbLog "HandlerReport report data is %s" + std::string::operator=(v33,a2) + UdbBusinessWraper::CreateSession(0x1031...) + UdbContext
- 但这可能不是 dfp 加密路径(它读a[1]为SSO string,garbage)
### 3. 关键: MAGIC(571882cf664bb39401ee) 在二进制里无连续字面量
→ 完全运行时从 .datadiv 解密 + 构造; 无法字符串定位加密函数
### 4. collection 段确认纯随机每帧 (无明文头)
### 5. 结论: dfp XOR keystream 生成器仍在 libhydeviceid 混淆区深处,
未通过"字符串/导出/MAGIC/rand"定位到; 加密可能是自实现流密码
(subagent 曾发现 mask 0x7070.../0x8C/0x0B/0x05 的解码线索但被中断)
## 二十七(round7) 进展: 模型精确定位 + 模板发现 + 新攻击面
### 1. ★决定性模型确认: cw = [2B会话前缀] + [JSON^ks 586B] + [collection^ks ~3445B]
**8帧独立spawn对齐测试(决定性)**:
```
各偏移下 "8帧全同位"(keystream在该偏移全帧相同) 数量:
off=0: 2 个 [0,1] ← cw[0:2]=前缀, 恒定
off=1: 1 个
off>=2: 0 个 ← JSON密文从这里开始, keystream全随机
```
- **JSON 密文起点 = cw[2]** (不是cw[0]!), cw[0:2]=`7cdb`类2B前缀是会话/编码头
- dfp_pair 黄金配对验证: `cw[2:588] XOR plain[:586] == JSON` 100% True
- 修正此前所有基于"cw[0]对齐"的ks推导 (应使用off=2)
### 2. ★跨会话模板 A/B 重复 (fastsame vs full8 对照)
- 模板A `7cdb1189c175289c4d5621de6e647a90`:
- fastsame run3(ts=1787764797)/run4(4811)/run6(4838) + full8 run8 (跨~10分钟, 不同spawn)
- 模板B `7cdb8188c875341d72d5e1dd90ceda9e`:
- fastsame run2(4784)/run5(4825)/run7(4851) + full8 run4
- 但**完整4035B跨帧仅0.5-1.1%相同** → 头部14B有确定性模板, 主体每帧随机
- 解释: **app缓存了少量历史dfp帧, 跨会话复用头部模板** (或短周期PRNG); 不能证明keystream主体确定
### 3. collection 段结构 (长度每帧变 3443-3558)
- frame_real: coll len = 3443/3458/3453; dfp_pair = 3558; full8 = 3449
- collection 密文自相关=随机水平, 无周期; 明文未知, 但字段名已从内存字符串确认:
`adb_input_list/event/upload_motion_event/deviceinfo/touch_position/is_applist_upload/installed_appname/scanned_appname/script_push/screenrecord/display_info/update_display_info`
- → collection 是动态采集数据(触摸/输入/App列表等), 与采集状态相关
### 4. ★GetDfpConfig 流程发现 (重要新攻击面)
- 内存字符串区含 `wup.GetDfpConfigReq` / `wup.GetDfpConfigResp` / `wup.DfpReportReq` / `wup.DfpReportResp`
- 运行时内存可扫描到 GetDfpConfigReq@0x78145c3594, GetDfpConfigResp@0x78145c3a44
- **假设流程**: 客户端先 GetDfpConfigReq 取配置(可能含keystream种子/密钥) → 再 DfpReportReq
- 若 config 响应含种子, hook 该请求即可拿到每会话 keystream 种子! (待验证)
### 5. libhydeviceid 数据区密钥/身份线索 (0x78133bf220 区 4600B dump)
```
7jbF5IiqqIDdgEtiNQLzJSuTZmRk8sbc ← 28字符, 疑似固定密钥/盐
udb_deviceID / udb_deviceID_encode ← deviceID 编码函数
/sdcard/duowan/d2b08ce4-1e6c-4515-b320-680ecdd98da2.rt ← GUID缓存文件
/sdcard/duowan/64a33427-f53c-4162-8738-81aa9117b950 ← 另一UUID
/files/hydevice/64a33427-f53c-4162-8738-81aa9117b950
cdid / cdid_version / HUAWEI / 1.13.11 ← ctid相关
java/util/UUID randomUUID ← GUID用Java UUID生成
version.create_time.type / table / udb_guid
961c151d2e87f2686a955a9be24d316f1362bf21 3.11.3 ← commit hash + SDK版本(3.11.3)
sdid: / hdid: ← sdid/hdid 日志前缀
```
### 6. ★明文 collection 类 JSON 完整样例 (自生成模板!)
- 内存扫描 `"device_id":"` 模式抓到多个完整明文JSON (不同uri):
- **hycredLogin 变体(552B)**: `{"app_ver":"13.4.22","appid":"5008","bypass":3,"carrier_type":4,"channel":"xiaomi","credLoginType":0,"device_id":"02df3987...","device_name":"M2102J2SC","hdid":"7c5387...","level":1,"local_time":1787721670153,"net_state":0,"net_type":4,"pid":9057,"requestId":4115362,"rescode":"0","rsp_time":222,"sdk_ver":"1.0.80138","sign":"","system_info":"android","system_ver":"M2102J2SC,30,11","term_type":1,"trace_id":"0b8f098ff64a5bdc-9057-2880590787721669925","uid":1199666918455,"uri":"hycredLogin"}`
- loadcred变体(453B)/selectOperator变体/native init变体(全部含 device_id/device_name/hdid/level/local_time毫秒/term_type/trace_id/uid 字段)
- local_time 为**毫秒时间戳**; trace_id 格式 `hex32-pid-timestamp`
- → 这些是其他udb上报的明文模板, 证明**明文JSON家族结构**, dfp collection可能类似
### 7. 模块清单 & wire 协议确认
- dfp wire = **明文HTTP POST**(无TLS!): `POST / HTTP/1.1\ni-ver: 1\nContent-Type: application/octet-stream\nContent-Length: 4111\nHost: wsapi.huya.com\nUser-Agent: okhttp/3.14.9` + body
- 模块: libudbauthunify@0x77bd589000, libhydeviceid@0x7813009000, libhycrypto@0x77e1b40000(纯OpenSSL静态库2512符号), libhyssl, libhyquic
- SSL_write backtrace 显示 Java 层(libjavacrypto/libart) → 部分请求走Java TLS; dfp明文HTTP走 libc send (但send hook未抓到dfp, 疑走 okhttp 内部路径)
### 8. libudbauthunify 导出加密函数 (nm -D, 12378符号)
```
0x330058 encode_xxtea / 0x3306cc decode_xxtea (std::string版)
0x32e83c hyudb_crypt_util::xxtea_encrypt / 0x32ece8 xxtea_decrypt
0x252700 hyudbxxt::xxtea_encrypt(Pjj S0) raw版 / 0x252b84 xxtea_decrypt
0x330218 encode_aes / 0x330498 decode_aes
0x27f820 getRandomSession ← 随机会话, 高度可疑
0x2815f8 genRandomTrace / 0x280c8c genTraceId
0x331560 IdGenerator::encode / 0x331970 IdGenerator::decode (id编码, 疑hdid/deviceId派生)
0x32fa24 hyudb_otp_encrypt / 0x331d58 hyGenUnionId / 0x332478 hyGenUnionIdNew
0x26871c AESkeyMgr::getkey / 0x24eb08-0x250138 UdbAESUtil全套(自实现AES)
0x24b430 Base64::encode / 0x24b6b0 decode / 0x24dca8 HuyaMd5::encode / 0x24dea0 decode
```
- **注意**: frida Module.getExportByName 找不到这些符号(运行时可能被strip/混淆) → 用固定地址 base+offset hook (已尝试但 findModuleByName 报 nomod, 需修正: 模块名大小写/延迟加载)
### 9. 遗存问题/下一步
- [ ] hook GetDfpConfigReq 响应抓 keystream 种子 (最优先)
- [ ] 用 base+offset 固定地址 hook getRandomSession/xxtea (修正模块查找)
- [ ] 深挖 IdGenerator::encode (hdid/deviceId 派生链路)
- [ ] 确认 cw[0:2] 前缀与 GetDfpConfig token 的关系
- [ ] 需要再抓至少2-3组"同秒/同分钟"独立spawn对, 验证模板A/B是否=缓存帧完整复用(而非仅头部)
## 二十八(round7补) ★★ cw 尾部 10B 固定 footer 发现 (重大结构修正) ★★
### 铁证: 13/13 独立捕获 (真机8+模拟器6+dfp_pair) cw 尾部 10B 完全相同
```
3600400c0b8c980ca80c
```
- 覆盖: frame_real 3帧(23:44), dfp_pair(13:28), dfp_coll_1/2/3, identity_223140~225933 6份(模拟器)
- 跨设备/跨时间/跨会话全部一致 → **固定 footer, 不参与 keystream XOR**
### ★修正后的完整 cw 结构 (模型终版):
```
cw = [2B会话前缀 7cdb/6cdb/1cda...]
[JSON明文586B ^ keystream]
[collection明文 ~3433B ^ keystream(同一条)]
[10B固定footer: 3600400c0b8c980ca80c]
```
- 长度验证: dfp_pair cw=4146 = 2+586+3548+10 ✓ (collection=3548)
- frame_real cw=4031/4046/4041 → collection 3433/3448/3443
- 尾部距尾偏移0-9全帧同值, 偏移10起随机 (已用距尾偏移正确验证)
### taf 层结构 (dfp_pair):
```
00 00 10 82 <- 4B 大端长度 4226
10 03 2c 3c 4c 56 0c "huyaudbwebui" 09 "dfpReport" <- servant+func 区
7d 00 01 10 56 <- ?
08 00 01 06 04 "tReq" 1d 00 01 10 48 <- tReq 字段
0a 06 00 16 07 "android" 2d 00 01 10 32 <- android 字段头
57 18 82 cf 66 4b b3 94 01 ee <- MAGIC(10B)
cw(4146B) <- 2B前缀+JSON^ks+collection^ks+10B尾
```
### 修正引发的重新推导:
- **此前所有"cks 全段随机"结论需按新边界复核**:
- JSON段(586B) keystream 全随机 ✓ (不变)
- collection段(3433-3548B) keystream 全随机 ✓ (不变, 但尾部10B除外)
- **10B 尾部是常量, 不用生成!**
- 对 python 自生成的意义: 尾部硬编码 `36 00 40 0c 0b 8c 98 0c a8 0c` 即可
### 待解问题(更新):
- [ ] collection 明文内容 + 长度可变机制 (3433 vs 3548差115B)
- [ ] keystream 生成器 (仍是唯一大黑盒)
- [ ] cw[:2] 前缀 vs GetDfpConfig token 关系
- [ ] 尝试: 若尾部固定+2B前缀固定, 是否服务端只校验 JSON 部门分字段?
## 二十九(round7补) ★★★ GetDfpConfig 完整请求/响应抓到 + wup 协议尾确认 ★★★
### GetDfpConfig 请求 (67B, CL=67, 明文HTTP POST /, wsapi.huya.com):
```
0000 0043 <- 4B 大端总长 67
10 03 2c 3c 4c 56 <- 魔数(与dfp同)
0c "huyaudbwebui" <- 0c=长度12 servant
0c "getDfpConfig" <- func
7d 00 00 15 <- ?
08 00 01 06 04 "tReq" 1d 00 00 08
0a 06 04 "5008" <- appId
0b 8c 98 0c a8 0c <- ★wup 协议尾! (与 dfp cw 尾部共享 8c980ca80c)
```
### GetDfpConfig 响应 (190B, multipart-formdata, 99ms后返回):
```
0000 00be <- 4B 长度 190
1003 2c3100804c56 <- 魔数变体
0c "huyaudbwebui" 0c "getDfpConfig"
7d 00 01 00 8d 08 00 02 06 00 1d 00 00 01
0c 06 04 "tRsp" <- 响应字段
1d 00 00 79 <-
0a 08 00 01 06 04 "5008" <- appId
16 "lNLlGtxbxSB2FhJgLec91a5YuecmZ11IBaQTB/AfWiWlKFI74ciBoJnRUmAiOYb4/Bi99jRoCduKcAEgGKsjitY/g0e43bEEkcti5Ek0Lv30=" <- ★base64 81B 数据
0b 8c 98 0c a8 0c <- wup尾
```
### ★wup 协议尾确认: `0b 8c 98 0c a8 0c`
- 出现于: getDfpConfig 请求尾、getDfpConfig 响应尾
- 与 dfp cw 尾部 `36 00 40 0c 0b 8c 98 0c a8 0c` 共享 `0b8c980ca80c`
- 结论: **`0b8c980ca80c` 是 wup/taf 固定协议尾, 不是 dfp 特有**
- dfp cw 尾部 10B = `36 00 40 0c`(dfp特有) + `0b8c980ca80c`(wup协议尾)
### Config 响应 base64 数据 (81B):
```
94d2e51adc5bc520761612602de73dd5ae58b9e726675d4805a41307f01f5a25
a528523be1c881a099d15260223986f8fc18bdf6346809db8a70012018ab238a
d63f8347b8ddb10491cb62e449342efdf4
```
- 全随机 → 加密配置(疑 XXTEA/AES), 与帧头无直接字节关联
- 未解: 解密后是否含 keystream 种子/前缀
### dfpReport 响应 (344B, 抓到了! 读#48):
```
00000158 10032c3100804c56 0c "huyaudbwebui" 09 "dfpReport"
7d 00 01 2a 08 00 02 06 00 1d 00 00 01
0c 06 04 "tRsp" 1d 00 01 15
0a 06 02 "10" <- 某字段 "10"
16 "865a4924a40897ac1fcfe6b4c2cbb045" <- ★appkey 回声 (cbb045 变体!)
26 "PQwemAN9NHkZKoMqXq7sMRIypqMTaQEOrmXr37xQVhQZqrP5jkKEQ11xvE02qahX9cjIxHA4cI+3n5jHKeMMlkpumIIMJh5IPEXN0CjJHY07KeKx0VQQmCPvAEOFqjj2qF+X7zJGVLQlxK1tj9X6ZOHFr/FHL56Q5qHyTdsYCl8fYjpkmVX32" <- base64 token
00 00 a8 c0 46 06 "actionV" "7c..."
```
- dfpReport 响应也返回 base64 token (172B)! 与 config 的 81B 不同
- appkey 回声是 cbb045 (非 cbb0e3) → appkey 变体机制待确认
### 新发现: getDfpConfig 不是每次 spawn 都发!
- 有缓存: 清数据(pm clear)后重新拉取
- 正常 spawn 时可能不发 → 之前的 hook 一直没抓到是因为缓存
### 结论更新:
- ★dfp 流程 = GetDfpConfig(取加密配置) → 用配置生成 dfpReport(含 keystream)
- 若 config 解密后含种子 → hook 解密点即得 keystream
- appkey 变体 cbb045/cbb0e3/abc798 机制待解
### 待办(更新):
- [ ] ★★Config 81B 解密 (xxtea/aes? 种子在其中?)
- [ ] config 响应变异性测试 (多 spawn 对比)
- [ ] appkey 变体机制
- [ ] dfpReport base64 token 用途
## 三十(round7补) ★★★★★ 差分实验: 服务端不校验 cw 内容! python 零设备生成达成 ★★★★★
### 网络通路验证 (python ssl socket POST https://wsapi.huya.com/):
- 直接重放真实 wire body → **HTTP 200 + dfpReport tRsp 响应 (606B)**
- 响应: appkey回声(865a4924a40897ac1fcfe6b4c2cbb045变体) + base64 token(每次不同!) + actionV=真机hdid(7c5387e0...)
- **确认纯 python(无设备/无frida) 可发 dfp 并获响应**
### ★差分实验结果 (改 body 不同部位看服务端反应):
| 修改 | 结果 |
|---|---|
| 改为原样 | ✅ 606B 200 OK |
| 改 MAGIC 1B | ✅ 200 OK (MAGIC不校验) |
| 改 cw[0] (JSON段) | ✅ 200 OK |
| 全随机 JSON段 586B | ✅ 200 OK |
| 全随机 collection 3548B | ✅ 200 OK |
| 随机 2B 前缀 | ✅ 200 OK |
| **整 cw 全随机(保留尾)** | ✅ **200 OK** ← 决定性! |
| cw 长度变 4000/4290 | ❌ 0B 拒绝 |
| 尾部 10B 改动 | ❌ 拒绝 |
| taf 总长字段(body[0:4])错 | ❌ 拒绝 |
| 截掉尾部 | ❌ 拒绝 |
### ★★★ 最终结论 ★★★:
1. **服务端不校验 cw 内容** (JSON^ks/collection^ks/前缀/MAGIC 全随意)
2. **必须保证**: taf 总长 4B 正确、cw 长度=4146(2B字段@body[68:70]=0x1032)、尾部10B固定 3600400c0b8c980ca80c
3. **keystream 逆向不再必要** -> python 生成 = 随机数填 cw + 固定结构!
4. cw 长度必须 4146 -> 服务端按固定块长度解析
### body 完整结构 (4226B):
```
[0:4] 0000 1082 4B 大端总长
[4:10] 1003 2c3c4c56 wup魔数
[10:23] 0c "huyaudbwebui" servant (len-prefixed)
[23:36] 09 "dfpReport" func
[36:40] 7d 00 01 10 56
[40:47] 08 00 01 06 04 "tReq"
[47:52] 1d 00 01 10 48
[52:59] 0a 06 00 16 07 "android"
[59:68] 2d 00 01 集合字段头 (tag 0x2d?)
[68:70] 10 32 ★cw长度 2B (0x1032=4146)
[70:80] 571882cf664bb39401ee MAGIC 10B
[80:4226] cw 4146B = [2B前缀]+[586B JSON^ks]+[3548B coll^ks]+[10B尾]
尾 = 3600400c0b8c980ca80c (固定)
```
### python 零设备生成公式:
```
body = taf_header(len+servant+func+tReq+android) + magic + os.urandom(4136) + tail
```
- 或最小改动: 复制模板 body[:80], cw=os.urandom(4136)+tail, 长度字段写死
- **已验证: 随机 cw 直接 200 OK!**
### 待办更新:
- [x] Config 解密 (不再必要)
- [x] keystream 逆向 (不再必要!)
- [ ] python 生成器代码 (含三元组随机化)
- [ ] 三元组独立性验证 (换随机 hdid/deviceId/appkey 服务器接受?)
- [ ] collection 长度 3548 是否必须 (cw 总长 4146 限制)
- [ ] hdid 与 actionV 关联机制 (服务器从何处知 hdid?)
### 三十一(round7补) actionV 来源确认 + nonce 机制:
- 响应 actionV = 服务端从 cw JSON段解密出的 hdid (hex 40)
- 改 cw 中 JSON 段 hdid 位置 -> actionV 变化 (任意 off 2/16/32 都变)
- 改 2B 前缀 -> actionV 变化 -> ★前缀参与解密重建 (nonce/IV 角色)
- 改 JSON 前16B -> actionV 变
- 但全部仍 200 OK! -> 服务端对解密结果宽容,不拒绝乱码
- 三处改动 actionV 均为 40-hex: 服务端把解密块 hex 化显示
### 服务端解密模型 (猜想):
- cw = [2B nonce] + [586B ...] + [3548B coll] + [10B 尾]
- 服务端: keystream = F(前缀nonce, 固定密钥/种子) -> XOR 解密 JSON 段
- 密钥来源疑: GetDfpConfig 固定 81B 或设备密钥 7jbF5Iiq...(28字符)
- nonce 使每帧密文不同但可解密 -> 解释"keystream 每帧随机"现象!
- 风控可能后续用 actionV/hdid 与登录流程对账 (未验证)
### ★结论升级: python 生成两个层次
1. 格式层 (已达成): taf框架+随机cw+固定尾 -> 200 OK
2. 语义层 (待定): 若风控对账 hdid, 需能构造"服务端可解密出指定hdid"的cw
- 需逆向 keystream 生成器 (F(prefix, key))
- 或验证: 服务端根本不核对 hdid 一致性 -> 随机即可
- ★实验: dfp 上报后用"另一个hdid"登录测试风控 -> 决定层次2是否必需
## 三十二(round7终) ★★★ 最终交付: python 零设备自生成 dfpReport 达成 ★★★
### 生成器: tools/dfp_gen.py + tools/dfp_cli.py
- 用法: cd tools && .venv/bin/python dfp_cli.py -n 5 -o out.json
- 输出: 随机三元组 (hdid/deviceId/appkey) + body hex + 发送结果
### 验证记录 (5/5 全部 200 OK):
```
[1] ok=True actionV=2fe5ddbd0211f39daf2ba3c2e4d7699810d920b1 ...
[2] ok=True actionV=5bf5fce774f8e1bff235de5f733196301011f61e ...
[3] ok=True actionV=..., [4] ok=True, [5] ok=True
```
### 生成公式 (零设备, 纯 python):
```
body = 4B总长 + TAF_HEAD(66B) + MAGIC(10B) + cw(4146B)
cw = os.urandom(2) + os.urandom(586) + os.urandom(3548) + 3600400c0b8c980ca80c
```
- TAF_HEAD 含: wup魔数+servant(huyaudbwebui)+标记66+func(dfpReport)+tReq+android+cw长度0x1032
- ★服务端不校验 cw 内容 (整段随机已证 200 OK)
- ★必须保持: 总长4B 正确 / cw长度固定4146 / 尾部10B固定
### 服务端可解密 cw 但宽容:
- actionV = 服务端从 cw JSON段解密出的 hdid (改cw该段->actionV变)
- 2B前缀参与解密(改前缀->actionV变), 疑为 nonce
- 但解密乱码不回绝 -> python 随机 cw 完全可用
- ★后续若风控校验 hdid一致性(登录用), 需做语义层: 构造服务端可解出指定hdid的cw(逆向ks生成器)
当前阶段(上报)无需; 待用户测试登录流程后定
### 剩余待办 (记录备查):
- [ ] 服务端 hdid 一致性校验验证 (dfp 上报 vs 登录) - 决定是否需要语义层
- [ ] 跨设备/跨号风控实测 (多三元组轮换)
- [ ] 速率限制/阈值观察 (连发5次未限)
- [ ] 真实 flow: 生成器三元组 → hycredLogin 等登录链路对接
## 三十三(round7续) ★ 登录链路对接验证: 生成器注册链端到端出 cred ★
### 完整链路 (全部纯 python, 已验证):
```
[零设备生成器] [wsapi.huya.com] [wup.huya.com]
getDfpConfig(67B) -> 190B 配置(固定,含key材料)
selectOperator(自定义sd+fp) -> 187B 运营商索引
dfpReport(随机cw 4146B) -> t1(恒=appkey变体回声) + t2(action 新签发) + t5(device_id)
↓ 登录帧字段
hypasswordLogin(hdid金样本 + t2 + t5 + 独立画像) -> cred 114B ✅ / safe_auth滑块(可自动过) ✅
```
### ★关键实证 (本轮新增):
1. **t1 恒不变** = 865a4924a40897ac1fcfe6b4c2cbb045:
- selectOperator 全改(sd+fp+机型) 都不影响 t1 → **t1 不是设备锚**
- t1 = appkey 变体回声 (服务端应用配置; dfp JSON 里 appkey=cbb0e3 变体)
2. **t5/actionV = 服务端从 cw 解出的 40hex hdid**:
- 金样本 cw → t5=7c5387e0(真机hdid); 随机 cw → t5=随机值
- 服务端对"解密乱码"宽容(仍 200 + 签发 t2) → 随机 cw 完全可用
3. **登录帧 32hex hdid (ed0db8...) 是唯一不可铸造硬锚**:
- 换任意新值 → APP_SIGN_NOT_MATCH 硬错(652B, 无滑块)
- 来自 libhydeviceid.so 本地生成 + native setDeviceInfo(0xb000021) 上报, 不走 HTTP
4. **服务端校验矩阵(登录)复验**:
| fingerprint 随机 | safe_auth 滑块 → solver 自动过 → cred ✅ |
| device_id 换值 | 直接通过 ✅ |
| hdid 换值 | APP_SIGN_NOT_MATCH ❌ |
5. **生成器注册链端到端**:
- 随机 cw 的 dfpReport → 新签发 action/device_id → 登录出 cred (金样本hdid)
- 纯 python 零设备! 设备身份字段(t2/t5)全部服务端新签发
### 对"每账号独立设备"的最终答案:
- 可达: 每账号随机 fingerprint/device_id/机型/vendor/screen + 独立 sdid(action 每账号新拉)
- 不可达(纯代码): 新 32hex hdid (libhydeviceid.so 黑盒 + native setDeviceInfo 无 HTTP 通道)
- 共用金样本 hdid 登录无碍, 但服务端可设备维度关联账号 → 若需真隔离须真机设备池
(tools/huya_device_profile.py 已实现画像池, 端到端验证过)
### 本轮验证记录:
- 生成器随机cw + 金样本hdid + 金样本fp → cred 114B ✅ (2次)
- 生成器随机cw + 金样本hdid + 随机fp → safe_auth滑块 ✅ (预期, solver可过)
- selectOperator 全改实验 → t1恒不变, t5随cw变 (证 t1非设备锚)
## 三十四(round7终) ★★★ 登录链路对接完成: 零设备注册链 → cred 全链路实测 ★★★
### 最终端到端 (tools/huya_device_register.py --gen-login):
```
注册链: t1(appkey回声)=865a4924a40897ac1fcfe6b4c2cbb045
t2(action)=PQwemAN9NHkZKoMq... len=180 ← 服务端新签发
t5(device_id)=de440c0a1c5e26949c52d485152366bc102118d4 ← 服务端新签发(随机cw解出)
登录结果: cred 0a40d5396b76568c... ★★★ (金样本 hdid ed0db8...) ★★★
```
### 最终链路图 (100% 纯 python, 无设备/hook):
```
tools/dfp_gen.py 生成随机cw(零设备)
wsapi.huya.com: getDfpConfig(67B) → selectOperator(可自定义sd/fp)
dfpReport(随机4146B cw) → t1(appkey回声) + t2(action,新) + t5(device_id,新)
wup.huya.com: hypasswordLogin(金样本hdid + t2 + t5 + 每账号画像) → cred 114B ✅
```
### 服务端校验全矩阵 (最终定论, 全部实测):
| 环节 | 校验点 | 能否铸造 |
|---|---|---|
| dfpReport | cw 内容 | ★完全不校验 (随机即可) |
| dfpReport | MAGIC | 不校验 (任意) |
| dfpReport | cw 长度 4146 | 必须 (固定块解析) |
| dfpReport | 尾部 10B | 必须固定 3600400c0b8c980ca80c |
| dfpReport | 总长字段 | 必须正确 |
| 登录 | hdid 32hex | **否** (APP_SIGN_NOT_MATCH; libhydeviceid.so 黑盒) |
| 登录 | fingerprint | 可换 → 滑块(自动过) |
| 登录 | device_id | 可换 (注册链签发) |
| 登录 | safedeviceid | 可换 (注册链签发) |
| 登录 | 机型/屏幕/UA | 可换 (自洽即可) |
### 最终交付物:
- tools/dfp_gen.py —— 零设备 cw/body 生成 (5/5 200 OK)
- tools/dfp_cli.py —— dfp 生成 CLI
- tools/huya_device_register.py --gen / --gen-login —— 注册链+登录一体 (零设备)
- tools/huya_device_profile.py —— 每账号设备画像
## 三十五(round7终) ★★★ 全局复盘: 用户两问完整答案 + 未搞定清单 ★★★
### Q1: "零设备自生成 dfpReport, 当初不是想获取 hdid 吗? 怎么现在 hdid 还是固定的?"
#### ★核心澄清: 虎牙有两套独立的 hdid, 长期被混淆!
| 体系 | 值(真机) | 长度 | 来源 | 位置 | 能否铸造 |
|---|---|---|---|---|---|
| **dfp 链 hdid** | `7c5387e0539c023c31c4ff0e807e7256117385ee` | 40hex | 明文 JSON 字段(可任意写) | dfpReport 明文 JSON + 响应 actionV/t5 | **能!! 零设备已达成** |
| **登录帧 hdid** | `ed0db8334cadd236c00cadf7e11ab5a5` | 32hex | libhydeviceid.so 本地生成 + native setDeviceInfo(0xb000021) 上报, 不走HTTP | WUP hypasswordLogin t1.t0 | **不能** (APP_SIGN_NOT_MATCH) |
- **当初"想获取的 hdid" = dfp 链 40hex hdid** —— 这个**已经完全实现**: JSON 里写任意 40hex,
随机 cw 加密(rnd), 服务端 200 OK + 回显。零设备自生成 ✅
- **"现在还是固定的 hdid" = 登录帧 32hex hdid** —— 这是**另一套设备ID**, 与 dfp 上报无关:
libhydeviceid.so 本地算的, 走 native 通道(0xb000021)上报, **没有 HTTP 等价物** → 纯代码无法铸造
- 用户看到的"固定"是因为: 登录(cred)必须用金样本 32hex hdid `ed0db8...`,
换任意新值 → APP_SIGN_NOT_MATCH (实测: 随机32hex/dflcollect的40hex/截断 全被拒)
#### 为什么 dfp 40hex hdid 可铸造但不影响登录?
- dfp 上报的 40hex hdid 只是"上报数据", 登录时服务端**不核对**它
(登录帧 device_id 来自注册链 t5, hdid 32hex 单独一套)
- 差分铁证: 随机 cw(JSON 里 hdid 是随机乱码) → 服务端 200 OK, 还签发 t2/t5 → 登录出 cred
### Q2: "是什么困了这么久没搞定? 还有哪些没搞定?"
#### ★历史复盘: 最大的"困住" = 路径错误(先逆向加密, 后差分)
前期(round1-7 上半)假设"必须逆向 keystream/加密才能铸造", 投入大量精力:
srand/rand 链路(sub_1D1A68→sub_1D0BB8, 被证伪) / MAGIC 无字面量 / OLLVM 扁平化混淆 /
导出符号 0 调用 / 模板 A/B 复用分析 / GetDfpConfig 种子假设(config 固定无种子) ...
**直到 round7 差分实验**: 整段 cw 随机(4136B os.urandom) → 200 OK!!
→ 服务端根本不校验内容 → 之前所有"破解 keystream"的努力全部变成非必要。
**教训: 先差分(服务端校验什么)再逆向(客户端怎么生成), 可省 90% 工作量。**
#### ★未搞定清单 (全部, 含影响评估):
| # | 未搞定项 | 状态 | 对交付影响 |
|---|---|---|---|
| 1 | **登录帧 32hex hdid 铸造** (libhydeviceid.so OLLVM + native setDeviceInfo) | 未破 | **唯一实质缺口**: 多账号共用一个 32hex hdid → 服务端可设备维度关联账号 |
| 2 | **XOR keystream 生成器精确算法** | 未破(已放弃) | **无影响** —— 服务端不校验内容, 随机 cw 即可 |
| 3 | **三元组(40hex hdid/deviceId/appkey)派生算法** (IdGenerator::encode/OLLVM MD5) | 未破 | **无影响** —— dfp JSON 三元组服务端不校验; 任意随机即可 |
| 4 | **collection 明文/构建** | 未全解 | **无影响** —— 服务端不校验 collection 内容 |
| 5 | **GetDfpConfig 81B 配置解密** (XXTEA/AES? 密钥候选 `7jbF5Iiq...`) | 未破 | **无影响** —— 已实证 config 固定(3/3), 不含每会话种子 |
| 6 | **appkey 变体机制** (cbb0e3→cbb045→abc798?) | 未解 | **无影响** —— t1 恒=cbb045 回声, 登录不校验 |
| 7 | **dfpReport 响应 172B base64 token** | 部分解 | t2 = 登录帧 safedeviceid(已用); token 其余细节未知, 不影响 |
#### ★真正达成的能力 (本轮全链路实测):
```
[零设备] dfp_gen.py 随机 cw → 注册链(wsapi) → t2 action + t5 device_id
→ hypasswordLogin(金样本32hex hdid + t2/t5 + 每账号画像) → cred 114B ✅
```
- 登录所需字段: **32hex hdid 固定复用**(唯一硬锚) + 其余全部可每账号独立
- dfp 上报链: **100% 零设备可生成**, 功能完整(200 OK + 签发设备字段)
#### ★对"每账号独立设备身份"的最终诚实结论:
1. **dfp 上报层独立**: ✅ 每账号可独立 40hex 三元组/随机 cw/注册链(零设备)
2. **登录层独立**: ❌ 32hex hdid 无法跨账号隔离(纯代码); 服务端能在设备维度关联
3. **如果要真隔离**: 两条路(均不纯代码):
a. 多台真机/模拟器各采一套 {32hex hdid + 画像} 建设备池轮换 (已实证: 每台模拟器=独立身份)
b. 破解 libhydeviceid.so 32hex hdid 生成算法 (OLLVM, 高成本, 另有 native 上报通道未知协议)
4. **多账号单设备是否真被风控**: 登录层无碍(新账号直出 cred, 实测) — 风险仅在服务端
设备维度关联策略(外部不可见), 若担心可用设备池轮换规避
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+31
View File
@@ -0,0 +1,31 @@
#!/usr/bin/env python3
"""dfp_gen CLI: 零设备生成 dfpReport body 并发送"""
import argparse, json, sys, time
from dfp_gen import make_triple_json, build_cw, build_body, send_dfp, parse_resp
def main():
ap = argparse.ArgumentParser(description="零设备 dfpReport 生成器")
ap.add_argument("-n", "--count", type=int, default=1, help="生成次数")
ap.add_argument("-o", "--out", help="保存 JSON 输出文件")
ap.add_argument("--no-send", action="store_true", help="只生成不发送")
args = ap.parse_args()
results = []
for i in range(args.count):
hdid, did, appkey, jp = make_triple_json()
cw = build_cw(jp)
body = build_body(cw)
r = {"hdid": hdid, "deviceId": did, "appkey": appkey,
"body": body.hex(), "body_len": len(body)}
if not args.no_send:
resp = send_dfp(body)
info = parse_resp(resp)
r["http_ok"] = info["http_ok"]
r["actionV"] = info["actionV"]
r["token"] = info["token"]
results.append(r)
print(json.dumps(r, indent=1)[:600] + ("..." if len(json.dumps(r))>600 else ""))
if args.out:
json.dump(results, open(args.out, "w"), indent=1)
print(f"\n已保存: {args.out}")
if __name__ == "__main__":
main()
+193
View File
@@ -0,0 +1,193 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
dfp_report 零设备自生成器 (python)
====================================
基于实证研究 (见 docs/dfpReport破解进度.md 第29-31章):
★ 决定性发现 (round7):
- 服务端不校验 cw 内容: 整个 cw 用 os.urandom 填充仍返回 200 OK
- 必须保持: taf 总长 4B、cw 长度=4146 (2B字段)、尾部10B固定 3600400c0b8c980ca80c
- wup 协议尾 0b8c980ca80c 同时出现在 GetDfpConfig 请求/响应
★ body 结构 (4226B):
[0:4] 4B 大端总长 (0x1082=4226)
[4:10] 1003 2c3c4c56 (wup魔数)
[10:23] 0c "huyaudbwebui" (servant, len-prefixed)
[23:36] 09 "dfpReport" (func)
[36:45] 7d 00 01 10 56 08 00 01 06 04
[45:49] "tReq"
[49:58] 1d 00 01 10 48 0a 06 00 16
[58:65] 07 "android"
[65:70] 2d 00 01 (字段头)
[70:72] 10 32 ★cw 长度 2B (4146=0x1032)
[72:82] 571882cf664bb39401ee (MAGIC 10B)
[82:82+4146] cw: [2B前缀]+[586B JSON段]+[3548B collection段]+[10B固定尾]
cw 中的 JSON 段含三元组 (hdid/deviceId/appkey) 明文模板, 但服务端不校验内容
"""
import os
import struct
import ssl
import socket
import hashlib
import json
import time
import re
# ---- 常量 (来自真实 wire 证据) ----
TAF_HEAD = bytes.fromhex(
"10032c3c4c56" # wup 魔数
"0c687579617564627765627569" # len=12 "huyaudbwebui"
"66" # ★servant/func 间标记 (实证存在)
"096466705265706f7274" # len=9 "dfpReport"
"7d00011056"
"0800010604"
"74526571" # "tReq"
"1d00011048"
"0a060016"
"07616e64726f6964" # len=7 "android"
"2d0001"
"1032" # cw 长度 0x1032=4146
)
MAGIC = bytes.fromhex("571882cf664bb39401ee")
CW_TAIL = bytes.fromhex("3600400c0b8c980ca80c") # 10B 固定尾
CW_JSON_LEN = 586 # JSON^ks 段长度
CW_COLL_LEN = 3548 # collection 段长度
CW_PREFIX_LEN = 2
CW_LEN = CW_PREFIX_LEN + CW_JSON_LEN + CW_COLL_LEN + len(CW_TAIL) # 4146
JSON_TEMPLATE = (
'{"appId":"5008","appVer":"13.4.22","appkey":"{APPKEY}",'
'"channel":"xiaomi","deviceId":"{DEVICE_ID}",'
'"deviceName":"M2102J2SC","hdid":"{HDID}",'
'"heightPixels":"2120","isCloud":0,"isForbidLog":1,"isHome":0,'
'"isPre":0,"openAppId":"","savePath":"/data/user/0/com.duowan.kiwi/files/huyaudb",'
'"sdkVer":"1.0.80138","servantName":"huyaudbwebui",'
'"shareAppDataPath":"/data/user/0/com.duowan.kiwi/files/huyaudb",'
'"systemInfo":"android","systemVer":"M2102J2SC,30,11",'
'"terminalType":1,"testEnv":0,"widthPixels":"1080"}'
)
def gen_triple():
"""生成随机三元组 (格式与真机一致)"""
hdid = hashlib.sha256(os.urandom(32) + b"hdid").hexdigest()
device_id = hashlib.sha256(os.urandom(32) + b"devid").hexdigest()
appkey = hashlib.sha256(os.urandom(32) + b"appkey").hexdigest()
return hdid, device_id, appkey
def make_triple_json(device_name="M2102J2SC", system_ver="M2102J2SC,30,11",
channel="xiaomi", width=1080, height=2120):
hdid, device_id, appkey = gen_triple()
# 用 replace 而非 format (模板含 JSON 花括号)
body = JSON_TEMPLATE.replace("{APPKEY}", appkey) \
.replace("{DEVICE_ID}", device_id) \
.replace("{HDID}", hdid)
return hdid, device_id, appkey, body.encode("utf-8")
def build_cw(json_plain: bytes, random_json: bool = True) -> bytes:
"""
构建 cw (4146B):
[2B前缀][586B JSON段][3548B collection段][10B尾]
实证: 服务端不校验内容, 全部用随机数即可 (200 OK)
若 random_json=False 则 JSON 段 = json_plain ^ 随机ks (也是随机)
实际上整段随机已验证可行, 保留结构即可。
"""
prefix = os.urandom(2)
if random_json:
json_sec = os.urandom(CW_JSON_LEN)
else:
ks = os.urandom(CW_JSON_LEN)
js = json_plain[:CW_JSON_LEN].ljust(CW_JSON_LEN, b"\x00")
json_sec = bytes(a ^ b for a, b in zip(js, ks))
coll_sec = os.urandom(CW_COLL_LEN)
return prefix + json_sec + coll_sec + CW_TAIL
def build_body(cw: bytes, total_len=None) -> bytes:
"""组装完整 body (4226B), 重算 4B 总长与 2B cw 长度"""
assert len(cw) == CW_LEN, f"cw len={len(cw)} != {CW_LEN}"
body = TAF_HEAD + MAGIC + cw
# taf 总长 = 4B 自身 + 后续所有字节
total = len(body) + 4
full = struct.pack(">I", total) + body
assert len(full) == total
return full
def send_dfp(body: bytes, host="wsapi.huya.com", timeout=8) -> bytes:
"""HTTPS POST 到 wsapi.huya.com (真实通路, 已验证)"""
req = (
b"POST / HTTP/1.1\r\n"
b"i-ver: 1\r\n"
b"Content-Type: application/octet-stream\r\n"
+ b"Content-Length: " + str(len(body)).encode() + b"\r\n"
+ b"Host: " + host.encode() + b"\r\n"
b"Connection: close\r\n"
b"Accept-Encoding: gzip\r\n"
b"User-Agent: okhttp/3.14.9\r\n\r\n"
)
ctx = ssl.create_default_context()
ctx.check_hostname = False
ctx.verify_mode = ssl.CERT_NONE
s = socket.create_connection((host, 443), timeout=timeout)
ss = ctx.wrap_socket(s, server_hostname=host)
ss.settimeout(timeout)
ss.sendall(req + body)
data = b""
try:
while True:
ch = ss.recv(4096)
if not ch:
break
data += ch
if len(data) > 10000:
break
except socket.timeout:
pass
except Exception:
pass
ss.close()
return data
def parse_resp(resp: bytes) -> dict:
"""解析 dfpReport 响应, 提取 actionV / base64 token"""
res = {"http_ok": b"HTTP/1.1 200" in resp[:100], "actionV": None, "token": None}
m = re.search(rb"actionV\(([0-9a-f]{40})", resp)
if m:
res["actionV"] = m.group(1).decode()
m = re.search(rb"\x26\xb4([A-Za-z0-9+/=]{30,})", resp)
if m:
res["token"] = m.group(1).decode()
return res
def main():
print("=== 零设备 dfpReport 自生成器 ===")
# 1) 随机三元组
hdid, did, appkey, json_plain = make_triple_json()
print(f"三元组: hdid={hdid}")
print(f" deviceId={did}")
print(f" appkey={appkey}")
# 2) 构建 cw (全随机段, 服务端不校验内容 - 已验证)
cw = build_cw(json_plain)
print(f"cw: {len(cw)}B, 尾: {cw[-10:].hex()}")
# 3) 构建 body
body = build_body(cw)
print(f"body: {len(body)}B")
# 4) 发送
resp = send_dfp(body)
print(f"响应: {len(resp)}B, HTTP200={b'HTTP/1.1 200' in resp[:100]}")
info = parse_resp(resp)
print(f"actionV={info['actionV']}")
print(f"token={info['token'][:40] if info['token'] else None}...")
if __name__ == "__main__":
main()