整理虎牙文档并接入设备画像指纹
This commit is contained in:
@@ -839,9 +839,15 @@ class HuyaAppPasswordLogin:
|
||||
# (App 渠道),缺字段注入浏览器会导致 UDB SDK 判定设备态不完整。
|
||||
from .web_cookie_fields import (
|
||||
fill_web_cookie_fields,
|
||||
qimei_feature_from_device_profile,
|
||||
)
|
||||
|
||||
full_cookie = fill_web_cookie_fields(cookie_str, guid="", uid=uid)
|
||||
full_cookie = fill_web_cookie_fields(
|
||||
cookie_str,
|
||||
guid="",
|
||||
uid=uid,
|
||||
qimei_feature=qimei_feature_from_device_profile(self.device_info),
|
||||
)
|
||||
appid_fixed = "; ".join(
|
||||
f"{key}={value}" if key != "udb_appid" else "udb_appid=5002"
|
||||
for key, value in cookie_pairs(full_cookie)
|
||||
|
||||
@@ -20,6 +20,7 @@ from __future__ import annotations
|
||||
import random
|
||||
import secrets
|
||||
import time
|
||||
from collections.abc import Mapping
|
||||
|
||||
# 统计/会话字段生成规则;9.1 抓包值为样例,格式保持对齐
|
||||
_FILL_MAP: tuple[tuple[str, str], ...] = (
|
||||
@@ -42,6 +43,32 @@ _BASE16 = "0123456789ABCDEF"
|
||||
_GAME_DID_CHARS = (
|
||||
"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_"
|
||||
)
|
||||
_DEFAULT_QIMEI_FEATURE = {
|
||||
"screen": {"resolution": "800x600", "availResolution": "800x600"},
|
||||
"navigator": {
|
||||
"deviceMemory": 16,
|
||||
"pdfViewerEnabled": True,
|
||||
"cookieEnabled": True,
|
||||
"productSub": "20030107",
|
||||
"hardwareConcurrency": 10,
|
||||
},
|
||||
"webgl": "Google Inc. (Apple)~ANGLE (Apple, ANGLE Metal Renderer: Apple M4, Unspecified Version)",
|
||||
"timezoneOffset": -480,
|
||||
}
|
||||
_DEVICE_GPU_FEATURES = {
|
||||
"xiaomi": ("Qualcomm", "Adreno (TM) 650"),
|
||||
"redmi": ("Qualcomm", "Adreno (TM) 650"),
|
||||
"vivo": ("Qualcomm", "Adreno (TM) 650"),
|
||||
"iqoo": ("Qualcomm", "Adreno (TM) 650"),
|
||||
"oppo": ("Qualcomm", "Adreno (TM) 650"),
|
||||
"oneplus": ("Qualcomm", "Adreno (TM) 650"),
|
||||
"samsung": ("ARM", "Mali-G78"),
|
||||
"honor": ("ARM", "Mali-G78"),
|
||||
"motorola": ("Qualcomm", "Adreno (TM) 642L"),
|
||||
"realme": ("Qualcomm", "Adreno (TM) 650"),
|
||||
"nubia": ("Qualcomm", "Adreno (TM) 650"),
|
||||
"gionee": ("Qualcomm", "Adreno (TM) 506"),
|
||||
}
|
||||
|
||||
|
||||
def _return_base16(a: int) -> str:
|
||||
@@ -96,6 +123,140 @@ def _game_did_generate(length: int = 35) -> str:
|
||||
return out
|
||||
|
||||
|
||||
_MASK64 = (1 << 64) - 1
|
||||
|
||||
|
||||
def _rotl64(value: int, shift: int) -> int:
|
||||
shift %= 64
|
||||
if not shift:
|
||||
return value & _MASK64
|
||||
return ((value << shift) | (value >> (64 - shift))) & _MASK64
|
||||
|
||||
|
||||
def _fmix64(value: int) -> int:
|
||||
value ^= value >> 33
|
||||
value = value * 0xFF51AFD7ED558CCD & _MASK64
|
||||
value ^= value >> 33
|
||||
value = value * 0xC4CEB9FE1A85EC53 & _MASK64
|
||||
value ^= value >> 33
|
||||
return value & _MASK64
|
||||
|
||||
|
||||
def _x64hash128(value: str, seed: int = 0) -> str:
|
||||
"""复刻 QIMEI SDK 的 x64hash128(MurmurHash3 变体)。
|
||||
|
||||
SDK 按 JavaScript ``charCodeAt`` 取每个 UTF-16 code unit 的低 8 位;尾部
|
||||
的第一个字节也保留其实现中的高 32 位布局,而不是标准 MurmurHash 布局。
|
||||
"""
|
||||
encoded = value.encode("utf-16-le", "surrogatepass")
|
||||
data = bytes(encoded[index] for index in range(0, len(encoded), 2))
|
||||
length = len(data)
|
||||
h1 = h2 = seed & _MASK64
|
||||
c1 = 0x87C37B91114253D5
|
||||
c2 = 0x4CF5AD432745937F
|
||||
|
||||
for offset in range(0, length - length % 16, 16):
|
||||
k1 = int.from_bytes(data[offset : offset + 8], "little")
|
||||
k2 = int.from_bytes(data[offset + 8 : offset + 16], "little")
|
||||
k1 = _rotl64(k1 * c1 & _MASK64, 31) * c2 & _MASK64
|
||||
h1 ^= k1
|
||||
h1 = (_rotl64(h1, 27) + h2) & _MASK64
|
||||
h1 = (h1 * 5 + 0x52DCE729) & _MASK64
|
||||
k2 = _rotl64(k2 * c2 & _MASK64, 33) * c1 & _MASK64
|
||||
h2 ^= k2
|
||||
h2 = (_rotl64(h2, 31) + h1) & _MASK64
|
||||
h2 = (h2 * 5 + 0x38495AB5) & _MASK64
|
||||
|
||||
tail = data[length - length % 16 :]
|
||||
tail_len = len(tail)
|
||||
k1 = k2 = 0
|
||||
for index in range(tail_len - 1, 7, -1):
|
||||
k2 ^= tail[index] << (8 * (index - 8))
|
||||
if tail_len >= 9:
|
||||
h2 ^= _rotl64(k2 * c2 & _MASK64, 33) * c1 & _MASK64
|
||||
for index in range(min(tail_len, 8) - 1, 0, -1):
|
||||
k1 ^= tail[index] << (8 * index)
|
||||
if tail_len >= 1:
|
||||
# QIMEI's JS implementation passes [charCodeAt(g)] here, placing the
|
||||
# first tail byte in the high 32-bit word.
|
||||
k1 ^= tail[0] << 32
|
||||
h1 ^= _rotl64(k1 * c1 & _MASK64, 31) * c2 & _MASK64
|
||||
|
||||
h1 ^= length
|
||||
h2 ^= length
|
||||
h1 = (h1 + h2) & _MASK64
|
||||
h2 = (h2 + h1) & _MASK64
|
||||
h1 = _fmix64(h1)
|
||||
h2 = _fmix64(h2)
|
||||
h1 = (h1 + h2) & _MASK64
|
||||
h2 = (h2 + h1) & _MASK64
|
||||
return f"{h1:016x}{h2:016x}"
|
||||
|
||||
|
||||
def _js_text(value: object) -> str:
|
||||
if value is None:
|
||||
return ""
|
||||
if isinstance(value, bool):
|
||||
return "true" if value else "false"
|
||||
return str(value)
|
||||
|
||||
|
||||
def _qimei_feature_string(feature: Mapping[str, object]) -> str:
|
||||
screen = feature.get("screen")
|
||||
screen = screen if isinstance(screen, Mapping) else {}
|
||||
navigator = feature.get("navigator")
|
||||
navigator = navigator if isinstance(navigator, Mapping) else {}
|
||||
values = (
|
||||
screen.get("resolution"),
|
||||
screen.get("availResolution"),
|
||||
navigator.get("deviceMemory"),
|
||||
navigator.get("pdfViewerEnabled"),
|
||||
navigator.get("cookieEnabled"),
|
||||
navigator.get("productSub"),
|
||||
navigator.get("hardwareConcurrency"),
|
||||
feature.get("webgl"),
|
||||
feature.get("timezoneOffset"),
|
||||
)
|
||||
return "###".join(_js_text(item) for item in values) + "###"
|
||||
|
||||
|
||||
def qimei_fingerprint_generate(feature: Mapping[str, object] | None = None) -> str:
|
||||
"""生成 QIMEI ``_qimei_fingerprint``;输入应为 SDK ``getFeature()`` 结果。"""
|
||||
source = feature or _DEFAULT_QIMEI_FEATURE
|
||||
return _x64hash128(_qimei_feature_string(source), 31)
|
||||
|
||||
|
||||
def qimei_feature_from_device_profile(
|
||||
device_info: Mapping[str, object] | None,
|
||||
) -> dict[str, object]:
|
||||
"""将 Android 设备画像映射为 QIMEI WebView 能力特征的近似值。"""
|
||||
info = device_info or {}
|
||||
width = str(info.get("width") or "1080")
|
||||
height = str(info.get("height") or "2400")
|
||||
vendor = str(info.get("vendor") or "").strip().lower()
|
||||
model = str(info.get("model") or "Android WebView").strip()
|
||||
gpu_vendor, renderer = _DEVICE_GPU_FEATURES.get(
|
||||
vendor, ("Qualcomm", f"{model} Graphics")
|
||||
)
|
||||
device_memory = info.get("device_memory") or 8
|
||||
hardware_concurrency = info.get("hardware_concurrency") or 8
|
||||
return {
|
||||
"screen": {
|
||||
"resolution": f"{width}x{height}",
|
||||
"availResolution": f"{width}x{height}",
|
||||
},
|
||||
"navigator": {
|
||||
"deviceMemory": device_memory,
|
||||
"pdfViewerEnabled": False,
|
||||
"cookieEnabled": True,
|
||||
"productSub": "20030107",
|
||||
"hardwareConcurrency": hardware_concurrency,
|
||||
},
|
||||
"webgl": f"Google Inc. ({gpu_vendor})~ANGLE ({gpu_vendor}, {renderer})",
|
||||
"timezoneOffset": -480,
|
||||
}
|
||||
|
||||
|
||||
def _rand_float() -> str:
|
||||
import random
|
||||
|
||||
@@ -108,6 +269,7 @@ def fill_web_cookie_fields(
|
||||
uid: int | str = 0,
|
||||
*,
|
||||
keep_guid: bool = True,
|
||||
qimei_feature: Mapping[str, object] | None = None,
|
||||
) -> str:
|
||||
"""补齐网页 Cookie 中缺失的字段。
|
||||
|
||||
@@ -116,6 +278,7 @@ def fill_web_cookie_fields(
|
||||
guid: wsLaunch 响应返回的会话 guid(32hex);缺核心字段时必填。
|
||||
uid: 虎牙 uid,用于 __yaoldyyuid 等字段。
|
||||
keep_guid: 已存在 guid 且非空时是否保留(默认 True)。
|
||||
qimei_feature: QIMEI SDK ``getFeature()`` 结果;未提供时使用稳定默认档案。
|
||||
|
||||
Returns:
|
||||
补齐后的 Cookie 字符串(原始字段值不变,仅补缺失项)。
|
||||
@@ -152,9 +315,8 @@ def fill_web_cookie_fields(
|
||||
add("game_did", _game_did_generate()) # 35 位随机(真实长度)
|
||||
|
||||
# qimei 皮肤字段
|
||||
# _qimei_fingerprint: QIMEI SDK SpookyHashV2(canvas指纹,31) 32hex;设备稳定,
|
||||
# 无真实 canvas 时可随机(格式 32hex 对齐),真实算法见 trace 文档
|
||||
add("_qimei_fingerprint", secrets.token_hex(16))
|
||||
# _qimei_fingerprint: QIMEI SDK x64hash128(浏览器能力特征,31) 32hex。
|
||||
add("_qimei_fingerprint", qimei_fingerprint_generate(qimei_feature))
|
||||
add("_qimei_h38", secrets.token_hex(19))
|
||||
|
||||
# 会话/统计字段
|
||||
@@ -209,4 +371,6 @@ __all__ = [
|
||||
"cookie_fields",
|
||||
"fill_web_cookie_fields",
|
||||
"missing_web_cookie_fields",
|
||||
"qimei_feature_from_device_profile",
|
||||
"qimei_fingerprint_generate",
|
||||
]
|
||||
|
||||
@@ -39,7 +39,7 @@ Web UI 通过 `web/backend/routers/huya.py` 调用它。
|
||||
│ ④ 本地铸证:nonce_forge(uid+k1) → cert_forge 铸证书 → envelope_forge │
|
||||
│ 补丁 WUP 信封(证书 + UID) │
|
||||
│ ⑤ 模拟扫码绑定四步流 QrRole(getQrId→scan→bind→tryQrLogin)→ biztoken│
|
||||
│ ⑥ POST /web/cookie/verify 兑换 → 全套网页 Cookie │
|
||||
│ ⑥ POST /web/cookie/verify → 认证 Cookie;本地补齐网页运行字段 │
|
||||
└──────────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
# 虎牙网页 Cookie 缺失字段补齐方案(2026-09-01 晚)
|
||||
# 虎牙网页 Cookie 缺失字段补齐方案(历史记录,2026-09-01 晚)
|
||||
|
||||
> 当前字段状态以 [HUYA_COOKIE路线状态.md](HUYA_COOKIE路线状态.md) 为准;本文保留当晚的抓包与联调过程。
|
||||
|
||||
> 目标:账号 19507362959(DB id=58,yyuid=1199664135026)跑通精英宝典 WSS
|
||||
> query_game_name(checkUserBindGameAccount)/ query_points(getUserScore)。
|
||||
@@ -19,10 +21,10 @@
|
||||
| `udb_anobiztoken` | anonymousLogin 响应 biztoken | ✅ 已接入 anon_device |
|
||||
| `udb_anouid` | anonymousLogin 响应 uid | ✅ 已接入 anon_device |
|
||||
| `udb_guiddata` | middle URL 第三段 hex32(客户端自选) | ✅ 已接入 anon_device |
|
||||
| `guid` | `HyUDBWebSDK-Exchange-2.4.js` 写 document.cookie | ❓ 脚本语义待复刻 |
|
||||
| `_qimei_uuid42` | QIMEI SDK 脚本写 document.cookie | ❓ 脚本语义待复刻 |
|
||||
| `__yamid_new` | 门户业务脚本写 document.cookie | ❓ 脚本语义待复刻 |
|
||||
| `game_did` | 门户业务脚本写 document.cookie | ❓ 脚本语义待复刻 |
|
||||
| `guid` | `HyUDBWebSDK-Exchange-2.4.js` 写 document.cookie | 会话值已接入;回退生成规则待确认 |
|
||||
| `_qimei_uuid42` | QIMEI SDK 脚本写 document.cookie | 结构已对齐;SDK 语义待复刻 |
|
||||
| `__yamid_new` | 门户业务脚本写 document.cookie | `room_normal.js` `generate32` 已复刻 |
|
||||
| `game_did` | 门户业务脚本写 document.cookie | 35 位、63 字符集规则已复刻 |
|
||||
|
||||
## 二、账号 58 现状(16 字段)
|
||||
|
||||
@@ -37,10 +39,10 @@ udb_appid, udb_guiddata` —— 4 个匿名设备字段已有(anon_device 协
|
||||
|
||||
| 字段 | 9.1 值形态 | 浏览器首次出现 | 补齐方案 |
|
||||
| --- | --- | --- | --- |
|
||||
| `guid` | 32 hex,`0a7d…` 前缀 | udbres.huya.com SDK 脚本 | 待 SDK 逆向;**格式** 32hex |
|
||||
| `game_did` | 35 位字母数字 | liveapi.ip/getIpLocation 后门户脚本 | 待门户脚本逆向;格式 alnum |
|
||||
| `_qimei_uuid42` | 42 hex,`1a901…` 前缀 | QIMEI SDK 脚本 | 待 SDK 逆向;格式 42hex |
|
||||
| `__yamid_new` | 32 hex 大写 | 门户脚本 | 待门户脚本逆向;格式 32hex 大写 |
|
||||
| `guid` | 32 hex,`0a7d…` 前缀 | udbres.huya.com SDK 脚本 | `wsLaunch` 会话值已接入;回退规则待确认 |
|
||||
| `game_did` | 35 位字母数字 | liveapi.ip/getIpLocation 后门户脚本 | 35 位、63 字符集已复刻 |
|
||||
| `_qimei_uuid42` | 42 hex,`1a901…` 前缀 | QIMEI SDK 脚本 | `1a` + 40hex;精确算法待复刻 |
|
||||
| `__yamid_new` | 32 hex 大写 | 门户脚本 | `generate32` 已复刻并通过 WSS 验证 |
|
||||
|
||||
浏览器 round1 同一字段多次出现均**值稳定**(跨 profile 稳定):`_qimei_fingerprint`
|
||||
恒为 `2822a43b5a3bd016fc844218c35c8a74`,`_qimei_uuid42` 前缀恒为 `1a901…`,
|
||||
@@ -106,6 +108,8 @@ cd /Users/yml/codes/live-hub-py
|
||||
|
||||
## 六、风险与验证要求
|
||||
|
||||
当前实现已经通过活动 WSS 查询链路验证。`_qimei_uuid42` 仍只保证结构,`guid` 的回退值和设备字段持久化仍需继续完善;详细状态见 [HUYA_COOKIE路线状态.md](HUYA_COOKIE路线状态.md)。
|
||||
|
||||
## 七、各通道 Cookie 依赖矩阵(2026-09-01 晚实测)
|
||||
|
||||
不同通道对网页设备态字段的依赖不同,补齐策略按通道区别:
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
# 虎牙网页 Cookie 合并审计与来源矩阵
|
||||
|
||||
更新时间:2026-09-01
|
||||
更新时间:2026-09-02
|
||||
|
||||
> 当前实现总览见 [HUYA_COOKIE路线状态.md](HUYA_COOKIE路线状态.md)。本文记录字段来源约束和历史审计证据。
|
||||
|
||||
## 结论
|
||||
|
||||
@@ -20,7 +22,7 @@
|
||||
| `sdid`、`hdid` | `df/collect` 的 `Set-Cookie`/响应 | 本次指纹响应覆盖旧值 |
|
||||
| `guid` | 网页/launch 会话的 GUID32 | 只接受显式 `device_info["guid"]` 或 Session 值 |
|
||||
| `udb_guiddata` | 浏览器设备状态字段,和 `guid` 是不同值 | 只接受显式值,不再与 `guid32` 复制 |
|
||||
| `game_did`、`__yamid_*`、QIMEI、`udb_anobiztoken` | 浏览器/设备 SDK 状态,部分具有关联或签名 | 无真实值时省略 |
|
||||
| `game_did`、`__yamid_*`、QIMEI、`udb_anobiztoken` | 浏览器/设备 SDK 状态,部分具有关联或签名 | 有真实值优先;缺失时按已验证算法或结构补齐,并保留证据边界 |
|
||||
| `udb_deviceid` | 浏览器设备 Cookie | 只接受格式正确且显式提供的值 |
|
||||
| `Hm_*`、`HMACCOUNT`、`*_rep_cnt`、`_yasids` | 页面统计与访问计数状态 | 只保留已有或显式提供值 |
|
||||
|
||||
@@ -28,7 +30,7 @@
|
||||
|
||||
同一网页会话内这些值保持稳定:
|
||||
|
||||
- `_qimei_uuid42`:42 位;`_qimei_h38`:38 位;`_qimei_fingerprint`:32 位。三者是 QIMEI 设备标识,不能用随机十六进制替代。
|
||||
- `_qimei_uuid42`:42 位;`_qimei_h38`:38 位;`_qimei_fingerprint`:32 位。三者是 QIMEI 设备标识;其中 fingerprint 已按 SDK 能力特征串算法复刻,uuid42 仍只保证结构。
|
||||
- `udb_anobiztoken`:344 位 URL-safe 字符串,和 `udb_anouid` 共同属于匿名设备状态,不能用重复 UUID 字节拼接。
|
||||
- `guid` 与 `udb_guiddata` 均为 32 位,但抓包值不同,不能复用同一画像字段。
|
||||
- `udb_deviceid` 为 `w_` 加 19 位数字;格式可以校验,但仅凭格式无法得到真实值。
|
||||
@@ -38,7 +40,7 @@
|
||||
|
||||
## 旧实现的问题
|
||||
|
||||
旧实现曾在空 Session 中生成 `__yamid_new`、`game_did`、三个 QIMEI 字段、`udb_anobiztoken`、`HMACCOUNT`、`_yasids` 和多个固定计数器。它们只能满足长度测试,不能证明值具有协议语义。
|
||||
旧实现曾在空 Session 中生成 `__yamid_new`、`game_did`、三个 QIMEI 字段、`udb_anobiztoken`、`HMACCOUNT`、`_yasids` 和多个固定计数器。它们只能满足长度测试,不能证明值具有协议语义;当前 fingerprint 已替换为 SDK 算法,uuid42 仍保留结构化回退。
|
||||
|
||||
旧实现还把 `device_info["guid32"][:32]` 同时写入 `guid` 和 `udb_guiddata`,并把手机号直接写入 `udb_passport`/`username`。这两点会造成设备身份和账号映射偏差。
|
||||
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
# 虎牙 Cookie 路线状态
|
||||
|
||||
更新时间:2026-09-02
|
||||
|
||||
## 当前结论
|
||||
|
||||
App 登录可以得到新鲜的 `udb_cred`、`udb_uid`、`yyuid`、`udb_biztoken`、`sdid` 和 `hdid`。匿名设备协议可以补齐 `udb_deviceid`、`udb_guiddata`、`udb_anobiztoken`、`udb_anouid`。活动 WSS 会话在缺少网页设备字段时先执行 `wsLaunch`,拿到会话 `guid` 后补齐 Cookie 并重建连接。
|
||||
|
||||
活动 WSS 的角色、积分、商品、详情和兑换记录查询已完成端到端验证;商城 WSS 和 HTTP RPC 的查询链路也已验证。真实兑换曾验证过一次,后续自动验证保持只读。
|
||||
|
||||
## 字段状态
|
||||
|
||||
| 字段 | 当前状态 | 实现或证据 |
|
||||
| --- | --- | --- |
|
||||
| `udb_cred`、`udb_uid`、`yyuid`、`udb_biztoken` | 已完成 | App 登录与 QR 绑定链 |
|
||||
| `sdid`、`hdid` | 已完成 | hydevice / `df/collect` |
|
||||
| `udb_deviceid`、`udb_guiddata`、`udb_anobiztoken`、`udb_anouid` | 已完成 | `anon_device.py` |
|
||||
| `guid` | 会话路径完成,回退算法未完全确认 | `wsLaunch` 返回值优先;回退值为结构仿造 |
|
||||
| `__yamid_new` | 已完成 | `room_normal.js` `generate32` Python 复刻 |
|
||||
| `game_did` | 已完成 | 35 位、63 字符集规则复刻 |
|
||||
| `_yasids` | 格式完成,精确共享种子未确认 | 与 `__yamid_new` 使用同类时钟生成器 |
|
||||
| `_qimei_uuid42` | 结构完成,SDK 内部算法待复刻 | `1a` + 40hex |
|
||||
| `_qimei_fingerprint` | 算法已复刻 | QIMEI `x64hash128(feature_string, 31)` |
|
||||
| 统计字段 | 已完成 | 线上查询已接受;部分字段仍是统计用途 |
|
||||
|
||||
## `_qimei_fingerprint` 算法
|
||||
|
||||
QIMEI 0.6.3 的 Canvas `id` 和 `geometry` 是 `getFeature()` 的两个字段,但 `_qimei_fingerprint` 使用的是另一条浏览器能力特征串:
|
||||
|
||||
```text
|
||||
screen.resolution###screen.availResolution###
|
||||
navigator.deviceMemory###navigator.pdfViewerEnabled###
|
||||
navigator.cookieEnabled###navigator.productSub###
|
||||
navigator.hardwareConcurrency###webgl###timezoneOffset###
|
||||
```
|
||||
|
||||
该字符串经过 SDK 内置的 `x64hash128`,种子为 `31`。Python 实现位于 `core/huya/web_cookie_fields.py`,测试向量与 SDK 输出一致。登录流程现在优先将 Android 设备画像映射为 WebView 近似特征;没有画像时再使用稳定的默认 Web 特征档案。调用方可以传入 SDK `getFeature()` 结果获得对应环境的精确值。
|
||||
|
||||
复核向量:
|
||||
|
||||
```text
|
||||
输入:800x600###800x600###16###true###true###20030107###10###Google Inc. (Apple)~ANGLE (Apple, ANGLE Metal Renderer: Apple M4, Unspecified Version)###-480###
|
||||
SDK/Python 输出:d07a45de567322edcd42312f38e610be
|
||||
```
|
||||
|
||||
## 尚未闭合的工程项
|
||||
|
||||
1. 将真实 WebView/Chrome 的 `getFeature()` 结果接入登录调用,替代当前的设备画像近似值。
|
||||
2. 对 `_qimei_uuid42` 做 SDK VM 级复刻,当前只保证结构。
|
||||
3. 明确 `guid` 的 `956a` / `966a` 版本差异和无 `wsLaunch` 时的真实生成规则。
|
||||
4. 让活动会话补齐后的 Cookie 持久化回账号,避免原始 16 字段账号每次任务重新生成设备字段。
|
||||
5. 为 `raw_cookie` 增加重建网页 Cookie 的服务入口。
|
||||
6. 商城下单、支付和支付状态轮询保持单独人工验证,不纳入只读回归。
|
||||
|
||||
## 证据入口
|
||||
|
||||
- [网页字段溯源](HUYA_WEB_FIELD_TRACE.md)
|
||||
- [Cookie 合并审计](HUYA_COOKIE合并审计与来源矩阵.md)
|
||||
- [浏览器抓包报告](../evidence/browser_cookie_harvest/report.md)
|
||||
- [虎牙精英宝典联调指南](HUYA精英宝典联调指南.md)
|
||||
@@ -1,4 +1,6 @@
|
||||
# 虎牙网页设备字段溯源(2026-09-01 浏览器实测实证)
|
||||
# 虎牙网页设备字段溯源(历史证据,2026-09-01 浏览器实测)
|
||||
|
||||
> 当前实现状态以 [HUYA_COOKIE路线状态.md](HUYA_COOKIE路线状态.md) 为准;本文保留原始浏览器证据与算法推导。
|
||||
|
||||
## 一、来源判定方法(有头 Chrome + 打点探针)
|
||||
|
||||
@@ -64,28 +66,28 @@ CBCC 06ECB5 0000 0001 B63016D6162BC990 ← reqable 8.31
|
||||
```
|
||||
首字节 `1a` 恒定;前 32 hex 符合 RFC 4122 UUID 布局(8-4-4-4-12)。
|
||||
|
||||
### _qimei_fingerprint = SpookyHashV2(canvas指纹, 31) 32hex
|
||||
### _qimei_fingerprint = x64hash128(feature_string, 31) 32hex
|
||||
qimeisdk-web 0.6.3 源码已定位:
|
||||
- 画 "qimei" 文本(Times New Roman 11pt)+ `#f60` 背景 + 透明度矩形
|
||||
- 三圆叠加(`#f2f`/`#2ff`/`#ff2` 40 半径)+ 环形 `#f9c` evenodd
|
||||
- `canvas.toDataURL()` → **SpookyHashV2**(r 函数,64 位,seed=31)→ 32hex
|
||||
- Canvas `id`/`geometry` 是 SDK 特征对象的独立字段;Cookie 指纹实际使用浏览器能力特征串
|
||||
→ SDK 内置 **x64hash128**(seed=31)→ 32hex。
|
||||
|
||||
## 四、溯源结论与建议
|
||||
|
||||
| 字段 | 来源 | 风控关联性 | 建议 |
|
||||
| --- | --- | --- | --- |
|
||||
| `guid` | 浏览器脚本(有固定段 `0a..966a`)| 高(设备与会话绑定) | **必须按结构仿造或复用 wsLaunch 会话值**;随机纯 hex 与真实结构不符 |
|
||||
| `__yamid_new` | 浏览器脚本(CBCC 前缀+种子)| 高(与 rootsid 同源)| 需要前缀 `CBCC` + 种子同步生成逻辑 |
|
||||
| `__yamid_new` | 浏览器脚本(CBCC 前缀+种子)| 高(与 rootsid 同源)| `room_normal.js` `generate32` 已复刻;精确 rootsid 共享计数仍待确认 |
|
||||
| `_qimei_uuid42` | QIMEI SDK 0.6.3(`1a` 前缀)| 中 | 前缀 `1a` + UUID 布局 |
|
||||
| `_qimei_fingerprint` | QIMEI SDK 0.6.3 SpookyHashV2 | 中 | **算法已完全逆向**(源码在手),可精确复现 |
|
||||
| `_qimei_fingerprint` | QIMEI SDK 0.6.3 x64hash128(feature_string, 31) | 中 | Python 已复刻,输入字段见路线状态文档 |
|
||||
| `game_did` | 浏览器脚本 | 中 | 长度需对齐(35 变体含 `-`)|
|
||||
| 统计字段(SoundValue 等)| 页面脚本 | 低 | 随机 OK |
|
||||
|
||||
**下一步选项**:
|
||||
1. `_qimei_fingerprint` 直接用 SpookyHashV2 实现(canvas 指纹用标准值替代)
|
||||
2. `guid` 采样更多样本确认固定段后按 `0a` + 结构仿造
|
||||
3. `__yamid_new` 需找到生成它的具体脚本(非 document.cookie 写入,可能在
|
||||
ubtk/埋点脚本里)做种子同步
|
||||
**当时的下一步记录(部分已完成,保留作历史)**:
|
||||
1. `_qimei_fingerprint` 已改为浏览器能力特征串的 `x64hash128`,见路线状态文档。
|
||||
2. `guid` 已优先使用 `wsLaunch` 会话值,回退固定段仍待确认。
|
||||
3. `__yamid_new` 已定位 `room_normal.js` `generate32` 并完成 Python 复刻。
|
||||
## 六、核心字段生成算法逆向结果(2026-09-01 晚,浏览器实测 + 源码定位)
|
||||
|
||||
### ✅ __yamid_new = room_normal.js generate32(UUID-v1 风格,已 Python 复刻验证)
|
||||
@@ -119,12 +121,12 @@ B = (t=35) => { const e="abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ012
|
||||
- 由 qimeisdk-web 0.6.3 内部 VM 生成(`_qimei_ssid`/`_qimei_last_get_id_time_<16hex>` 同源)
|
||||
- 当前实现对齐结构(1a 前缀 + 42hex 随机化),精确算法待继续
|
||||
|
||||
### ⚠️ _qimei_fingerprint = SpookyHashV2(canvas指纹,31),待 canvas 复现
|
||||
- SDK 0.6.3 内置 x64hash128 与 canvas 指纹逻辑(文本+三圆+环形)
|
||||
- 设备稳定(同 profile 跨会话不变);当前实现随机 32hex 格式对齐
|
||||
- 复现需在真实环境执行 canvas(headless 下 canvas 指纹固定,可抽取一次固化为常量)
|
||||
### ✅ _qimei_fingerprint = x64hash128(feature_string,31),已完成 Python 复刻
|
||||
- Cookie 输入由屏幕分辨率、浏览器能力、WebGL 和时区字段按 `###` 拼接。
|
||||
- Python 实现位于 `core/huya/web_cookie_fields.py`,测试向量与 SDK 输出一致。
|
||||
- 没有实时浏览器能力对象时,生成器使用稳定的默认 Web 特征档案。
|
||||
|
||||
### 已修正生成器(core/huya/web_cookie_fields.py, commit 待定)
|
||||
### 已修正生成器(core/huya/web_cookie_fields.py)
|
||||
- `__yamid_new` → `_yamid_new_generate32()`(UUID-v1 时间戳复刻)
|
||||
- `game_did` → `_game_did_generate()`(35 位 63 字符集)
|
||||
- `_yasids` → 与 `__yamid_new` 同源同刻生成
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
# 虎牙精英宝典联调指南
|
||||
|
||||
更新时间:2026-09-01
|
||||
更新时间:2026-09-02
|
||||
|
||||
> 当前 Cookie 字段状态见 [HUYA_COOKIE路线状态.md](HUYA_COOKIE路线状态.md)。
|
||||
|
||||
## 本次联调结论
|
||||
|
||||
@@ -11,12 +13,12 @@
|
||||
| WUP 登录 | 通过 | `cred=114B`,返回真实 UID |
|
||||
| hydevice 指纹 | 通过 | 返回 `sdid`、`hdid` |
|
||||
| QR 绑定 | 通过 | `getQrId`、`bindQrLoginUser`、`tryQrLogin(stage=2)` |
|
||||
| 网页 Cookie | 待重新登录验证 | 必须包含 9.1 网页设备态字段 |
|
||||
| 精英宝典任务 | 待验证 | 仅允许 WSS,会话失败即任务失败 |
|
||||
| 网页 Cookie | 已验证 | App 登录补齐网页字段;活动会话优先复用 `wsLaunch` guid |
|
||||
| 精英宝典任务 | 已验证 | 活动 WSS 查询、商品、详情和兑换记录链路已跑通 |
|
||||
|
||||
旧登录结果中的 Cookie 缺少网页设备态,已不再作为成功登录或任务链路使用。
|
||||
|
||||
纯 App/HTTP 登录不会由 `web/cookie/verify` 下发网页设备 Cookie。浏览器只用于
|
||||
纯 App/HTTP 登录不会由 `web/cookie/verify` 下发完整网页设备 Cookie。浏览器只用于
|
||||
人工调试和抓取来源,未集成到脚本;登录链已接入匿名设备态协议
|
||||
(`core/huya/anon_device.py`)补 `udb_deviceid`/`udb_guiddata`/`udb_anobiztoken`/
|
||||
`udb_anouid`,剩余脚本字段(`guid`/`game_did`/`_qimei_uuid42`/`__yamid_new`)
|
||||
@@ -50,8 +52,9 @@
|
||||
Cookie 缺少网页设备态(udb_deviceid, udb_guiddata, udb_anobiztoken),已停止活动 HTTP 回退
|
||||
```
|
||||
|
||||
`guid`/`game_did`/`_qimei_uuid42`/`__yamid_new` 四个脚本字段无服务端签发接口,
|
||||
登录时缺失只警告不阻断保存,WSS 判定也不再要求(风控风险自担)。
|
||||
`guid`/`game_did`/`_qimei_uuid42`/`__yamid_new` 四个脚本字段无统一服务端签发接口;
|
||||
当前登录链已接入 `guid` 会话值、`__yamid_new`、`game_did` 和 fingerprint 算法,
|
||||
`_qimei_uuid42` 仍使用结构化回退,WSS 判定不要求脚本字段全部来自浏览器。
|
||||
|
||||
- WSS 握手、`wsLaunch`、`getConfig` 或业务 RPC 任一步失败:任务直接失败并记录具体阶段,便于修复 Cookie 或协议参数。
|
||||
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
# 项目文档索引
|
||||
|
||||
更新时间:2026-09-02
|
||||
|
||||
## 当前入口
|
||||
|
||||
- [虎牙 Cookie 路线状态](HUYA_COOKIE路线状态.md):当前实现、字段状态、验证边界和后续工作。
|
||||
- [虎牙网页设备字段溯源](HUYA_WEB_FIELD_TRACE.md):浏览器证据、字段来源和生成算法。
|
||||
- [虎牙 Cookie 合并审计与来源矩阵](HUYA_COOKIE合并审计与来源矩阵.md):字段来源约束和合并策略。
|
||||
- [虎牙精英宝典联调指南](HUYA精英宝典联调指南.md):工作台联调顺序和只读验证方法。
|
||||
|
||||
## 虎牙业务与协议
|
||||
|
||||
- [虎牙精英宝典 9.1 全流程分析](HUYA_精英宝典-9.1-全流程分析.md)
|
||||
- [虎牙精英宝典后端与抓包对照](HUYA_精英宝典-后端与9.1抓包对照.md)
|
||||
- [虎牙精英宝典 8.31 Python 重分析](HUYA_精英宝典-8.31-PY重新分析.md)
|
||||
- [虎牙精英宝典联调指南](HUYA精英宝典联调指南.md)
|
||||
- [网页 Cookie 缺失字段历史方案](HUYA_CHAT_缺失字段补齐方案.md)
|
||||
- [虎牙 App 登录总览](HUYA_APP_OVERVIEW.md)
|
||||
- [虎牙纯 Python 自动登录总结](虎牙纯Python全自动登录-总结与交接.md)
|
||||
|
||||
## 虎牙设备与加密逆向
|
||||
|
||||
- [HDID 算法生成](HUYA_HDID_ALGORITHM_GEN.md)
|
||||
- [HDID 研究记录](HUYA_HDID_RESEARCH.md)
|
||||
- [标识符命名规范](HUYA_ID_NOMENCLATURE.md)
|
||||
- [铸币机终局总结](HUYA_铸币机-终局总结.md)
|
||||
- [App 登录协议抓包实证](虎牙App密码登录协议-抓包实证.md)
|
||||
- [App 登录 WUP 明文抓包](虎牙App密码登录-WUP明文抓包.md)
|
||||
- [App 登录加密算法动态验证](虎牙App登录-加密算法动态验证.md)
|
||||
- [App 登录 APK 静态分析](虎牙App登录-APK静态分析.md)
|
||||
- [App 登录逆向笔记](虎牙App登录逆向笔记.md)
|
||||
- [扫码登录闭环逆向](虎牙扫码登录闭环逆向.md)
|
||||
- [hyCred 签发侦察笔记](虎牙hyCred签发-侦察笔记.md)
|
||||
|
||||
## 设备注册与运行工具
|
||||
|
||||
- [dfpReport 生成流程](dfpReport生成流程.md)
|
||||
- [dfpReport 破解进度](dfpReport破解进度.md)
|
||||
- [Frida 绕过配方](Frida绕过配方.md)
|
||||
- [Hook 脚本运行与捕获约定](Hook脚本运行与捕获约定.md)
|
||||
- [模拟器存活闪退诊断](模拟器存活闪退诊断报告.md)
|
||||
- [登录系统全流程定案](登录系统全流程定案.md)
|
||||
|
||||
## 鱼翅充值接口
|
||||
|
||||
- [对接信息](鱼翅充值api/对接信息.md)
|
||||
- [账户信息查询](鱼翅充值api/账户接口/账户信息查询.md)
|
||||
- [创建订单](鱼翅充值api/订单接口/1.创建订单_必要.md)
|
||||
- [查询订单](鱼翅充值api/订单接口/2.查询订单_必要.md)
|
||||
- [订单异步通知](鱼翅充值api/订单接口/3.订单异步通知_推荐.md)
|
||||
|
||||
## 文档约定
|
||||
|
||||
1. 带日期的抓包和逆向笔记属于历史证据,结论以对应日期为准。
|
||||
2. 当前代码行为和未完成项以“虎牙 Cookie 路线状态”为准。
|
||||
3. Cookie、UID、订单号和设备标识进入文档时使用脱敏值;真实数据只保存在本地 evidence 目录。
|
||||
4. 兑换、下单和支付属于写操作,除非明确确认,不纳入自动回归测试。
|
||||
@@ -15,13 +15,13 @@
|
||||
→ 信封补丁: 换cert + (uid不同则patch t3) [envelope_forge]
|
||||
→ 扫码绑定流 getQrId→scanQrPicNotify→bindQrLoginUser→tryQrLogin
|
||||
→ POST /web/cookie/verify {"appId":5002} ← 关键最后一步!
|
||||
→ 全套网页Cookie: udb_cred/yyuid/udb_uid/udb_passport/username/udb_biztoken...
|
||||
→ 认证 Cookie + 网页运行字段: udb_cred/yyuid/udb_uid/udb_passport/username/udb_biztoken...
|
||||
```
|
||||
|
||||
### 一键工具
|
||||
| 命令 | 功能 |
|
||||
|---|---|
|
||||
| `.venv/bin/python tools/full_web_cookie.py <账号> <密码>` | 密码→全套网页Cookie(任意账号, 纯协议)|
|
||||
| `.venv/bin/python tools/full_web_cookie.py <账号> <密码>` | 密码→认证 Cookie 与网页运行字段(任意账号, 纯协议)|
|
||||
| `.venv/bin/python tools/full_auto_safe.py <账号> <密码>` | 同上, 且新账号safe_auth滑块自动过 |
|
||||
| `.venv/bin/python tools/full_auto_test.py <账号> <密码>` | 密码→cred→铸证→bind 全链路自测 |
|
||||
| `.venv/bin/python tools/nonce_forge.py` | nonce生成器自检(对设备实采MATCH) |
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
"""虎牙网页 Cookie 字段生成器测试。"""
|
||||
|
||||
from core.huya.web_cookie_fields import (
|
||||
_qimei_feature_string,
|
||||
_x64hash128,
|
||||
cookie_fields,
|
||||
fill_web_cookie_fields,
|
||||
missing_web_cookie_fields,
|
||||
qimei_feature_from_device_profile,
|
||||
qimei_fingerprint_generate,
|
||||
)
|
||||
|
||||
|
||||
def test_qimei_hash_matches_sdk_vectors():
|
||||
assert _x64hash128("", 31) == "24700f9f1986800ab4fcc880530dd0ed"
|
||||
assert _x64hash128("a", 31) == "f8b09930c1df727ef75677a1d6ae5bd6"
|
||||
assert _x64hash128("abc", 31) == "31844fd43df93041e52bcddd13312671"
|
||||
|
||||
|
||||
def test_qimei_fingerprint_uses_sdk_feature_string():
|
||||
feature = {
|
||||
"screen": {"resolution": "800x600", "availResolution": "800x600"},
|
||||
"navigator": {
|
||||
"deviceMemory": 16,
|
||||
"pdfViewerEnabled": True,
|
||||
"cookieEnabled": True,
|
||||
"productSub": "20030107",
|
||||
"hardwareConcurrency": 10,
|
||||
},
|
||||
"webgl": "Google Inc. (Apple)~ANGLE (Apple, ANGLE Metal Renderer: Apple M4, Unspecified Version)",
|
||||
"timezoneOffset": -480,
|
||||
}
|
||||
assert _qimei_feature_string(feature).endswith("###-480###")
|
||||
assert qimei_fingerprint_generate(feature) == "d07a45de567322edcd42312f38e610be"
|
||||
fields = cookie_fields(fill_web_cookie_fields("", "", qimei_feature=feature))
|
||||
assert fields["_qimei_fingerprint"] == "d07a45de567322edcd42312f38e610be"
|
||||
|
||||
|
||||
def test_fill_replaces_empty_values_and_completes_core_fields():
|
||||
result = fill_web_cookie_fields("guid=; __yamid_new=; game_did=", "a" * 32, 1)
|
||||
fields = cookie_fields(result)
|
||||
assert fields["guid"] == "a" * 32
|
||||
assert fields["__yamid_new"]
|
||||
assert fields["game_did"]
|
||||
assert fields["_qimei_fingerprint"] == qimei_fingerprint_generate()
|
||||
assert not missing_web_cookie_fields(result)
|
||||
|
||||
|
||||
def test_device_profile_builds_stable_webview_feature_approximation():
|
||||
feature = qimei_feature_from_device_profile({
|
||||
"vendor": "vivo",
|
||||
"model": "V2370A",
|
||||
"width": "1080",
|
||||
"height": "2412",
|
||||
})
|
||||
assert feature["screen"] == {
|
||||
"resolution": "1080x2412",
|
||||
"availResolution": "1080x2412",
|
||||
}
|
||||
assert feature["navigator"]["pdfViewerEnabled"] is False
|
||||
assert "Adreno" in str(feature["webgl"])
|
||||
first = qimei_fingerprint_generate(feature)
|
||||
second = qimei_fingerprint_generate(qimei_feature_from_device_profile({
|
||||
"vendor": "vivo",
|
||||
"model": "V2370A",
|
||||
"width": "1080",
|
||||
"height": "2412",
|
||||
}))
|
||||
assert first == second
|
||||
assert first != qimei_fingerprint_generate()
|
||||
Reference in New Issue
Block a user