- df/collect 响应含40hex hdid(每次新值)但 WUP 登录拒绝(652B) -> 非同一体系 - runner.js 输出 __HDID__, get_huya_sdid 一并返回 - 文档: hdid 来源/可铸造性/共用风险评估 + 设备池方案
131 lines
6.1 KiB
Python
131 lines
6.1 KiB
Python
#!/usr/bin/env python3
|
|
"""多账号设备画像生成器 —— 每账号一套独立设备身份 (2026-08-26 实测定论)。
|
|
|
|
背景 (服务端校验矩阵, 全部对 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)) |