Files
live-hub-py/docs/虎牙hyCred签发-侦察笔记.md
T

354 lines
19 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.
# hyCred 签发协议 — 侦察笔记(2026-08-25 傍晚)
> 目标:任意账号取得有效 hyCred(114B),配合 tools/cert_forge 实现全自动多账号。
> 当前状态:**铸证已闭环**(见《虎牙证书算法闭环-差分实证.md》),本文记录签发链侦察结果。
## 已确认事实
1. **cred 是长寿命凭证**:早上抓的 hyCred 在下午(且中间用户重新登录过一次后)
依然能通过 bind 铸证(实验C)。=> 每账号只需采集一次,非会话级短时效。
2. **cred 每次登录轮换值但旧的仍有效**:今晨 `0a80a762...` ≠ 午后 `0a50ec12...`,两者均可用。
3. **密码登录响应不含 cred**`evidence/wup_response_login.bin`(1514B) 无 114B/152b64 结构。
4. **cred 存于本地登录态文件**`BusinessCfg::saveLoginData` (@0x265fb0 / 0x265a6c)
-> JSON(含"cred"字段) -> UdbAESUtil AES 加密 -> `UdbFileUtil::writeFileEx` 写盘。
`BusinessCfg::getCred(uid)` @0x265524 纯本地读(UdbLock+日志),无网络。
5. **签发走 cdn.wup.huya.com 长连接隧道**HAR 里只有 CONNECT,载荷不可见;
相关消息族符号齐全:
- `MsgRequestLgnCred` / `MsgResponseLgnCred`(请求/响应)
- `HandlerRequestLoginCred`(@0x3a1820) / `HandlerResponseLoginCred`(@0x3a182c)
- 已知方法: `handlerCredLoginOverTimeMsg` @0x3a00d0
- `UdbObjCreator_Msg{Request,Response}LgnCred``AppLgnCredentialLoginReq/Resp`(wup JCE)
- `MsgGetCred`/`HandlerGetCred`、Anony 变体、`MsgResponseUpdateCred`
6. **dckey/check ≠ hyCred**udbdf.huya.com/dckey/check 返回设备钥 dckey
(固定前缀 AAAAAMC1eP4iV43WYoI57ZOu0),与账号 cred 不同物。
## 本次抓取(evidence/cred_issue_trace.json
用户重登时捕获 saveLoginData bean 内存头:
`..."hy_300023887"...len前缀"1199666914671"..."4C5fozan"...`
bean 为 BusBeansLoginData 结构体,cred 字段在更深偏移,待下次 dump 全量)
vtable 探测结论:slot2/3 是析构/通用槽(打中 MsgLoop::doMSG/sendMessage),
真正 onHandler 未命中 —— 下次应挂 `ResponseMsgHandler` 派生 onHandler 或
直接挂 `hyudb_packet_util::cred_pack/cred_unpack`(@0x333170) 收发两端。
## 三条候选路线(按性价比)
| 路线 | 做法 | 成本 | 备注 |
|---|---|---|---|
| A 混合采集(立即可用) | 手机登目标号一次,Frida 自动抓 cred 入库;此后该号永久全自动铸证 | 低 | 已验证可行,脚本齐备 |
| B wsapi 短连接复刻 | 若 MsgRequestLgnCred 可经 huyaudbwebui servant 走 wsapi.huya.com HTTPS | 中 | 需 msgId+JCE 结构+OTP加密链(已有) 组包试验 |
| C 长连接完整复刻 | 实现 cdn.wup.huya.com WSP 握手/心跳/加密隧道 | 高 | 最后手段 |
## 下次继续的抓手
1. 重跑 hook_cred_issue.py 增强版:saveLD_bean dump 640B 定位 cred 字段偏移 +
backtrace 10 帧(找推送来源 handler+ 挂 `cred_unpack`@0x333170 抓网络侧明文。
2. 静态:`MsgRequestLgnCred::getClassName`@0x2a37d0 / 各消息 getClassName 取类名串,
在 rodata 找 类名→msgId 表(参考 MsgGetH5InfoEx=184549387 的命名规律)。
3. 若走 B:用 tools/huya_wup_encoder.py 的信封 + OTP 加密链组 RequestLgnCred 发 wsapi 试探。
## ★ 2026-08-25 晚 · 关键突破:cred 来源定位
增强 hookbean 768B 全量 + backtrace)在用户重登时抓到决定性调用链:
```
BusinessCfg::saveLoginData ← _Z13saveLoginDataiR17BusBeansLoginData
← HandlerResponseLoginPassport ★
← udbjce::JceInputStream (wup/JCE 反序列化)
```
- **cred 随"护照登录"响应一次性下发**BusBeansLoginData 内),不是单独消息。
- 手机 App 的登录主通道是长连接上的 LoginPassport;我们的 HTTPS
`/open/hy/passwordLogin` 是简化通道,响应(1514B)只有 uid/name/mask/token
无 cred(已二次扫描确认:无 `0a 80` 高熵段、无 114B 结构)。
- bean 内存可见字段样例: "hy_300023887"、uid串、"01******7524"掩码、
user_action JSON 片段 —— 与 BusBeansLoginData 字段对应。
- 证据: /tmp/credissue2.log + evidence/cred_issue_trace.json(第二次覆盖保存)
### 路线修正
- 路线 B 修正为「补全 HTTPS passwordLogin 的请求字段」:App 端若也存在
HTTPS 登录路径,其请求参数比我们的最小实现多 ⇒ 响应可能就含 cred。
下一步:静态对比 HandlerRequestLoginPassport 组包 vs huya_wup_encoder 差异;
或 mitmproxy 抓一次 App 真实密码登录流量对照。
- 若确认必须长连接 → 路线 C(WSP 客户端)或路线 A(混合采集)兜底。
## ★ B1 静态第一铲(rodata 字符串集群 @0x1c36db
登录态 JSON 字段名: appId / sdkVer / cred / timestamp / biztoken_vec
新发现 24 字符密钥: ViDjMAKmkzvYEUFmrMdk9YPd (@0x1c3704, 疑似登录态文件AES钥或密钥表第4条)
消息类名串: MsgRequestLgnCred(@0x1c385a) / hyAppOtpLogin / LgnOtpLogin
下一步: 反汇编 BusBeansRequestLoginPassport::toString(JsonUtil)@0x2a6118 提取请求全字段,
对照 huya_wup_encoder 补齐 → 试发 HTTPS 端点看响应是否带 cred。
## ★ BusBeansLoginData 完整字段表(B1 第二铲)
toString 委托链: LoginData(0x26ca64) -> BusBeansAppLoginData::toString(0x26d518)
AppLoginData 字段(JCE 序列):
regOrigin userIdState uid hyid passport cred cookie mobileMask
emailMask userId token timestamp subUid hyOpenId isHuya status
biztoken_vec thirdParams
=> 护照登录响应一次含 cred/cookie(biztoken)/token; 铸证所需全在响应内。
剩余: RequestLoginPassport 六字段的 JCE tag 号 + 传输通道(wsapi or 长连接)。
## ★ Round2: 载荷格式=JSON(非二进制JCE)!
UdbRequestMsg<RequestLoginPassport>::unPackageMsg(@0x2a57cc) =
UdbContext::unPackageContext(str) -> JsonUtil::loadFromString
=> 消息上下文为JSON字符串, 字段名即线上的键:
请求: {name,password,userAction,isAuthLogin,lgnExtParam,bizAppids}
响应bean: {regOrigin,...,cred,cookie,token,...,biztoken_vec}
剩余: msgId定位 + servant/endpoint映射(huyaudbwebui?) + 密码格式(sha1?)
## Round3: msgId 线索
类名串引用点在 UdbObjCreator_Msg*::C2 注册构造器内:
- "MsgRequestLoginPassport" 邻近 movz #0x1001
- "MsgResponseLoginPassport" 邻近 movz #0x2001
=> 疑似 (方向类型<<12)|序号 或注册type常量; 完整id需反汇编
UdbObjCreator_MsgInit::C2 的 registerMsg(id,className,creator) 调用序列。
已知参照: MsgGetH5InfoEx=184549387(0x0B00000B)。
## Round4: 注册常量=0x20
Request/Response LoginPassport 两个 Creator::C2 均以 movz w0,#0x20 构造
(UdbClassFactory), 地址 0x289b28/0x289c2c。0x20 与证书 key_idx 字节(0x20)同值!
=> 猜测: 0x20 是该消息族的类型号, 线上完整 msgId 需结合工厂注册表;
或 key_idx 本就是消息族类型号的镜像(服务端按它选处理分支/密钥)。
捷径备选: 设备hook UdbMsgHandler::sendMessage 抓真实线上id(需用户重登一次)。
## ★ Round5: msgId 实锤
@0x289b48: mov w1,#0x1001; x2="MsgRequestLoginPassport";
bl UdbMsgFactory::RegisterMsg(uint, char const*)
=> MsgRequestLoginPassport id=0x1001(4097), Response=0x2001。
(前轮0x20为operator new大小, 红鲱鱼)
剩余: 传输端点+servant、password编码、活体发送测试。
## Round6: 传输通道确认
servant 字符串 huyaudbwebui 与 Handler 族类名同池出现在 libudbauthunify.so
(另见 wupudbrequest_v0 / KFWAH2cbkkNgFAgQPzriFPcU 等邻串)。
=> MsgRequestLoginPassport(0x1001) 经 huyaudbwebui servant 走 WUP;
dfp 的 func 名不在本库(getDfpConfig 计数=0), func 名或由Java层/运行时拼接。
下一步(需用户重登一次): hook UdbMsgHandler::sendMessage 抓真实线上包
(id/servant/func/JSON载荷), 一次拿全所有待定参数。
## Round7: sendMessage(0x1001) 实锤触发!
hook UdbMsgHandler::sendMessage 双重载: 登录时 id=0x1001 出现2次
(另有 0x0B000003/0x21/0x25 长连接族 + 0x102b/0x1072 等)。
wire id 双体系: 0x0Bxxxxxx=业务推送族; udb请求用裸id(0x1001...)。
payload读取bug: 参数是char*非std::string, 下轮改readCString重抓即得完整JSON包。
证据: evidence/cred_issue_trace.json (sendmsg:260 domsg:17)
## ★★ Round8: LoginPassport 完整请求包到手!
sendMessage(0x1001, json) 载荷(char*直读):
{"bizAppids":[],"isAuthLogin":false,"lgnExtParam":{},
"name":"hy_300023887","password":"<sha1hex40>","userAction":"{...}"}
- password=SHA1 hex(40) 与 tools/huya_password_sha1 一致
- 六字段与静态预测完全吻合; 空对象/空数组原样传
剩余: 响应侧(doMSG a2偏移待修, cred响应未读到) + 信封包装层。
证据: evidence/cred_issue_trace.json
## Round9: bean为指针对象, 改挂getCred出口
saveLoginData bean(1536B dump)=C++对象, cred在堆上经指针引用, 非内联。
最直接抓点: BusinessCfg::getCred(ulong,string&out)@0x265524
-> onLeave 读 out(x2) std::string 即152字符hyCred原文。
scripts/hook_beandump.py 已建(时序模板可复用), 下轮改钩子地址重跑一次登录即得。
另: 响应JSON键已确认含"cred"键(0x1c36e8)。
## ★ 终局验证(全链路)
活体采集(桥调getCred,零重登) -> hyCred(CiAXHUiF.../114B)
-> cert_forge 铸新证书 -> bindQrLoginUser stage=2
-> tryQrLogin uid=1199666914671 + biztoken344B ✅
多账号流程定型: 每号设备登录一次+桥采cred入库, 此后永久全自动。
纯Python长连接签发客户端(0x1001+JSON)留作后续可选优化, 参数已全部备齐。
## ★ 纯Python路线启动(用户需求: 密码进cookie出, 零手动)
wsapi信封实测(getDfpConfig 67B): [len][10 03][,LV]["huyaudbwebui"]["getDfpConfig"][}][params]
rodata: wupudbrequest_v0 与 huyaudbwebui 同池 => 任意udb消息HTTPS透传func名。
实施: tools/huya_passport_client.py
step1 复刻信封(huya_wup_encoder同构)
step2 内层帧: id=0x1001+JSON, 盲发试错读错误码定位格式
step3 响应解JSON取cred -> cert_forge -> bind -> cookie
## 纯Python路线·传输层修正
登录6.har实证: 护照登录走 CONNECT https://wup.huya.com TLS隧道(代理不可见),
wsapi/wupudbrequest_v0 仅用于dfp等短连流程。
内层帧突破口: createWupPackage(wup::UniPacket)@0x274ea4
- 函数体内引用 wupudbrequest_v0 与 huyaudbwebui
下步: 反汇编该函数+其调用链(经PLT), 提取长连接帧格式(msgId+JSON的JCE摆放),
然后 Python直连 wup.huya.com:443 组 TAF UniPacket 发送取cred。
## 新目标R1: createWupPackage签名破解
_Z16createWupPackageP...PKci @0x274ea4 (0x610B):
args=(UniPacket* pkt, const char* payload, int id)
pkt+0x0=u16 3, +0x8=id(w8<-arg), +0xc0=u16 3;
func硬编码"wupudbrequest_v0"; payload=x1。
=> 对应TAF RequestPacket{iVer,cPktType,iReqId,servant,func,sBuffer,...}
下步: 找PLT调用点看运行时实参(payload格式/id值), 复刻帧->Python直连wup.huya.com:443。
## ★★★ 纯Python护照登录打通!
build_password_login_wup(前会话复刻包) POST https://wup.huya.com
body=原始TAF字节(非b64!), CT=application/multipart-formdata, Dalvik UA
→ HTTP200 588B结构化TAF响应(huyaudbwebui/hypasswordLogin回显)!
假密码测试通过=帧格式实锤。下步: 解响应取rc/cred字段,真密码登录取cred->铸证->cookie。
## R2: 响应解析+签名缺口定位
TAF响应解析成功: _wup_data/_wup_header 双map, JSON上下文回显
{id:4097(=0x1001!), associationId:8193(0x2001), session, traceId, step:1}
错误码: APP_SIGN_NOT_MATCH "APP签名不匹配" + SIGNAL_SERVICE_RET。
签名密钥候选: KFWAH2cbkkNgFAgQPzriFPcU (24字符) 注册于AESkeyMgr::C2@0x26fcc8!
(与证书钥4VYc/xXED/3FMH同管理体系,疑为请求签名专用钥)
另发现 ybEWdvkjGjPQa2ugireHJCLL 邻串。
下步: 反汇编AESkeyMgr::C2取全部钥表+id; 结合登录时bigaes钩子事件复刻sign算法。
注: 发包body=raw TAF字节(b64是旧文档误导); HTTP200帧已通。
## R3: 登录响应拿到真实数据!
金样本逐字节复刻(仅差1B)重发 -> HTTP200 1571B!
响应含: uid=1199666914671, hy_300023887, mobileMask=001******7524,
b64url长串(AQA1Ie6L1o...疑似token/biztoken), uc_states。
APP_SIGN_NOT_MATCH消失(此前系字段值不全所致)。
无明文"cred"键 -> 字段为JCE二进制嵌套, 下轮完整解析_wup_data映射取cred。
## R4: 响应bean结构初解
_wup_data 响应map解析(部分): tag0=INT uid(1199666914671), tag1=INT,
tag2=name, tag3=BYTES, tag7=INT... 解析器在type=0xe(LIST)处断掉。
原始流中含b64url长串(AQA1Ie6L1o...)即token/cookie形态字段。
下步: 补全taf_dump的0xe/嵌套处理, 完整映射BusBeansAppLoginData取cred。
## R5: 响应sBuffer精确定位
响应外层map: key"_wup_data"(06 09..) -> val head 1d 00, 长度JCE优化编码
(01 04 bf => 实际915B, taf_dump已证实len=915)。bean体起于 0a 0a(tag0嵌套struct)
与请求同构。旧dump在该bean的tag5(LIST元素为struct)处解析器断掉。
下步: 直接用core.huya.taf_protocol切出915B slice完整递归解析,
按BusBeansAppLoginData字段表(regOrigin..cred@?)对号取cred。
## R6 ★★★ 响应bean完整解析 - cred字段定位!
BusBeansAppLoginData响应结构(纯Python解析):
t0{头:ver/meta/timestamp}, t1{业务bean}:
t0=i64 uid(1199666914671)
t3=BYTES[114] 起始0a50 => hyCred本体!
t5子struct: t3=STR4[344]"AQA1Ie6L1o..."(biztoken/cookie)
mobileMask/uc_states齐全
=> 纯Python链路全通: 密码->TAF包->wup.huya.com->cred+token!
下轮: 精确切片cred(处理长度前缀) -> cert_forge -> 活体bind验证 -> 目标完结。
## R7: cred切片偏差 - 待锚点校正
候选114B起始'72 0a50...'多出1字节; 去首后113B≠114 断言失败,
铸证bind被拒(uid=None,符合预期)。字符串锚点(STR4[344]/mobileMask/uc_states)
在t5子struct解析完全对齐 => 用它们回推t3 bytes精确边界。
注: 本次登录用假密码+复刻session, 服务端仍返回完整bean(会话级缓存/重放容忍),
cred有效性需真密码登录后二次验证。
## R8 收尾状态
t3字段精确解码: head 3d 00(INT8 elem) 00 72(INT8 len=114) -> 值114B
起始 72 0a 50... 与桥采cred格式(直接0a20...)不同 => 此字段或为包装/加密态cred,
或因假密码+复刻session返回的是降级数据。真伪需真密码登录实测。
已完成(全部验证): 密码SHA1->TAF组包->POST wup.huya.com->HTTP200->bean解析->
uid/token/mobileMask提取。待办: 真密码登录取新鲜cred->cert_forge->bind验证cookie。
工具就绪: huya_wup_encoder.build_password_login_wup + 本笔记响应解析器代码。
## 真密码实测(R8末)
hy_300023887/aa778899 SHA1=金样本哈希(同一账号历史登录)。
真密码登录: HTTP200 1571B, 响应含新鲜114B字段(0a40开头, 每次登录变化)。
铸证bind -> 40020 CERT_BIZERR_CREATE_ERROR 被拒。
=> 该t3 bytes[114]与桥采cred格式有差异(桥采0a20开头), 或非plain cred
(可能为加密态/hyid), 需对照BusBeansAppLoginData精确tag定位真cred字段。
纯Python链路已通到最后一环; 差异点已锁定在响应bean的cred字段识别。
## 终验结论(本轮)
真密码登录返回的t3 bytes[114](0a40开头)铸证被拒(40020), 新旧nonce均拒。
对照桥采有效cred(0a20开头,bind通过): 该字段非plain cred。
可能性: ①响应中cred经会话钥加密(需解密) ②该字段为hyid/其他
③真cred在bean其他位置(t2 bytes[162]? t4 bytes[177]?)。
下一步(精确): 设备登录时hook HandlerResponseLoginPassport::onHandler+
saveLoginData, 对照同账号纯Python响应字节, 直接对出cred真实位置/形态。
全部传输+组包+解析代码已就绪且验证通过。
## 对照实验定论
设备当前cred(0a30开头,bind有效)不在纯Python登录响应中 => 每次登录铸新cred,
响应t3即新鲜cred本体。但新鲜cred铸证bind仍40020。
关键观察: cred第二字节 0x20/0x30/0x40/0x50 轮换 -- 与证书key_idx同域!
=> cred内部含钥代次信息, 新鲜cred或绑定登录会话上下文(需配套session/sdid),
或响应态与存储态之间有一步转换(saveLoginData前?)。
下一步: 设备登录时hook saveLoginData入参bean与getCred出口做同会话字节对照,
精确定位 响应t3 -> 存储cred 的转换函数。
## 关键发现: 钥代次轮换 + 响应cred非直接可用
cred字节1轮换: 0x10/0x20/0x30/0x40/0x50/0x70/0x80(观测值)=代次计数器。
干净流水线(登录->新nonce铸证->立即bind)仍40020。
对照: 桥采cred(0a20,设备会话内)始终有效。
=> 密码登录响应中的cred与getCred返回的cert-cred是两态:
登录后app还有后续步骤(getAppComomData等)才得到可用cert-cred,
或响应cred需会话上下文解密/兑换。
下一步: hook getCred的调用链回溯(谁写入该值), 或登录后逐func试调
(hyanonymousCredlogin/getAppComomData)观察cred0文件变化。
## ★★★★ 终局: 纯Python全链路贯通(零设备)!
完整闭环实测通过:
密码aa778899 -> SHA1 -> build_password_login_wup(hypasswordLogin)
-> POST wup.huya.com(raw TAF) -> HTTP200
-> 响应t3=新鲜cred(114B,0a开头,每次登录轮换)
-> cert_forge(新nonce+key_idx0x20) -> 证书194B
-> bindQrLoginUser -> ok:true uid=1199666914671 biztoken344B ✅
关键教训: 40020拒绝系旧wupData信封过期(session/nonce TTL), 与cred无关!
信封需新鲜抓取(keycap钩子桥触发即可)。
多账号运营: 账号密码即全部所需, 全自动出cookie。
## 新账号safe_auth定性: 可自动过的滑块!
App渠道(WUP)对新账号返回 pt_auth.html(aq.huya.com,param=加密token),
主chunk解析证实走 udbrtt.huya.com + slide滑块(slideBlock*字段)
= 与 core/huya/verification/solver.py 已实现的 udbrtt get3->verify3 闭环同源!
(web渠道才是qr_auth需扫码; app渠道是滑块,可自动)
待做: solver适配param上下文(aq页初始化udbrtt的方式), 验证通过后重发WUP登录即得cred。
## ★ 信封规则定论(v2紧凑窗口对照实验, 2026-08-25夜)
工具: tools/envelope_forge.py(TAF原位补丁) + scripts/probe_envelope_rules2.py。
同一份新鲜信封(239s旧)在紧凑窗口内连续4次bind:
| 实验 | 操作 | 结果 | 结论 |
|---|---|---|---|
| X0 对照 | 原t3+信封原nonce换可信新cred铸证 | ✅ ok:true | 信封可复用, nonce无独立时效 |
| X1 错配 | 原t3 + 新账号cred铸证 | ❌ 40020 | 服务端校验 cert.cred↔t3.uid 一致 |
| X2 换号 | t3补丁成新uid + 新账号cred铸证 | ❌ 40020 | 仍有隐藏账号绑定 => 见下 |
| X3 改session | 三处session一致随机化 | ❌ 100 SYS_ERROR | session被服务端实时校验, 不可动 |
X2根因分析: 信封内uid仅t3一处(BE@688, metaJSON里uid=0/associationId=0x0B000030
为固定魔数); 补齐t3仍拒 => 最自洽解释: **证书nonce是SDK在"某账号登录态"下生成
的账号绑定令牌**(服务端可凭nonce反查签发账号)。与d29290a终局"新nonce成功"不矛盾
(那次同为可信账号自用)。
工程铁律(修正早前"短TTL"误判):
1. 铸证必须【复用信封原证书的rnd/指纹/key_idx】, 仅替换cred字段 —— 纯随机nonce必40020;
2. t3.uid 必须与 cert 内 cred 所属账号一致;
3. session三处(iRequestId/metaJSON/v0尾4B)保持原值, 不可随机化;
4. 同账号信封长期复用(X0@239s✅; 数小时前老信封E-B未单独验证但无失败证据);
5. 跨账号 = 每号设备首登一次(hook_cert_keycap自动抓该号信封)后永久纯Python。
safe_auth闭环(app_login_flow.login_cred)已实现: pt_auth页JS逆向完成,
urlParamMap原样回传+wupData空串可过; 新账号触发风控时自动解udbrtt滑块并重发WUP登录。