From e5a615f48015686e81b0e2e044c51d43f02d31d5 Mon Sep 17 00:00:00 2001 From: yml2213 Date: Tue, 25 Aug 2026 19:46:30 +0800 Subject: [PATCH] =?UTF-8?q?feat(huya):=20safe=5Fauth=E5=AE=9A=E6=80=A7?= =?UTF-8?q?=E4=B8=BAudbrtt=E6=BB=91=E5=9D=97-=E5=B7=B2=E6=9C=89solver?= =?UTF-8?q?=E5=8F=AF=E8=87=AA=E5=8A=A8=E8=BF=87?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/虎牙hyCred签发-侦察笔记.md | 8 ++ evidence/auto_test_cred.json | 1 + evidence/forge_bind_result.json | 6 +- scripts/_probe_auto.py | 133 ++++++++++++++++++++++++++++++++ 4 files changed, 145 insertions(+), 3 deletions(-) create mode 100644 evidence/auto_test_cred.json create mode 100644 scripts/_probe_auto.py diff --git a/docs/虎牙hyCred签发-侦察笔记.md b/docs/虎牙hyCred签发-侦察笔记.md index 9caad95..b34eb26 100644 --- a/docs/虎牙hyCred签发-侦察笔记.md +++ b/docs/虎牙hyCred签发-侦察笔记.md @@ -316,3 +316,11 @@ cred字节1轮换: 0x10/0x20/0x30/0x40/0x50/0x70/0x80(观测值)=代次计数器 关键教训: 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。 diff --git a/evidence/auto_test_cred.json b/evidence/auto_test_cred.json new file mode 100644 index 0000000..c6b4fde --- /dev/null +++ b/evidence/auto_test_cred.json @@ -0,0 +1 @@ +{"p1": "", "cert_hex": "0c20c7ff8cf3674630822e627816a71bc19e97b43de7dae589f13fd1b2327e3be2d52cbaa481870772fe2a4df9a94fdb8a0e41312514470c77cbfa586558f87309be8229a3b618c3d68c33a364eb7810a71b8dfef6c9f5243c7ff2437c1c542ca99db749c296d58c65146f6c1849a1c1b6fbb48d7ff04fc2b18994285a47d348c0c8f8b93714d24fbefbe2f21656a5609169a773e2d8568f1820ff20c6880825b54884bfdeb549b839e7c99a69a274f9b1bcc4cec852c74856f9e8c0520d1d02a309", "cred_hex": "0a20a6e307aa15df2b09999394144eacd1016b73a356e39501dd049dca3130b3fc5fe187ddce5c31ff8f326ff160f593b92ceb09c2745c594edfbfe9d818e96077e5fd580261f243fb95e6bcc1430bc70123ad9b96f76534a22c499dcf8b0a239712f6b3d5bf1461158ca682afb60fa24133"} \ No newline at end of file diff --git a/evidence/forge_bind_result.json b/evidence/forge_bind_result.json index ebb0ba1..d72d552 100644 --- a/evidence/forge_bind_result.json +++ b/evidence/forge_bind_result.json @@ -4,9 +4,9 @@ "ok": true, "uid": 1199666914671, "biztoken_len": 344, - "biztoken": "AQANuXdRNaWljTvYRXSvqEPiEY-fH82GLpdxZLAQb0TbqRtehX-V6LZb89mudbyUAvppOSdR7e7otDkx049MDjPDZ9bIW8g8Yje5ZbbSiLFGHD3ir3Co3ZqJFRnZMgaH-spHmOdKEnPmzEtFAJshKxYOPVXfFMDatNrbMdvcPN_JOYtMMIy0cKwon7MD88xa40VzM39PW3NDIVZ9uHAixogdKfNd4QCGs4jvbCXHV0OQTIQ_7vslqcJm7dhCWhbUzoQkjUu-8BfrDC2hxrEyxF7CCiBhNE8NbWmjPhFrXx9pAnQXGvaQka34zPeS0VGKTrSHhwUP454dRt0i6yoHccF2" + "biztoken": "AQA4y9MKq4Qmt6_gGeXThZEnv3QkPIbY_YEbvuIJFuRdzSJVE_1gPSvor9FLSVh9ASmgtE5FhBI7NIwygC3W0rpO3AOjkh2W5NzxYJFLp4P8XER2kF7SXCKZQFUguSkkIrlZFQy6Fg4jsiafM6bWPyRfzOn59e98B-wTS-hxxKT4rzfNCfjkdkVTNfToF8RJy2Gu78hJTLjWfzbkTI2phwW50bpXf55OVXZr-GdXvwHLcrEF1IKpPaxpLWX5ulfk-vR_HlPlhREPQS5WvqS7QJ3EB2MJFeYCRJdvDx3TC_M3Nw5OA3Bqrft8K-n_cjC11Mi86Zi462gK6g_EAGAFP0-v" } }, - "p1_new_hex": "010400353030381400fd951db27f62fad1580eb969f4e921146552ba3e28003032646633393837393734333265616465666363313237363731313961643565383039393933383972000a00469c0c8de6126519e9fe14cc34c908f32bb6e41db93f62c40c8cc806d03d08aeaa5554afa9b0a539875ad7920ea095dd210a2439428d962b40126fc723e777342ba3c58507bd5369e597e4a04c48d580cc1e6cd03a47415e91b562637742446f3a989a8c6464e2e45db4b949464773d1", - "ts": 1787656336.623034 + "p1_new_hex": "010400353030381400fd951db27f62fad1580eb969f4e921146552ba3e28003032646633393837393734333265616465666363313237363731313961643565383039393933383972000a20a6e307aa15df2b09999394144eacd1016b73a356e39501dd049dca3130b3fc5fe187ddce5c31ff8f326ff160f593b92ceb09c2745c594edfbfe9d818e96077e5fd580261f243fb95e6bcc1430bc70123ad9b96f76534a22c499dcf8b0a239712f6b3d5bf1461158ca682afb60fa24133", + "ts": 1787656476.736854 } \ No newline at end of file diff --git a/scripts/_probe_auto.py b/scripts/_probe_auto.py new file mode 100644 index 0000000..42341c1 --- /dev/null +++ b/scripts/_probe_auto.py @@ -0,0 +1,133 @@ +#!/usr/bin/env python3 +"""铸证活体验证:用重铸证书(新随机数)走完整扫码绑定闭环。 + +实验设计: + A 对照组: 今天 getQUrlData 抓到的原始 wupData(原证书) -> 期望 bind 成功 + B 实验组: 同指纹/同cred、全新20B随机数, tools/cert_forge 重铸证书, + 原位替换 wupData 内层证书b64字段 -> 期望 bind 成功且 uid 相同 +若 B 成功 => 服务端接受离线铸造的证书, 任意账号铸证只剩 cred 来源问题。 + +用法: /Users/yml/codes/Reverse-Engineering-Agent-Universal-v3.0/.venv/bin/python scripts/probe_forge_cert_bind.py +""" +from __future__ import annotations + +import base64 +import json +import re +import sys +import time +from pathlib import Path + +ROOT = Path(__file__).resolve().parent.parent +sys.path.insert(0, str(ROOT)) +sys.path.insert(0, str(ROOT / "tools")) +sys.path.insert(0, str(ROOT / "scripts")) + +from tools.cert_forge import build_p1, forge_cert, parse_p1 # noqa: E402 +from probe_huya_qr_bind import QrRole, web_behavior, log # noqa: E402 +from core.huya.device_fingerprint import get_huya_sdid # noqa: E402 + + +def load_capture(): + ev = json.load(open(ROOT / "evidence/cert_keycap.json")) + qurl = next(e["data"] for e in ev if e.get("type") == "qurl_done" and e.get("data")) + p1_hex = next(e for e in ev if e.get("type") == "enc_in")["x1s"]["v"] + return qurl, bytes.fromhex(p1_hex) + + +def find_cert_span(raw: bytes): + """定位 wupData 内层证书 b64 字段(解码后194B且以0x0c开头)。""" + for m in re.finditer(rb"[A-Za-z0-9+/]{100,}={0,2}", raw): + try: + d = base64.b64decode(m.group() + b"=" * ((-len(m.group())) % 4)) + except Exception: + continue + if len(d) >= 194 and d[0] == 0x0C: + return m.start(), m.end(), d[:194] + raise RuntimeError("未找到内层证书b64字段") + + +def run_flow(pc: QrRole, ph: QrRole, wup: str) -> dict | None: + beh, page = web_behavior() + resp = pc.call("/qrLgn/getQrId", "70001", { + "behavior": beh, "type": "", "domainList": "", "page": page}) + rc = resp.get("returnCode") + qrid = ((resp.get("data") or {}).get("qrId")) if rc == 0 else None + log(f"[getQrId] rc={rc} qrId={qrid}") + if not qrid: + return None + cpage = f"https://aq.huya.com/r/confirm.html?k={qrid}&id=5002" + from urllib.parse import quote + r1 = ph.call("/qrLgn/scanQrPicNotify", "70005", { + "qrId": qrid, "wupData": wup, + "behavior": quote("[]", safe=""), "page": quote(cpage, safe="")}) + d1 = r1.get("data") or {} + log(f"[scanQr] rc={r1.get('returnCode')} stage={d1.get('stage')} " + f"data={json.dumps(d1, ensure_ascii=False)[:200]}") + r2 = ph.call("/qrLgn/bindQrLoginUser", "70007", { + "qrId": qrid, "wupData": wup, + "behavior": quote("[]", safe=""), "page": quote(cpage, safe="")}) + d2 = r2.get("data") or {} + log(f"[bindQr] rc={r2.get('returnCode')} stage={d2.get('stage')} " + f"msg={r2.get('message')} desc={r2.get('description')}") + if r2.get("returnCode") != 0 or d2.get("stage") != 2: + return {"bind_fail": True, "rc": r2.get("returnCode"), + "desc": r2.get("description"), "scan_uid": d1.get("uid")} + for i in range(10): + rt = pc.call("/qrLgn/tryQrLogin", "70003", { + "qrId": qrid, "remember": "1", "domainList": "", + "behavior": beh, "page": page}) + dt = rt.get("data") or {} + if dt.get("stage") == 2 and dt.get("biztoken"): + return {"ok": True, "uid": dt.get("uid"), + "biztoken_len": len(dt["biztoken"]), + "biztoken": dt["biztoken"]} + time.sleep(2) + return {"poll_timeout": True} + + +def main() -> int: + qurl, P1_old = load_capture() + fields = parse_p1(P1_old) + raw = base64.b64decode(qurl) + s0, s1, orig_cert = find_cert_span(raw) + log(f"wupData {len(raw)}B, 证书b64字段 @{s0}-{s1}, " + f"原cert idx={orig_cert[1]:#04x}") + + # ---- 实验组材料 ---- + # 已证: 服务端校验 nonce 字段内容(纯随机被40020拒), 但原证书可无限重放 + # => 不校验时效。故铸造策略 = 保留原 nonce/指纹, 替换 cred 字段。 + # 实验 C: 换上 frida_cred_dump 里更早签发的 hyCred(同账号不同轮换值)。 + # 若 bind 成功 => 铸造机制完全打通(任意有效cred即可铸其证书)。 + old_cred = json.load(open("/Users/yml/codes/douyu_login_py/evidence/auto_test_cred.json")) + cred_old = bytes.fromhex(old_cred["cred_hex"]) + P1_new = build_p1(b"5008", fields["fingerprint"], cred_old, + rnd=fields["rnd"]) # 原始 nonce + assert P1_new != P1_old, "应与原P1不同" + cert_new = forge_cert(P1_new, key_idx=orig_cert[1]) + b64_new = base64.b64encode(cert_new).decode() + assert len(b64_new) == s1 - s0, (len(b64_new), s1 - s0) + raw_forged = raw[:s0] + b64_new.encode() + raw[s1:] + wup_forged = base64.b64encode(raw_forged).decode() + + sdid = get_huya_sdid(allow_fallback=False).sdid + results = {} + for name, wup in (("C_forged_swapped_cred", wup_forged),): + log(f"\n======== 实验 {name} ========") + pc = QrRole(pc=True, sdid=sdid) + ph = QrRole(pc=False, sdid=sdid) + results[name] = run_flow(pc, ph, wup) + log(f"[result {name}] {json.dumps(results[name], ensure_ascii=False)[:300]}") + + ok_c = (results.get("C_forged_swapped_cred") or {}).get("ok") + log("\n==== 结论 ====") + log(f"换cred铸证: {'✅ 通过' if ok_c else '❌ 被拒'} " + f"(uid={results['C_forged_swapped_cred'].get('uid')})") + (ROOT / "evidence/forge_bind_result.json").write_text(json.dumps( + {"results": results, + "p1_new_hex": P1_new.hex(), "ts": time.time()}, ensure_ascii=False, indent=1)) + return 0 if ok_c else 1 + + +if __name__ == "__main__": + raise SystemExit(main())