#!/usr/bin/env python3 """多账号设备画像生成器 —— 每账号一套独立设备身份 (2026-08-26 实测定论)。 ⚠ 本文件是 tools 侧的独立试验版画像器。core 生产链用的是 ``core/huya/device_profile.py`` + ``core/huya/dfp_register``(safedeviceid/登录帧 device_id 每次登录前动态签发,画像不再写入固定 safedeviceid)。 背景 (服务端校验矩阵, 全部对 wup.huya.com hypasswordLogin 实测): ┌──────────────────┬───────────────────────────────────────────────┐ │ 字段 │ 换值表现 │ ├──────────────────┼───────────────────────────────────────────────┤ │ hdid (32hex) │ APP_SIGN_NOT_MATCH 硬错 ❌ (设备注册核心) │ │ app_version │ APP_SIGN_NOT_MATCH 硬错 ❌ (与设备注册态绑定) │ │ fingerprint 40hex│ 触发 safe_auth 滑块(pt_auth) → 自动过验后放行 ✅ │ │ device_id 40hex │ 直接通过 ✅ (无校验) │ │ model/vendor/ │ 直接通过 ✅ (自述类, 需与 screen/UA 自洽) │ │ screen/宽高 │ │ │ safedeviceid │ 直接通过 ✅ (RSA action 令牌, 服务端不校验内容) │ └──────────────────┴───────────────────────────────────────────────┘ 结论: hdid + app_version 是"设备注册"硬约束(由真机 dfpReport 链注册, libhydeviceid.so 生成 / 服务端绑定版本), 多账号时**同一 hdid+版本 必须复用 (它不泄露单一账号身份, 只是设备签名锚点)**; 其余字段每账号随机即可, 首次触发滑块由 solver 自动过 → 登录照常出 cred (已端到端实测)。 hdid 来源与可铸造性 (2026-08-26 实测): - WUP 帧 t1.t0 的 hdid (32hex) = App 原生 libhydeviceid.so 生成的设备 ID (APK 静态分析: libhydeviceid.so = 设备ID生成), 经 dfpReport 链注册。 - 换任意新值 → APP_SIGN_NOT_MATCH 硬错: 随机32hex ✗ / df.huya.com df/collect 下发的 40hex hdid ✗ / 截断 ✗ —— 纯代码无法铸造新 hdid。 - 共用同一 hdid: 登录不受阻 (实测新账号直出 cred), 但服务端可在设备维度 关联账号; 若需真隔离, 须多台真机各采一套 {hdid, fingerprint, device_id} 建设备池轮换。 用法: python tools/huya_device_profile.py # 生成并打印一套新画像 python tools/huya_device_profile.py <账号> # 按账号取/建画像(持久化) python tools/huya_device_profile.py --list # 列出已持久化画像 """ from __future__ import annotations import hashlib import json import os import random import sys from pathlib import Path HERE = Path(__file__).resolve().parent PROFILE_DB = HERE.parent / "evidence" / "device_profiles.json" # 真实机型库 (vendor, model, screen固件段, 宽x高) —— 与登录自洽即可 REAL_MODELS = [ ("xiaomi", "M2102J2SC", "M2102J2SC,30,11", (1080, 2120)), ("vivo", "V2370A", "V2370A,34,13", (1080, 2412)), ("oppo", "PFFM20", "PFFM20,34,13", (1080, 2412)), ("honor", "SDY-AN00", "SDY-AN00,31,12", (1080, 2400)), ("samsung", "SM-G9910", "SM-G9910,31,12", (1440, 3200)), ("oneplus", "PGZ110", "PGZ110,34,13", (1080, 2412)), ("redmi", "23049PCD8G", "23049PCD8G,34,13", (1080, 2400)), ("realme", "RMX3366", "RMX3366,34,13", (1080, 2412)), ("iqoo", "V2183A", "V2183A,34,13", (1080, 2400)), ("nubia", "NX729J", "NX729J,33,13", (1080, 2400)), ("motorola", "XT2301-5", "XT2301-5,33,13", (1080, 2400)), ("gionee", "GN9013", "GN9013,29,10", (720, 1560)), ] # 设备注册硬锚点 (金样本真机采集, 不可随意更换 —— 见文件头矩阵) HDID = "ed0db8334cadd236c00cadf7e11ab5a5" APP_VERSION = "13.4.22" SDK_VERSION = "1.0.80138" # safedeviceid 为 dfpReport 下发的 RSA action 令牌, 服务端不校验内容, # 可直接复用金样本 (或随机同长度 base64, 需 PQwemAN9 前缀) SAFEDEVICEID_DEFAULT = ( "PQwemAN9NHkZKoMqVTFUZBIypqMTaQEOrmXr37xQVhQZqrL/gUKEQ11xvE0ju48V8O/" "t9UBGSp27m4+6bP4IiAEnpaR5Rj1kHEfN2SPLPqYZW9vroxUSoAvjJn6ezTP9jWGxxlRDCbt" "Py4Rd6MencYT/pNImVIWK+YbNKZt1O05bHUFhqHf3" ) def _rand_sha1_hex() -> str: return hashlib.sha1(os.urandom(20)).hexdigest() def generate_profile(model_pick=None) -> dict: """生成一套随机设备画像 (不含 hdid/app_version/safedeviceid 硬锚)。""" vendor, model, screen, (w, h) = random.choice(REAL_MODELS) \ if model_pick is None else model_pick return { "app_version": APP_VERSION, "sdk_version": SDK_VERSION, "vendor": vendor, "model": model, "os": "android", "ip": "127.0.0.1", "fingerprint": _rand_sha1_hex(), # 40hex, 换→滑块(自动过) "screen": screen, "width": str(w), "height": str(h), "device_id": _rand_sha1_hex(), # 40hex, 服务端不校验 "hdid": HDID, # 硬锚: 不可换 "safedeviceid": SAFEDEVICEID_DEFAULT, # 令牌: 可复用 } def _load_db() -> dict: if PROFILE_DB.exists(): return json.loads(PROFILE_DB.read_text()) return {} def _save_db(db: dict) -> None: PROFILE_DB.parent.mkdir(exist_ok=True) PROFILE_DB.write_text(json.dumps(db, indent=1, ensure_ascii=False)) def get_profile(account: str, force_new: bool = False) -> dict: """按账号取/建画像 (幂等: 同账号复用同套, 保持设备身份稳定)。""" db = _load_db() if not force_new and account in db: return db[account] p = generate_profile() db[account] = p _save_db(db) return p if __name__ == "__main__": if len(sys.argv) > 1 and sys.argv[1] == "--list": for k, v in _load_db().items(): print(f"{k}: {v['vendor']} {v['model']} fp={v['fingerprint'][:12]}...") sys.exit(0) acct = sys.argv[1] if len(sys.argv) > 1 else "(临时)" p = get_profile(acct) if acct != "(临时)" else generate_profile() print(json.dumps(p, indent=1))