feat(huya): 分身 CK 支持 cred0 解密补全,WSS 帧改为 JCE WebSocketCommand

- scripts/huya_clone_ck.py: root 读取小X分身/真身 WebView Cookie,默认
  用 AES-128-ECB(UDB 主密钥前 16B) 解 cred0 注入 udb_cred,新增
  --anon-device 补匿名设备字段、--no-cred0 退回旧行为
- core/huya/wss_client.py: 帧层改用浏览器 HUYA.WebSocketCommand(JCE)
  结构,vData 直接放 Wup 原包,按 WUP iRequestId 匹配响应,初始化顺序
  对齐 RegisterGroup→wsLaunch→VerifyCookie→getConfig→UpdateUserExps
- tests: 帧级断言改为新结构,假 WS 按 requestId 兑现
- docs: 新增分身 CK 提取、HAR 提取 CK 与积分中心说明

实测:四个分身 cred0 解出 udb_cred(39/43 字段),WSS
PlayMallUI.queryAllocation 返回 code=200、18 个商品;tests -k huya 394 passed
This commit is contained in:
yml2213
2026-10-26 11:23:29 +08:00
parent 6fc453017c
commit 03bc0818dd
9 changed files with 1188 additions and 156 deletions
+127
View File
@@ -0,0 +1,127 @@
# 精英宝典 HAR:完整 CK 与积分中心商品
> 抓包:`/Users/yml/Desktop/抓包/hy/10.25/精英宝典日常皮肤-1.har`
> 时间:2026-10-25 · 页面:`m-shop.huya.com/points.html?pagecode=heping_baodian_credit_center_page&sourceId=yellowcarlist`
## 一、结论
- 这是 **Web H5 抓包**(WebInspector,117 条请求),不是 App 抓包。
- 请求里的 `PlayMallUI.queryAllocation` 带 **完整浏览器 CK**:
- 原始 `tReq.tag0.tag4` 有 79 个键(含重复),去重后 **43 个字段 / 2338B**;
- 包含 `udb_cred`、`udb_biztoken`、`udb_deviceid`、`udb_guiddata`、`udb_anobiztoken`;
- 9 个 WSS 必填字段**全齐**,可以直接走 WSS。
- 账号:`hy_302081117` / UID `1199668650719`。
- 当前积分中心返回 **18 个「精英专享」商品**;现有 `parse_points_center_gifts()` 解析结果与抓包一致,核心解析代码不需要改。
- 赛季:配置 `scoreCenterConfigId=15`,活动 ID `3203`,`精英宝典福利季丨Q4`,有效期 **2026-10-09 00:00 → 2026-12-31 23:59:59(UTC+8)**。
## 二、HAR 里哪些请求有用
真正有业务数据的只有 7 条 `cdnws.api.huya.com` 的 WUP 请求:
| 函数 | 作用 |
|---|---|
| `launch.wsLaunch` | 建立 WSS 会话,拿服务端地址 |
| `launch.pingSignal` | 心跳 |
| `huyauserui.getUserProfile` | 用户资料 |
| `wupui.getUserFollowWeChatOfficialStatus` | 微信关注状态 |
| `PlayMallUI.queryAllocation` | **积分中心商品/配置(核心)** |
| `launch.queryHttpDns` | HTTP DNS |
其余是静态资源、埋点 `ylog/metric/statwup`、商城图片等。
## 三、完整 CK 在哪
`queryAllocation` 请求解码后:
```text
tReq.tag0.tag0 = 1199668650719 # UID
tReq.tag0.tag4 = <完整 document.cookie>
tReq.tag2 = heping_baodian_credit_center_page
tReq.tag1.tag6 = yellowcarlist
```
注意:HAR 里的原始 Cookie 有大量重复键(79 个),必须先按 key 去重、保留最后一次出现的值,得到 43 个字段。直接把原始 4090B 字符串塞进系统会带旧值。
脚本 `scripts/huya_har_ck.py` 会自动做这一步。
```bash
# 摘要 + 18 个商品
.venv/bin/python scripts/huya_har_ck.py \
"/Users/yml/Desktop/抓包/hy/10.25/精英宝典日常皮肤-1.har" --goods
# 落盘为 `账号----ck`(权限 0600)
.venv/bin/python scripts/huya_har_ck.py \
"/Users/yml/Desktop/抓包/hy/10.25/精英宝典日常皮肤-1.har" \
--out /tmp/huya_har_ck.txt
# JSON(默认不含完整 CK;加 --print 才包含)
.venv/bin/python scripts/huya_har_ck.py xxx.har --json
```
本次实测已生成 `/tmp/huya_har_ck.txt`:
```text
hy_302081117----<完整 CK>
```
## 四、18 个精英专享商品(2026-10-25 抓包快照)
| # | SPU | 名称 | 积分 | 库存 |
|---|---|---|---|---|
| 01 | `hy-8328509` | 套装-雪山精英 | 600 | 5054 |
| 02 | `hy-8347511` | 套装-青涩年华 | 488 | 6920 |
| 03 | `hy-8327363` | 套装-暗影哥特 | 888 | 3293 |
| 04 | `hy-8349003` | 套装-浪漫波比 | 1280 | 2207 |
| 05 | `hy-8315965` | 套装-浪漫天命 | 360 | 3298 |
| 06 | `hy-8348009` | 套装-牛仔情诗 | 550 | 454 |
| 07 | `hy-8335113` | 黑色高级特训官上衣 | 360 | 800 |
| 08 | `hy-8315982` | 幸运币礼包(小) | 20 | 9235 |
| 09 | `hy-8344526` | 荣耀勋章礼包(小) | 20 | 1951 |
| 10 | `hy-8338110` | 套装-甜星爱神 | 1280 | 246 |
| 11 | `hy-8335118` | 荣耀勋章礼包(中) | 300 | 448 |
| 12 | `hy-8329225` | 荣耀勋章礼包(大) | 900 | 549 |
| 13 | `hy-8329234` | 幸运币礼包(中) | 300 | 2301 |
| 14 | `hy-8328508` | 幸运币礼包(大) | 900 | 6647 |
| 15 | `hy-8348012` | 套装-甜心信使 | 1280 | 197 |
| 16 | `hy-8331702` | 浪漫波比头盔 | 1680 | 28 |
| 17 | `hy-8346046` | 浪漫波比背包 | 1680 | 442 |
| 18 | `hy-8324889` | 蜘蛛突变者大礼包 | 5500 | 78 |
> 积分取 `tag34.tag26`(600/488/888...)。同一商品里的 `tag9/tag10` 是 6000/4880/8880,别误当成积分;现有解析器用的就是 `tag34.tag26`,实测与页面一致。
>
> 库存取 `tag14`(与 SKU `tag17[0].tag3` 一致)。`tag13` 在本次抓包里是累计兑换/销量(例如雪山精英 `tag13=43194`、`tag14=5054`),不能当剩余库存;旧抓包无 `tag14` 时解析器回退 `tag13`。
## 五、赛季与日常任务
`queryAllocation` 响应 `tag2[1].tag2_2`:
- `tag0`:配置 `15`、名称 `精英宝典福利季`、副标题 `精英宝典福利季丨Q4`、活动 `3203`;
- `tag10/tag11`:`1791475200000` / `1798732799000`;
- `tag4`:`{8000, 0, 0, 1, 0}`(页面侧积分/状态快照)。
日常任务(`tag1`):
1. 开通宝典并订阅「和平精英福利提醒」→ **5 积分**;
2. 开通宝典并每天观看和平精英直播 10 分钟 → **2 积分**。
任务和商品库存都会变,实际执行前以实时 `queryAllocation` 为准。
## 六、验证
用 HAR 提取的完整 CK 实时调用现有 HTTP 通道:
```text
play_mall_query_allocation(uid=1199668650719, deep=True)
→ code=200
→ parse_points_center_gifts → 18 个商品
→ 第一个:套装-雪山精英 600 积分
```
本次抓包的 CK 已通过该验证;由于 `udb_cred` 存在,也可以走 WSS。
## 七、注意事项
- `/tmp/huya_har_ck.txt` 是登录凭据,文件权限已 `0600`;导入系统后请删除。
- HAR 本身也含完整 Cookie,不要提交 Git、不要外发。
- 商品库存是抓包瞬间的值,正式下单前必须重新查询。
- `scripts/*` 被 `.gitignore` 忽略;脚本是本地工具,若要入库需 `git add -f scripts/huya_har_ck.py`。
+190
View File
@@ -0,0 +1,190 @@
# 小X分身虎牙 CK 提取(Android / root)
> 2026-10-25 实测设备:OnePlus PHK110,Android 16,SukiSU Ultra v4.2.1。
> 结论:**root 后读每个分身的 WebView Cookies SQLite 即可拿到可用 CK**;不需要改 App,也不需要 Frida。
## 一、设备上的分身
小X分身并不是把多个账号塞进同一个进程,而是给每个分身分配独立的包/UID:
| 实例 | 进程(示例) | UID | 数据目录 |
|---|---|---|---|
| 主分身 1 | `com.bly.dkplat:p0` 内的 `com.duowan.kiwi` | 10363 | `/data/user/0/com.bly.dkplat/chaos/data/user/1/com.duowan.kiwi` |
| 主分身 2 | `com.bly.dkplat:p3` 内的 `com.duowan.kiwi` | 10363 | `/data/user/0/com.bly.dkplat/chaos/data/user/2/com.duowan.kiwi` |
| 独立分身 A | `dkplugin.duq.lls:p0` 内的 `com.duowan.kiwi` | 10365 | `/data/user/0/dkplugin.duq.lls/virtual/data/user/0/com.duowan.kiwi` |
| 独立分身 B | `dkplugin.zoc.bcn:p0` 内的 `com.duowan.kiwi` | 10366 | `/data/user/0/dkplugin.zoc.bcn/virtual/data/user/0/com.duowan.kiwi` |
| 真身(可选) | `com.duowan.kiwi` | 10364 | `/data/user/0/com.duowan.kiwi` |
每个分身真正登录态落在:
```text
<数据目录>/app_webview/Default/Cookies
```
这是 WebView 的 SQLite Cookie 库,`value` 是明文(当前设备上 `encrypted_value` 全为 0)。
## 二、为什么必须 root
无 root 时以下路径都走不通(已逐项实测):
- `run-as`:分身和真身都 `not debuggable`
- `am dumpheap`:`Process not debuggable`
- `content query`:`CFileProvider` / `DKFileProvider` 均未导出
- `/sdcard/Android/data/...`:只有 cache / 日志,没有 Cookie
- WebView 调试:`/proc/net/unix` 没有 `webview_devtools_remote`
- `adb backup`:包没有 `ALLOW_BACKUP`,且 Android 16 对 targetSdk 33+ 的限制
root 后直接 `cat` 上面的 `Cookies` 即可,不需要停 App。
## 三、一键提取
脚本:`scripts/huya_clone_ck.py`
```bash
# 只提 4 个分身,终端看脱敏摘要
.venv/bin/python scripts/huya_clone_ck.py
# 连真身一起提,落盘为 `账号----ck`(文件权限 0600)
.venv/bin/python scripts/huya_clone_ck.py --include-real --out /tmp/huya_cks.txt
# 需要直接看完整 CK 时(会打印到终端,注意不要留 shell 历史)
.venv/bin/python scripts/huya_clone_ck.py --include-real --print
# 用活动积分接口验证(sid 换成当前活动;活动结束返回 603 也算登录态被接受)
.venv/bin/python scripts/huya_clone_ck.py --include-real --verify-sid 2203
```
脚本行为:
1. `adb shell su -c id` 必须返回 `uid=0`,否则直接报错并提示去 SukiSU 授权;
2. 自动发现 `dkplugin.*` 的 virtual 数据目录,以及 `com.bly.dkplat/chaos/data/user/*/com.duowan.kiwi`;
3. `cat` 每个分身的 `Cookies` 到临时文件,用 SQLite 读取 `host_key like '%huya.com'` 的非空 Cookie;
4. 默认调用现有 `fill_web_cookie_fields()` 补齐网页设备态,并把 `udb_appid` 修正为 `5002`;
5. 按 `(udb_uid, udb_passport)` 去重,避免真身/分身重复账号;
6. 可用 `--out` 输出 `账号----ck`,直接贴进协作台账的「贴 / 换 CK」。
> 脚本默认**不打印完整 CK**,只打印实例、UID、账号、字段数、WSS 是否可用和验证结果;必须显式 `--print` 或 `--out` 才拿明文。
## 四、实测结果(2026-10-25)
| 实例 | UID | 账号 | Cookie 字段 | 接口验证 |
|---|---|---|---|---|
| `clone:dkplat/1` | 1199668927843 | `hy_302407019` | 38 | 603 活动已结束 |
| `clone:dkplat/2` | 1199668931394 | `hy_302409741` | 38 | 603 活动已结束 |
| `clone:duq.lls` | 1199668932116 | `hy_302407828` | 38 | 603 活动已结束 |
| `clone:zoc.bcn` | 1199668932128 | `hy_302407842` | 38 | 603 活动已结束 |
| `real:huya` | 1199668930633 | `hy_302408724` | 38 | 603 活动已结束 |
`query_user_score(sid=2203)` 返回 `603 活动已结束` 说明请求已经带着有效登录态进入了业务层;同一接口用伪造 Cookie 调用时没有 `tRsp` 数据,可直接区分。
## 五、已知限制:WSS 字段不全
虎牙 App 的 `udb_cred` 不在 WebView Cookie 库里,而在:
```text
<数据目录>/files/huyaudb/cred0
```
`cred0` 是加密落盘(`UDB_SHAREAPP_DATA` / `credAnonymous` 同样加密)。**2026-10-25 已实现离线解密,见第八节**;下面三条是解密之前的历史结论:
- **HTTP cdnws 通道可用**:实测 `getUserScore` 能正常进入业务层;
- ~~**WSS 通道不可用**~~:当时缺 `udb_cred` / `udb_deviceid` / `udb_guiddata` / `udb_anobiztoken`,会回退 HTTP;现已由 cred0 + 匿名设备态协议补齐;
- ~~需要 Frida hook 或重新签发 cred~~:不需要,直接 AES-128-ECB 解密即可。
- 另一种拿到完整 WSS CK 的方式是从已登录的 H5 抓包提取,见 `docs/虎牙/HAR提取CK与积分中心.md`;注意 `udb_cred` 是账号级凭据,不能从别的账号挪用。
## 六、注意事项
- CK 就是登录凭据,`--out` 生成的文件权限已设为 `0600`;不要提交到 Git、不要发群。
- 不要对分身执行 `pm clear` / 卸载 / 清数据,否则 Cookie 和登录态一起没了。
- 虎牙 App 更新后 Cookie 库结构一般不变;如果 `Cookies` 变成 `encrypted_value`,脚本只会在备注里提示,不会输出错误 CK。
- root 授权被回收后,`su: inaccessible`,重新去 SukiSU 给 Shell 授权即可。
## 七、手机 CK 转网页 CK 实测(2026-10-25)
用一个小X分身账号做了完整链路验证,测试过程中不输出 CK 值:
| 步骤 | 结果 |
|---|---|
| WebView 原始 CK | 17 字段;无 `udb_cred` / `guid` / `udb_deviceid` / `udb_guiddata` / `udb_anobiztoken` |
| `fill_web_cookie_fields()` 补齐后 | 38 字段;仍缺 `udb_cred` / `udb_deviceid` / `udb_guiddata` / `udb_anobiztoken` |
| `checkLogin`(原始 CK) | HTTP 200,`isLogined=true`,UID 匹配 |
| `POST /web/cookie/verify` | HTTP 200,`returnCode=0`,`data=null`,**无 Set-Cookie** |
| 打开积分中心 H5 | HTTP 200,**无 Set-Cookie**,Cookie 数量不变 |
| `PlayMallUI.queryAllocation`(原始 CK) | `code=200`,`msg=成功`,18 个商品 |
| `PlayMallUI.queryAllocation`(38 字段 CK) | `code=200`,`msg=成功`,18 个商品 |
| 网页请求后的 CK 再查询 | `code=200`,`msg=成功`,18 个商品 |
结论:
1. **手机 WebView 的 CK 本身已经能作为虎牙网页 HTTP CK 使用**,可以直接调用积分中心等 H5/HTTP 活动接口。
2. 虎牙**没有**提供一个“提交手机 CK、由服务端下发完整网页 CK”的兑换接口。`web/cookie/verify` 成功时不回 `Set-Cookie`,积分中心页面也不会补发登录 Cookie。
3. 本地 `fill_web_cookie_fields()` 只补齐 `guid`、QIMEI、页面统计等网页运行字段,不会生成或取得 `udb_cred`。
4. 因此手机 CK 可以稳定支持 **HTTP 活动查询**;依赖完整 9 字段的 WSS、部分领取/兑换类动作仍不可用,除非另外解密 `cred0` 或从已登录 H5 抓包取得 `udb_cred`。
5. 本次只验证了只读活动查询,没有执行积分领取、兑换或支付。
## 八、cred0 离线解密:手机 CK 直接升级为完整 WSS CK(2026-10-25)
### 8.1 结论
`files/huyaudb/cred0` 是 **AES-128-ECB + 零填充(zero padding)** 加密的登录历史 JSON,
密钥是虎牙 UDB 主密钥的前 16 字节:
```text
HuyaUdb1928374650qwertyuiop[:16] → "HuyaUdb192837465"
```
解密后结构(`loginHistory[0].apploginData`):
| 字段 | 含义 |
|---|---|
| `cred` | 152 字符 base64url,**就是 `udb_cred`**(114 字节 hyCred) |
| `uid` / `passport` | 账号 UID 与 `hy_xxxx` 账号 |
| `cookie.biztoken` | 网页 `udb_biztoken`(344 字符) |
| `cookie.status` / `cookie.version` | `udb_status` / `udb_version` |
| `biztoken_vec[0].biztoken` | App 通道 216 字符 biztoken(与网页不同) |
同一套密钥也能解 `credAnonymous`(匿名态)和 `UDB_SHAREAPP_DATA`。
### 8.2 脚本支持
```bash
# 默认:WebView Cookie + cred0 凭据 + 网页设备态补齐(39 字段)
.venv/bin/python scripts/huya_clone_ck.py --serial <serial> --json
# 完整 WSS CK:再加匿名设备态四字段(43 字段)
.venv/bin/python scripts/huya_clone_ck.py --serial <serial> --anon-device --out /tmp/cks.txt
```
新增参数:
- `--no-cred0`:只取 WebView Cookie,不解密 cred0;
- `--anon-device`:额外走 `core/huya/anon_device.py` 的 middle + anonymousLogin,
补齐 `udb_deviceid` / `udb_guiddata` / `udb_anobiztoken` / `udb_anouid`(需要外网)。
JSON 摘要新增 `wss_ready`(登录态字段齐全)与 `missing_device`(待匿名设备态补齐的字段)。
### 8.3 实测(2026-10-25,四个分身)
| 实例 | 账号 | 无 `--anon-device` | 加 `--anon-device` | WSS `PlayMallUI.queryAllocation` |
|---|---|---|---|---|
| `clone:dkplat/1` | `hy_302407019` | 39 字段,有 `udb_cred` | 43 字段 | `code=200`,18 个商品 |
| `clone:dkplat/2` | `hy_302409741` | 39 | 43 | 未单独跑(同链路) |
| `clone:duq.lls` | `hy_302407828` | 39 | 43 | `code=200`,18 个商品 |
| `clone:zoc.bcn` | `hy_302407842` | 39 | 43 | 未单独跑(同链路) |
对照:同一个 CK 在 Go 参考实现(`~/codes/huya-go`)里调用
`webActUI.getActPrizeList(sid=2277)` 也是 `status=200 / 18 项`,两边商品一致。
### 8.4 顺带修掉的 WSS 帧层 Bug
原先 `core/huya/wss_client.py` 用「6 字节自定义头 + 尾部字节」拼帧,服务端对
`launch.wsLaunch` 一直不回包(日志 `wsLaunch 无响应`)。按 `huya-go` 的实现改为浏览器真正的
`HUYA.WebSocketCommand`(TAF/JCE 结构:`iCmdType` / `vData` / `lRequestId` / `traceId` /
`iEncryptType` / `lTime` / `sMD5`)后:
- `vData` 直接放 `Wup.encode()` 原包,不再拼 `TAIL_BYTES`;
- 响应按 WUP `iRequestId` 匹配(不再 FIFO);
- 初始化顺序对齐:`RegisterGroup(16)` → `wsLaunch(-1)` → `VerifyCookie(10)` →
`getConfig` → `UpdateUserExps(23)` → `RegisterGroup(16)` → `HeartBeat(20)`。
修改后 `tests/ -k huya` 全绿(394 passed),并用真号 CK 实测 WSS 返回 18 个商品。