Files
live-hub-py/tools/huya_device_register.py
T
yml2213 eccc00e090 docs/code(huya): 设备标识符命名规范落地 - 全库HDID32/GUID32/DEVID40前缀化去混
- docs/HUYA_ID_NOMENCLATURE.md: 规范总表 + 三条强制规则 + 代码变量对应
- 4 篇主文档头部去混声明 + §11.x 被证伪假设行标注更正
- 代码: GOLDEN_HDID→GOLDEN_HDID32, device_profile.HDID→HDID32, wup_encoder/app_login 注释前缀化
- 后续所有表述必须以 GUID32(doLaunch) / HDID32(登录t1.t0) / DEVID40(t5) 前缀书写
2026-08-28 23:02:21 +08:00

248 lines
11 KiB
Python

"""虎牙 App 设备指纹注册链 —— 纯 Python 直连 wsapi.huya.com (无登录门槛).
链路 (servant=huyaudbwebui, 均为 TAF/WUP 信封 POST):
getDfpConfig -> selectOperator -> dfpReport
-> 响应下发 t1(32hex appkey回声) / t2(base64 action=safedeviceid) / t5(40hex device_id)
实测结论 (2026-08-26/27):
- 三接口均可纯 Python 直连 (HTTP 200)。
- dfpReport 响应 t5 == WUP 登录帧的 device_id; 响应 t2 == 登录帧的 safedeviceid ——
注册链响应就是登录帧设备字段的签发源。用注册链新签发的 t2/t5 登录 -> cred ✅。
- t1(32hex) 不是设备锚: 恒=appkey 变体回声; selectOperator 全改(sd+fp+机型)不影响它。
- ★(2026-08-27 突破) 服务端不校验 dfpReport 加密体(cw)内容: 随机 4146B cw 照样
200 + 签发新 t2/t5。t5(device_id) = 服务端从 cw 解出的 40hex hdid, 随机 cw 时
为随机值但服务端宽容。-> 无需真机加密体, python 零设备注册链达成 (--gen)。
- 唯一不可铸造: WUP 登录帧 32hex HDID32 (登录t1.t0设备证书; libhydeviceid.so 本地
生成 + native setDeviceInfo 上报, 不走 HTTP; 与 GUID32(doLaunch sGuid) 两套体系,
见 docs/HUYA_ID_NOMENCLATURE.md) -> 多账号必须复用金样本 HDID32 (APP_SIGN_NOT_MATCH 硬错)。
用途:
登录前注册链拿"新鲜 action" + [重放|随机] dfpReport 作为 safedeviceid,
配合每账号随机 fingerprint/device_id/机型 (soft 字段) 使用。
用法:
python tools/huya_device_register.py # 重放注册链并打印 t1/t2/t5
python tools/huya_device_register.py --gen # ★生成器随机cw注册链 (零设备)
python tools/huya_device_register.py --gen-login <acct> <pwd> # ★零设备注册链->登录
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_HDID32 = "ed0db8334cadd236c00cadf7e11ab5a5" # HDID32 (登录帧t1.t0设备证书, 纯代码不可铸)
# 抓包真机加密体(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
return parse_dfp_response(b)
def parse_dfp_response(b: bytes) -> tuple[str, str, str]:
"""解析 dfpReport 响应 -> (t1, t2, t5)."""
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 gen_dfp_report(chain) -> tuple[str, str, str]:
"""dfpReport 用生成器随机 cw (零设备) -> (t1, t2, t5).
★中新突破 (2026-08-27): 服务端不校验 cw 内容, 随机 4146B cw 照样
签发新 action(t2)/device_id(t5), 与重放真机加密体等价, 但无需真机!
"""
sys.path.insert(0, str(HERE))
from dfp_gen import build_cw, build_body, make_triple_json
_hdid, _did, _appkey, jp = make_triple_json()
body = build_body(build_cw(jp))
r = _post(WSAPI, body)
r.raise_for_status()
return parse_dfp_response(r.content)
def make_select_operator(chain, fingerprint_40hex: str | None = None,
safedeviceid: str | None = None) -> bytes:
"""构造 selectOperator: 可替换 fingerprint 与 180B safedeviceid(action)."""
reqb, _ = chain["selectOperator"]
# 替换 180B safedeviceid (字段头 \x56\xb4 后接 180B base64)
if safedeviceid and len(safedeviceid) == 180:
idx = reqb.find(b"\x56\xb4")
if idx >= 0:
reqb = reqb[: idx + 2] + safedeviceid.encode() + reqb[idx + 2 + 180:]
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:]
return reqb
def gen_fresh_identity() -> tuple[str, str, str]:
"""生成器全链路注册 (零设备) -> (t2 action, t5 device_id, t1 appkey回声)."""
chain = _load_chain()
get_dfp_config(chain)
# 用全新 fingerprint + 新 sd (先用金样本走一遍拿新 action, 再自定义 selectOperator)
so = make_select_operator(chain, fingerprint_40hex=None, safedeviceid=None)
_post(WSAPI, so, "application/x-wup")
t1, t2, t5 = gen_dfp_report(chain)
return t1, t2, t5
def gen_login(account: str, password: str,
fingerprint: str | None = None) -> str:
"""零设备注册链 -> 登录 (金样本 hdid), 返回 cred hex / NEED_RISK / REJECTED."""
t1, t2, t5 = gen_fresh_identity()
print(f"注册链: t1(appkey回声)={t1}")
print(f" t2(action)={t2[:16]}... len={len(t2)}")
print(f" t5(device_id)={t5}")
return login_with_registered(t2, t5, account, password, fingerprint=fingerprint)
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_HDID32,
fingerprint: str | None = None) -> str:
"""用注册链下发的 ACTION(注册链t2)/DEVID40(注册链t5) + 金样本 HDID32(登录t1.t0) 登录, 返回 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 "--gen-login" in sys.argv:
acct = sys.argv[sys.argv.index("--gen-login") + 1]
pwd = sys.argv[sys.argv.index("--gen-login") + 2]
out = gen_login(acct, pwd)
print("登录结果:", ("cred " + out[:16] + "...") if out.startswith(("0a", "0b")) else out)
elif "--gen" in sys.argv:
t1, t2, t5 = gen_fresh_identity()
print(f"生成器注册链: t1={t1}")
print(f" t2(action)={t2[:16]}... len={len(t2)}")
print(f" t5(device_id)={t5}")
elif "--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不可铸造.")