复刻QIMEI UUID42并默认使用无头浏览器
This commit is contained in:
@@ -96,7 +96,12 @@ class Cdp:
|
||||
return r.get("result", {}).get("value")
|
||||
|
||||
|
||||
def launch(profile: Path, port: int, headless: bool = False) -> tuple[subprocess.Popen, str]:
|
||||
def launch(profile: Path, port: int, headless: bool = True) -> tuple[subprocess.Popen, str]:
|
||||
"""启动用于 CDP 抓取的 Chrome。
|
||||
|
||||
默认使用无头模式,避免登录/抓取流程弹出窗口;传入 ``headless=False``
|
||||
仍可在需要人工观察页面时启用可视化窗口。
|
||||
"""
|
||||
args = [
|
||||
str(CHROME),
|
||||
"--no-first-run", "--no-default-browser-check",
|
||||
@@ -170,9 +175,9 @@ def cookie_names_from_header(headers: dict) -> list[str]:
|
||||
return [part.strip().split("=", 1)[0] for part in raw.split(";") if part.strip()]
|
||||
|
||||
|
||||
async def capture_round(profile: Path, tag: str, port: int):
|
||||
async def capture_round(profile: Path, tag: str, port: int, *, headless: bool = True):
|
||||
"""抓取一轮:导航序列 + 全量 Cookie 快照。"""
|
||||
proc, ws_url = launch(profile, port)
|
||||
proc, ws_url = launch(profile, port, headless=headless)
|
||||
try:
|
||||
async with websockets.connect(ws_url, max_size=64 * 1024 * 1024) as raw_ws:
|
||||
cdp = Cdp(raw_ws)
|
||||
@@ -294,14 +299,14 @@ async def capture_round(profile: Path, tag: str, port: int):
|
||||
proc.kill()
|
||||
|
||||
|
||||
async def probe_anon(profile: Path, port: int):
|
||||
async def probe_anon(profile: Path, port: int, *, headless: bool = True):
|
||||
"""复用已有 profile:复放 udblgn 匿名流程,解密请求/响应体。
|
||||
|
||||
抓取会话内 anonymousLogin 与 web/middle 的 postData 与响应体,
|
||||
然后带全套 Cookie 用 requests 独立重放 anonymousLogin,检查
|
||||
服务端是否在匿名流程中下发 guid/udb_guiddata/匿名 token 等字段。
|
||||
"""
|
||||
proc, ws_url = launch(profile, port)
|
||||
proc, ws_url = launch(profile, port, headless=headless)
|
||||
try:
|
||||
async with websockets.connect(ws_url, max_size=64 * 1024 * 1024) as raw_ws:
|
||||
cdp = Cdp(raw_ws)
|
||||
@@ -562,6 +567,11 @@ def main():
|
||||
ap.add_argument("--profile", default="/tmp/huya-browser-p1")
|
||||
ap.add_argument("--port", type=int, default=9333)
|
||||
ap.add_argument("--out", default=str(OUT_DIR))
|
||||
ap.add_argument(
|
||||
"--headed",
|
||||
action="store_true",
|
||||
help="显示 Chrome 窗口(默认使用无头模式)",
|
||||
)
|
||||
args = ap.parse_args()
|
||||
OUT_DIR = Path(args.out)
|
||||
profile = Path(args.profile)
|
||||
@@ -572,7 +582,7 @@ def main():
|
||||
if args.mode == "round2" and not (profile / "Default").exists():
|
||||
print("round2 需要先运行 round1(同一 profile)")
|
||||
sys.exit(2)
|
||||
asyncio.run(capture_round(profile, args.mode, args.port))
|
||||
asyncio.run(capture_round(profile, args.mode, args.port, headless=not args.headed))
|
||||
elif args.mode == "probe":
|
||||
# 匿名流程只在"无 udb_deviceid 持久态"的全新 profile 触发;
|
||||
# probe 模式内部使用临时全新 profile,跑完自动清理。
|
||||
@@ -581,7 +591,7 @@ def main():
|
||||
|
||||
tmp = tempfile.mkdtemp(prefix="huya-probe-")
|
||||
try:
|
||||
asyncio.run(probe_anon(Path(tmp), args.port))
|
||||
asyncio.run(probe_anon(Path(tmp), args.port, headless=not args.headed))
|
||||
finally:
|
||||
shutil.rmtree(tmp, ignore_errors=True)
|
||||
else:
|
||||
@@ -589,4 +599,4 @@ def main():
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
main()
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
- ``guid/_qimei_uuid42/__yamid_new/game_did`` 由页面 SDK 脚本写 document.cookie,
|
||||
无服务端签发接口。其中 ``guid`` 与会话强绑定:**wsLaunch 响应返回的新 guid 必须
|
||||
回写进 cookie**,否则 getConfig 无响应(2026-09-01 实测);
|
||||
``_qimei_uuid42/__yamid_new/game_did`` 为设备标识,随机值格式对齐即可;
|
||||
``_qimei_uuid42/__yamid_new/game_did`` 为设备标识,已按浏览器脚本算法生成;
|
||||
- 其余统计/会话字段(SoundValue/Hm_*/rep_cnt 等)服务端仅记录,可随机生成。
|
||||
|
||||
2026-09-01 全链路实测:账号 id=58 补齐后 checkUserBindGameAccount 返回
|
||||
@@ -20,7 +20,9 @@ from __future__ import annotations
|
||||
import random
|
||||
import secrets
|
||||
import time
|
||||
import zlib
|
||||
from collections.abc import Mapping
|
||||
from datetime import UTC, datetime
|
||||
|
||||
# 统计/会话字段生成规则;9.1 抓包值为样例,格式保持对齐
|
||||
_FILL_MAP: tuple[tuple[str, str], ...] = (
|
||||
@@ -36,7 +38,7 @@ _FILL_MAP: tuple[tuple[str, str], ...] = (
|
||||
# + v1 版本位 + 时钟序列 + 48 位随机 node)。浏览器 epoch = new Date(1582,10,15,
|
||||
# 0,0,0,0).getTime() 实测 -12216643543000(GMT+8)。CBCC 前缀是时间戳自然结果。
|
||||
# - game_did: vplayerUI.js 35 位随机,字符集 64 去掉末尾 '_'(63 选 1)。
|
||||
# - _qimei_uuid42: QIMEI SDK 生成,cookie 值首字节固定 '1a'(本地存储 44hex 截断)。
|
||||
# - _qimei_uuid42: QIMEI SDK 生成,时间字段 + CRC32 替换段 + 随机半字节段。
|
||||
|
||||
_SDK_EPOCH_1582 = -12216643543000 # 浏览器实测 JS Date(1582,10,15) 本地时区 ms
|
||||
_BASE16 = "0123456789ABCDEF"
|
||||
@@ -263,6 +265,41 @@ def _rand_float() -> str:
|
||||
return f"{random.random():.17f}"
|
||||
|
||||
|
||||
def _qimei_uuid42_generate(
|
||||
now_ns: int | None = None,
|
||||
*,
|
||||
cookie_enabled: bool = True,
|
||||
random_value: float | None = None,
|
||||
) -> str:
|
||||
"""逐字复刻 QIMEI Web SDK 0.6.3 的 42 位 ``h38`` 生成器。
|
||||
|
||||
``now_ns`` 保留旧回退函数的纳秒参数约定;SDK 实际只读取本地时间的
|
||||
年/月/日/时/分/秒,因此毫秒和纳秒不会进入结果。时间前缀的年份和月份
|
||||
使用 SDK 的十六进制编码,最后三位来自 ``navigator.cookieEnabled``。
|
||||
``random_value`` 仅用于固定测试;生产调用沿用 ``Math.random()`` 等价的
|
||||
Python ``random.random()``,并将理论上的 1.0 夹到合法半字节上界 15。
|
||||
"""
|
||||
now_ms = time.time_ns() // 1_000_000 if now_ns is None else now_ns // 1_000_000
|
||||
current = datetime.fromtimestamp(now_ms / 1000, tz=UTC).astimezone()
|
||||
|
||||
# SDK: Number(year % 100).toString(16).padStart(2, "0") +
|
||||
# Number(month + 1).toString(16) + each remaining component padded to 2.
|
||||
year_hex = f"{current.year % 100:x}".rjust(2, "0")
|
||||
month_hex = f"{current.month:x}"
|
||||
date_parts = "".join(
|
||||
f"{value:x}".rjust(2, "0")
|
||||
for value in (current.day, current.hour, current.minute, current.second)
|
||||
)
|
||||
prefix = year_hex + month_hex + date_parts + ("100" if cookie_enabled else "000")
|
||||
|
||||
value = random.random() if random_value is None else float(random_value)
|
||||
nibble = min(15, max(0, int(value * 16)))
|
||||
repeated = f"{nibble:x}" * 20
|
||||
template = prefix + "----" + repeated + "----"
|
||||
replacement = f"{zlib.crc32(template.encode('ascii')) & 0xFFFFFFFF:08x}"
|
||||
return prefix + replacement[:4] + repeated + replacement[4:]
|
||||
|
||||
|
||||
def fill_web_cookie_fields(
|
||||
cookie: str,
|
||||
guid: str,
|
||||
@@ -308,8 +345,8 @@ def fill_web_cookie_fields(
|
||||
elif "guid" not in pairs:
|
||||
# guid 结构: '0a' + 10hex + '956a'/'966a' + 16hex(浏览器实测固定段,见 trace 文档)
|
||||
add("guid", "0a" + secrets.token_hex(5) + "966a" + secrets.token_hex(8))
|
||||
# _qimei_uuid42: QIMEI SDK 产物,cookie 首字节 '1a' + UUID 布局 32hex + 8hex 附加
|
||||
add("_qimei_uuid42", "1a" + secrets.token_hex(20))
|
||||
# _qimei_uuid42: QIMEI SDK 0.6.3 的时间/CRC32/随机半字节组合。
|
||||
add("_qimei_uuid42", _qimei_uuid42_generate())
|
||||
# __yamid_new: room_normal.js generate32 精确复刻(UUID-v1 时间戳,含 CBCC 前缀规律)
|
||||
add("__yamid_new", _yamid_new_generate32())
|
||||
add("game_did", _game_did_generate()) # 35 位随机(真实长度)
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
| `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 | 结构已对齐;SDK 语义待复刻 |
|
||||
| `_qimei_uuid42` | QIMEI SDK 脚本写 document.cookie | 0.6.3 时间/CRC32/随机半字节算法已复刻 |
|
||||
| `__yamid_new` | 门户业务脚本写 document.cookie | `room_normal.js` `generate32` 已复刻 |
|
||||
| `game_did` | 门户业务脚本写 document.cookie | 35 位、63 字符集规则已复刻 |
|
||||
|
||||
@@ -41,13 +41,13 @@ udb_appid, udb_guiddata` —— 4 个匿名设备字段已有(anon_device 协
|
||||
| --- | --- | --- | --- |
|
||||
| `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;精确算法待复刻 |
|
||||
| `_qimei_uuid42` | 42 hex,时间前缀 | QIMEI SDK 脚本 | 14 位时间前缀 + CRC32 替换段 + 随机半字节 |
|
||||
| `__yamid_new` | 32 hex 大写 | 门户脚本 | `generate32` 已复刻并通过 WSS 验证 |
|
||||
|
||||
浏览器 round1 同一字段多次出现均**值稳定**(跨 profile 稳定):`_qimei_fingerprint`
|
||||
恒为 `2822a43b5a3bd016fc844218c35c8a74`,`_qimei_uuid42` 前缀恒为 `1a901…`,
|
||||
`guid` 前缀恒为 `0a7dfaa2…` → 存在固定前缀/哈希结构,**随机值可能被风控识别**,
|
||||
需还原生成算法。
|
||||
恒为 `2822a43b5a3bd016fc844218c35c8a74`,`_qimei_uuid42` 按秒内时间前缀稳定,
|
||||
`guid` 前缀恒为 `0a7dfaa2…`。QIMEI 时间/CRC32/随机半字节生成算法已由 SDK VM
|
||||
固定输入探针闭合,随机字段仍会在每次生成时变化。
|
||||
|
||||
### 3.2 统计/会话字段(随机即可,服务端仅记录)
|
||||
|
||||
@@ -108,7 +108,7 @@ cd /Users/yml/codes/live-hub-py
|
||||
|
||||
## 六、风险与验证要求
|
||||
|
||||
当前实现已经通过活动 WSS 查询链路验证。`_qimei_uuid42` 仍只保证结构,`guid` 的回退值和设备字段持久化仍需继续完善;详细状态见 [HUYA_COOKIE路线状态.md](HUYA_COOKIE路线状态.md)。
|
||||
当前实现已经通过活动 WSS 查询链路验证。`guid` 的回退值和设备字段持久化仍需继续完善;详细状态见 [HUYA_COOKIE路线状态.md](HUYA_COOKIE路线状态.md)。
|
||||
|
||||
## 七、各通道 Cookie 依赖矩阵(2026-09-01 晚实测)
|
||||
|
||||
|
||||
@@ -30,7 +30,7 @@
|
||||
|
||||
同一网页会话内这些值保持稳定:
|
||||
|
||||
- `_qimei_uuid42`:42 位;`_qimei_h38`:38 位;`_qimei_fingerprint`:32 位。三者是 QIMEI 设备标识;其中 fingerprint 已按 SDK 能力特征串算法复刻,uuid42 仍只保证结构。
|
||||
- `_qimei_uuid42`:42 位;`_qimei_h38`:38 位;`_qimei_fingerprint`:32 位。三者是 QIMEI 设备标识;其中 fingerprint 与 uuid42 均已按 SDK 0.6.3 算法复刻。
|
||||
- `udb_anobiztoken`:344 位 URL-safe 字符串,和 `udb_anouid` 共同属于匿名设备状态,不能用重复 UUID 字节拼接。
|
||||
- `guid` 与 `udb_guiddata` 均为 32 位,但抓包值不同,不能复用同一画像字段。
|
||||
- `udb_deviceid` 为 `w_` 加 19 位数字;格式可以校验,但仅凭格式无法得到真实值。
|
||||
@@ -40,7 +40,7 @@
|
||||
|
||||
## 旧实现的问题
|
||||
|
||||
旧实现曾在空 Session 中生成 `__yamid_new`、`game_did`、三个 QIMEI 字段、`udb_anobiztoken`、`HMACCOUNT`、`_yasids` 和多个固定计数器。它们只能满足长度测试,不能证明值具有协议语义;当前 fingerprint 已替换为 SDK 算法,uuid42 仍保留结构化回退。
|
||||
旧实现曾在空 Session 中生成 `__yamid_new`、`game_did`、三个 QIMEI 字段、`udb_anobiztoken`、`HMACCOUNT`、`_yasids` 和多个固定计数器。它们只能满足长度测试,不能证明值具有协议语义;当前 fingerprint 与 uuid42 均已替换为 SDK 0.6.3 算法,仍需以同一会话的线上结果确认风控关联性。
|
||||
|
||||
旧实现还把 `device_info["guid32"][:32]` 同时写入 `guid` 和 `udb_guiddata`,并把手机号直接写入 `udb_passport`/`username`。这两点会造成设备身份和账号映射偏差。
|
||||
|
||||
|
||||
+19
-6
@@ -19,7 +19,7 @@ App 登录可以得到新鲜的 `udb_cred`、`udb_uid`、`yyuid`、`udb_biztoken
|
||||
| `__yamid_new` | 已完成 | `room_normal.js` `generate32` Python 复刻 |
|
||||
| `game_did` | 已完成 | 35 位、63 字符集规则复刻 |
|
||||
| `_yasids` | 格式完成,精确共享种子未确认 | 与 `__yamid_new` 使用同类时钟生成器 |
|
||||
| `_qimei_uuid42` | 结构完成,SDK 内部算法待复刻 | `1a` + 40hex |
|
||||
| `_qimei_uuid42` | 已完成 | QIMEI SDK 0.6.3 的 14 位时间前缀 + CRC32 替换段 + 20 位随机半字节段 |
|
||||
| `_qimei_fingerprint` | 算法已复刻 | QIMEI `x64hash128(feature_string, 31)` |
|
||||
| 统计字段 | 已完成 | 线上查询已接受;部分字段仍是统计用途 |
|
||||
|
||||
@@ -46,11 +46,10 @@ 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. 商城下单、支付和支付状态轮询保持单独人工验证,不纳入只读回归。
|
||||
2. 明确 `guid` 的 `956a` / `966a` 版本差异和无 `wsLaunch` 时的真实生成规则。
|
||||
3. 让活动会话补齐后的 Cookie 持久化回账号,避免原始 16 字段账号每次任务重新生成设备字段。
|
||||
4. 为 `raw_cookie` 增加重建网页 Cookie 的服务入口。
|
||||
5. 商城下单、支付和支付状态轮询保持单独人工验证,不纳入只读回归。
|
||||
|
||||
## 证据入口
|
||||
|
||||
@@ -58,3 +57,17 @@ SDK/Python 输出:d07a45de567322edcd42312f38e610be
|
||||
- [Cookie 合并审计](HUYA_COOKIE合并审计与来源矩阵.md)
|
||||
- [浏览器抓包报告](../evidence/browser_cookie_harvest/report.md)
|
||||
- [虎牙精英宝典联调指南](HUYA精英宝典联调指南.md)
|
||||
|
||||
## `_qimei_uuid42` VM 复刻(2026-09-02)
|
||||
|
||||
固定时间 `1700000000000`、固定随机值时,SDK 观测值如下:
|
||||
|
||||
```text
|
||||
random=0 17b0f060d1410085cc000000000000000000003207
|
||||
random=0.25 17b0f060d14100037f4444444444444444444437df
|
||||
random=0.5 17b0f060d1410053db888888888888888888883ff6
|
||||
random=0.75 17b0f060d14100966bcccccccccccccccccccc1cf3
|
||||
random=.999 17b0f060d141003174ffffffffffffffffffffd85d
|
||||
```
|
||||
|
||||
每个值都可以拆成 `14 + 4 + 20 + 4 = 42` 位。时间前缀由本地时间组成:年份末两位转十六进制并补两位、月份转十六进制、日/时/分/秒转十六进制并各补两位,最后追加 `cookieEnabled ? 100 : 000`。中间 20 位是 `floor(Math.random() * 16)` 对应的十六进制半字节重复。两个 `----` 占位组成模板后,SDK 对模板 ASCII 字节计算标准 CRC32(多项式 `0xEDB88320`),将 8 位小写结果的前后四位分别替换占位,最后调用 `toLocaleLowerCase()`。Python 实现位于 `core/huya/web_cookie_fields.py`,并用上述固定向量回归;随机上界按 15 处理,避免理论输入 `1.0` 产生两位半字节。VM 追踪在 `/tmp/qvmtrace17.out`,生成器入口为 `e=6064`,模板拼接和替换发生在 `m≈5015..5137`。该实现只用于查询链路的设备态生成,不触发兑换、下单或支付。
|
||||
|
||||
@@ -33,7 +33,7 @@
|
||||
| --- | --- | --- |
|
||||
| `__yamid_new` | 88 条写入中无此 key,但最终 cookie 有值 | **CBCC 前缀 + 递增种子**(见下) |
|
||||
| `game_did` | 同上 | 30-35 位字母数字 |
|
||||
| `_qimei_uuid42` | 同上 | `1a` 前缀 + 32hex UUID 结构 + 10hex |
|
||||
| `_qimei_uuid42` | 同上 | 14 位时间前缀 + 8 位 CRC32 替换值 + 20 位随机半字节 + 4 位 CRC32 尾段 |
|
||||
|
||||
## 三、设备字段结构规律(真实样本)
|
||||
|
||||
@@ -58,13 +58,13 @@ CBCC 06ECB5 0000 0001 B63016D6162BC990 ← reqable 8.31
|
||||
`yamid=CBCC6479D9B0...` ↔ `rootsid=CBCC647A8A20...`(同会话,种子差 0xB1≈177)
|
||||
- `0001` 固定,尾部大段随机
|
||||
|
||||
### _qimei_uuid42 = `1a` + 40hex(32hex UUID 结构 + 10hex 附加)
|
||||
### _qimei_uuid42 = 14 位时间前缀 + CRC32 替换段 + 20 位随机半字节 + CRC32 尾段
|
||||
```
|
||||
1a81f140c08100b89fb1be450f640501568684b2ea ← reqable 8.31
|
||||
1a901123a1f10045427c1c73420287f44f49d11b45 ← round2 fresh
|
||||
1a901171b051007527940c2acef15dcd2342b6ff ← live 窗口
|
||||
17b0f060d14100037f4444444444444444444437df ← 固定时间/随机 .25
|
||||
17b0f060d1410053db888888888888888888883ff6 ← 固定时间/随机 .5
|
||||
17b0f060d141003174ffffffffffffffffffffd85d ← 固定时间/随机 .999
|
||||
```
|
||||
首字节 `1a` 恒定;前 32 hex 符合 RFC 4122 UUID 布局(8-4-4-4-12)。
|
||||
时间前缀的编码为:年份末两位十六进制(补两位)+ 月份十六进制 + 日/时/分/秒各两位十六进制 + `cookieEnabled` 标志(`100`/`000`)。模板 `prefix + "----" + nibble * 20 + "----"` 的标准 CRC32 小写结果会拆成前后四位填回两个占位;`nibble = floor(Math.random() * 16)`,因此合法范围为 `0` 到 `f`。
|
||||
|
||||
### _qimei_fingerprint = x64hash128(feature_string, 31) 32hex
|
||||
qimeisdk-web 0.6.3 源码已定位:
|
||||
@@ -79,7 +79,7 @@ qimeisdk-web 0.6.3 源码已定位:
|
||||
| --- | --- | --- | --- |
|
||||
| `guid` | 浏览器脚本(有固定段 `0a..966a`)| 高(设备与会话绑定) | **必须按结构仿造或复用 wsLaunch 会话值**;随机纯 hex 与真实结构不符 |
|
||||
| `__yamid_new` | 浏览器脚本(CBCC 前缀+种子)| 高(与 rootsid 同源)| `room_normal.js` `generate32` 已复刻;精确 rootsid 共享计数仍待确认 |
|
||||
| `_qimei_uuid42` | QIMEI SDK 0.6.3(`1a` 前缀)| 中 | 前缀 `1a` + UUID 布局 |
|
||||
| `_qimei_uuid42` | QIMEI SDK 0.6.3 VM 生成器 `e=6064` | 中 | Python 已按时间编码、CRC32 和随机半字节逐项复刻 |
|
||||
| `_qimei_fingerprint` | QIMEI SDK 0.6.3 x64hash128(feature_string, 31) | 中 | Python 已复刻,输入字段见路线状态文档 |
|
||||
| `game_did` | 浏览器脚本 | 中 | 长度需对齐(35 变体含 `-`)|
|
||||
| 统计字段(SoundValue 等)| 页面脚本 | 低 | 随机 OK |
|
||||
@@ -116,10 +116,17 @@ B = (t=35) => { const e="abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ012
|
||||
- **长度 35**(旧随机 30 位是错的);字符集 64 去掉末尾 `_`(63 选 1)
|
||||
- cookie 名 `game_did`,无 cookie 时生成并写入
|
||||
|
||||
### ⚠️ _qimei_uuid42 结构已定,算法待深挖
|
||||
- cookie 值:首字节 `1a` 固定 + 40hex(localStorage 是 44hex,cookie 为截断子串)
|
||||
- 由 qimeisdk-web 0.6.3 内部 VM 生成(`_qimei_ssid`/`_qimei_last_get_id_time_<16hex>` 同源)
|
||||
- 当前实现对齐结构(1a 前缀 + 42hex 随机化),精确算法待继续
|
||||
### ✅ _qimei_uuid42 = QIMEI SDK VM 生成器(已 Python 复刻)
|
||||
|
||||
清理 localStorage/cookie 后对 QIMEI SDK 0.6.3 做固定时间、固定随机值追踪,定位到 VM 生成器 `e=6064`。它先拼出:
|
||||
|
||||
```text
|
||||
14 位时间前缀 + "----" + 20 位随机半字节重复串 + "----"
|
||||
```
|
||||
|
||||
随后计算模板 ASCII 字节的标准 CRC32,将 8 位小写结果的前 4 位和后 4 位分别填入占位,最后调用 `toLocaleLowerCase()`。例如 `random=0.25` 的模板是 `17b0f060d14100----44444444444444444444----`,替换值是 `037f37df`,结果为 `17b0f060d14100037f4444444444444444444437df`。`navigator.cookieEnabled` 只影响时间前缀末三位(`100`/`000`)。
|
||||
- 由 qimeisdk-web 0.6.3 内部 VM 生成(`_qimei_ssid`/`_qimei_last_get_id_time_<16hex>` 同源);字段本身为 42 位小写十六进制。
|
||||
- Python 实现按 SDK 时间、CRC32 和随机上界复刻,固定向量覆盖 `0`、`.25`、`.5`、`.75`、`.999`。
|
||||
|
||||
### ✅ _qimei_fingerprint = x64hash128(feature_string,31),已完成 Python 复刻
|
||||
- Cookie 输入由屏幕分辨率、浏览器能力、WebGL 和时区字段按 `###` 拼接。
|
||||
@@ -130,7 +137,7 @@ B = (t=35) => { const e="abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ012
|
||||
- `__yamid_new` → `_yamid_new_generate32()`(UUID-v1 时间戳复刻)
|
||||
- `game_did` → `_game_did_generate()`(35 位 63 字符集)
|
||||
- `_yasids` → 与 `__yamid_new` 同源同刻生成
|
||||
- `_qimei_uuid42` → `1a` 前缀 + 42hex
|
||||
- `_qimei_uuid42` → `_qimei_uuid42_generate()`(14 位时间前缀 + CRC32 替换段 + 20 位随机半字节 + CRC32 尾段)
|
||||
- `guid`(无会话值回退)→ `0a` + 10hex + `966a` + 16hex 结构
|
||||
|
||||
## 七、逆向生成器端到端验证(2026-09-01 晚,账号 18397482576 重新登录后)
|
||||
@@ -138,7 +145,7 @@ B = (t=35) => { const e="abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ012
|
||||
新版生成器(web_cookie_fields.py commit 8fef04d)产出字段:
|
||||
- guid=`0a43b01a366c966a7fe26a281e1d3c88`(0a 前缀 + 966a 固定段)
|
||||
- __yamid_new=`CBCC65D6C5200001F176114BCF181E25`(UUIDv1 时间戳)
|
||||
- _qimei_uuid42=`1a552b4b...`(1a 前缀)
|
||||
- _qimei_uuid42=42 位小写十六进制(SDK VM 时间/CRC32 结构)
|
||||
- game_did=35 位
|
||||
- _yasids 与 yamid 同源(CBCC65D6C5 前缀一致)
|
||||
|
||||
|
||||
@@ -53,8 +53,8 @@ Cookie 缺少网页设备态(udb_deviceid, udb_guiddata, udb_anobiztoken),已
|
||||
```
|
||||
|
||||
`guid`/`game_did`/`_qimei_uuid42`/`__yamid_new` 四个脚本字段无统一服务端签发接口;
|
||||
当前登录链已接入 `guid` 会话值、`__yamid_new`、`game_did` 和 fingerprint 算法,
|
||||
`_qimei_uuid42` 仍使用结构化回退,WSS 判定不要求脚本字段全部来自浏览器。
|
||||
当前登录链已接入 `guid` 会话值、`__yamid_new`、`game_did`、fingerprint 和
|
||||
`_qimei_uuid42` 的 SDK 算法,WSS 判定不要求脚本字段全部来自浏览器。
|
||||
|
||||
- WSS 握手、`wsLaunch`、`getConfig` 或业务 RPC 任一步失败:任务直接失败并记录具体阶段,便于修复 Cookie 或协议参数。
|
||||
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
from pathlib import Path
|
||||
|
||||
from core.huya import browser_harvest
|
||||
|
||||
|
||||
class _FakeStderr:
|
||||
def readline(self):
|
||||
return "DevTools listening on ws://127.0.0.1/devtools/browser/test\n"
|
||||
|
||||
|
||||
class _FakeProcess:
|
||||
stderr = _FakeStderr()
|
||||
|
||||
def kill(self):
|
||||
pass
|
||||
|
||||
|
||||
def test_launch_defaults_to_headless(monkeypatch):
|
||||
calls = []
|
||||
|
||||
def fake_popen(args, **kwargs):
|
||||
calls.append((args, kwargs))
|
||||
return _FakeProcess()
|
||||
|
||||
def no_http(*args, **kwargs):
|
||||
raise OSError("test: no CDP endpoint")
|
||||
|
||||
monkeypatch.setattr(browser_harvest.subprocess, "Popen", fake_popen)
|
||||
monkeypatch.setattr(browser_harvest.urllib.request, "urlopen", no_http)
|
||||
|
||||
browser_harvest.launch(Path("/tmp/test-huya-profile"), 9555)
|
||||
|
||||
args, kwargs = calls[0]
|
||||
assert "--headless=new" in args
|
||||
assert kwargs["stdout"] is browser_harvest.subprocess.DEVNULL
|
||||
|
||||
|
||||
def test_launch_can_show_window_when_requested(monkeypatch):
|
||||
calls = []
|
||||
|
||||
def fake_popen(args, **kwargs):
|
||||
calls.append(args)
|
||||
return _FakeProcess()
|
||||
|
||||
def no_http(*args, **kwargs):
|
||||
raise OSError("test: no CDP endpoint")
|
||||
|
||||
monkeypatch.setattr(browser_harvest.subprocess, "Popen", fake_popen)
|
||||
monkeypatch.setattr(browser_harvest.urllib.request, "urlopen", no_http)
|
||||
|
||||
browser_harvest.launch(Path("/tmp/test-huya-profile"), 9556, headless=False)
|
||||
|
||||
assert "--headless=new" not in calls[0]
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
from core.huya.web_cookie_fields import (
|
||||
_qimei_feature_string,
|
||||
_qimei_uuid42_generate,
|
||||
_x64hash128,
|
||||
cookie_fields,
|
||||
fill_web_cookie_fields,
|
||||
@@ -46,6 +47,49 @@ def test_fill_replaces_empty_values_and_completes_core_fields():
|
||||
assert not missing_web_cookie_fields(result)
|
||||
|
||||
|
||||
def test_qimei_uuid42_matches_sdk_vectors_and_random_upper_bound():
|
||||
base = 1700000000000000000
|
||||
assert _qimei_uuid42_generate(base, random_value=0.25) == (
|
||||
"17b0f060d14100037f4444444444444444444437df"
|
||||
)
|
||||
assert _qimei_uuid42_generate(base, random_value=0.5) == (
|
||||
"17b0f060d1410053db888888888888888888883ff6"
|
||||
)
|
||||
upper = _qimei_uuid42_generate(base, random_value=1.0)
|
||||
assert len(upper) == 42
|
||||
assert upper[18:38] == "f" * 20
|
||||
assert all(ch in "0123456789abcdef" for ch in upper)
|
||||
|
||||
|
||||
def test_qimei_uuid42_encodes_local_date_and_cookie_flag():
|
||||
base = 1700000000000000000
|
||||
assert _qimei_uuid42_generate(base, random_value=0, cookie_enabled=False) == (
|
||||
"17b0f060d140008b2b00000000000000000000dad6"
|
||||
)
|
||||
|
||||
|
||||
def test_qimei_uuid42_handles_year_zero_padding():
|
||||
# 2000-01-01 08:00:00 local time; SDK emits 00 + 1 + 08 + 00 + 00 + 100.
|
||||
value = _qimei_uuid42_generate(946684800000000000, random_value=0)
|
||||
assert value[:14] == "00101080000100"
|
||||
|
||||
|
||||
def test_sdk_uuid42_trace_vectors_have_confirmed_outer_layout():
|
||||
"""锁定 SDK 追踪确认的 14+4+20+4 外层布局,不约束未闭合的替换算法。"""
|
||||
vectors = {
|
||||
0.0: "17b0f060d1410085cc000000000000000000003207",
|
||||
0.25: "17b0f060d14100037f4444444444444444444437df",
|
||||
0.5: "17b0f060d1410053db888888888888888888883ff6",
|
||||
0.75: "17b0f060d14100966bcccccccccccccccccccc1cf3",
|
||||
0.999: "17b0f060d141003174ffffffffffffffffffffd85d",
|
||||
}
|
||||
for random_value, value in vectors.items():
|
||||
assert len(value) == 42
|
||||
assert value[:14] == "17b0f060d14100"
|
||||
assert value[18:38] == f"{int(random_value * 16):x}" * 20
|
||||
assert all(char in "0123456789abcdef" for char in value)
|
||||
|
||||
|
||||
def test_device_profile_builds_stable_webview_feature_approximation():
|
||||
feature = qimei_feature_from_device_profile({
|
||||
"vendor": "vivo",
|
||||
|
||||
Reference in New Issue
Block a user