Files
live-hub-py/docs/虎牙hyCred签发-侦察笔记.md
yml2213 eccc00e090 docs/code(huya): 设备标识符命名规范落地 - 全库HDID32/GUID32/DEVID40前缀化去混
- docs/HUYA_ID_NOMENCLATURE.md: 规范总表 + 三条强制规则 + 代码变量对应
- 4 篇主文档头部去混声明 + §11.x 被证伪假设行标注更正
- 代码: GOLDEN_HDID→GOLDEN_HDID32, device_profile.HDID→HDID32, wup_encoder/app_login 注释前缀化
- 后续所有表述必须以 GUID32(doLaunch) / HDID32(登录t1.t0) / DEVID40(t5) 前缀书写
2026-08-28 23:02:21 +08:00

590 lines
34 KiB
Markdown
Raw Permalink 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.
<!-- ⚠️ 命名规范强制约束:本档中所有设备标识必须带前缀。规范见 docs/HUYA_ID_NOMENCLATURE.md。
GUID32=doLaunch sGuid(32hex, launch体系) | HDID32=登录帧t1.t0设备证书(32hex) |
DEVID40=注册链t5/t2.t8 device_id(40hex) | MID16=mid(16hex) | ACTION=注册链t2授权令牌。
旧段落中裸词按上下文语义理解:登录相关=HDID32launch/铸币=GUID3240hex=DEVID40。 -->
# 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登录。
## ★★★★ nonce 生成机制完全破解 + 任意账号零设备闭环(2026-08-26凌晨)
### 动态hook实锤(scripts/hook_nonce_chain.py + /tmp/opencode/nonce_chain.json)
在真机上对 getQUrlData→getOtpEx→gen_biz_token→xxtea 全链路 hook, 拿到运行时实参:
```
serviceTime = 当前毫秒时间戳 (0x1a039b1fc81 ≈ 1.787e12)
nonce_next = counter | (serviceTime << 16) # counter 同st内递增, 从0开始
xxtea key输入 = uid_str + k1 # k1 = BusinessCfg::this+0x10
xxtea key = MD5hex(uid_str + k1)[:16] # 注意取hex字符串前16字符!
xxtea data = pack(serviceTime) + pack(nonce_next) # 各8B小端
xxtea 输出 = ceil(len/4)+1 words, 末尾word存原长度 → 16B输入=20B输出
```
**rnd = XXTEA(pack(st) + pack(counter|st<<16), key=MD5hex(uid+k1)[:16])**
### 关键发现
1. **k1 是设备级常量**: 对 uid=1199666914671 和 uid=1199666911746 两次调用,
k1 均为 `865a4924a40897ac1fcfe6b4c2cbb0e3`, 不随账号变化!
(证据: /tmp/opencode/k1_multi.json, gbt_in.k1 两次同值)
2. **nonce 完全本地可算**: serviceTime=本地时钟, counter从0开始, k1固定
→ 任意账号用 目标uid+k1 即可离线铸造合法 nonce!
3. **Python 复刻逐字节一致**: tools/nonce_forge.py 对设备两轮 rnd
(80dd1dfa... / 0ba8f8b7...) 完全 MATCH。
### 终极验证: 纯协议任意账号 bind + cookie 全通!
| 实验 | 结果 |
|---|---|
| 同账号本地nonce铸证 bind (hy_300023887) | ✅ ok:true biztoken344B |
| **跨账号**本地nonce铸证+patch uid (hy_300024708→uid1199666911746) | ✅ ok:true biztoken344B |
| **新账号从未设备登录** 全链路: 密码→cred→本地nonce→bind→cookie | ✅ 全套cookie(udb_cred/udb_biztoken...) |
### 修正早前错误结论(X2"nonce账号绑定令牌"实为误判)
X2(换uid+换cred+复用旧nonce)→40020 的真正原因:
**nonce 内编码了 uid**(通过 MD5(uid+k1) 派生key), 服务端用 t3.uid 重新派生key解密 nonce,
旧nonce(绑旧uid)配新uid 自然解不开 → 40020。
**正确做法**: 换账号时用【目标uid】重新计算 nonce, 而非复用旧nonce!
→ 这也解释了为什么"纯随机nonce必40020": 随机值无法用uid+k1解出合法st/counter结构。
### 新的工程铁律(v3, 推翻v2的"每号设备首登一次")
1. nonce = XXTEA(pack(st)+pack(counter|st<<16), key=MD5hex(uid_str+k1)[:16]), 本地可算;
2. k1 = 设备常量(865a4924...), 一次采集永久可用(evidence/nonce_k1.json);
3. 任意账号: 密码→登录取cred→目标uid+k1重算nonce→铸证→bind→cookie, 全程零设备!
4. 信封本身(TAF帧)可长期复用/补丁(uid/cert), 不依赖账号。
### 资产更新
- tools/nonce_forge.py: 本地nonce生成器(XXTEA+MD5, 自检对设备实采)
- scripts/probe_nonce_bind.py: 本地nonce bind验证脚本(同/跨账号)
- evidence/nonce_k1.json: k1设备常量证据
---
## 附录: 网页 cookie 登录态的服务端验证 (活动页 webActUI TAF, 2026-08-26 深夜新增)
### 目标
纯 Python 直连活动页 TAF 接口, 确认"注入/复刻的网页 cookie"在服务端是**已登录**状态,
而不只是浏览器页面自说自话(document.cookie/localStorage 可伪造)。
### 两条通道与结论
| 通道 | 结果 |
|---|---|
| HTTP POST `cdnws.api.huya.com/?baseinfo=<b64>&timestamp=<ms>` | ❌ **volc-dcdn CDN 封锁非浏览器 TLS**: requests/curl/curl_cffi(chrome131等) 全部超时或 502, 仅真实浏览器可过 |
| WebSocket `wss://wsapi.huya.com/?baseinfo=<b64>` | ✅ **纯 Python(websocket-client) 可直连**, 本验证走此通道 |
### webActUI TAF 信封格式 (逐字节逆向, 与仓库 _Writer JCE 编码一致)
```
[4B len] 10 03 2c 3c 40 <reqid> 56 08 <service> 66 <len> <func> 7d
00 01 <L1 2B> 08 00 01 06 04 tReq 1d 00 01 <L2 2B> <value>
8c 98 0c a8 0c [2c 36 <len> <trace> 4c 5c 66 00]
value = JCE: 0a 0a t0 uid(INT64大端) t1"" t2"" t3 UA t4 cookie(STR4) t5 ZERO t6"" 0b <func尾> 0b
L2 = len(value); L1 = L2 + 14 (两样本一致)
baseinfo(URL参数,b64) = JCE: t0 uid t1"" t2 UA t3 "HUYA&ZH&2052" t4..t7 空 t8 cookie(STR4) t9 trace t10 空MAP
认证帧(WS, 裸JCE无信封) = t0 uid t1 UA t2 cookie(STR4) t3"" t4 1 t5 "HUYA&ZH&2052" t2..t6 空尾
WS帧包装 = 00 03 1d 00 01 [2B len][4B len] + 信封(去4B前缀), len = inner - (12+svc+func)
```
- getActUserTaskDetail 的 func 尾 = `11 62 2f` = JCE **t1/INT16 标签+actId**(0x622F=25135, 活动"精英宝典福利季Q3"的 actId, 与 getActInfo 响应一致)。
- HTTP 响应体 = **base64(TAF信封)**; WS 响应 = 包装头 + 信封。
### 三个致命坑 (全部实测定位)
1. **身份一致性**: baseinfo / 认证帧 / task 帧的 uid+cookie 必须**全部同一账号**,
否则服务端回 `wup request biz execption:-1`(tars iErrorCode 5000, status 255)。
早先失败原因 = 预热帧是旧账号捕获, task 帧是新账号 → 身份冲突。
2. **actId 尾字段**: `11 62 2f` 是 3 字节(标签1字节+INT16值2字节), 只发 `62 2f` 会被服务端解析失败(-1)。
3. **最小序列**: 只需【认证帧 + getActUserTaskDetail 两帧】; launch/getConfig/等非必需。
无 cookie 对照 → 905(未登录), 带 cookie → 200 请求成功 + 6 任务项(服务端确认)。
### 验证结果 (2026-08-26 02:31, 新账号 hy_300026595 / uid 1199666913640)
```
[self_check] 通过: baseinfo/body/auth 与捕获金样本逐字节一致
[A] 带 cookie: 249B status=200 msg=请求成功 任务项=6 => ✅ 服务端确认已登录
[B] 无 cookie: 96B status=905 => ❌ 未登录 (对照成立)
```
结论: 纯Python产出的网页 cookie 在活动服务端确认为**已登录**, 无需浏览器。
### 资产
- tools/huya_activity_verify.py: 纯Python WS 验证工具 (自检金样本 + A/B 对照)
- evidence/activity_taf_golden.json: baseinfo/body/auth 金样本(逐字节)
- evidence/ws_warmup_frames.json: 8.26 浏览器预热帧(旧会话, 参考)
- evidence/ws_current_session_frames.json: 浏览器当前会话帧(新账号, 证明格式)
- evidence/activity_verify_result.txt: 最终验证输出
- /tmp/cdp_now*.py: CDP 抓当前浏览器帧的脚本(浏览器 200 对照)
---
## 附录B: 多账号设备画像 —— 设备/指纹/风控数据归属与校验矩阵 (2026-08-26 新增)
### 需求
号很多, 不能所有账号共用同一台"设备身份", 需要: ①分清哪些字段是设备、
哪些是指纹、哪些是风控; ②找到设备/指纹"注册"类接口, 让每账号有独立设备态。
### 整个 App 登录链路的数据归属 (appId=5008 / hypasswordLogin)
| 环节 | 数据 | 归属 | 说明 |
|---|---|---|---|
| WUP 登录 t1.t0 | **hdid** 32hex | **设备(注册锚)** | libhydeviceid.so 生成, dfp 链注册; **换→APP_SIGN_NOT_MATCH 硬错** |
| WUP 登录 t1.t1 | app_version | **设备(签名相关)** | 与 hdid 绑定校验; **换→同样硬错** |
| WUP 登录 t2.t2 | **fingerprint 40hex** | **指纹** | SHA1 设备指纹; 换→safe_auth 滑块(**可自动过**) |
| WUP 登录 t2.t8 | device_id 40hex | 指纹(session hash) | dfpReport t5 下发值; 换→**直接通过** |
| WUP 登录 t1/t2 其余 | vendor/model/os/screen/宽高 | **声明型设备信息** | 自述, 换→直接通过 (需与 screen/UA 自洽) |
| WUP 登录 t0.t5 | safedeviceid(b64) | 指纹令牌 | RSA action, 服务端不校验内容; 换→通过 |
| WUP 登录 t0.t2 | metadata JSON (session/traceId/uid) | 会话 | 账号绑定 |
| WUP 登录 t0.t8 | user_action JSON | **风控(行为)** | 轨迹采集; 金样本固定值, 可批量复用 |
| 证书 P1 | fingerprint + nonce + cred | 指纹+账号 | fingerprint 设备绑定; 换 cred/nonce 即多账号 |
| 绑定流 | sdid (df/collect) | 指纹 | **每次调用返回新 sdid**, 天然每账号不同 |
| 前置链 | getDfpConfig/selectOperator/dfpReport/dckey | 设备指纹注册 | 见下"注册类接口" |
### 服务端校验矩阵 (全部对 wup.huya.com 实测, 2026-08-26)
```
字段变更 | 结果
hdid 32hex | 652B APP_SIGN_NOT_MATCH (硬错, 无滑块)
app_version | 652B APP_SIGN_NOT_MATCH (硬错, 无滑块)
fingerprint 40hex | 2107B 含 pt_auth 滑块URL → solver自动过 → 放行 ✅
device_id 40hex | 1571B cred ✅ (直接通过)
vendor/model/screen | 1571B cred ✅ (直接通过)
safedeviceid 内容 | 1571B cred ✅ (直接通过)
multi 全换(除hdid/版本)| 滑块(可过) ✅
```
### 设备/指纹"注册类"接口 (已定位)
1. **df.huya.com/web/df/token + df/collect** —— 指纹采集注册;每次调用从服务端
取新 sdid (工具: core/huya/device_fingerprint.get_huya_sdid; **已实测每次返回
新值**, 天然满足"每账号一个")。
2. **wsapi.huya.com huyaudbwebui: getDfpConfig → selectOperator → dfpReport
→ udbdf.huya.com/dckey/check** —— App 渠道完整设备注册链: 上报设备采集数据,
服务端下发稳定设备指纹(32hex)/session hash(40hex)/action(RSA加密)。
dfpReport 加密体未破解(待破解#2), 纯Python走注册链暂不可行。
3. **数美 libtscsdk.so (cfgc.zztfly.com)** —— 第三方设备指纹 SDK, 登录前初始化。
### 多账号工程方案 (已实现并端到端验证)
- 每账号一套画像: fingerprint/device_id/机型/vendor/screen 随机生成并持久化
(evidence/device_profiles.json); **hdid+app_version 复用金样本**(硬锚, 不泄
露单账号身份, 只是 App 签名设备锚点)。
- 首次用新画像登录 → 触发 safe_auth 滑块 → solver (YOLO+opencv, 282px级) 自动
过验 → 重发 → cred; 证书 P1 指纹也用该账号画像指纹, 全链设备一致。
- sdid 每次 df/collect 自动新取, 绑定流天然隔离。
- 实测 (hy_300026595, realme RMX3366 画像): cred 114B → bind rc 0 → biztoken
344B → cookie verify 200 → 全套 cookie ✅
### 资产
- tools/huya_device_profile.py: 每账号设备画像生成/持久化 (get_profile 幂等)
- tools/app_login_flow.py: login_cred 支持 device_info 参数 (滑块自动过)
- tools/full_web_cookie.py: 全链接入画像 (--new-device 强制换新画像)
- evidence/device_profiles.json: 账号→画像映射
### 追问: hdid 从哪来? 所有账号共用一个会不会出问题? (2026-08-26 追加实测)
**hdid 来源**:
- WUP 登录帧 t1.t0 的 hdid(`ed0db8334cadd236c00cadf7e11ab5a5`, 32hex)是
**App 原生 libhydeviceid.so 生成的设备 ID**(APK 静态分析: libhydeviceid.so
= "设备 ID 生成"), 经 App 渠道 dfpReport 链注册, 与 app_version(appSign)
绑定签名校验。
- 纯代码铸造新 hdid **不可行**(全部实测):
| 尝试 | 结果 |
|---|---|
| 随机 32hex hdid | 652B APP_SIGN_NOT_MATCH |
| df.huya.com df/collect 下发的 40hex hdid (每次新值) | 652B APP_SIGN_NOT_MATCH |
| 40hex 截断前 32 位 | 652B APP_SIGN_NOT_MATCH |
| 金样本 hdid | cred ✅ |
**共用风险**: 登录层无碍(新账号直出 cred, 无 qr_auth); 风险在服务端
**设备维度账号关联**(一台设备 N 账号)。若需真隔离: 从多台真机各抓一套
WUP 帧 {hdid, fingerprint, device_id, safedeviceid} 建设备池, 账号轮换分配;
或破解 dfpReport 加密体(逆向笔记待破解#2)后纯代码注册新设备。
**附带发现**: df/collect 响应含 40hex hdid 字段(与 sdid 同返), 已让
`core/huya/device_fingerprint.get_huya_sdid` 一并返回 (runner.js 输出
`__HDID__` 行); 它与 WUP 的 32hex hdid 非同一体系, 不能互用。
### 追问2: 注册上报接口到底在哪? (2026-08-26 实测闭环)
用户疑问: "真机可以(注册设备), 为啥代码不能根据逻辑生成上报注册? 肯定是少了
什么注册上报接口吧" —— **接口一直存在且已能纯 Python 直连, 之前只是没实现**:
**注册链 (servant=huyaudbwebui, 全部 POST wsapi.huya.com, TAF/WUP 信封, 无登录门槛)**:
```
getDfpConfig (67B 请求) -> 响应 80B 配置 (前64+后16, 疑似 AES key/IV 材料)
selectOperator(419B 请求) -> 响应运营商索引 1 (请求含 40hex 设备指纹上报)
dfpReport (3988B 请求) -> 响应下发设备身份三件套:
t1 = 865a4924a40897ac1fcfe6b4c2cbb045 32hex 设备指纹(稳定)
t2 = PQwemAN9... 180B base64 = 登录帧 safedeviceid (每次新签发)
t5 = 7c5387e0539c023c31c4ff0e807e7256117385ee 40hex = 登录帧 device_id
```
**实测结论 (全部纯 Python 直连验证)**:
1. 三接口重放均 HTTP 200, 响应与真机 HAR 一致。
2. **dfpReport 响应 t5 == 登录帧 device_id; t2 == 登录帧 safedeviceid** ——
注册链响应就是登录帧设备字段的签发源。
3. **用注册链新签发的 action(t2) + t5 登录 → 直接出 cred ✅** (工具已含此路径)。
4. t1(32hex) 不是登录帧 hdid: 作 hdid 登录 → APP_SIGN_NOT_MATCH 硬错。
5. **设备身份绑定在 dfpReport 请求的加密体(10B魔数 571882cf664bb39401ee +
加密采集数据)上**: selectOperator 里改 fingerprint/机型不影响 t1/t5 →
不触发重新注册。加密体生成算法未破解 (逆向笔记 待破解#2), 因此目前
只能"重放真机加密体" -> 拿到的设备身份永远等于抓包那台真机。
6. hdid 由 libhydeviceid.so 本地生成并经 native setDeviceInfo(msgType
0xb000021) 上报, **不走 HTTP (全部 HAR 无此帧)** -> 纯代码无法铸造新 hdid。
**因此完整答案**:
- "注册上报接口" = wsapi.huya.com 三连 (getDfpConfig->selectOperator->dfpReport),
已实现 tools/huya_device_register.py (证据: evidence/dfp_chain_golden.json)。
- 但**全新设备铸造**缺两环: ① dfpReport 加密体生成算法 (未破解, 决定设备身份);
② hdid 的 native 注册通道 (setDeviceInfo, 无 HTTP 等价)。这两环不解决,
纯代码只能"以真机加密体身份"登录 (device_id/safedeviceid 服务端签发,
每账号可用新 action), 做不到"每账号一台全新设备"。
- 每账号隔离现状 (已实测): fingerprint/device_id/机型随机 + 共用金样本 hdid
+ 注册链新鲜 action -> cred/cookie 全通。