From 45b5e9128caede48d846c9e23a3f117beddaa9c3 Mon Sep 17 00:00:00 2001 From: yml2213 Date: Fri, 28 Aug 2026 10:44:56 +0800 Subject: [PATCH] =?UTF-8?q?docs(huya):=20=E6=96=B0=E5=A2=9E=E6=80=BB?= =?UTF-8?q?=E8=A7=88=E5=8E=BB=E5=9B=B0=E6=83=91=E5=9C=B0=E5=9B=BE=20+=20?= =?UTF-8?q?=E4=BB=A3=E7=A0=81=E9=A3=8E=E9=99=A9=E6=A0=87=E8=AE=B0=20+=20?= =?UTF-8?q?=E7=9C=9F=E6=9C=BA=20Frida=20=E7=A8=B3=E5=AE=9A=E6=B3=A8?= =?UTF-8?q?=E5=85=A5=E9=80=9A=E9=81=93?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - docs/HUYA_APP_OVERVIEW.md: 整体流程/三形态hdid区分/登录入口盘点/随机化矩阵/风险清单/真机Frida现状 - core/huya 各模块 docstring 加风险标记与 hdid 名称混淆提示, 指向总览 - tools/app_login_flow 标注已过时(未接注册链), tools/huya_device_profile 标注与 core 策略关系 - scripts/phone_stable_capture.py + diag_phone_lifecycle.py: 真机 spawn+art_callsite 稳定抓帧/四组诊断 - evidence/diag_phone/: 真机基线/attach/稳定抓帧实验证据 (90s 存活无 EGL 崩) --- core/huya/app_login.py | 5 + core/huya/device_fingerprint.py | 2 +- core/huya/device_profile.py | 5 + core/huya/dfp_register.py | 5 + core/huya/wup_encoder.py | 2 +- docs/HUYA_APP_OVERVIEW.md | 110 +++++++++ evidence/diag_phone/phone_a.json | 195 +++++++++++++++ evidence/diag_phone/phone_d.json | 33 +++ evidence/diag_phone/phone_stable_capture.json | 164 +++++++++++++ scripts/diag_phone_lifecycle.py | 201 ++++++++++++++++ scripts/phone_stable_capture.py | 227 ++++++++++++++++++ tools/app_login_flow.py | 4 + tools/huya_device_profile.py | 4 + 13 files changed, 955 insertions(+), 2 deletions(-) create mode 100644 docs/HUYA_APP_OVERVIEW.md create mode 100644 evidence/diag_phone/phone_a.json create mode 100644 evidence/diag_phone/phone_d.json create mode 100644 evidence/diag_phone/phone_stable_capture.json create mode 100644 scripts/diag_phone_lifecycle.py create mode 100644 scripts/phone_stable_capture.py diff --git a/core/huya/app_login.py b/core/huya/app_login.py index fd953d5..c3d493d 100644 --- a/core/huya/app_login.py +++ b/core/huya/app_login.py @@ -12,6 +12,11 @@ 当前继续使用已注册样本;新注册链动态更新的是 safedeviceid 和 device_id。 注册链(``core/huya/dfp_register``)每次登录前执行,失败即抛错终止(``HuyaAppLoginError``), 不读取画像里的旧固定值,也不静默回退旧链。 + +⚠ 风险与边界(详见 ``docs/HUYA_APP_OVERVIEW.md``): + - hdid 全账号固定共享 -> 服务端可设备维度关联账号; + - 滑块 UA / session / traceId 用金样本常量 -> 与随机机型画像不自洽; + - qr_auth(扫码)/dx_auth(短信) 无自动闭环 -> 碰上直接失败(QR_AUTH_REQUIRED)。 """ from __future__ import annotations diff --git a/core/huya/device_fingerprint.py b/core/huya/device_fingerprint.py index 0d7f1a4..f3ce002 100644 --- a/core/huya/device_fingerprint.py +++ b/core/huya/device_fingerprint.py @@ -32,7 +32,7 @@ class HuyaSdidResult: """sdid 获取结果。""" sdid: str = "" - hdid: str = "" # df/collect 同响应下发的 40hex 设备ID; 与 WUP 登录的 32hex hdid 非同一体系 + hdid: str = "" # df/collect 同响应下发的 40hex 设备ID; 与 WUP 登录的 32hex hdid 非同一体系(见 docs/HUYA_APP_OVERVIEW.md §二) source: str = "" message: str = "" diff --git a/core/huya/device_profile.py b/core/huya/device_profile.py index 996083f..0c7dc83 100644 --- a/core/huya/device_profile.py +++ b/core/huya/device_profile.py @@ -6,6 +6,11 @@ ``safedeviceid``(RSA action 令牌)与登录帧 ``device_id`` 由 :mod:`core.huya.dfp_register` 在每次 WUP 登录前经新设备注册链实时签发, 不再写入画像,也不再允许任何固定金样本令牌被多账号复用。 + +⚠ 字段状态速览(详见 ``docs/HUYA_APP_OVERVIEW.md`` §四): + - 随机: fingerprint / 机型 / 屏幕 / 宽高 / device_id(画像侧); + - 固定: hdid(32hex 硬锚,全账号同一) / app_version / sdk_version; + - 动态签发: safedeviceid、登录帧 device_id。 """ from __future__ import annotations diff --git a/core/huya/dfp_register.py b/core/huya/dfp_register.py index b1c98f5..919f152 100644 --- a/core/huya/dfp_register.py +++ b/core/huya/dfp_register.py @@ -13,6 +13,11 @@ 错误语义: 链上任何一步失败(模板缺失/损坏、HTTP 异常、响应缺字段)都会抛 ``DfpRegistrationError``。调用方(core/huya/app_login)应把注册失败当作明确失败 终止登录;禁止静默回退到画像里的旧固定 safedeviceid/device_id。 + +⚠ 名称混淆提示(详见 ``docs/HUYA_APP_OVERVIEW.md`` §二): + - 这里签发的 t2 = 登录帧 safedeviceid、t5 = 登录帧 device_id(40hex); + - 登录帧 t1.t0 的 32hex hdid 是另一体系(libhydeviceid.so 设备ID,服务端硬锚), + 本模块 ``_random_triple`` 里的 64hex "hdid" 只是随机 cw 的 JSON 占位,非真身份。 """ from __future__ import annotations diff --git a/core/huya/wup_encoder.py b/core/huya/wup_encoder.py index 0f6a929..cf92b09 100644 --- a/core/huya/wup_encoder.py +++ b/core/huya/wup_encoder.py @@ -179,7 +179,7 @@ def _build_wup_data(w: _Writer, uid_str: str, sha1_password: str, # -- t1: 设备信息 struct -- di = device_info w.struct_begin(1) - w.string(0, hdid) + w.string(0, hdid) # t1.t0 = 32hex 设备ID (libhydeviceid.so 硬锚, 不可随机/换, 换→APP_SIGN_NOT_MATCH) w.string(1, di.get("app_version", "13.4.22")) w.string(2, di.get("sdk_version", "1.0.80138")) w.string(3, "") diff --git a/docs/HUYA_APP_OVERVIEW.md b/docs/HUYA_APP_OVERVIEW.md new file mode 100644 index 0000000..ee1139e --- /dev/null +++ b/docs/HUYA_APP_OVERVIEW.md @@ -0,0 +1,110 @@ +# 虎牙 App 协议链 总览(去困惑地图) + +> 目的:一张图讲清“整体流程 / 几个 hdid 区分 / 几个登录入口 / 哪里随机 / 哪里有风险”, +> 避免在代码和多个零散文档里来回翻。**先看本文,再按需深入具体文档。** +> +> 关联文档:`docs/dfpReport破解进度.md`(破解细节)、`docs/dfpReport生成流程.md`(零设备注册链)、 +> `docs/模拟器存活闪退诊断报告.md`(Frida 存活实验,模拟器)、`docs/虎牙纯Python全自动登录-总结与交接.md`。 + +--- + +## 一、生产登录整体流程(唯一推荐的入口) + +入口:`core/huya/app_login.py` → `login_huya_app_password()` / `HuyaAppPasswordLogin`。 +Web UI 通过 `web/backend/routers/huya.py` 调用它。 + +``` +┌──────────────────────────────────────────────────────────────────────┐ +│ 每次 WUP 登录前 │ +│ ① 新设备注册链 core/huya/dfp_register.register_device() │ +│ getDfpConfig → selectOperator → dfpReport(随机cw) │ +│ 返回 (t1, safedeviceid, device_id) ← 全新,非画像旧值 │ +│ 失败 → 抛 DfpRegistrationError → HuyaAppLoginError,终止,不回退 │ +│ │ +│ ② WUP 密码登录 wup_password_login_raw() │ +│ POST wup.huya.com hypasswordLogin(safedeviceid/device_id 用①的)│ +│ ├─ 正常 → 解出 114B cred + 真实 uid │ +│ └─ pt_auth 滑块 → solve_safe_auth() 自动过验 → 重发登录(≤3轮) │ +│ │ +│ ③ 本地铸证:nonce_forge(uid+k1) → cert_forge 铸证书 → envelope_forge │ +│ 补丁 WUP 信封(证书 + UID) │ +│ ④ 模拟扫码绑定四步流 QrRole(getQrId→scan→bind→tryQrLogin)→ biztoken│ +│ ⑤ POST /web/cookie/verify 兑换 → 全套网页 Cookie │ +└──────────────────────────────────────────────────────────────────────┘ +``` + +**结论**:这是“绕开 App UI、复现协议”的纯协议链。它绕过了滑块(pt_auth), +但 **qr_auth(扫码)/ dx_auth(短信)没有自动闭环**——碰上直接失败,属已知边界。 + +--- + +## 二、几个 “hdid” 的区分(最容易混的点) + +代码里出现三种都叫“hdid”的东西,**不是同一个**: + +| # | 形态 | 示例 | 所在 | 是否登录硬锚 | 能否随机 | +|---|---|---|---|---|---| +| 1 | **WUP 登录帧 t1.t0 的 32hex hdid** | `ed0db8334cadd236c00cadf7e11ab5a5` | `core/huya/wup_encoder.py`(`hdid` 参数);`device_profile.HDID` | ✅ **服务端硬锚**,`libhydeviceid.so` native 生成上报,不走 HTTP | ❌ 换掉 → `APP_SIGN_NOT_MATCH` | +| 2 | **dfpReport 响应 t5 / device_id(40hex)** | `7c5387e0539c023c31c4ff0e807e7256117385ee` | 注册链 `dfp_register`;登录帧 `device_id` | ❌ 与 #1 非同一体系(`device_fingerprint.py` 有明示) | ✅ 随机 cw 时服务端签发随机值 | +| 3 | **注册链随机 cw 里的 “hdid”(64hex sha256)** | — | `dfp_register._random_triple` / `tools/dfp_gen.py` | ❌ 仅凑 JSON 模板形态,服务端不校验 cw 内容 | ✅ 每次随机 | + +**一句话**:#1 是真身份、不可换、全项目唯一;#2 是登录帧 `device_id` 的签发源,动态; +#3 是诊断占位。混淆来源主要是 dfpReport JSON 明文里把 40hex 设备 ID 也叫 `hdid`。 + +--- + +## 三、登录入口盘点(哪个该用、哪些是历史) + +### 生产链(core/huya,Web UI 调用) +| 入口 | 功能 | 状态 | +|---|---|---| +| `core/huya/app_login.py` → `login_huya_app_password` | **App 协议全链路**(①~⑤) | ✅ **推荐**,已接注册链 | +| `core/huya/login.py` → `login_huya_password` | Web 密码登录 | ✅ 独立 Web 渠道 | +| `core/huya/sms_login.py` → `login_huya_sms` | 短信登录 | ✅ | +| `core/huya/auto_register.py` | 手机号自动注册 | ✅ | +| `web/backend/routers/huya.py` | Web UI 接口(单账号/批量) | ✅ | + +### 工具 / 历史(tools,试验用,与生产分离,勿在生产调用) +| 文件 | 用途 | 注意 | +|---|---|---| +| `tools/huya_device_register.py` | 注册链 CLI(`--gen` 零设备 / `--login`) | 与 `core/huya/dfp_register` 同逻辑的工具侧版 | +| `tools/app_login_flow.py` | 旧 App 登录流 | ⚠ 仍读画像 `safedeviceid` 回退金样本,**未接注册链,已过时** | +| `tools/huya_wup_encoder.py` | 旧 WUP 编码器副本 | 正式版在 `core/huya/wup_encoder.py` | +| `tools/full_auto_safe.py` / `full_auto_test.py` / `full_web_cookie.py` | 一键试验脚本 | 依赖 `evidence/` 信封/证书模板,非生产 | +| `tools/web_cookie_login.py` | 网页版密码登录 | Web 渠道独立 | +| `tools/huya_device_profile.py` | 旧画像生成器 | ⚠ 仍含固定 `safedeviceid`,与 core 策略(动态签发)不一致 | + +--- + +## 四、随机化矩阵(哪里随机 / 固定 / 动态签发) + +| 字段 | 状态 | 来源 | 风险 | +|---|---|---|---| +| **hdid (32hex)** | **固定**(全账号同一) | `device_profile.HDID` 金样本 | ⚠ 多账号设备维度关联(主要风险) | +| safedeviceid (t2) | **动态签发**(每次登录前) | 注册链 `dfp_register` | 低 | +| device_id (t5) | **动态签发**(随机 cw 时为随机值) | 注册链 `dfp_register` | 低 | +| fingerprint | **随机**(每账号) | `generate_profile` sha1 | 低 | +| 机型 / 屏幕 / 宽高 | **随机**(每账号从 `REAL_MODELS` 选) | `generate_profile` | 低 | +| app_version / sdk_version | **固定** | 常量 `13.4.22` / `1.0.80138` | 低(需与 hdid 配套) | +| 滑块 UA | **固定** 小米 M2102J2SC | `app_login.APP_UA_MOBILE` | ⚠ 与随机机型画像不一致 | +| session / traceId / 登录行为 JSON | **固定金样本** | `_golden_session_assets()` | ⚠ 多账号相同,异常画像 | +| QrRole context / reqid / page_id | **随机** | `QrRole`(uuid/random) | 低 | + +--- + +## 五、风险与未解决项(诚实清单) + +1. **hdid 固定共享** → 服务端可在设备维度把多个账号关联起来(见 §2 #1)。纯代码无法铸造新 hdid;换掉即 `APP_SIGN_NOT_MATCH`。 +2. **滑块 UA / session / traceId 固定** → 与随机机型画像不自洽,长期易形成异常画像。 +3. **qr_auth / dx_auth 无自动闭环** → 碰上直接失败(`QR_AUTH_REQUIRED`)。 +4. **无稳定性数据** → 没有 20/50/100 次连续成功率、多账号/IP/时段矩阵。 +5. **Frida attach 主进程必被 msaoaidsec 杀**(真机/模拟器均实测)→ 见 §六。 + +--- + +## 六、真机 Frida 现状(2026-08-27 实测) + +- **裸 attach 主进程**:真机同样被 `libmsaoaidsec.so` 静默 `_exit`(KeepAlive 换 pid,UI 几乎无感)。模拟器版详见诊断报告。 +- **真机稳定注入通道(已打通)**:`spawn` 挂起中加载 `bypass_msaoaid_maps_art_callsite.js`(RE 库)再 `resume` → **长时存活 + 可 hook**(90s 无 detached,抓到 `libudbauthunify.so` 基址)。真机 GPU 正常,**不再触发模拟器的 EGL 崩溃**。 +- 脚本:`scripts/phone_stable_capture.py`(真机稳定抓帧)、`scripts/diag_phone_lifecycle.py`(真机四组对照诊断)。 +- 含义:真机上做长时 Frida 抓帧/取证是可行的(不再受模拟器 ~4s stale 窗口限制);但**登录/验证码流程仍不要带 frida**(msaoaidsec 会标记)。 diff --git a/evidence/diag_phone/phone_a.json b/evidence/diag_phone/phone_a.json new file mode 100644 index 0000000..b98e7ba --- /dev/null +++ b/evidence/diag_phone/phone_a.json @@ -0,0 +1,195 @@ +{ + "group": "A", + "t_spawn": 25.5, + "pid": 7289, + "timeline": [ + { + "t": 0.4, + "proc": 7289, + "ui_ok": true, + "focus": "902807b mode=0 rootTaskId=276 u0 com.duowan.kiwi/com.duowan.kiwi.homepage.Homepage" + }, + { + "t": 1.2, + "proc": 7289, + "ui_ok": true, + "focus": "902807b mode=0 rootTaskId=276 u0 com.duowan.kiwi/com.duowan.kiwi.homepage.Homepage" + }, + { + "t": 2.0, + "proc": 7289, + "ui_ok": true, + "focus": "902807b mode=0 rootTaskId=276 u0 com.duowan.kiwi/com.duowan.kiwi.homepage.Homepage" + }, + { + "t": 2.7, + "proc": 7289, + "ui_ok": true, + "focus": "902807b mode=0 rootTaskId=276 u0 com.duowan.kiwi/com.duowan.kiwi.homepage.Homepage" + }, + { + "t": 3.4, + "proc": 7289, + "ui_ok": true, + "focus": "902807b mode=0 rootTaskId=276 u0 com.duowan.kiwi/com.duowan.kiwi.homepage.Homepage" + }, + { + "t": 4.0, + "proc": 7289, + "ui_ok": true, + "focus": "902807b mode=0 rootTaskId=276 u0 com.duowan.kiwi/com.duowan.kiwi.homepage.Homepage" + }, + { + "t": 4.8, + "proc": 7289, + "ui_ok": true, + "focus": "902807b mode=0 rootTaskId=276 u0 com.duowan.kiwi/com.duowan.kiwi.homepage.Homepage" + }, + { + "t": 5.5, + "proc": 7289, + "ui_ok": true, + "focus": "902807b mode=0 rootTaskId=276 u0 com.duowan.kiwi/com.duowan.kiwi.homepage.Homepage" + }, + { + "t": 6.2, + "proc": 7289, + "ui_ok": true, + "focus": "902807b mode=0 rootTaskId=276 u0 com.duowan.kiwi/com.duowan.kiwi.homepage.Homepage" + }, + { + "t": 6.9, + "proc": 7289, + "ui_ok": true, + "focus": "902807b mode=0 rootTaskId=276 u0 com.duowan.kiwi/com.duowan.kiwi.homepage.Homepage" + }, + { + "t": 7.6, + "proc": 7289, + "ui_ok": true, + "focus": "902807b mode=0 rootTaskId=276 u0 com.duowan.kiwi/com.duowan.kiwi.homepage.Homepage" + }, + { + "t": 8.3, + "proc": 7289, + "ui_ok": true, + "focus": "902807b mode=0 rootTaskId=276 u0 com.duowan.kiwi/com.duowan.kiwi.homepage.Homepage" + }, + { + "t": 9.0, + "proc": 7289, + "ui_ok": true, + "focus": "902807b mode=0 rootTaskId=276 u0 com.duowan.kiwi/com.duowan.kiwi.homepage.Homepage" + }, + { + "t": 9.7, + "proc": 7289, + "ui_ok": true, + "focus": "902807b mode=0 rootTaskId=276 u0 com.duowan.kiwi/com.duowan.kiwi.homepage.Homepage" + }, + { + "t": 10.4, + "proc": 7289, + "ui_ok": true, + "focus": "902807b mode=0 rootTaskId=276 u0 com.duowan.kiwi/com.duowan.kiwi.homepage.Homepage" + }, + { + "t": 11.1, + "proc": 7289, + "ui_ok": true, + "focus": "902807b mode=0 rootTaskId=276 u0 com.duowan.kiwi/com.duowan.kiwi.homepage.Homepage" + }, + { + "t": 11.7, + "proc": 7289, + "ui_ok": true, + "focus": "902807b mode=0 rootTaskId=276 u0 com.duowan.kiwi/com.duowan.kiwi.homepage.Homepage" + }, + { + "t": 12.4, + "proc": 7289, + "ui_ok": true, + "focus": "902807b mode=0 rootTaskId=276 u0 com.duowan.kiwi/com.duowan.kiwi.homepage.Homepage" + }, + { + "t": 13.1, + "proc": 7289, + "ui_ok": true, + "focus": "902807b mode=0 rootTaskId=276 u0 com.duowan.kiwi/com.duowan.kiwi.homepage.Homepage" + }, + { + "t": 13.9, + "proc": 7289, + "ui_ok": true, + "focus": "902807b mode=0 rootTaskId=276 u0 com.duowan.kiwi/com.duowan.kiwi.homepage.Homepage" + }, + { + "t": 14.5, + "proc": 7289, + "ui_ok": true, + "focus": "902807b mode=0 rootTaskId=276 u0 com.duowan.kiwi/com.duowan.kiwi.homepage.Homepage" + }, + { + "t": 15.3, + "proc": 7289, + "ui_ok": true, + "focus": "902807b mode=0 rootTaskId=276 u0 com.duowan.kiwi/com.duowan.kiwi.homepage.Homepage" + }, + { + "t": 16.0, + "proc": 7289, + "ui_ok": true, + "focus": "902807b mode=0 rootTaskId=276 u0 com.duowan.kiwi/com.duowan.kiwi.homepage.Homepage" + }, + { + "t": 16.7, + "proc": 7289, + "ui_ok": true, + "focus": "902807b mode=0 rootTaskId=276 u0 com.duowan.kiwi/com.duowan.kiwi.homepage.Homepage" + }, + { + "t": 17.4, + "proc": 7289, + "ui_ok": true, + "focus": "902807b mode=0 rootTaskId=276 u0 com.duowan.kiwi/com.duowan.kiwi.homepage.Homepage" + }, + { + "t": 18.1, + "proc": 7289, + "ui_ok": true, + "focus": "902807b mode=0 rootTaskId=276 u0 com.duowan.kiwi/com.duowan.kiwi.homepage.Homepage" + }, + { + "t": 18.8, + "proc": 7289, + "ui_ok": true, + "focus": "902807b mode=0 rootTaskId=276 u0 com.duowan.kiwi/com.duowan.kiwi.homepage.Homepage" + }, + { + "t": 19.5, + "proc": 7289, + "ui_ok": true, + "focus": "902807b mode=0 rootTaskId=276 u0 com.duowan.kiwi/com.duowan.kiwi.homepage.Homepage" + }, + { + "t": 20.2, + "proc": 7289, + "ui_ok": true, + "focus": "902807b mode=0 rootTaskId=276 u0 com.duowan.kiwi/com.duowan.kiwi.homepage.Homepage" + }, + { + "t": 20.9, + "proc": 7289, + "ui_ok": true, + "focus": "902807b mode=0 rootTaskId=276 u0 com.duowan.kiwi/com.duowan.kiwi.homepage.Homepage" + }, + { + "t": 21.7, + "proc": 7289, + "ui_ok": true, + "focus": "902807b mode=0 rootTaskId=276 u0 com.duowan.kiwi/com.duowan.kiwi.homepage.Homepage" + } + ], + "crash_count": 3, + "crash_tail": " 00000078345d85f0 x5 0000000000000000 x6 0000000000000000 x7 6a6e6e6aff636473\n08-27 04:55:13.862 31092 31092 F DEBUG : x8 00000000000009c0 x9 0000000000000000 x10 0000000000000000 x11 00000000000000a2\n08-27 04:55:13.862 31092 31092 F DEBUG : x12 0000007885d447f0 x13 0000000000000028 x14 0000007895b19050 x15 0000007885fb3ff0\n08-27 04:55:13.862 31092 31092 F DEBUG : x16 0000000000000000 x17 000000791a2759dc x18 000000791d49a000 x19 0000007885e94640\n08-27 04:55:13.862 31092 31092 F DEBUG : x20 0000007895b19010 x21 0000000000001000 x22 0000007885e87800 x23 0000007885e94630\n08-27 04:55:13.862 31092 31092 F DEBUG : x24 0000007895b19384 x25 000000000000000b x26 0000007885e864c0 x27 000000788d7873f8\n08-27 04:55:13.862 31092 31092 F DEBUG : x28 0000000000000001 x29 0000007fdf595000\n08-27 04:55:13.862 31092 31092 F DEBUG : lr 00000078345db630 sp 0000007fdf595000 pc 00000078345d9910 pst 0000000020001000\n08-27 04:55:13.862 31092 31092 F DEBUG : backtrace:\n08-27 04:55:13.862 31092 31092 F DEBUG : #00 pc 00000078345d9910 \n08-27 04:55:13.862 31092 31092 F DEBUG : #01 pc 00000078345db630 \n" +} \ No newline at end of file diff --git a/evidence/diag_phone/phone_d.json b/evidence/diag_phone/phone_d.json new file mode 100644 index 0000000..1fde5f2 --- /dev/null +++ b/evidence/diag_phone/phone_d.json @@ -0,0 +1,33 @@ +{ + "group": "D", + "pid": 9944, + "attach_ms": 168.2, + "timeline": [ + { + "t": 0.2, + "proc": 9944, + "ui_ok": true, + "focus": "dbb4ed9 mode=0 rootTaskId=277 u0 com.duowan.kiwi/com.duowan.kiwi.homepage.Homepage" + }, + { + "t": 0.9, + "proc": 9944, + "ui_ok": true, + "focus": "dbb4ed9 mode=0 rootTaskId=277 u0 com.duowan.kiwi/com.duowan.kiwi.homepage.Homepage" + }, + { + "t": 1.7, + "proc": 11364, + "ui_ok": true, + "focus": "dbb4ed9 mode=0 rootTaskId=277 u0 com.duowan.kiwi/com.duowan.kiwi.homepage.Homepage" + } + ], + "detached": [ + { + "reason": "application-requested", + "detail": "None" + } + ], + "crash_count": 0, + "crash_tail": "" +} \ No newline at end of file diff --git a/evidence/diag_phone/phone_stable_capture.json b/evidence/diag_phone/phone_stable_capture.json new file mode 100644 index 0000000..fa71705 --- /dev/null +++ b/evidence/diag_phone/phone_stable_capture.json @@ -0,0 +1,164 @@ +{ + "events": [ + { + "type": "mod-base", + "tag": "mod-base", + "val": "0x77ba455000", + "ts": 1787884118264 + }, + { + "type": "ready", + "tag": "ready", + "val": "all hooked", + "ts": 1787884118265 + } + ], + "timeline": [ + { + "t": 0.1, + "proc": 15977, + "pid_changed": false + }, + { + "t": 3.2, + "proc": 15977, + "pid_changed": false + }, + { + "t": 6.4, + "proc": 15977, + "pid_changed": false + }, + { + "t": 9.5, + "proc": 15977, + "pid_changed": false + }, + { + "t": 12.7, + "proc": 15977, + "pid_changed": false + }, + { + "t": 15.8, + "proc": 15977, + "pid_changed": false + }, + { + "t": 19.0, + "proc": 15977, + "pid_changed": false + }, + { + "t": 22.1, + "proc": 15977, + "pid_changed": false + }, + { + "t": 25.3, + "proc": 15977, + "pid_changed": false + }, + { + "t": 28.4, + "proc": 15977, + "pid_changed": false + }, + { + "t": 31.6, + "proc": 15977, + "pid_changed": false + }, + { + "t": 34.7, + "proc": 15977, + "pid_changed": false + }, + { + "t": 37.9, + "proc": 15977, + "pid_changed": false + }, + { + "t": 41.0, + "proc": 15977, + "pid_changed": false + }, + { + "t": 44.2, + "proc": 15977, + "pid_changed": false + }, + { + "t": 47.4, + "proc": 15977, + "pid_changed": false + }, + { + "t": 50.5, + "proc": 15977, + "pid_changed": false + }, + { + "t": 53.7, + "proc": 15977, + "pid_changed": false + }, + { + "t": 56.8, + "proc": 15977, + "pid_changed": false + }, + { + "t": 60.0, + "proc": 15977, + "pid_changed": false + }, + { + "t": 63.1, + "proc": 15977, + "pid_changed": false + }, + { + "t": 66.3, + "proc": 15977, + "pid_changed": false + }, + { + "t": 69.4, + "proc": 15977, + "pid_changed": false + }, + { + "t": 72.6, + "proc": 15977, + "pid_changed": false + }, + { + "t": 75.7, + "proc": 15977, + "pid_changed": false + }, + { + "t": 78.9, + "proc": 15977, + "pid_changed": false + }, + { + "t": 82.0, + "proc": 15977, + "pid_changed": false + }, + { + "t": 85.2, + "proc": 15977, + "pid_changed": false + }, + { + "t": 88.3, + "proc": 15977, + "pid_changed": false + } + ], + "detached": [] +} \ No newline at end of file diff --git a/scripts/diag_phone_lifecycle.py b/scripts/diag_phone_lifecycle.py new file mode 100644 index 0000000..cb59452 --- /dev/null +++ b/scripts/diag_phone_lifecycle.py @@ -0,0 +1,201 @@ +#!/usr/bin/env python3 +"""真机版 存活/闪退四组对照诊断器 (M2102J2SC 5dd8c93f, fs152 15.2.2). + +背景: 模拟器 GC3VE 上结论 = attach 主进程必死(msaoaidsec 静默 _exit) / +spawn+脚本必 EGL 崩。换真机后两项都要重测 —— 真机 GPU 正常、无模拟器特征, +msaoaidsec 在真机上的 frida 检测行为是未知数。 + +A. baseline 无 frida 正常启动 (应该活) +B. spawn-zero spawn 立即 resume, 不加载任何脚本 (模拟器上稳定活) +C. spawn-bypass spawn 挂起中加载 art_callsite 补丁后 resume (模拟器上 EGL 崩) +D. attach 正常启动 7s 后 attach 主进程, 裸 attach (模拟器上静默 _exit) + +指标(与模拟器版一致, 可交叉对比): + proc_main /proc//cmdline 精确等于 com.duowan.kiwi + ui_focus dumpsys window mCurrentFocus 是否含包名 + crash logcat -b crash 的 Fatal/signal/Abort 计数 + detached frida session detached reason/detail + +用法: + /Users/yml/codes/Reverse-Engineering-Agent-Universal-v3.0/.venv/bin/python \ + scripts/diag_phone_lifecycle.py [a|b|c|d] [serial] [remote] +""" +from __future__ import annotations + +import json +import re +import subprocess +import sys +import time +from pathlib import Path + +import frida + +SERIAL = sys.argv[2] if len(sys.argv) > 2 else "5dd8c93f" +REMOTE = sys.argv[3] if len(sys.argv) > 3 else "127.0.0.1:31878" +PACKAGE = "com.duowan.kiwi" +REPO = Path("/Users/yml/codes/douyu_login_py") +OUT_DIR = REPO / "evidence" / "diag_phone" +RE = Path("/Users/yml/codes/Reverse-Engineering-Agent-Universal-v3.0") +ART_CALLSITE = RE / "evidence" / "scripts" / "bypass_msaoaid_maps_art_callsite.js" + +DURATION = 22.0 # 单轮观察时长(s) + + +def sh(*a): + return subprocess.run(a, capture_output=True, text=True) + + +def adb(*a): + return sh("adb", "-s", SERIAL, *a) + + +def main_pid(): + r = adb("shell", "pidof", PACKAGE) + for p in r.stdout.strip().split(): + rr = adb("shell", "cat", f"/proc/{p}/cmdline") + if rr.stdout.strip("\x00").strip() == PACKAGE: + return int(p) + return None + + +def focus_state(): + r = adb("shell", "dumpsys", "window") + m = re.search(r"mCurrentFocus=Window\{([^}]+)\}", r.stdout) + if m: + f = m.group(1) + return (PACKAGE in f), f + return False, "?" + + +def crash_tail(): + r = adb("shell", "logcat", "-d", "-b", "crash", "-t", "60") + n = r.stdout.count("Fatal") + r.stdout.count("Abort message") + r.stdout.count("signal ") + return n, r.stdout[-1200:] + + +def snapshot(label): + p = main_pid() + ok, foc = focus_state() + return {"label": label, "proc": p, "ui_ok": ok, "focus": foc} + + +DETACHED = [] + + +def detach_cb(reason, detail): + DETACHED.append({"reason": reason, "detail": str(detail)}) + print(f" [detached] reason={reason} detail={detail}", flush=True) + + +def run_observe(d, pid, label, extra=None): + """观察进程到超时或主进程死亡, 每 0.5s 记一次存活. 返回 timeline.""" + t0 = time.time() + timeline = [] + last_pid = pid + while time.time() - t0 < DURATION: + ok, foc = focus_state() + cur = main_pid() + timeline.append({"t": round(time.time() - t0, 1), "proc": cur, "ui_ok": ok, "focus": foc}) + if cur is None or (last_pid is not None and cur != last_pid): + break + if extra is not None and extra(): + break + time.sleep(0.5) + return timeline + + +def case_a(): + adb("shell", "logcat", "-c") + adb("shell", "am", "force-stop", PACKAGE) + time.sleep(1.5) + adb("shell", "monkey", "-p", PACKAGE, "-c", "android.intent.category.LAUNCHER", "1") + t0 = time.time() + time.sleep(3) + pid = main_pid() + tl = run_observe(None, pid, "A") + return {"group": "A", "t_spawn": round(time.time() - t0, 1), "pid": pid, "timeline": tl} + + +def case_b(): + d = frida.get_device_manager().add_remote_device(REMOTE) + adb("shell", "logcat", "-c") + adb("shell", "am", "force-stop", PACKAGE) + time.sleep(1.5) + t0 = time.time() + pid = d.spawn([PACKAGE]) + d.resume(pid) + tl = run_observe(d, pid, "B", extra=lambda: False) + return {"group": "B", "pid": pid, "t_spawn": round(time.time() - t0, 1), + "timeline": tl, "detached": DETACHED} + + +def case_c(): + d = frida.get_device_manager().add_remote_device(REMOTE) + adb("shell", "logcat", "-c") + adb("shell", "am", "force-stop", PACKAGE) + time.sleep(1.5) + t0 = time.time() + pid = d.spawn([PACKAGE]) + suspend_start = time.time() + session = d.attach(pid) + session.on("detached", detach_cb) + sc = session.create_script(ART_CALLSITE.read_text()) + sc.load() + suspend_ms = (time.time() - suspend_start) * 1000 + d.resume(pid) + print(f" [C] spawn pid={pid} 挂起{suspend_ms:.0f}ms(art_callsite) resumed", flush=True) + tl = run_observe(d, pid, "C", extra=lambda: False) + try: + session.detach() + except Exception: + pass + return {"group": "C", "pid": pid, "suspend_ms": round(suspend_ms, 1), + "timeline": tl, "detached": DETACHED} + + +def case_d(): + d = frida.get_device_manager().add_remote_device(REMOTE) + adb("shell", "logcat", "-c") + adb("shell", "am", "force-stop", PACKAGE) + time.sleep(1.5) + adb("shell", "monkey", "-p", PACKAGE, "-c", "android.intent.category.LAUNCHER", "1") + time.sleep(7) + pid = main_pid() + t_att = time.time() + try: + session = d.attach(pid) + session.on("detached", detach_cb) + att_ms = (time.time() - t_att) * 1000 + print(f" [D] attach pid={pid} 耗时{att_ms:.0f}ms", flush=True) + except Exception as exc: + return {"group": "D", "pid": pid, "error": f"attach失败: {exc}", + "detached": DETACHED} + tl = run_observe(d, pid, "D", extra=lambda: False) + try: + session.detach() + except Exception: + pass + return {"group": "D", "pid": pid, "attach_ms": round(att_ms, 1), + "timeline": tl, "detached": DETACHED} + + +def main(): + which = (sys.argv[1] if len(sys.argv) > 1 else "a").lower() + OUT_DIR.mkdir(parents=True, exist_ok=True) + cases = {"a": case_a, "b": case_b, "c": case_c, "d": case_d} + result = cases[which]() + n, tail = crash_tail() + result["crash_count"] = n + result["crash_tail"] = tail + out = OUT_DIR / f"phone_{which}.json" + out.write_text(json.dumps(result, indent=1, ensure_ascii=False), encoding="utf-8") + print(f"[{which.upper()}] -> {out}") + print(" 主进程最终:", result["timeline"][-1] if result["timeline"] else None) + print(" crash:", n) + if DETACHED: + print(" detached:", DETACHED) + + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/scripts/phone_stable_capture.py b/scripts/phone_stable_capture.py new file mode 100644 index 0000000..13bdfcc --- /dev/null +++ b/scripts/phone_stable_capture.py @@ -0,0 +1,227 @@ +#!/usr/bin/env python3 +"""真机稳定抓帧: spawn + art_callsite 补丁 + 长时 hook. + +模拟器结论: spawn+脚本必 EGL 崩(约4s), 只能抢 stale 窗口。 +真机 M2102J2SC 实测: spawn 挂起加载 art_callsite 补丁后 12s+ 稳定存活 +(无 detached, 无 EGL 崩) —— 本脚本验证两点: + 1) 更长存活 (默认 90s), 主进程不被换/不闪退 (watch 逻辑) + 2) hook 能力真实可用: 抓 libudbauthunify.so 的 setSafeDeviceId/createWupDeviceInfo + (32hex hdid) + SSL_write (hypasswordLogin 帧) + 内存扫描 32hex + +用法: + /Users/yml/codes/Reverse-Engineering-Agent-Universal-v3.0/.venv/bin/python \ + scripts/phone_stable_capture.py [duration_s] [serial] [remote] +输出: + evidence/diag_phone/phone_stable_capture.json (事件 + 存活时间线) +""" +from __future__ import annotations + +import json +import re +import subprocess +import sys +import time +from pathlib import Path + +import frida + +SERIAL = sys.argv[2] if len(sys.argv) > 2 else "5dd8c93f" +REMOTE = sys.argv[3] if len(sys.argv) > 3 else "127.0.0.1:31878" +DURATION = float(sys.argv[1]) if len(sys.argv) > 1 and sys.argv[1].replace(".", "").isdigit() else 90.0 +PACKAGE = "com.duowan.kiwi" +REPO = Path("/Users/yml/codes/douyu_login_py") +OUT = REPO / "evidence" / "diag_phone" / "phone_stable_capture.json" +RE = Path("/Users/yml/codes/Reverse-Engineering-Agent-Universal-v3.0") +ART_CALLSITE = RE / "evidence" / "scripts" / "bypass_msaoaid_maps_art_callsite.js" + +MAIN_JS = r""" +'use strict'; +function readStdString(p){ + if (p.isNull()) return null; + try{ + var first = p.readU8(); + if ((first & 1) === 0) { + var len = first >> 1; + return (len>0 && len<256) ? p.add(1).readUtf8String(len) : ''; + } else { + var data = p.readPointer(); + var len = p.add(8).readU64(); + return (len>0 && len<1024) ? data.readUtf8String(len) : null; + } + }catch(e){ return null; } +} +function hexb(p,n){try{return Array.from(new Uint8Array(p.readByteArray(n))).map(b=>('0'+b.toString(16)).slice(-2)).join('');}catch(e){return '';}} +function head(p,n){try{return p.readCString(n);}catch(e){return '';}} +function emit(tag,val){ send({type:tag, tag:tag, val:''+(val||''), ts:Date.now()}); } +var hooked=false, scanned=false; + +function hookUdb(){ + if (hooked) return; + var md = Process.findModuleByName('libudbauthunify.so'); + if (!md) return; + emit('mod-base', ''+md.base); + try{ + Interceptor.attach(md.base.add(0x26A2E0), { + onEnter:function(a){ this.sd=readStdString(a[1]); this.hd=readStdString(a[2]); }, + onLeave:function(){ + var hd=(this.hd||''); emit('SETSD', hd+' | sd='+(this.sd||'').slice(0,24)); + if(/^[0-9a-f]{32}$/.test(hd)) emit('FOUND_HDID_32', hd); + } + }); + }catch(e){ emit('hook-err','setSd '+e); } + try{ + Interceptor.attach(md.base.add(0x2746A0), { + onLeave:function(ret){ + try{ var s=readStdString(ret.add(152)); if(s){ emit('WUPDEV_HDID',''+s); if(/^[0-9a-f]{32}$/.test(s)) emit('FOUND_HDID_32',''+s); } }catch(e){} + } + }); + }catch(e){ emit('hook-err','wupdev '+e); } + emit('ready','all hooked'); + hooked = true; +} + +function hookSSL(){ + try{ + var r=new ApiResolver('module'); + r.enumerateMatchesSync('exports:*!SSL_write').forEach(function(m){ + Interceptor.attach(m.address,{onEnter:function(a){ + var len=a[2].toInt32(); + if(len<50||len>50000)return; + var h=head(a[1],Math.min(len,600)); + if(h.indexOf('hypasswordLogin')>=0){ + emit('WUP_LOGIN_FULL', len+'B '+hexb(a[1],len)); + } + }}); + }); + send({type:'sslwrite-hooked', tag:'ssl'}); + }catch(e){} +} + +function scanMem(){ + var found = {}; + try{ + var ranges = Process.enumerateRanges({protection:'r--', coalesce:true}); + var total = ranges.length, scannedB = 0; + ranges.forEach(function(r, ri){ + try{ + var size = r.size; + if (size <= 0) return; + var step = 256*1024; + var off = 0; + while (off < size){ + var n = Math.min(step, size - off); + var buf = Memory.readByteArray(r.base.add(off), n); + if (buf){ + var sbuf = new Uint8Array(buf); + var s = ''; + for (var i=0;i {cur}", flush=True) + break + time.sleep(3) + print(f"[*] done, {len(events)} events, alive_pid={start_pid == main_pid()}", flush=True) + OUT.write_text(json.dumps({"events": events, "timeline": timeline, "detached": det}, indent=1), encoding="utf-8") + print(f"[*] saved {OUT}", flush=True) + + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/tools/app_login_flow.py b/tools/app_login_flow.py index 5bde80b..cf49b82 100644 --- a/tools/app_login_flow.py +++ b/tools/app_login_flow.py @@ -1,6 +1,10 @@ #!/usr/bin/env python3 """虎牙App渠道纯Python登录流 —— 含新账号 safe_auth 滑块自动过验。 +⚠ 已过时:本工具仍读取画像里的固定 safedeviceid 并在缺失时回退金样本,**未接 +``core/huya/dfp_register`` 新设备注册链**。生产请用 ``core/huya/app_login.py`` +(每次登录前注册链动态签发 safedeviceid/device_id)。本文件仅作历史参考。 + 链路(全部实测/逆向自 pt_auth 页JS, 见 work/safe_auth/pt_auth.pretty.js): 账号密码 -> WUP hypasswordLogin (POST wup.huya.com) |- 正常: 响应bean直出新鲜cred(114B) diff --git a/tools/huya_device_profile.py b/tools/huya_device_profile.py index d832927..fcbed16 100644 --- a/tools/huya_device_profile.py +++ b/tools/huya_device_profile.py @@ -1,6 +1,10 @@ #!/usr/bin/env python3 """多账号设备画像生成器 —— 每账号一套独立设备身份 (2026-08-26 实测定论)。 +⚠ 本文件是 tools 侧的独立试验版画像器。core 生产链用的是 +``core/huya/device_profile.py`` + ``core/huya/dfp_register``(safedeviceid/登录帧 +device_id 每次登录前动态签发,画像不再写入固定 safedeviceid)。 + 背景 (服务端校验矩阵, 全部对 wup.huya.com hypasswordLogin 实测): ┌──────────────────┬───────────────────────────────────────────────┐ │ 字段 │ 换值表现 │