- 定位完整调用链: 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)
6.9 KiB
6.9 KiB
虎牙扫码登录闭环逆向(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)
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 结构地图(998B,base64 后随请求提交)
| 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 |
四、关键实验结论
- 身份完全由尾部证书 blob 决定:
- 无 cookie / 垃圾 cookie / 真·APP WUP token 当
udb_biztoken→ 全部无视, 绑定结果始终 = 抓包手机上登录的账号(hy_300023887 uid=1199666914671)。 - 先用 WUP 把 hy_299961311 登到同一设备身份再扫码 → 绑定仍是 hy_300023887 (服务端不做"设备最新登录"映射)。
- 无 cookie / 垃圾 cookie / 真·APP WUP token 当
- 整个 web 登录不需要密码:getQrId → bind(wupData) → tryQrLogin 三步即出会话。
- 证书可复用、可并发:同一 wupData 已复用 10+ 次(跨 ~1h),5 路并发 2s 全成功。
- 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 仍有效)。
六、复现命令
.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 登录态 JSON(aes 明文 1106B)
{"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 桥直呼(全自动触发,无需人工)
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.py(cred+wupData) 数据: evidence/final_capture.json, final_wupdata.bin, frida_cred_dump.json