Files
live-hub-py/docs/虎牙扫码登录闭环逆向.md
T
yml2213 2076510df9 feat(huya): frida证书生成链逆向 - 全链路hook工具与文档
- 定位完整调用链: HYUDBMSDKCommon桥接 -> MsgGetH5InfoEx(184549387)
  -> HandlerGetH5InfoEx::onHandler@0x38d548 -> getOtp@0x2689f4
  -> hyudb_otp_encrypt@0x32fa24 -> createWupRequestData@0x38dab0
- 实测参数表: otp(uid串,2,轮换idx,'5008',SESS_HEX32,hyCred114B,4,nonce)
- 密钥字符串确认: 'HuyaUdb1928374650qwertyuiop' (encrypt x3 参数)
- Java桥直呼触发: LoginProxy.getQUrlData/getH5InfoEx/getCred 可编程调用
- 稳定时序固化: spawn挂起->bypass->resume->10s->patch_guard->hooks
  (attach已运行进程必被检测闪退)
- 发现App登录态JSON含现成web cookie.biztoken(344B)
2026-08-25 14:21:06 +08:00

142 lines
6.9 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 虎牙扫码登录闭环逆向(2026-08-25)
数据来源:用户抓包 `~/Desktop/抓包/hy/8.24`(手机扫码登录-1 / -手机端2 / -pc端2.har
探针脚本:`scripts/probe_huya_qr_bind.py`
## 一、协议链(qrLgn JSON 协议,纯 HTTPS,无原生 WUP 套接字)
```
PC角色(appId=5002, sign=1ce3bf68..., version=2.6, context WB-xxx)
① POST /qrLgn/getQrId uri=70001 -> data.qrId
⑥ POST /qrLgn/tryQrLogin uri=70003 轮询 stage: 0->1->2
stage=2 时返回 uid + biztoken(344B) => 构造 cookie 即登录成功
手机角色(appId=5131, sign=0ba67962ab9e12387648efeae2750777, version=2.5, context H5-xxx)
② GET https://aq.huya.com/r/confirm.html?k=<qrId>&id=5002 (App WebView 内)
③ POST /qrLgn/scanQrPicNotify uri=70005 data={qrId,wupData,...} -> stage=1 + 登录方信息
④ POST /qrLgn/bindQrLoginUser uri=70007 data={qrId,wupData,...} -> stage=2
```
注:`-1.har` 里还有旧版协议 `udbrtt.huya.com/auth/qr/notify|verify`confirm 页 s/confirm.html),
新流程走 qrLgn 直连,二者 wupData 结构相同。
## 二、web 会话构造(已验证 rc=0)
```python
cookies = {
'udb_uid': str(uid), 'yyuid': str(uid),
'udb_passport': 'hy_xxx', 'username': 'hy_xxx',
'udb_version': '1.0', 'udb_biztoken': <tryQrLogin 返回的 344B>,
'udb_origin': '1', 'udb_status': '1',
}
# 验证: udbreg /web/v2/modifyPswByCheck 无cookie/垃圾token -> rc=2005 USER_NO_LOGIN
# 扫码会话最小cookie -> rc=0 ★(不需要 udb_cred/udb_deviceid/udb_other
```
## 三、wupData 结构地图(998Bbase64 后随请求提交)
| offset | 内容 | 服务端校验 |
|---|---|---|
| 15 | servant `huyaudbwebui` func `default` | - |
| 70-273 | meta JSON `{associationId:184549392,id:184549392,session,traceId,type:2,uid:0,...}` | ❌ session/traceId 乱改仍通过 |
| 274 | `"5008"` | - |
| 282-463 | blob1 181B(与 safedeviceid 同前缀 `PQwemAN9...`) | ❌ 整段替换金标准 safedeviceid 仍通过 |
| 473-506 | hdid `ed0db8334cadd236c00cadf7e11ab5a5`=金标准) | 未单独测 |
| 508-686 | 设备信息 13.4.22 / 1.0.801386 / 127.0.0.1 / xiaomi / M2102J2SC+指纹hash / android / 1080x2120 / device_id hash | 未单独测 |
| **701-962** | **尾部 blob 261B —— 登录证书** | ✅ 破坏→`rc=40020 CERT_BIZERR_CREATE_ERROR`;清空→`rc=100 SYS_ERROR`;缺失→`rc=101 PARAM_ERROR` |
## 四、关键实验结论
1. **身份完全由尾部证书 blob 决定**
- 无 cookie / 垃圾 cookie / 真·APP WUP token 当 `udb_biztoken` → 全部无视,
绑定结果始终 = 抓包手机上登录的账号(hy_300023887 uid=1199666914671)。
- 先用 WUP 把 hy_299961311 登到同一设备身份再扫码 → 绑定仍是 hy_300023887
(服务端不做"设备最新登录"映射)。
2. **整个 web 登录不需要密码**getQrId → bind(wupData) → tryQrLogin 三步即出会话。
3. **证书可复用、可并发**:同一 wupData 已复用 10+ 次(跨 ~1h),5 路并发 2s 全成功。
4. APP WUP 密码登录(tools/huya_wup_encoder.py)对任意账号可用,
其响应中的 token(346B) 与 web biztoken(344B) 格式同族(解码后均以 `01 00` 开头),
但**不能**直接互换使用。
## 五、剩余缺口:每账号一张证书
尾部 blob 是与"App 内已登录会话"绑定的证书,生成逻辑在 App 原生 SDK
confirm.html 会先调 `udbdf.huya.com/dckey/check`,请求体本身加密)。
可行路径:
- **A. 每账号一次种子采集**:手机登录某号 → 触发一次任意 udb H5 confirm 流程 →
mitmproxy 抓 `bindQrLoginUser` 的 wupData 存库 → 该账号永久全自动。
(批量成本 = 每号一次人工,之后零人工)
- **B. 逆向证书生成算法**dckey/check 协议 + 原生加密,周期长不确定。
- 待观察:证书有效期(当前样本 >1h 仍有效)。
## 六、复现命令
```bash
.venv/bin/python scripts/probe_huya_qr_bind.py <账号> <密码> \
--har "/Users/yml/Desktop/抓包/hy/8.24/手机扫码登录-手机端2.har"
# 成功记录: evidence/qr_login_success.json(含有效 biztoken,已 gitignore
```
---
## 七、Frida 逆向补充(2026-08-25 下午,手机实测)
### 7.1 调用链(已全部定位)
```
JS callHandler("HYUDBMSDKCommon"/"HYUDBMSDKQUrlCommon")
└ com.huyaudbunify.HuyaAuth.getH5InfoEx(uid) / getQUrlData(uid,"","")
msgId: MsgGetH5InfoEx=184549387(0x0B00000B), meta id 同族 184549392
└ HandlerGetH5InfoEx::onHandler @0x38d548 (libudbauthunify.so)
├ BusinessCfg::getOtp @0x2689f4
│ ├ loadLoginData() ← 本地存储登录态(hyCred 等)
│ ├ copyBase64StringToVectChar(safedeviceid)
│ ├ getServiceTime + nonce_next → nonce = (time_ms<<16)|seq
│ ├ AESkeyMgr key轮换(b2 参数, 1..14 循环)
│ └ hyudb_otp_encrypt @0x32fa24
└ WupDataPackage<AppCommonData>::createWupRequestData @0x38dab0
```
### 7.2 hook 实测的 otp 输入输出
| 参数 | 值 |
|---|---|
| x0 s1 | uid 十进制字符串 ("1199666914671") |
| w1 | 恒 2 |
| w2 b2 | AESkeyMgr 轮换索引 (1..14 循环) |
| x3 s2 | "5008" |
| x4 s3 | SESS_HEX32 "865a4924a40897ac1fcfe6b4c2cbb0e3" |
| x5 s4 | hyCred 二进制 (114B, base64 后即 udb_cred 152字符) |
| w6 | 恒 4 |
| x7 | nonce |
| out | [2B头][AES密文] (登录态 146B / 匿名 ~118B) |
### 7.3 两层 AES 明文(已抓全)
- 内层 P2 (131B): `\x02\x0c\x00<12B xxtea(arg7)>...` cred_pack 结构
- 外层 P1 (187B): `\x01\x04\x00"5008"\x14\x00<20B随机>\x00<指纹40hex><72 00 0a><10B><cred二进制>`
- 尾部证书 = base64([2B头][AES(P1 补零到192)]) = 194B ✓ 与样本吻合
### 7.4 关键发现:App 登录态 JSONaes 明文 1106B
```json
{"biztoken_vec":[{"bizAppid":"5008","biztoken":"AhNEUAts...","errCode":0}],
"cookie":{"biztoken":"AQCRaYXu...(344B web令牌!)","uid":"...","version":"1.0"},
"cred":"CmAozmok10sOm1AwUNs...", ...}
```
→ App 存储里就有现成的 **web cookie.biztoken**`HuyaAuth.getLoginDataCookies(uid)` 可取。
### 7.5 Java 桥直呼(全自动触发,无需人工)
```js
var seed = Java.use('com.duowan.kiwi.base.login.udb.HuyaLoginProxy');
var F = Java.ClassFactory.get(seed.class.getClassLoader()); // 双loader注意:
// HuyaAuth 系用 com.huyaudbunify.util.HuyaAccountSaveUtils 所在 loader
var inst = F.use('com.hysdkproxy.LoginProxy').getInstance();
inst.getQUrlData(1199666914671, "", ""); // → 1332字符证书wupData
HA.getCred(uid); // → ResGetCred{hyCred(152), yyCred}
```
### 7.6 稳定 hook 时序(不可变)
spawn挂起 → attach → 仅装 bypass_msaoaid_maps_art_callsite → resume → 等10s
→ patch_guard_block_termination → 业务hooks。**attach 已运行进程必闪退。**
工具脚本: scripts/hook_final_capture.py(全量采集)/ hook_cred_dump.pycred+wupData
数据: evidence/final_capture.json, final_wupdata.bin, frida_cred_dump.json