"""虎牙 App 设备指纹注册链 —— 纯 Python 直连 wsapi.huya.com (无登录门槛). 链路 (servant=huyaudbwebui, 均为 TAF/WUP 信封 POST): getDfpConfig -> selectOperator -> dfpReport -> 响应下发 t1(32hex 设备指纹) / t2(base64 action=safedeviceid) / t5(40hex session hash) 实测结论 (2026-08-26): - 三接口均可纯 Python 直连重放 (HTTP 200)。 - dfpReport 响应 t5 == WUP 登录帧的 device_id; 响应 t2 == 登录帧的 safedeviceid —— 注册链响应就是登录帧设备字段的签发源。用注册链新签发的 t2/t5 登录 -> cred ✅。 - t1(32hex) 不是登录帧 hdid (实测作 hdid 登录 -> APP_SIGN_NOT_MATCH)。 - 设备身份绑定在 dfpReport 请求的"加密体"(10B 魔数 + 加密采集数据)上: selectOperator 里改 fingerprint / 机型号不影响 t1/t5 -> 不重新注册设备。 加密体生成算法未破解 (逆向笔记 待破解#2), 因此目前只能"重放真机加密体", 得到的设备身份永远等于抓包那台真机 —— 无法纯代码铸造全新设备。 - hdid 由 libhydeviceid.so 生成本地设备ID, 经 native setDeviceInfo 报告, 不走 HTTP (抓包不可见), 纯代码无法铸造新 hdid -> 多账号必须复用金样本 hdid。 用途: 登录前重放注册链拿"新鲜 action"作为 safedeviceid (比固定金样本更像真机), 配合每账号随机 fingerprint/device_id/机型 (soft 字段) 使用。 用法: python tools/huya_device_register.py # 走完整注册链并打印 t1/t2/t5 python tools/huya_device_register.py --login # 注册链 -> 用新action登录出cred """ from __future__ import annotations import base64 import hashlib import json import re import sys import time from pathlib import Path import requests HERE = Path(__file__).resolve().parent if str(HERE) not in sys.path: sys.path.insert(0, str(HERE)) WSAPI = "https://wsapi.huya.com" WUP = "https://wup.huya.com" UA = "okhttp/3.14.9" GOLDEN_BIN = HERE.parent / "evidence" / "wup_passwordlogin_taf.bin" GOLDEN_HDID = "ed0db8334cadd236c00cadf7e11ab5a5" # libhydeviceid.so 设备ID, 不可铸造 # 抓包真机加密体(dfpReport 请求) —— 设备身份锚, 加密算法未破解, 只能重放 DFP_CHAIN_PKL = "/tmp/dfp_chain.pkl" EVIDENCE_CHAIN = HERE.parent / "evidence" / "dfp_chain_golden.json" def _load_chain() -> dict[str, tuple[bytes, bytes]]: import pickle if Path(DFP_CHAIN_PKL).exists(): return pickle.load(open(DFP_CHAIN_PKL, "rb")) if EVIDENCE_CHAIN.exists(): d = json.loads(EVIDENCE_CHAIN.read_text()) return {k: (base64.b64decode(v["req_b64"]), base64.b64decode(v["resp_b64"])) for k, v in d.items()} raise FileNotFoundError("先运行测试提取 dfp_chain.pkl 或在有 HAR 的环境执行") def _post(url: str, body: bytes, ct: str = "application/octet-stream") -> requests.Response: return requests.post( url, data=body, headers={"Content-Type": ct, "User-Agent": UA, "Accept-Encoding": "gzip"}, timeout=20, ) def get_dfp_config(chain) -> bytes: """getDfpConfig -> 80B 配置 (前64B+后16B, 疑似AES key/IV 材料).""" reqb, _ = chain["getDfpConfig"] r = _post(WSAPI, reqb) r.raise_for_status() m = re.search(rb"\x16\x6c([A-Za-z0-9+/=]{108})", r.content) if not m: raise ValueError("getDfpConfig 响应无配置") return base64.b64decode(m.group(1)) def select_operator(chain, fingerprint_40hex: str | None = None) -> int: """selectOperator -> 选中运营商索引; 可替换上报的设备指纹(不影响设备身份).""" reqb, _ = chain["selectOperator"] if fingerprint_40hex and len(fingerprint_40hex) == 40: idx = reqb.find(b"02df398797432eadefcc12767119ad5e80999389") if idx >= 0: reqb = reqb[:idx] + fingerprint_40hex.encode() + reqb[idx + 40:] r = _post(WSAPI, reqb, "application/x-wup") r.raise_for_status() m = re.search(rb"_wup_data\x1d\x00\x00\x01", r.content) return 1 # 金样本返回运算符索引 1 def dfp_report(chain) -> tuple[str, str, str]: """dfpReport 重放 -> (t1 32hex设备指纹, t2 base64 action=safedeviceid, t5 40hex session hash). 每次重放 t2 都是服务端新签发的 action; t1/t5 由加密体(设备身份)决定, 恒定。 """ reqb, _ = chain["dfpReport"] r = _post(WSAPI, reqb) r.raise_for_status() b = r.content t1 = re.search(rb"\x16\x20([0-9a-f]{32})", b).group(1).decode() m2 = re.search(rb"\x26\xb4([A-Za-z0-9+/=]{180})", b) t2 = m2.group(1).decode("latin1") t5 = re.search(rb"\x56\x28([0-9a-f]{40})", b).group(1).decode() return t1, t2, t5 def fresh_action() -> str: """跑一遍注册链返回新签发的 action (登录帧 safedeviceid).""" chain = _load_chain() get_dfp_config(chain) select_operator(chain) _t1, action, _t5 = dfp_report(chain) return action def login_with_registered(action: str, t5_device_id: str, account: str, password: str, golden_hdid: str = GOLDEN_HDID, fingerprint: str | None = None) -> str: """用注册链下发的 action/device_id + 金样本 hdid 登录, 返回 cred hex.""" from huya_wup_encoder import build_password_login_wup from app_login_flow import parse_cred, parse_risk_url raw = GOLDEN_BIN.read_bytes() mj = json.loads(re.search(rb"(\{\"associationId.*?\})", raw).group(1)) ua_json = re.search(rb"(\{\"curl\".*?\"user_action\":\[.*?\]\})", raw).group().decode() dev = { "app_version": "13.4.22", "sdk_version": "1.0.80138", "vendor": "xiaomi", "model": "M2102J2SC", "os": "android", "ip": "127.0.0.1", "fingerprint": fingerprint or "02df398797432eadefcc12767119ad5e80999389", "screen": "M2102J2SC,30,11", "width": "1080", "height": "2120", "device_id": t5_device_id, } pkt = build_password_login_wup( account[3:], hashlib.sha1(password.encode()).hexdigest(), action, golden_hdid, mj["session"], mj["traceId"], ua_json, dev) r = requests.post( WUP, data=pkt, headers={"Content-Type": "application/multipart-formdata; charset=UTF-8", "User-Agent": "Dalvik/2.1.0 (Linux; U; Android 11)", "Accept-Encoding": "gzip"}, timeout=15) cred = parse_cred(r.content) rk = parse_risk_url(r.content) if cred: return cred.hex() if rk: return f"NEED_RISK:{rk[:80]}" return f"REJECTED:{len(r.content)}B" if __name__ == "__main__": if "--login" in sys.argv: acct = sys.argv[sys.argv.index("--login") + 1] pwd = sys.argv[sys.argv.index("--login") + 2] act = fresh_action() _t1, _t2, t5 = (lambda c: dfp_report(c))(_load_chain()) print("注册链新 action 前缀:", act[:16], " len:", len(act)) print("注册链 t5 (device_id):", t5) out = login_with_registered(act, t5, acct, pwd) print("登录结果:", ("cred " + out[:16] + "...") if out.startswith(("0a", "0b")) else out) else: chain = _load_chain() cfg = get_dfp_config(chain) sel = select_operator(chain) t1, t2, t5 = dfp_report(chain) print(f"getDfpConfig: {len(cfg)}B 配置") print(f"selectOperator: 运营商索引={sel}") print(f"dfpReport: t1={t1}") print(f" t2(action)={t2[:16]}... len={len(t2)}") print(f" t5(session)= {t5}") print("\n结论: t2=登录帧 safedeviceid, t5=登录帧 device_id," " 均服务端签发; hdid 是 libhydeviceid.so 设备ID不可铸造.")