diff --git a/docs/虎牙hyCred签发-侦察笔记.md b/docs/虎牙hyCred签发-侦察笔记.md index fa94972..06ab03e 100644 --- a/docs/虎牙hyCred签发-侦察笔记.md +++ b/docs/虎牙hyCred签发-侦察笔记.md @@ -458,3 +458,65 @@ WS帧包装 = 00 03 1d 00 01 [2B len][4B len] + 信封(去4B前缀), len = inner - evidence/ws_current_session_frames.json: 浏览器当前会话帧(新账号, 证明格式) - evidence/activity_verify_result.txt: 最终验证输出 - /tmp/cdp_now*.py: CDP 抓当前浏览器帧的脚本(浏览器 200 对照) + +--- + +## 附录B: 多账号设备画像 —— 设备/指纹/风控数据归属与校验矩阵 (2026-08-26 新增) + +### 需求 +号很多, 不能所有账号共用同一台"设备身份", 需要: ①分清哪些字段是设备、 +哪些是指纹、哪些是风控; ②找到设备/指纹"注册"类接口, 让每账号有独立设备态。 + +### 整个 App 登录链路的数据归属 (appId=5008 / hypasswordLogin) + +| 环节 | 数据 | 归属 | 说明 | +|---|---|---|---| +| WUP 登录 t1.t0 | **hdid** 32hex | **设备(注册锚)** | libhydeviceid.so 生成, dfp 链注册; **换→APP_SIGN_NOT_MATCH 硬错** | +| WUP 登录 t1.t1 | app_version | **设备(签名相关)** | 与 hdid 绑定校验; **换→同样硬错** | +| WUP 登录 t2.t2 | **fingerprint 40hex** | **指纹** | SHA1 设备指纹; 换→safe_auth 滑块(**可自动过**) | +| WUP 登录 t2.t8 | device_id 40hex | 指纹(session hash) | dfpReport t5 下发值; 换→**直接通过** | +| WUP 登录 t1/t2 其余 | vendor/model/os/screen/宽高 | **声明型设备信息** | 自述, 换→直接通过 (需与 screen/UA 自洽) | +| WUP 登录 t0.t5 | safedeviceid(b64) | 指纹令牌 | RSA action, 服务端不校验内容; 换→通过 | +| WUP 登录 t0.t2 | metadata JSON (session/traceId/uid) | 会话 | 账号绑定 | +| WUP 登录 t0.t8 | user_action JSON | **风控(行为)** | 轨迹采集; 金样本固定值, 可批量复用 | +| 证书 P1 | fingerprint + nonce + cred | 指纹+账号 | fingerprint 设备绑定; 换 cred/nonce 即多账号 | +| 绑定流 | sdid (df/collect) | 指纹 | **每次调用返回新 sdid**, 天然每账号不同 | +| 前置链 | getDfpConfig/selectOperator/dfpReport/dckey | 设备指纹注册 | 见下"注册类接口" | + +### 服务端校验矩阵 (全部对 wup.huya.com 实测, 2026-08-26) +``` +字段变更 | 结果 +hdid 32hex | 652B APP_SIGN_NOT_MATCH (硬错, 无滑块) +app_version | 652B APP_SIGN_NOT_MATCH (硬错, 无滑块) +fingerprint 40hex | 2107B 含 pt_auth 滑块URL → solver自动过 → 放行 ✅ +device_id 40hex | 1571B cred ✅ (直接通过) +vendor/model/screen | 1571B cred ✅ (直接通过) +safedeviceid 内容 | 1571B cred ✅ (直接通过) +multi 全换(除hdid/版本)| 滑块(可过) ✅ +``` + +### 设备/指纹"注册类"接口 (已定位) +1. **df.huya.com/web/df/token + df/collect** —— 指纹采集注册;每次调用从服务端 + 取新 sdid (工具: core/huya/device_fingerprint.get_huya_sdid; **已实测每次返回 + 新值**, 天然满足"每账号一个")。 +2. **wsapi.huya.com huyaudbwebui: getDfpConfig → selectOperator → dfpReport + → udbdf.huya.com/dckey/check** —— App 渠道完整设备注册链: 上报设备采集数据, + 服务端下发稳定设备指纹(32hex)/session hash(40hex)/action(RSA加密)。 + dfpReport 加密体未破解(待破解#2), 纯Python走注册链暂不可行。 +3. **数美 libtscsdk.so (cfgc.zztfly.com)** —— 第三方设备指纹 SDK, 登录前初始化。 + +### 多账号工程方案 (已实现并端到端验证) +- 每账号一套画像: fingerprint/device_id/机型/vendor/screen 随机生成并持久化 + (evidence/device_profiles.json); **hdid+app_version 复用金样本**(硬锚, 不泄 + 露单账号身份, 只是 App 签名设备锚点)。 +- 首次用新画像登录 → 触发 safe_auth 滑块 → solver (YOLO+opencv, 282px级) 自动 + 过验 → 重发 → cred; 证书 P1 指纹也用该账号画像指纹, 全链设备一致。 +- sdid 每次 df/collect 自动新取, 绑定流天然隔离。 +- 实测 (hy_300026595, realme RMX3366 画像): cred 114B → bind rc 0 → biztoken + 344B → cookie verify 200 → 全套 cookie ✅ + +### 资产 +- tools/huya_device_profile.py: 每账号设备画像生成/持久化 (get_profile 幂等) +- tools/app_login_flow.py: login_cred 支持 device_info 参数 (滑块自动过) +- tools/full_web_cookie.py: 全链接入画像 (--new-device 强制换新画像) +- evidence/device_profiles.json: 账号→画像映射 diff --git a/docs/虎牙纯Python全自动登录-总结与交接.md b/docs/虎牙纯Python全自动登录-总结与交接.md index 165b587..a77c73d 100644 --- a/docs/虎牙纯Python全自动登录-总结与交接.md +++ b/docs/虎牙纯Python全自动登录-总结与交接.md @@ -72,3 +72,25 @@ - 设备指纹: core/huya/device_fingerprint.py (get_huya_sdid) - 证据: evidence/* (金样本wup_passwordlogin_taf.bin, nonce_k1.json等) - 协议细节全记录: docs/虎牙hyCred签发-侦察笔记.md (按Round追加) + +--- + +## 六、多账号设备隔离(2026-08-26 追加) + +**问题**: 号多, 全部账号共用一台设备身份 → 需按账号区分设备/指纹。 + +**数据归类(对 wup.huya.com 实测矩阵)**: +- 设备硬锚(不可换): hdid(32hex) + app_version → 换即 APP_SIGN_NOT_MATCH +- 指纹(换→滑块自动过): fingerprint(40hex) +- 声明型(换→直接过): vendor/model/os/screen/宽高/device_id/safedeviceid +- 行为风控: user_action JSON / IP / safe_auth 滑块 + +**注册类接口**: +- df.huya.com/web/df/token+collect → sdid(每次新值, 天然每账号不同) +- wsapi huyaudbwebui getDfpConfig→selectOperator→dfpReport + udbdf dckey/check + (App 设备注册链, 加密体未破解, 当前不可纯Python走) + +**方案(已端到端验证)**: tools/huya_device_profile.py 每账号生成并持久化 +一套画像(fingerprint/device_id/机型随机, hdid+app_version 复用金样本); +首次换画像触发滑块由 solver 自动过; 证书 P1 指纹与该账号画像一致; +sdid 每次新取。→ full_web_cookie.py 已接入, 跑通全套 cookie。 diff --git a/evidence/device_profiles.json b/evidence/device_profiles.json new file mode 100644 index 0000000..c2ac24d --- /dev/null +++ b/evidence/device_profiles.json @@ -0,0 +1,17 @@ +{ + "hy_300026595": { + "app_version": "13.4.22", + "sdk_version": "1.0.80138", + "vendor": "realme", + "model": "RMX3366", + "os": "android", + "ip": "127.0.0.1", + "fingerprint": "f7dcb845b14474dd8d46d9b51cc6cb3106cb8b51", + "screen": "RMX3366,34,13", + "width": "1080", + "height": "2412", + "device_id": "1f901165eb740240db56446e5bb7d2ddc06fc260", + "hdid": "ed0db8334cadd236c00cadf7e11ab5a5", + "safedeviceid": "PQwemAN9NHkZKoMqVTFUZBIypqMTaQEOrmXr37xQVhQZqrL/gUKEQ11xvE0ju48V8O/t9UBGSp27m4+6bP4IiAEnpaR5Rj1kHEfN2SPLPqYZW9vroxUSoAvjJn6ezTP9jWGxxlRDCbtPy4Rd6MencYT/pNImVIWK+YbNKZt1O05bHUFhqHf3" + } +} \ No newline at end of file diff --git a/tools/app_login_flow.py b/tools/app_login_flow.py index e8d4fc6..5bde80b 100644 --- a/tools/app_login_flow.py +++ b/tools/app_login_flow.py @@ -92,14 +92,25 @@ _DEV = { } -def wup_password_login_raw(account: str, password: str, timeout: int = 15) -> bytes: - """发送WUP密码登录, 返回原始响应字节(不解析)。""" +def wup_password_login_raw(account: str, password: str, timeout: int = 15, + device_info: dict | None = None, + safedeviceid: str | None = None, + hdid: str | None = None) -> bytes: + """发送WUP密码登录, 返回原始响应字节(不解析)。 + + device_info: 每账号设备画像 (tools/huya_device_profile.get_profile); + 缺省用金样本设备 _DEV。换 fingerprint/device_id/机型 会触发 + safe_auth 滑块 (自动可过); hdid/app_version 是设备注册硬锚, 不可换 + (实测换之 → APP_SIGN_NOT_MATCH)。 + """ uid = account[3:] if account.startswith("hy_") else account mj, ua, sd = _golden_assets() + dev = device_info or _DEV pkt = build_password_login_wup( - uid, hashlib.sha1(password.encode()).hexdigest(), sd, - "ed0db8334cadd236c00cadf7e11ab5a5", - mj["session"], mj["traceId"], ua, _DEV) + uid, hashlib.sha1(password.encode()).hexdigest(), + safedeviceid or dev.get("safedeviceid") or sd, + hdid or dev.get("hdid") or "ed0db8334cadd236c00cadf7e11ab5a5", + mj["session"], mj["traceId"], ua, dev) r = requests.post( WUP_URL, data=pkt, headers={"Content-Type": "application/multipart-formdata; charset=UTF-8", @@ -185,10 +196,15 @@ def solve_safe_auth(risk_url: str, proxies=None, max_retry: int = 2) -> dict: raise AppLoginError(f"safe_auth 过验失败: {last_err or '无authId'}") -def login_cred(account: str, password: str, max_rounds: int = 3) -> bytes: - """账号密码 -> 新鲜cred。触发safe_auth时自动解滑块并重发登录。""" +def login_cred(account: str, password: str, max_rounds: int = 3, + device_info: dict | None = None) -> bytes: + """账号密码 -> 新鲜cred。触发safe_auth时自动解滑块并重发登录。 + + device_info: 每账号独立设备画像 (tools/huya_device_profile.get_profile), + 多账号时让每个账号呈现不同设备指纹/机型, 避免统一画像被风控关联。 + """ for rnd in range(max_rounds): - resp = wup_password_login_raw(account, password) + resp = wup_password_login_raw(account, password, device_info=device_info) cred = parse_cred(resp) if cred: return cred diff --git a/tools/full_web_cookie.py b/tools/full_web_cookie.py index d58344c..b7fe405 100644 --- a/tools/full_web_cookie.py +++ b/tools/full_web_cookie.py @@ -5,11 +5,18 @@ 不再需要复用设备信封的原nonce —— 任意账号用 目标uid+k1 本地重算即过bind! 依赖: k1 设备常量(evidence/nonce_k1.json 或默认值, 不随账号变)。 +多账号设备隔离 (2026-08-26 实测): 每账号生成独立设备画像 + (tools/huya_device_profile.get_profile) —— fingerprint/device_id/机型随机, + hdid+app_version 为设备注册硬锚共用一个金样本。首次换画像触发 safe_auth + 滑块自动过验。证书 P1 的 fingerprint 用该账号画像的指纹, 保持全链一致。 + 用法: .venv/bin/python tools/full_web_cookie.py <账号> <密码> + .venv/bin/python tools/full_web_cookie.py <账号> <密码> --new-device 产出: 打印全部cookie并写 evidence/web_cookies_full.txt """ import sys, json, re, hashlib, time, base64 ACCT, PWD = sys.argv[1], sys.argv[2] +FORCE_NEW_DEV = "--new-device" in sys.argv sys.argv = [sys.argv[0]] from urllib.parse import quote sys.path.insert(0, 'scripts'); sys.path.insert(0, 'tools') @@ -19,6 +26,7 @@ from nonce_forge import K1_DEFAULT, gen_nonce from probe_huya_qr_bind import QrRole, web_behavior from core.huya.device_fingerprint import get_huya_sdid from app_login_flow import QrAuthRequiredError, login_cred +from huya_device_profile import get_profile def real_uid(acct: str) -> int: @@ -34,16 +42,20 @@ def real_uid(acct: str) -> int: print(f"== 账号 {ACCT} ==") +# 0) 每账号独立设备画像 (首次生成并持久化; 换机身份触发滑块自动过验) +DEV = get_profile(ACCT, force_new=FORCE_NEW_DEV) +print(f"设备画像: {DEV['vendor']} {DEV['model']} fp={DEV['fingerprint'][:12]}...") + # 1) 登录取 cred (safe_auth滑块自动过) try: - cred = login_cred(ACCT, PWD) + cred = login_cred(ACCT, PWD, device_info=DEV) except QrAuthRequiredError as e: print('❌', e); sys.exit(2) except Exception as e: print(f'❌ 登录未返回cred: {e}'); sys.exit(1) print('新鲜cred:', len(cred), cred[:8].hex()) -# 2) 取真实uid + 本地重算nonce 铸证 +# 2) 取真实uid + 本地重算nonce 铸证 (P1指纹用本账号画像, 全链设备一致) uid = real_uid(ACCT) env = Envelope.load() orig = base64.b64decode(env.cert_b64) @@ -51,7 +63,7 @@ f = parse_p1(decrypt_cert(orig)) st = int(time.time() * 1000) rnd = gen_nonce(uid, K1_DEFAULT, service_time_ms=st, counter=0) print(f"本地nonce: uid={uid} st={st} rnd={rnd.hex()}") -P1 = build_p1(f["app_id"], f["fingerprint"], cred, rnd=rnd) +P1 = build_p1(f["app_id"], DEV["fingerprint"].encode(), cred, rnd=rnd) cert = base64.b64encode(forge_cert(P1, key_idx=orig[1])).decode() assert len(cert) == env.cert_len, (len(cert), env.cert_len) diff --git a/tools/huya_device_profile.py b/tools/huya_device_profile.py new file mode 100644 index 0000000..2369060 --- /dev/null +++ b/tools/huya_device_profile.py @@ -0,0 +1,122 @@ +#!/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 (已端到端实测)。 + +用法: + 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)) \ No newline at end of file