Files
live-hub-py/docs/虎牙hyCred签发-侦察笔记.md
T
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

34 KiB
Raw Blame History

hyCred 签发协议 — 侦察笔记(2026-08-25 傍晚)

目标:任意账号取得有效 hyCred(114B),配合 tools/cert_forge 实现全自动多账号。 当前状态:铸证已闭环(见《虎牙证书算法闭环-差分实证.md》),本文记录签发链侦察结果。

已确认事实

  1. cred 是长寿命凭证:早上抓的 hyCred 在下午(且中间用户重新登录过一次后) 依然能通过 bind 铸证(实验C)。=> 每账号只需采集一次,非会话级短时效。
  2. cred 每次登录轮换值但旧的仍有效:今晨 0a80a762... ≠ 午后 0a50ec12...,两者均可用。
  3. 密码登录响应不含 credevidence/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}LgnCredAppLgnCredentialLoginReq/Resp(wup JCE)
    • MsgGetCred/HandlerGetCred、Anony 变体、MsgResponseUpdateCred
  6. dckey/check ≠ hyCredudbdf.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::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"误判):

  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。
实验 结果
同账号本地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设备常量证据

目标

纯 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 全通。