- 实测校验矩阵: hdid+app_version 硬锚(APP_SIGN_NOT_MATCH), fingerprint 换→safe_auth滑块(自动过), device_id/机型/safedeviceid 直接过 - 定位注册接口: df/token+collect(sdid每次新值), wsapi dfpReport链(未破解) - tools/huya_device_profile.py: 每账号画像生成/持久化(幂等) - app_login_flow.login_cred / full_web_cookie 接入画像, 端到端出全套cookie
29 KiB
hyCred 签发协议 — 侦察笔记(2026-08-25 傍晚)
目标:任意账号取得有效 hyCred(114B),配合 tools/cert_forge 实现全自动多账号。 当前状态:铸证已闭环(见《虎牙证书算法闭环-差分实证.md》),本文记录签发链侦察结果。
已确认事实
- cred 是长寿命凭证:早上抓的 hyCred 在下午(且中间用户重新登录过一次后) 依然能通过 bind 铸证(实验C)。=> 每账号只需采集一次,非会话级短时效。
- cred 每次登录轮换值但旧的仍有效:今晨
0a80a762...≠ 午后0a50ec12...,两者均可用。 - 密码登录响应不含 cred:
evidence/wup_response_login.bin(1514B) 无 114B/152b64 结构。 - cred 存于本地登录态文件:
BusinessCfg::saveLoginData(@0x265fb0 / 0x265a6c) -> JSON(含"cred"字段) -> UdbAESUtil AES 加密 ->UdbFileUtil::writeFileEx写盘。BusinessCfg::getCred(uid)@0x265524 纯本地读(UdbLock+日志),无网络。 - 签发走 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
- 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 握手/心跳/加密隧道 | 高 | 最后手段 |
下次继续的抓手
- 重跑 hook_cred_issue.py 增强版:saveLD_bean dump 640B 定位 cred 字段偏移 +
backtrace 10 帧(找推送来源 handler)+ 挂
cred_unpack@0x333170 抓网络侧明文。 - 静态:
MsgRequestLgnCred::getClassName@0x2a37d0 / 各消息 getClassName 取类名串, 在 rodata 找 类名→msgId 表(参考 MsgGetH5InfoEx=184549387 的命名规律)。 - 若走 B:用 tools/huya_wup_encoder.py 的信封 + OTP 加密链组 RequestLgnCred 发 wsapi 试探。
★ 2026-08-25 晚 · 关键突破:cred 来源定位
增强 hook(bean 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::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":"","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 bytes114铸证被拒(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"误判):
- 铸证必须【复用信封原证书的rnd/指纹/key_idx】, 仅替换cred字段 —— 纯随机nonce必40020;
- t3.uid 必须与 cert 内 cred 所属账号一致;
- session三处(iRequestId/metaJSON/v0尾4B)保持原值, 不可随机化;
- 同账号信封长期复用(X0@239s✅; 数小时前老信封E-B未单独验证但无失败证据);
- 跨账号 = 每号设备首登一次(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])
关键发现
- k1 是设备级常量: 对 uid=1199666914671 和 uid=1199666911746 两次调用,
k1 均为
865a4924a40897ac1fcfe6b4c2cbb0e3, 不随账号变化! (证据: /tmp/opencode/k1_multi.json, gbt_in.k1 两次同值) - nonce 完全本地可算: serviceTime=本地时钟, counter从0开始, k1固定 → 任意账号用 目标uid+k1 即可离线铸造合法 nonce!
- 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的"每号设备首登一次")
- nonce = XXTEA(pack(st)+pack(counter|st<<16), key=MD5hex(uid_str+k1)[:16]), 本地可算;
- k1 = 设备常量(865a4924...), 一次采集永久可用(evidence/nonce_k1.json);
- 任意账号: 密码→登录取cred→目标uid+k1重算nonce→铸证→bind→cookie, 全程零设备!
- 信封本身(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>×tamp=<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 响应 = 包装头 + 信封。
三个致命坑 (全部实测定位)
- 身份一致性: baseinfo / 认证帧 / task 帧的 uid+cookie 必须全部同一账号,
否则服务端回
wup request biz execption:-1(tars iErrorCode 5000, status 255)。 早先失败原因 = 预热帧是旧账号捕获, task 帧是新账号 → 身份冲突。 - actId 尾字段:
11 62 2f是 3 字节(标签1字节+INT16值2字节), 只发62 2f会被服务端解析失败(-1)。 - 最小序列: 只需【认证帧 + 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/版本)| 滑块(可过) ✅
设备/指纹"注册类"接口 (已定位)
- df.huya.com/web/df/token + df/collect —— 指纹采集注册;每次调用从服务端 取新 sdid (工具: core/huya/device_fingerprint.get_huya_sdid; 已实测每次返回 新值, 天然满足"每账号一个")。
- wsapi.huya.com huyaudbwebui: getDfpConfig → selectOperator → dfpReport → udbdf.huya.com/dckey/check —— App 渠道完整设备注册链: 上报设备采集数据, 服务端下发稳定设备指纹(32hex)/session hash(40hex)/action(RSA加密)。 dfpReport 加密体未破解(待破解#2), 纯Python走注册链暂不可行。
- 数美 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: 账号→画像映射