多账号设备画像: 设备/指纹/风控数据归类 + 每账号独立画像全链接入
- 实测校验矩阵: hdid+app_version 硬锚(APP_SIGN_NOT_MATCH), fingerprint 换→safe_auth滑块(自动过), device_id/机型/safedeviceid 直接过 - 定位注册接口: df/token+collect(sdid每次新值), wsapi dfpReport链(未破解) - tools/huya_device_profile.py: 每账号画像生成/持久化(幂等) - app_login_flow.login_cred / full_web_cookie 接入画像, 端到端出全套cookie
This commit is contained in:
+24
-8
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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))
|
||||
Reference in New Issue
Block a user