Files
live-hub-py/scripts/probe_nonce_bind.py
T
yml2213 97e6d14139 feat(huya): nonce生成机制完全破解 - 任意账号零设备出完整CK闭环
动态hook实锤(scripts/hook_nonce_chain.py):
  rnd = XXTEA(pack(st) + pack(counter|st<<16), key=MD5hex(uid_str + k1)[:16])
  - st=毫秒时间戳, counter同st内递增
  - k1=设备常量865a4924...(不随账号变, evidence/nonce_k1.json)
  - Python复刻(tools/nonce_forge.py)对设备两轮rnd逐字节MATCH

推翻v2误判(X2'nonce账号绑定令牌'实为nonce内编码uid):
  - 换账号用目标uid重算nonce而非复用旧nonce!
  - 纯随机nonce必40020因解不出合法st/counter结构

实测: 同账号+跨账号(hy_300024708从未设备登录)本地nonce铸证bind均通过,
  full_web_cookie/full_auto_safe 任意账号密码->全套cookie(udb_cred/udb_biztoken)
2026-08-26 00:49:01 +08:00

116 lines
5.0 KiB
Python

#!/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())