From 8efd3b8d02af8fd2b90f238d4017fbacebcaff34 Mon Sep 17 00:00:00 2001 From: yml2213 Date: Wed, 26 Aug 2026 02:35:42 +0800 Subject: [PATCH] =?UTF-8?q?=E7=BA=AFPython=E6=B4=BB=E5=8A=A8=E9=A1=B5TAF?= =?UTF-8?q?=E6=9C=8D=E5=8A=A1=E7=AB=AF=E9=AA=8C=E8=AF=81=E7=99=BB=E5=BD=95?= =?UTF-8?q?=E6=80=81:=20webActUI=20WS=E9=80=9A=E9=81=93=20+=20=E9=87=91?= =?UTF-8?q?=E6=A0=B7=E6=9C=AC=E5=AF=B9=E6=8B=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - tools/huya_activity_verify.py: 认证帧+getActUserTaskDetail两帧即验证 (200请求成功+6任务项=已登录; 无cookie对照905=未登录) - 信封格式定论: L2=len(value), L1=L2+14, 身份一致性要求, actId尾字段=11 62 2f - HTTP cdnws端点被volc-dcdn封锁非浏览器TLS, WS wsapi可纯Python直连 - evidence: 金样本(逐字节)+ 浏览器当前会话帧 + 验证结果 --- docs/虎牙hyCred签发-侦察笔记.md | 52 ++++ evidence/activity_taf_golden.json | 14 + evidence/activity_verify_result.txt | 11 + evidence/req_getActInfo.ws.bin | Bin 0 -> 103 bytes evidence/req_getActUserTaskDetail.ws.bin | Bin 0 -> 1402 bytes evidence/web_cookies_full.txt | 11 + evidence/ws_current_session_frames.json | 30 +++ evidence/ws_current_task_frame.bin | Bin 0 -> 1327 bytes evidence/ws_warmup_frames.json | 18 ++ tools/huya_activity_verify.py | 313 +++++++++++++++++++++++ 10 files changed, 449 insertions(+) create mode 100644 evidence/activity_taf_golden.json create mode 100644 evidence/activity_verify_result.txt create mode 100644 evidence/req_getActInfo.ws.bin create mode 100644 evidence/req_getActUserTaskDetail.ws.bin create mode 100644 evidence/web_cookies_full.txt create mode 100644 evidence/ws_current_session_frames.json create mode 100644 evidence/ws_current_task_frame.bin create mode 100644 evidence/ws_warmup_frames.json create mode 100644 tools/huya_activity_verify.py diff --git a/docs/虎牙hyCred签发-侦察笔记.md b/docs/虎牙hyCred签发-侦察笔记.md index cc09ca7..fa94972 100644 --- a/docs/虎牙hyCred签发-侦察笔记.md +++ b/docs/虎牙hyCred签发-侦察笔记.md @@ -406,3 +406,55 @@ X2(换uid+换cred+复用旧nonce)→40020 的真正原因: - 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=×tamp=` | ❌ **volc-dcdn CDN 封锁非浏览器 TLS**: requests/curl/curl_cffi(chrome131等) 全部超时或 502, 仅真实浏览器可过 | +| WebSocket `wss://wsapi.huya.com/?baseinfo=` | ✅ **纯 Python(websocket-client) 可直连**, 本验证走此通道 | + +### webActUI TAF 信封格式 (逐字节逆向, 与仓库 _Writer JCE 编码一致) +``` +[4B len] 10 03 2c 3c 40 56 08 66 7d +00 01 08 00 01 06 04 tReq 1d 00 01 +8c 98 0c a8 0c [2c 36 4c 5c 66 00] +value = JCE: 0a 0a t0 uid(INT64大端) t1"" t2"" t3 UA t4 cookie(STR4) t5 ZERO t6"" 0b 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 对照) diff --git a/evidence/activity_taf_golden.json b/evidence/activity_taf_golden.json new file mode 100644 index 0000000..fe040e9 --- /dev/null +++ b/evidence/activity_taf_golden.json @@ -0,0 +1,14 @@ +{ + "uid": 1199666911746, + "ua": "webh5&0.0.1&websocket&&diypc_52775", + "cookie_spaced": "huya_ua=webh5&0.0.1&websocket&&diypc_52775; web_qrlogin_confirm_id=f4fc0532-6b23-4ad2-973a-f3ee9b6a8bfe; udb_cred=CnC8njCnuw0aYtHxt59no_-LlhNxOYl1lSkUaxJbPHn4htOSlnOvS9_29BscOyI384Jp0B8A-ePHLDgpIznm76nHH9vFoJ0gzbQNjgVwPyMBULBDEjWsWCeYmpIr1CiB0HpC_nqa5KxI_GrXrB_Wa2RF; udb_uid=1199666911746; yyuid=1199666911746; udb_passport=hy_300024708; username=hy_300024708; udb_version=1.0; udb_biztoken=AQBY2017dyTd0W_SZD19bd9UT_Lkqzup5yOggzlZXrmxx7q3YofMCivhcL3xN0vYwQHw8gdHwqsYUAaUrOZnE9Ik_L9_exOiqradBRPmvxy8f5YjqL1PmbZfB-BkiW8wiXJBRqG9dSKngHHgaRxCO0g3JQBwLov44IB9r8MKPukNUvaR47FMSGvXhspurOL3gaHs5wGMoZn3p9iHcHfCwiOhQuwQ3vx_ib62NPRP6Q2qspvzNoZHodMOosyqHJ3Nd_AqgS_8JeUUrarccfhFZd6nYZ44FGnJPhxKJW4BWREYR4wDy-RdpjZcjflJ9XP_noBnCIFIAzrflcIt80ZIroQt; udb_origin=0; udb_status=1; udb_other=%7B%22lt%22%3A%221787628882646%22%2C%22isRem%22%3A%221%22%7D----13059903110; __yasmid=0.04376401031386312; __yamid_tt1=0.04376401031386312; __yamid_new=CBCA2A4A3C6000016A3F11CF13B016EA; huya_hd_rep_cnt=2; __yaoldyyuid=1199666911746; _yasids=__rootsid%3DCBCA2A4A3DB00001C653D2D0B2F014C3; huya_web_rep_cnt=7", + "cookie_tight": "huya_ua=webh5&0.0.1&websocket&&diypc_52775;web_qrlogin_confirm_id=f4fc0532-6b23-4ad2-973a-f3ee9b6a8bfe;udb_cred=CnC8njCnuw0aYtHxt59no_-LlhNxOYl1lSkUaxJbPHn4htOSlnOvS9_29BscOyI384Jp0B8A-ePHLDgpIznm76nHH9vFoJ0gzbQNjgVwPyMBULBDEjWsWCeYmpIr1CiB0HpC_nqa5KxI_GrXrB_Wa2RF;udb_uid=1199666911746;yyuid=1199666911746;udb_passport=hy_300024708;username=hy_300024708;udb_version=1.0;udb_biztoken=AQBY2017dyTd0W_SZD19bd9UT_Lkqzup5yOggzlZXrmxx7q3YofMCivhcL3xN0vYwQHw8gdHwqsYUAaUrOZnE9Ik_L9_exOiqradBRPmvxy8f5YjqL1PmbZfB-BkiW8wiXJBRqG9dSKngHHgaRxCO0g3JQBwLov44IB9r8MKPukNUvaR47FMSGvXhspurOL3gaHs5wGMoZn3p9iHcHfCwiOhQuwQ3vx_ib62NPRP6Q2qspvzNoZHodMOosyqHJ3Nd_AqgS_8JeUUrarccfhFZd6nYZ44FGnJPhxKJW4BWREYR4wDy-RdpjZcjflJ9XP_noBnCIFIAzrflcIt80ZIroQt;udb_origin=0;udb_status=1;udb_other=%7B%22lt%22%3A%221787628882646%22%2C%22isRem%22%3A%221%22%7D----13059903110;__yasmid=0.04376401031386312;__yamid_tt1=0.04376401031386312;__yamid_new=CBCA2A4A3C6000016A3F11CF13B016EA;huya_hd_rep_cnt=2;__yaoldyyuid=1199666911746;_yasids=__rootsid%3DCBCA2A4A3DB00001C653D2D0B2F014C3;guid=0a89c7f84ccf8d6a35014ec44fc48a4b;Hm_lvt_51700b6c722f5bb4cf39906a596ea41f=1787678541;Hm_lpvt_51700b6c722f5bb4cf39906a596ea41f=1787678541;HMACCOUNT=B2C9EFDC9345FD76;huya_web_rep_cnt=20", + "trace": "aa712e291575bd13:aa712e291575bd13:0:0", + "reqid": 12, + "baseinfo_hex": "030000011751b85a0216002622776562683526302e302e3126776562736f636b6574262664697970635f3532373735360c48555941265a482632303532460056006c76008700000439687579615f75613d776562683526302e302e3126776562736f636b6574262664697970635f35323737353b207765625f71726c6f67696e5f636f6e6669726d5f69643d66346663303533322d366232332d346164322d393733612d6633656539623661386266653b207564625f637265643d436e43386e6a436e75773061597448787435396e6f5f2d4c6c684e784f596c316c536b5561784a6250486e3468744f536c6e4f7653395f32394273634f79493338344a70304238412d6550484c446770497a6e6d37366e48483976466f4a30677a62514e6a67567750794d42554c4244456a5773574365596d70497231436942304870435f6e7161354b78495f47725872425f57613252463b207564625f7569643d313139393636363931313734363b2079797569643d313139393636363931313734363b207564625f70617373706f72743d68795f3330303032343730383b20757365726e616d653d68795f3330303032343730383b207564625f76657273696f6e3d312e303b207564625f62697a746f6b656e3d41514259323031376479546430575f535a44313962643955545f4c6b717a757035794f67677a6c5a58726d7878377133596f664d43697668634c33784e30765977514877386764487771735955416155724f5a6e4539496b5f4c395f65784f69717261644252506d767879386635596a714c31506d625a66422d426b6957387769584a425271473964534b6e67484867615278434f3067334a5142774c6f76343449423972384d4b50756b4e557661523437464d5347765868737075724f4c33676148733577474d6f5a6e33703969486348664377694f68517577513376785f696236324e505250365132717370767a4e6f5a486f644d4f6f737971484a334e645f417167535f384a65555572617263636668465a64366e595a343446476e4a5068784b4a5734425752455952347744792d5264706a5a636a666c4a3958505f6e6f426e43494649417a72666c63497438305a49726f51743b207564625f6f726967696e3d303b207564625f7374617475733d313b207564625f6f746865723d2537422532326c7425323225334125323231373837363238383832363436253232253243253232697352656d253232253341253232312532322537442d2d2d2d31333035393930333131303b205f5f7961736d69643d302e30343337363430313033313338363331323b205f5f79616d69645f7474313d302e30343337363430313033313338363331323b205f5f79616d69645f6e65773d43424341324134413343363030303031364133463131434631334230313645413b20687579615f68645f7265705f636e743d323b205f5f79616f6c6479797569643d313139393636363931313734363b205f7961736964733d5f5f726f6f7473696425334443424341324134413344423030303031433635334432443042324630313443333b20687579615f7765625f7265705f636e743d379625613935613733373933363762643566643a613935613733373933363762643566643a303a30a80c", + "body_hex": "0000057a10032c3c400c560877656241637455496614676574416374557365725461736b44657461696c7d0001051e0800010604745265711d000105100a0a030000011751b85a02160026003622776562683526302e302e3126776562736f636b6574262664697970635f353237373547000004d0687579615f75613d776562683526302e302e3126776562736f636b6574262664697970635f35323737353b7765625f71726c6f67696e5f636f6e6669726d5f69643d66346663303533322d366232332d346164322d393733612d6633656539623661386266653b7564625f637265643d436e43386e6a436e75773061597448787435396e6f5f2d4c6c684e784f596c316c536b5561784a6250486e3468744f536c6e4f7653395f32394273634f79493338344a70304238412d6550484c446770497a6e6d37366e48483976466f4a30677a62514e6a67567750794d42554c4244456a5773574365596d70497231436942304870435f6e7161354b78495f47725872425f57613252463b7564625f7569643d313139393636363931313734363b79797569643d313139393636363931313734363b7564625f70617373706f72743d68795f3330303032343730383b757365726e616d653d68795f3330303032343730383b7564625f76657273696f6e3d312e303b7564625f62697a746f6b656e3d41514259323031376479546430575f535a44313962643955545f4c6b717a757035794f67677a6c5a58726d7878377133596f664d43697668634c33784e30765977514877386764487771735955416155724f5a6e4539496b5f4c395f65784f69717261644252506d767879386635596a714c31506d625a66422d426b6957387769584a425271473964534b6e67484867615278434f3067334a5142774c6f76343449423972384d4b50756b4e557661523437464d5347765868737075724f4c33676148733577474d6f5a6e33703969486348664377694f68517577513376785f696236324e505250365132717370767a4e6f5a486f644d4f6f737971484a334e645f417167535f384a65555572617263636668465a64366e595a343446476e4a5068784b4a5734425752455952347744792d5264706a5a636a666c4a3958505f6e6f426e43494649417a72666c63497438305a49726f51743b7564625f6f726967696e3d303b7564625f7374617475733d313b7564625f6f746865723d2537422532326c7425323225334125323231373837363238383832363436253232253243253232697352656d253232253341253232312532322537442d2d2d2d31333035393930333131303b5f5f7961736d69643d302e30343337363430313033313338363331323b5f5f79616d69645f7474313d302e30343337363430313033313338363331323b5f5f79616d69645f6e65773d43424341324134413343363030303031364133463131434631334230313645413b687579615f68645f7265705f636e743d323b5f5f79616f6c6479797569643d313139393636363931313734363b5f7961736964733d5f5f726f6f7473696425334443424341324134413344423030303031433635334432443042324630313443333b677569643d30613839633766383463636638643661333530313465633434666334386134623b486d5f6c76745f35313730306236633732326635626234636633393930366135393665613431663d313738373637383534313b486d5f6c7076745f35313730306236633732326635626234636633393930366135393665613431663d313738373637383534313b484d4143434f554e543d423243394546444339333435464437363b687579615f7765625f7265705f636e743d32305c66000b11622f0b8c980ca80c2c3625616137313265323931353735626431333a616137313265323931353735626431333a303a304c5c6600", + "func_tail_hex": "11622f", + "baseinfo_trace": "a95a7379367bd5fd:a95a7379367bd5fd:0:0", + "auth_hex": "030000011751b85a021622776562683526302e302e3126776562736f636b6574262664697970635f3532373735270000043a687579615f75613d776562683526302e302e3126776562736f636b6574262664697970635f35323737353b207765625f71726c6f67696e5f636f6e6669726d5f69643d66346663303533322d366232332d346164322d393733612d6633656539623661386266653b207564625f637265643d436e43386e6a436e75773061597448787435396e6f5f2d4c6c684e784f596c316c536b5561784a6250486e3468744f536c6e4f7653395f32394273634f79493338344a70304238412d6550484c446770497a6e6d37366e48483976466f4a30677a62514e6a67567750794d42554c4244456a5773574365596d70497231436942304870435f6e7161354b78495f47725872425f57613252463b207564625f7569643d313139393636363931313734363b2079797569643d313139393636363931313734363b207564625f70617373706f72743d68795f3330303032343730383b20757365726e616d653d68795f3330303032343730383b207564625f76657273696f6e3d312e303b207564625f62697a746f6b656e3d41514259323031376479546430575f535a44313962643955545f4c6b717a757035794f67677a6c5a58726d7878377133596f664d43697668634c33784e30765977514877386764487771735955416155724f5a6e4539496b5f4c395f65784f69717261644252506d767879386635596a714c31506d625a66422d426b6957387769584a425271473964534b6e67484867615278434f3067334a5142774c6f76343449423972384d4b50756b4e557661523437464d5347765868737075724f4c33676148733577474d6f5a6e33703969486348664377694f68517577513376785f696236324e505250365132717370767a4e6f5a486f644d4f6f737971484a334e645f417167535f384a65555572617263636668465a64366e595a343446476e4a5068784b4a5734425752455952347744792d5264706a5a636a666c4a3958505f6e6f426e43494649417a72666c63497438305a49726f51743b207564625f6f726967696e3d303b207564625f7374617475733d313b207564625f6f746865723d2537422532326c7425323225334125323231373837363238383832363436253232253243253232697352656d253232253341253232312532322537442d2d2d2d31333035393930333131303b205f5f7961736d69643d302e30343337363430313033313338363331323b205f5f79616d69645f7474313d302e30343337363430313033313338363331323b205f5f79616d69645f6e65773d43424341324134413343363030303031364133463131434631334230313645413b20687579615f68645f7265705f636e743d323b205f5f79616f6c6479797569643d313139393636363931313734363b205f7961736964733d5f5f726f6f7473696425334443424341324134413344423030303031433635334432443042324630313443333b20687579615f7765625f7265705f636e743d313036004001560c48555941265a4826323035322c36004c5c6600", + "auth_cookie_spaced": "huya_ua=webh5&0.0.1&websocket&&diypc_52775; web_qrlogin_confirm_id=f4fc0532-6b23-4ad2-973a-f3ee9b6a8bfe; udb_cred=CnC8njCnuw0aYtHxt59no_-LlhNxOYl1lSkUaxJbPHn4htOSlnOvS9_29BscOyI384Jp0B8A-ePHLDgpIznm76nHH9vFoJ0gzbQNjgVwPyMBULBDEjWsWCeYmpIr1CiB0HpC_nqa5KxI_GrXrB_Wa2RF; udb_uid=1199666911746; yyuid=1199666911746; udb_passport=hy_300024708; username=hy_300024708; udb_version=1.0; udb_biztoken=AQBY2017dyTd0W_SZD19bd9UT_Lkqzup5yOggzlZXrmxx7q3YofMCivhcL3xN0vYwQHw8gdHwqsYUAaUrOZnE9Ik_L9_exOiqradBRPmvxy8f5YjqL1PmbZfB-BkiW8wiXJBRqG9dSKngHHgaRxCO0g3JQBwLov44IB9r8MKPukNUvaR47FMSGvXhspurOL3gaHs5wGMoZn3p9iHcHfCwiOhQuwQ3vx_ib62NPRP6Q2qspvzNoZHodMOosyqHJ3Nd_AqgS_8JeUUrarccfhFZd6nYZ44FGnJPhxKJW4BWREYR4wDy-RdpjZcjflJ9XP_noBnCIFIAzrflcIt80ZIroQt; udb_origin=0; udb_status=1; udb_other=%7B%22lt%22%3A%221787628882646%22%2C%22isRem%22%3A%221%22%7D----13059903110; __yasmid=0.04376401031386312; __yamid_tt1=0.04376401031386312; __yamid_new=CBCA2A4A3C6000016A3F11CF13B016EA; huya_hd_rep_cnt=2; __yaoldyyuid=1199666911746; _yasids=__rootsid%3DCBCA2A4A3DB00001C653D2D0B2F014C3; huya_web_rep_cnt=10" +} \ No newline at end of file diff --git a/evidence/activity_verify_result.txt b/evidence/activity_verify_result.txt new file mode 100644 index 0000000..851f68f --- /dev/null +++ b/evidence/activity_verify_result.txt @@ -0,0 +1,11 @@ +[self_check] 通过: baseinfo/body/auth 与捕获金样本逐字节一致 +== 账号 [0] account: hy_300026595 uid=1199666913640 cookies=10 == + +-- [A] 带 cookie 调 getActUserTaskDetail (复刻浏览器序列) -- + 响应 249B | status=200 msg=请求成功 任务项=6 + => ✅ 已登录 (服务端确认: 请求成功 + 用户任务数据) + +-- [B] 无 cookie 对照 (应失败/错误) -- + 响应 96B | status=905 msg=None + +结论: ✅ cookie 登录态服务端验证通过 diff --git a/evidence/req_getActInfo.ws.bin b/evidence/req_getActInfo.ws.bin new file mode 100644 index 0000000000000000000000000000000000000000..2436470330063a18de915fd8ba95246cd3da6de7 GIT binary patch literal 103 zcmWe&*0FKm4C5$IO>#^w3H40lN>42TvOM$B@@p9w#5foj*;q<~QVV4n7&y5Ylk~+H q)EKyXX7H@w(J@m^GfXv4O-;2lH8!?1vouJu!Voa9GVqB>V*mgVq!}Fm literal 0 HcmV?d00001 diff --git a/evidence/req_getActUserTaskDetail.ws.bin b/evidence/req_getActUserTaskDetail.ws.bin new file mode 100644 index 0000000000000000000000000000000000000000..db69050a3acdb41c65538cd3fca72c4f1a47f9e0 GIT binary patch literal 1402 zcmb7E+m7Qz5Y4a)TJ6rmA|ZH!2c(&&&C)lw?G2W&ok^Tb66Z46Orn)q?Q5Ki?bwbl zk@y2XfKT8P_zXS?CnGHqi$M6HR9&a4tNZjRzkc@S$Dgj>zbITwwX=E8`^tB-vzNNb z15dYYH{-$X=f>6N-@RyDeepb-x`%J;)$5lppEVj+-%NgAefD+Zwn4wIcUTLz;4OFy zZ)-n|?5&&K-a0`Q+e|>BD0CZ*=YOn^6=z4T{C9e*PGN^+7x_WR>?rhtWX}Re@rY*w z0i!0h5N;Cgpr$Nh-t@5R$`<94<+-h+V=+5%9YqgyDctDcu>gFT8D%EOVZ@q)-Fj4- z%N^X!wsT(gt+5f3b!N_Xp_$KQhGZ?Z&8mkb(vN{AsZDom4BCF&JB53ZhK3>Mov07| zlQkJ_{P)GU8fx=_*1p>;(uM9W_i-(Mq!hP9e-Q+RR*n*>J=W@bheH{p8 znNli4C=%MLs!wC*yTm+A<0#3Lb;U3M01**Ltz-R4A>X@yi`MzMo1{S$D)241bh3g| z7H!>7Q776G0Vq1vg98?9wrWG!a^(2~8*C4!V=Ppr@1J(7$7ElY;sGxsZ>R_P+8*F? z1oCAuF$&3djN*_k=PI8k<|@3Cds{Y;nOmB{A>ocT9q;q9l00F#ISk->D7scnOW zR0NNGZF=a+&g?$)4a4WtQa6E*`xC7gL^&ZnO-`iY{rI>Y&2v5_VrMw(=8x+%J|^Y> z``k!{qC1RMA&%w1unkWy0&_h%789J8EU+jVji+NeL5DQXPorpMM9$ER(&}LJ@yKE7 z!Jjdy@6P85Pi)&;cUBG!mn%X#-LOAim-qbz(H7IY<&+fdsyTJy&C1?*yT1H5W?`g- zdau({Pl>m)dzl1Qy(F4s7nY+WsKu&0QJ!WzJEjVJLbA1+C^w>ZgU~Lk%?(y-3q?t! zNRlK(2|Y)VUfUp@y8FL!&YswA)*r+`kY#`&1TDrYp6+V_)gwo+NC|*7fF+6{x_~vt zvJCz!9=e61Yr2Y5qGFxa!UB-0xC5cyfmo{-cWUc0vTKJWZp`d3Q=YbucFsSE^?a(p zNfpMDD9UR02Dd+UXls`aIu&pmwSk5@01_Ry{Ilo*mt×tamp= + 响应 = base64(TAF信封)。**该端点被 volc-dcdn CDN 封锁非浏览器 TLS + (requests/curl/curl_cffi 均超时/502), 仅真实浏览器可直连。** + * WebSocket wss://wsapi.huya.com/?baseinfo= —— **纯 Python 可直连**, + 本工具走这条通道。 +- 认证/验证序列 (最小两帧, 已实测 200 请求成功): + * 认证帧 (裸 JCE struct): uid + UA + cookie(STR4), 向连接注册账号身份 + * getActUserTaskDetail (webActUI TAF 信封, 按目标账号重建) + getActUserTaskDetail 返回 200 请求成功 + 用户任务列表 + => 服务端确认该 cookie 已登录; 无 cookie 对照 => 905 错误。 +- 关键坑 (2026-08-26 逆向定位): + * baseinfo/认证帧/task 帧的 uid+cookie 必须全部一致, 否则服务端回 + "wup request biz execption:-1" (iErrorCode 5000)。 + * task 帧尾部 actId 字段 = JCE t1/INT16 标签+值 `11 62 2f` (3字节), + 缺标签字节会解析失败。 + * 活动页会自行补充页面级 cookie (guid/huya_ua/__yasmid/__yamid_* 等), + 纯 Python 产出的 10 个登录 cookie 已足够, 无需页面 cookie。 + +用法: + python tools/huya_activity_verify.py [cookie文件] [账号块索引] +默认 cookie 文件: evidence/web_cookies_full.txt, 账号块 0。 +""" +import base64 +import json +import os +import random +import re +import struct +import sys +import time +import urllib.parse + +sys.path.insert(0, os.path.join(os.path.dirname(os.path.abspath(__file__)), "..")) +try: + from websocket import create_connection +except ImportError: + print("缺少依赖: pip install websocket-client") + sys.exit(1) +from tools.huya_wup_encoder import _Writer # noqa: E402 + +HERE = os.path.dirname(os.path.abspath(__file__)) +UA_ACT = "webh5&0.0.1&websocket&&diypc_52775" +UA_BROWSER = ("Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 " + "(KHTML, like Gecko) Chrome/151.0.0.0 Safari/537.36") + + +# --------------------------------------------------------------------------- +# cookie 解析 +# --------------------------------------------------------------------------- +def parse_cookie_blocks(path): + """evidence/web_cookies_full.txt 多账号分块解析 (按空行分块, 行内以;分隔)。""" + blocks = [] + cur = None + with open(path) as f: + for raw in f: + line = raw.strip() + if not line: + cur = None + continue + if line.startswith('#'): + cur = {'account': line.lstrip('#').strip(), 'cookies': {}} + blocks.append(cur) + continue + if cur is None: + cur = {'account': 'unknown', 'cookies': {}} + blocks.append(cur) + for pair in line.split(';'): + pair = pair.strip() + if not pair or '=' not in pair: + continue + k, v = pair.split('=', 1) + cur['cookies'][k.strip()] = v.strip() + return blocks + + +# --------------------------------------------------------------------------- +# TAF 组包 (webActUI 活动接口, 与浏览器字节一致, 金样本对拍通过) +# --------------------------------------------------------------------------- +def make_trace_id() -> str: + return f"{random.getrandbits(64):016x}:{random.getrandbits(64):016x}:0:0" + + +def build_baseinfo(uid: int, ua: str, cookie_str: str, trace: str) -> bytes: + """cdnws/wsapi 的 baseinfo 原始字节 (JCE struct, b64进URL)。""" + w = _Writer() + w.int64(0, uid) + w.string(1, "") + w.string(2, ua) + w.string(3, "HUYA&ZH&2052") + w.string(4, "") + w.string(5, "") + w.int8(6, 0) + w.string(7, "") + w.string(8, cookie_str) # cookie (len>255 -> STR4) + w.string(9, trace) + w.map_begin(10, 0) + return w.get() + + +def build_body(servant: str, func: str, uid: int, ua: str, cookie_str: str, + trace: str, reqid: int = 1, func_tail: bytes = b"", + with_trace: bool = True) -> bytes: + """构造 webActUI TAF 信封 (含4B长度前缀)。已与捕获金样本逐字节一致。""" + w = _Writer() + w.struct_begin(0) + w.struct_begin(0) + w.int64(0, uid) + w.string(1, "") + w.string(2, "") + w.string(3, ua) + w.string(4, cookie_str) # cookie (STR4) + w.int8(5, 0) + w.string(6, "") + w.struct_end() + w.buf += func_tail # func 特有尾部 (value 内, 如 actId) + w.struct_end() + value = w.get() + + body = bytearray() + body += b"\x10\x03\x2c\x3c\x40" + bytes([reqid]) + b"\x56" + body += b"\x08" + servant.encode() + body += b"\x66" + bytes([len(func)]) + func.encode() + b"\x7d" + body += b"\x00\x01" + struct.pack(">H", len(value) + 14) + body += b"\x08\x00\x01\x06\x04tReq\x1d\x00\x01" + struct.pack(">H", len(value)) + body += value + body += b"\x8c\x98\x0c\xa8\x0c" + if with_trace: + tr = trace.encode() + body += b"\x2c\x36" + bytes([len(tr)]) + tr + b"\x4c\x5c\x66\x00" + return struct.pack(">I", len(body)) + bytes(body) + + +def wrap_ws(inner: bytes) -> bytes: + """4B前缀信封 -> WS 帧 (00 03 1d 00 01 [2B len][4B len][inner])。 + + len = 信封(去4B长度前缀) - 固定头(12 + svc长 + func长), 与捕获帧一致。 + """ + e = inner[4:] + svc_len = e[7] # 56 + func_len = e[9 + svc_len] # 66 + hdr = 12 + svc_len + func_len + ln = len(e) - hdr + return b"\x00\x03\x1d\x00\x01" + ln.to_bytes(2, "big") + ln.to_bytes(4, "big") + e + + +ACT_ID = 0x622F # 25135, 金样本 task 帧尾部 11 62 2f +ACT_ID_TAIL = b"\x11" + struct.pack(">H", ACT_ID) # JCE: t1/INT16 标签 + 值 + + +def build_auth_frame(uid: int, ua: str, cookie_spaced: str) -> bytes: + """认证帧 (裸 JCE struct, 无 TAF 信封; 注册连接的账号身份)。 + + 结构 (与实抓逐字节一致): t0 uid + t1 UA + t2 cookie(STR4) + t3 空 + + t4 1 + t5 版本 + t2..t6 空尾字段。包装头 00 0a 1d 00 01 [2B len]。 + """ + w = _Writer() + w.int64(0, uid) + w.string(1, ua) + w.string(2, cookie_spaced) + w.string(3, "") + w.int8(4, 1) + w.string(5, "HUYA&ZH&2052") + w.int8(2, 0) + w.string(3, "") + w.int8(4, 0) + w.int8(5, 0) + w.string(6, "") + body = w.get() + return b"\x00\x0a\x1d\x00\x01" + (len(body) - 7).to_bytes(2, "big") + body + + +def build_task_frame(uid: int, cookie_tight: str, reqid: int = 5) -> bytes: + inner = build_body("webActUI", "getActUserTaskDetail", uid, UA_ACT, + cookie_tight, make_trace_id(), reqid=reqid, + func_tail=ACT_ID_TAIL) + return wrap_ws(inner) + + +# --------------------------------------------------------------------------- +# 响应解析 (验证只需状态/消息/条目) +# --------------------------------------------------------------------------- +def extract_msg(resp: bytes): + """从 TAF 响应中提取消息串 (JCE t1 STR1, 常见: 请求成功/活动已下线/…)。""" + msgs = re.findall(rb"[\xe4-\xe9][\x80-\xbf][\x80-\xbf]", resp) + txt = "".join(m.decode() for m in msgs) + if not txt: + m = re.search(rb"STATUS_RESULT_DESC[\x16]\x1c([\x20-\x7e]+)", resp) + if m: + txt = m.group(1).decode(errors="replace") + return txt or None + + +def task_ok(resp: bytes) -> tuple: + """返回 (is_ok, msg, item_count)。""" + msg = extract_msg(resp) + # 状态码: 成功 0a 01 0x00c8=200 / 活动下线 0a 01 0x0259 / 错误 01 0x0389=905 等 + st = None + m = re.search(rb"\x0a\x01(..)", resp) + if m: + st = int.from_bytes(m.group(1), "big") + else: + m = re.search(rb"\x01(..)\x81\x13\x88", resp) # tars 错误: 业务码 + iErrorCode5000 + if m: + st = int.from_bytes(m.group(1), "big") + # 任务条目 = value 内任务 STRUCT 头 (每任务以 0a 01 62 开头) + items = 0 + if st == 200: + items = len(re.findall(rb"\x0a\x01\x62\x2f\x12\x00", resp)) or len(re.findall(rb"\x0a\x01\x62", resp)) + return (st == 200 and "请求成功" in msg, st, msg, items) + + +# --------------------------------------------------------------------------- +# 验证主流程 +# --------------------------------------------------------------------------- +def verify_ws(uid: int, cookie_tight: str, with_cookie: bool = True, + timeout: float = 30) -> dict: + """WS 通道验证登录态: 认证帧(注册连接身份) + getActUserTaskDetail。 + + 经验 (2026-08-26 实测): 帧身份必须与连接 baseinfo 一致 —— + baseinfo/认证帧/task 帧必须同一 uid+cookie, 否则服务端回 + "wup request biz execption:-1"。认证帧 + task 帧两帧即足够。 + """ + cookie_spaced = "; ".join(p.strip() for p in cookie_tight.split(";") if "=" in p) + if not with_cookie: + cookie_spaced, cookie_tight = "", "" + bi = build_baseinfo(uid, UA_ACT, cookie_spaced, make_trace_id()) + url = "wss://wsapi.huya.com/?" + urllib.parse.urlencode( + {"baseinfo": base64.b64encode(bi).decode()}) + ws = create_connection(url, timeout=15, header={ + "User-Agent": UA_BROWSER, "Origin": "https://zt.huya.com"}) + out = {} + try: + ws.send(build_auth_frame(uid, UA_ACT, cookie_spaced), opcode=2) + time.sleep(0.3) + ws.send(build_task_frame(uid, cookie_tight, reqid=5), opcode=2) + time.sleep(0.5) + ws.settimeout(timeout) + deadline = time.time() + timeout + task_resp = None + while time.time() < deadline: + try: + r = ws.recv() + except Exception: + break + rp = r.encode("latin1") if isinstance(r, str) else r + if b"getActUserTaskDetail" in rp[:90]: + task_resp = rp + break + out["task_resp_len"] = len(task_resp) if task_resp else 0 + if task_resp: + ok, st, msg, items = task_ok(task_resp) + out.update(ok=ok, status=st, msg=msg, items=items) + else: + out.update(ok=False, msg="无 getActUserTaskDetail 响应") + finally: + ws.close() + return out + + +def self_check() -> bool: + """金样本回归: 与捕获 baseinfo/body 逐字节比对, 防 builder 漂移。""" + golden_path = os.path.join(HERE, "..", "evidence", "activity_taf_golden.json") + if not os.path.exists(golden_path): + print("[self_check] 无金样本 evidence/activity_taf_golden.json, 跳过") + return True + g = json.load(open(golden_path)) + bi = build_baseinfo(g["uid"], g["ua"], g["cookie_spaced"], g["baseinfo_trace"]) + assert bi == bytes.fromhex(g["baseinfo_hex"]), "baseinfo 与金样本不一致!" + body = build_body("webActUI", "getActUserTaskDetail", g["uid"], g["ua"], + g["cookie_tight"], g["trace"], reqid=g["reqid"], + func_tail=bytes.fromhex(g["func_tail_hex"])) + assert body == bytes.fromhex(g["body_hex"]), "body 与金样本不一致!" + auth = build_auth_frame(g["uid"], g["ua"], g.get("auth_cookie_spaced", g["cookie_spaced"])) + assert auth[7:] == bytes.fromhex(g.get("auth_hex", "")), "auth 帧与金样本不一致!" + print("[self_check] 通过: baseinfo/body/auth 与捕获金样本逐字节一致") + return True + + +def main(): + self_check() + path = sys.argv[1] if len(sys.argv) > 1 else os.path.join( + HERE, "..", "evidence", "web_cookies_full.txt") + idx = int(sys.argv[2]) if len(sys.argv) > 2 else 0 + blocks = parse_cookie_blocks(path) + blk = blocks[idx] + ck = blk["cookies"] + uid = int(ck.get("udb_uid") or ck.get("yyuid") or 0) + cookie_tight = ";".join(f"{k}={v}" for k, v in ck.items()) + print(f"== 账号 [{idx}] {blk['account']} uid={uid} cookies={len(ck)} ==") + + print(f"\n-- [A] 带 cookie 调 getActUserTaskDetail (复刻浏览器序列) --") + ra = verify_ws(uid, cookie_tight) + print(f" 响应 {ra['task_resp_len']}B | status={ra.get('status')} msg={ra.get('msg')} " + f"任务项={ra.get('items')}") + print(" =>", "✅ 已登录 (服务端确认: 请求成功 + 用户任务数据)" + if ra.get("ok") else "❌ 未确认") + + print(f"\n-- [B] 无 cookie 对照 (应失败/错误) --") + rb = verify_ws(uid, cookie_tight, with_cookie=False) + print(f" 响应 {rb['task_resp_len']}B | status={rb.get('status')} msg={rb.get('msg')}") + + print("\n结论:", "✅ cookie 登录态服务端验证通过" + if ra.get("ok") else "❌ 未通过 (cookie 可能过期或活动已结束)") + + +if __name__ == "__main__": + main() \ No newline at end of file