From ccfa742e3bb37f45aabe36c446ad4314eb59ce1a Mon Sep 17 00:00:00 2001 From: yml2213 Date: Fri, 28 Aug 2026 21:52:12 +0800 Subject: [PATCH] =?UTF-8?q?feat(huya):=20=E9=93=B8=E5=B8=81=E6=9C=BA?= =?UTF-8?q?=E6=89=93=E9=80=9A=20-=20doLaunch=20tReq=20=E7=BB=93=E6=9E=84bu?= =?UTF-8?q?g=E4=BF=AE=E5=A4=8D(=E7=BC=BA=E5=A4=96=E5=B1=82struct=5Fbegin)+?= =?UTF-8?q?=E5=8F=8C=E9=9B=A8=E7=A1=AE=E5=AE=9A=E6=80=A7=E5=AE=9E=E8=AF=81?= =?UTF-8?q?(sGuid=3Df(mid))?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - encode_live_launch_req: 双层 struct_begin(0)+struct_end 收尾 (服务器tag0 tId 期望STRUCT) - build 只发 tReq 单键 (对齐真机 launch 帧, 不再追加 platform/version 等) - parse_launch_rsp: gzip 解压 + \x06\x20(32hex) sGuid 可靠提取 - live 矩阵实证: 同指纹同 sGuid / 变异指纹新 sGuid / device_id 不驱动, mid 驱动 - docs §11.8: 破案链 + 实证表 + 铸币闭环 (mid→doLaunch→sGuid→登录链) --- docs/HUYA_HDID_ALGORITHM_GEN.md | 31 +++++++++++++++++++++++++++++++ tools/huya_launch_mint.py | 22 ++++++++++++++++------ 2 files changed, 47 insertions(+), 6 deletions(-) diff --git a/docs/HUYA_HDID_ALGORITHM_GEN.md b/docs/HUYA_HDID_ALGORITHM_GEN.md index af5f405..cab33fc 100644 --- a/docs/HUYA_HDID_ALGORITHM_GEN.md +++ b/docs/HUYA_HDID_ALGORITHM_GEN.md @@ -350,3 +350,34 @@ lib 内置大量模拟器检测串(qemu_pipe / mumuvmm / genymotion / windroye - 前置条件不变:**live 复测前先落 docs §11.5 步骤 1 的确定性实验设计**(同一虚拟指纹 多次请求 → sGuid 是否一致 / 是否 = f(字段))。若 sGuid 随机签发,则铸币=整套身份模板 随机化后由登录链验证服务端接受度。 + +## §11.8 铸币机打通:doLaunch 结构 bug 定案 + 服务端确定性签发 (2026-08-28) + +### 破案链 +1. **真机全通道对拍**:多轮被动抓包/WG 全解密/Frida SSL 明文 — wup.huya.com 之外的 + launch servant 真帧 (queryHttpDns) = **map 只有 tReq 一个键**,值 = JCE struct: + `0a`(struct_begin tag0) + 字段…… (非此前追加多键 + 0x0c 包装)。 +2. **tReq 恒拒根因**:`encode_live_launch_req` 少写外层 struct_begin(0) — UserId 结构体 + 被直接顶到顶层,服务器解析 LiveLaunchReq 时 tag0(tId) 读到 UserId 内部 lUid 的 + ZERO_TAG(0x0c=type12) → 恒定错误 `read 'struct' type mismatch, tag: 0, get type: 12`。 +3. 修复 = 双层 struct_begin(0) + 收尾 struct_end() + map 只留 tReq 键。 + +### live 实证 (POST https://wup.huya.com 根路径) +| 指纹 | sGuid (tRsp.tag0, 32hex) | 确定性 | +|---|---|---| +| 默认 (mid=1e8bdf7d4f7a01d3, device_id=f2f6…) | `0a7dfaa882938a6ab502511452142c57` | 多次同值 ✓ | +| mid=9c41d0a7b3e5f281 (+device_id 换) | `0a7dd9dc1190916aba02dcf4ddf45b78` | 两次同值 ✓ | +| mid=31415f26a7c8b9d0, imei=861234… | `0a7d90756f90916a9e023aa03ba0e901` | 两次同值 ✓ | +| 仅 device_id 变 (mid 不变) | = 默认值 | device_id 不驱动 | + +**结论:sGuid = f(mid)** —— 服务端对同一 mid 确定性签发同一 sGuid;mid 一换 sGuid 即变。 +mid = LiveUserbase→tUAEx→t5=sMId (16hex),**可任意铸造** → doLaunch 收 sGuid → +登录帧 UserId.tag0=32hex hdid 用的就是该 sGuid。§11 旧结论"GUID 唯一不可铸造"正式推翻; +**纯算法铸币闭环 = mid(铸) → doLaunch → sGuid → 登录链**。 + +### 工具落地 +`tools/huya_launch_mint.py`: +- `encode_live_launch_req` 外层 struct_begin/end 修复; +- build 只发 tReq 单键 (对齐真机帧); +- `parse_launch_rsp` 支持 gzip + `\x06\x20(32hex)` sGuid 可靠提取; +- `--live` 一条命令出 sGuid; 确定性矩阵可复现。 diff --git a/tools/huya_launch_mint.py b/tools/huya_launch_mint.py index bdcc287..9440576 100644 --- a/tools/huya_launch_mint.py +++ b/tools/huya_launch_mint.py @@ -32,6 +32,7 @@ from __future__ import annotations import argparse +import re import struct import sys from pathlib import Path @@ -69,6 +70,8 @@ def encode_live_launch_req(profile: dict) -> bytes: model, qimei, luid, apn, net_type """ w = _Writer() + # 外层 LiveLaunchReq struct (tag0) —— 缺它整体结构上移一层 + w.struct_begin(0) w.struct_begin(0) # UserId tId w.int64(0, int(profile.get("luid", 0))) _w_string_or_skip(w, 1, profile.get("guid")) @@ -93,6 +96,7 @@ def encode_live_launch_req(profile: dict) -> bytes: w.struct_end() w.int16(2, int(profile.get("b_support_domain", 1))) # bSupportDomain + w.struct_end() # 外层 LiveLaunchReq return w.get() @@ -102,12 +106,9 @@ def build_live_launch_wup(profile: dict, request_id: int | None = None) -> bytes request_id = int.from_bytes(__import__("os").urandom(4), "big") & 0x7FFFFFFF req_jce = encode_live_launch_req(profile) - # App 端 a09.getOtherParams() 还会追加 platform/version/channel/(yyuid/uid/imei) 键 + # 真机实测 (WG 全解密捕获): launch servant 的 map 只有 "tReq" 一个键, + # 不追加 platform/version/channel 等 (此前追加属过度拟合, 服务器仍拒值) entries = [("tReq", ("bytes", req_jce))] - for k in ("platform", "version", "channel", "yyuid", "uid", "imei"): - v = profile.get(k) - if v: - entries.append((k, ("string", v))) sb = _Writer() sb.map_begin(0, len(entries)) for k, (kind, val) in entries: @@ -202,6 +203,15 @@ def parse_launch_rsp(resp: bytes) -> dict: """ if not HAVE_TAF: raise RuntimeError("缺少 core.huya.taf_protocol, 无法解码") + # HTTP 直连响应可能 gzip 压缩 (1f 8b) + if resp[:2] == b"\x1f\x8b": + import gzip as _g + try: + resp = _g.decompress(resp) + except Exception: + pass + # 可靠路径: tRsp 结构内 tag0 = sGuid(STRING4 0x06, len 0x20=32hex) + _sguid_re = re.search(rb"\x06\x20([0-9a-f]{32})", resp) # WUP 帧带 4 字节大端长度前缀 (len = 4+body); 剥离后解析 if len(resp) >= 4: declared = struct.unpack(">I", resp[:4])[0] @@ -209,7 +219,7 @@ def parse_launch_rsp(resp: bytes) -> dict: resp = resp[4:] i = TafInputStream(resp) header: dict[str, object] = {} - sguid_candidates: list[str] = [] + sguid_candidates: list[str] = ([_sguid_re.group(1).decode()] if _sguid_re else []) structs: list[dict] = [] buf = i.buf while True: