docs(huya): 模拟器存活闪退诊断报告与 Frida 探测脚本证据
- 诊断报告: attach 主进程静默退出/EGL 崩溃, 仅约 4s 窗口可抓帧 - scripts: attach/spawn/hook/emu 系列 Frida 脚本与抓帧/验证工具 - evidence: identity/reqchain/frame/inputbuf/magic_buf/propedge 抓取样本, emu_* 存活对比, diag_* 策略实验, baseline 裸测基准
This commit is contained in:
@@ -0,0 +1,207 @@
|
||||
# 模拟器闪退/卡启动/验证码不加载 —— 存活与崩溃诊断报告
|
||||
|
||||
> 日期: 2026-08-27
|
||||
> 环境: 模拟器 GC3VE (Android 12 API32, arm64, MuMu系, Kitsune Mask 26.4 rooted)
|
||||
> App: 虎牙 com.duowan.kiwi 13.4.22 (115315)
|
||||
> frida: server 15.2.2 (fs152, tcp:31878) + RE仓库 .venv (frida-py 15.2.2)
|
||||
> 本文档只回答一件事: **"App到底什么时候活着、什么时候闪退、为什么"**, 判断方法全部可复核。
|
||||
|
||||
## 〇、先纠正一个根本性错误: 如何判断"是否存活/是否闪退"
|
||||
|
||||
之前(包括文档既有结论)用以下方式判断进程状态, **全部不可靠**:
|
||||
|
||||
| 旧方法 | 为什么错 |
|
||||
|---|---|
|
||||
| `frida enumerate_processes()` 里 pid 存在 | spawn 后枚举有延迟/漏报; 进程死后 agent 会话残影还在; **多次实测"显示死"但 /proc 里明明活着** |
|
||||
| `pidof com.duowan.kiwi` 取第一个 pid | 会混入子进程 `com.duowan.kiwi:cloudpatch` / `:logcat`(它们的 cmdline 以包名开头); **把子进程当主进程 attach → 白费** |
|
||||
| 脚本结束后看到黑屏就以为闪退 | 屏幕回桌面 ≠ 进程死: **App 有 KeepAlive, UI Activity 被崩掉后进程会被系统保活重启**, pid 可能换新 |
|
||||
| `d.kill(pid)` / force-stop 后说"App死了" | 是我们自己杀的, 不算闪退 |
|
||||
|
||||
**本次统一的事实来源 (evidence/diag_lifecycle/diag_*.json 均按此采集):**
|
||||
|
||||
1. **主进程存活** = `/proc/<pid>/cmdline` 读出来**精确等于** `com.duowan.kiwi`(排除 `:cloudpatch`/`:logcat` 子进程)。`pidof` 每一 pid 都过 cmdline 过滤。
|
||||
2. **UI 是否闪退** = `dumpsys window | grep mCurrentFocus` 里是否还包含 `com.duowan.kiwi`(回到 `app.lawnchair` 即 UI 已退出)。
|
||||
3. **是否发生崩溃** = `logcat -d -b crash` 是否有 `Fatal`/`signal`/`Abort message`。native 崩溃(如 EGL)会写 tombstone, msaoaidsec 静默 `_exit` 则 crash buffer 无记录——**两种死法必须分开看**。
|
||||
|
||||
---
|
||||
|
||||
## 一、四组对照实验结果 (2026-08-27 实测)
|
||||
|
||||
脚本: `scripts/diag_lifecycle.py [a|b|c|d]`, 证据: `evidence/diag_lifecycle/diag_{a,b,c,d}.json`
|
||||
|
||||
| 组 | 做法 | 主进程存活 | UI 前台 | crash buffer | 结论 |
|
||||
|---|---|---|---|---|---|
|
||||
| **A** | 无 frida, monkey 正常启动 | ✅ 全程存活 (pid 不变) | ✅ Homepage | 0 | **无 frida 完全正常, 不闪退** |
|
||||
| **B** | frida spawn, **挂起 0.11s 立即 resume, 不加载任何脚本** | ✅ 全程存活 | ✅ Homepage | 0 | **spawn 本身不杀 App!** |
|
||||
| **C** | frida spawn, 挂起**加载 2 个 bypass 脚本** (art_callsite+mask_frida) 再 resume | ❌ 进程死 | ❌ 回桌面 | 1 条/轮: EGL | **挂起加载脚本 → EGL 崩** |
|
||||
| **D** | 正常启动 7s 后 **attach 主进程**(裸, 无脚本) | ❌ 进程死(静默) | ❌ 回桌面 | 0 | **attach 主进程 → msaoaidsec 静默 _exit** |
|
||||
|
||||
### 可重复性: B、C 各单独跑 3 轮 (2026-08-27 06:22~06:26)
|
||||
|
||||
| 轮次 | B (零挂起 spawn) | C (挂起+加载2脚本) |
|
||||
|---|---|---|
|
||||
| r1 | ✅ 存活 25.8s+ UI=Homepage crashes=0 | ⚠️ 存活 26s 但 UI 解析异常 (focus=?), crashes=0 |
|
||||
| r2 | ✅ 存活 25.8s+ UI=Homepage crashes=0 | ❌ +8.5s 进程 None, UI 回桌面, crashes=1 |
|
||||
| r3 | ✅ 存活 25.8s+ UI=Homepage crashes=0 | ❌ +8.4s 进程 None, UI 回桌面, crashes=1 |
|
||||
| 汇总 | **3/3 稳定存活** | **2/3 明确 EGL 崩, 1/3 存活但异常** |
|
||||
|
||||
C 组死亡时间线 (r3 完整轨迹): `+2.1s proc 存活 UI=Homepage → +4.2s pid 换新(KeepAlive 重启) UI 回桌面 → +8.4s 无主进程`; crash buffer:
|
||||
```
|
||||
signal 6 (SIGABRT) Abort message: 'Failed to create context, error = EGL_NOT_INITIALIZED'
|
||||
tid RenderThread, pid <原pid> → com.duowan.kiwi
|
||||
```
|
||||
注: 脚本 OUT 固定为 diag_c.json 每轮覆盖, 最终文件是最后一轮的数据; rounds 输出见 /tmp/diag_{b,c}.log 系列与 evidence/diag_lifecycle/。
|
||||
|
||||
### 关键崩理解读
|
||||
|
||||
**C 组 / 今天早上 v8/v9 的 tombstone (tombstone_24~27) 全部同型:**
|
||||
```
|
||||
signal 11 (SIGSEGV), code 1 (SEGV_MAPERR), fault addr 0x74206665722060 (ASCII 内容!)
|
||||
#00 __strchr_aarch64+36 / libc.so
|
||||
#01 strstr+24 / libc.so
|
||||
#02 android::egl_display_t::initialize+824 / libEGL.so
|
||||
#03 EglManager::initialize / libhwui.so
|
||||
#04 RenderThread::requireGlContext / libhwui.so
|
||||
```
|
||||
以及 `signal 6 (SIGABRT) Abort message: 'Failed to create context, error = EGL_NOT_INITIALIZED'`
|
||||
→ 崩溃线程是 **RenderThread(EGL 初始化)**, 和 msaoaidsec 无关。
|
||||
|
||||
**为什么会这样 (经 E6/E7 修正):** frida `spawn` 会把进程挂起在非常早期(zygote fork 后、JVM/GL 尚未初始化)。**关键实验 E6/E7: 挂起只 10~18ms、仅加载 1 个补丁脚本, attach 后 RenderThread 仍 100% EGL 崩 (Bad access / Trace/BPT trap, 4/4 轮)** → 说明触发 EGL 崩的**不是挂起时长, 而是 attach 注入本身**(frida agent 初始化与 EGL 显示初始化竞争)。纯 spawn 不 attach (B组) 则完全无此问题。
|
||||
|
||||
**D 组: attach 主进程 = 真 frida 检测。** 裸 attach 无任何 hook, 2~4s 静默死, crash buffer 无记录 → `libmsaoaidsec.so` 检测到 agent 后 `_exit`(RE 仓库 STATUS: `EXIT_SELF`)。这与 EGL 崩是**两条完全独立的死因**。
|
||||
|
||||
### 补充实验 E0~E7: attach 与死因的精确归因 (2026-08-27 06:30~06:36)
|
||||
|
||||
脚本: `scripts/diag_egl_strategy.py`, 证据: `evidence/diag_egl_strategy/egl_strategy.json`
|
||||
|
||||
| case | 做法 | 挂起 | 结果 | 死因 |
|
||||
|---|---|---|---|---|
|
||||
| E0 | spawn→attach(空)→resume | 3ms | ❌ ~2.07s pid换新(KeepAlive) | 静默_exit (msaoaidsec) |
|
||||
| E1 | spawn→attach+mask_frida→resume | 15ms | ❌ ~2.07s pid换新 | 静默_exit (mask无效) |
|
||||
| E3 | spawn→resume (不attach) | 0ms | ✅ 存活30s+ ×2 | — (B组复现) |
|
||||
| E4 | spawn→resume→1.6s后attach | 1673ms | ❌ attach后~2.5s死 | 静默_exit |
|
||||
| E5 | spawn→resume→5s后attach | 5183ms | ❌ attach后~4.1s死 | 静默_exit |
|
||||
| E6 | spawn→attach+art_callsite→resume | 11~18ms | ❌ 4.1s死 ×2 | **EGL崩 (RendererThread)** |
|
||||
| E7 | spawn→attach+art_callsite+mask→resume | 10~12ms | ❌ 4.1s死 ×2 | **EGL崩 (RendererThread)** |
|
||||
|
||||
**结论:**
|
||||
1. **attach 必定触发检测/崩溃**, 无论时机(挂起中 3ms 还是延迟 5s)、无论加载什么脚本、无论挂起多短。空 attach → msaoaidsec 静默 `_exit`; attach+bypass 脚本 → msaoaidsec 的清理路径被补丁掉后转为 EGL 崩溃。
|
||||
2. `mask_frida_maps_only.js` 单独使用**无效**(E1 仍静默死)——RE 仓库的 maps 遮蔽思路在此版 msaoaidsec 的模拟器行为下失败, 仅真机组合(G2-0055 多面)才有效。
|
||||
3. **唯一无外挂存活路径 = 纯 spawn→resume 不 attach**(B组/E3), 代价是无任何 hook 能力。
|
||||
|
||||
### 抓包三策略实测 (v11: scripts/emu_hook_zero_suspend.py, 2026-08-27 06:40)
|
||||
|
||||
| 模式 | 做法 | 结果 |
|
||||
|---|---|---|
|
||||
| zero | 纯spawn→resume (无hook) | ✅ 存活30s+ 但**0事件** (抓不到协议) |
|
||||
| stale | 挂起中加载art_callsite+mask→resume→+0.5s挂hook | ⚠️ 存活~4.2s: **55事件 + actionV=234548246275beedf8d93a242ef50e8f1178c3b2** (GC3VE真实身份) → 随后EGL崩 KeepAlive换pid |
|
||||
| race | resume后2s attach+2脚本+hook | ❌ attach后~2.2s静默死: 17事件(早期RESP) 无actionV |
|
||||
|
||||
**→ 抓包能力排序: stale > race > zero。** stale 在死前窗口(约4s)能完整抓到 dfpReport+actionV+dckey 链路(evidence: /tmp/v11_stale.json, 55事件), 与历史成功的 `hook_emu_stable.py`/`emu_spawn_bypass_dfp.json` 一致——**"抓帧即成功, App 随后崩是固有代价"**。race 模式(B组存活后延迟 attach)被 msaoaidsec 静默杀, 抓不到关键帧。
|
||||
|
||||
---
|
||||
|
||||
## 二、历史"成功"记录的真相 (为什么之前以为 attach 能用)
|
||||
|
||||
- `evidence/emu_attach_full.json` (08-26 20:33, 48 事件) 是**当时 attach 脚本的产物**——但当时脚本用
|
||||
```python
|
||||
for p in d.enumerate_processes():
|
||||
if 'kiwi' in p.name or 'duowan' in p.name: # ← 致命
|
||||
```
|
||||
frida 枚举里**主进程 name 显示为 "虎牙直播"**, 只有 `com.duowan.kiwi:cloudpatch`/`:logcat` 含 "kiwi" 字样。
|
||||
**→ 当时 attach 到的是 cloudpatch 子进程, 从未真正 hook 主进程。** 所谓"模拟器 attach 不崩"是子进程的假象。
|
||||
- `hook_emu_stable.py`/`spawn_g2055_survival.py` 的 spawn 成功: 实质是 **spawn 后挂起很短(只 load 2 个极快脚本)就 resume**, 命中"低挂起存活窗口"; 但 `spawn_survival.json` 也记录了 4 轮里 6s/6s/6s/3s 全死——大部分轮次仍触发 EGL 崩, 只是脚本在死前刚好抓到 dfp 报文就算"成功"。
|
||||
- **结论校准**: spawn 路线历史上"成功即抓到 dfp"并非稳定存活, 而是抢在 EGL 崩之前的窗口抓到数; 本次 C 组 3 轮 2 崩也验证了这一点。真正稳定的是 B 组(零挂起)。
|
||||
|
||||
---
|
||||
|
||||
## 三、"验证码无法正常加载"的机制与实测
|
||||
|
||||
### 3.1 登录验证码的真实链路 (本次实测)
|
||||
|
||||
1. 账号密码页点"立即登录" → 弹协议 → 同意 → 进入 **`OakVerifyActivity`** (com.huya.kiwi.crossplatform.common.webview.verification)
|
||||
2. 内部是 **WebView 加载极验/数美 H5 验证页**, 日志可见验证码 SDK 类 `cn.fly.verify.*` (数美 fly verify), UI 出现 **"安全验证 / 请拖动下方滑块完成拼图"**
|
||||
3. 单次流程可见 dfpReport(3800B) → actionV → dckey(744B) 链路照常上报
|
||||
|
||||
### 3.2 实测结论 (模拟器 GC3VE)
|
||||
|
||||
- **无 frida (A 组)**: 账号密码登录 → 滑块验证码**完整加载**(UI dump 出现"请拖动下方滑块完成拼图"+ WebView 容器)。→ **验证码不加载不是模拟器环境的锅**。
|
||||
- 之前"验证码不加载"发生的场景 = App 在 **frida 挂起/attach 被检测**的状态下启动或运行, 或 msaoaidsec 检测到 agent 后 App 处于被风控标记状态(行为上报 Poseidon/dfp collection 带 `hooked:...` 字段, 服务端不下发验证 H5)。
|
||||
- 推论: **登录流程不要带 frida**; 需要 hook 取证时, 用"零挂起 spawn + 抓完即杀"。
|
||||
|
||||
### 3.3 模拟器检测面 (本次静态+动态确认)
|
||||
|
||||
| 层 | 检测内容 | 证据 |
|
||||
|---|---|---|
|
||||
| Java 层 | `/dev/qemu_pipe`, `/dev/socket/qemud`, `/sys/qemu_trace`, `/system/bin/qemu-props`, `libc_malloc_debug_qemu.so` | classes.dex 字符串; `ryxq.nuj.F()` |
|
||||
| Java 层 | `Build.HARDWARE/PRODUCT/FINGERPRINT/BRAND` goldfish/sdk/generic, `ro.kernel.qemu` | `ryxq.xzj.c()/q()`, `ryxq.nuj.L()/N()` |
|
||||
| Java 层 | `/system/build.prop`, `/proc/tty/drivers`, `/proc/cpuinfo` goldfish | `ryxq.nuj.J()` |
|
||||
| Java 层 | su 文件 (`/system/bin/su` 等 5 路径) | `ryxq.xzj.e()` |
|
||||
| Native | `KH_NSDKThreadDe` 线程读 `/system/etc/mumu-configs/currentApp` 等 MuMu 特有路径 (logcat avc 可证) | libnsdt.so |
|
||||
| UI | 服务端下发 `EmulatorCard` (HUYA.EmulatorCard) 组件 | classes8/9 dex |
|
||||
| 配置式 | "Get emulator files feature from config / model feature from config" (可从远程配置增删检测项) | classes6.dex |
|
||||
|
||||
本机现状: MuMu 的 `fakeProductInfoIfNeed` 对 com.duowan.kiwi **未匹配**任何伪装模板 → App 看到的 build 属性是**原始 qemu 泄漏**:
|
||||
```
|
||||
ro.build.hv.platform=qemu ← 明显泄漏
|
||||
qemu.hw.mainkeys=1
|
||||
```
|
||||
其余 (ro.kernel.qemu 等) 已由 MuMu 内核隐藏。**Java 层 goldfish/su 检查当前在 GC3VE 上不命中**(brand=Google, fingerprint=release-keys, su 被 Magisk hide 处理), 所以无 frida 时 App 照常给验证码。
|
||||
|
||||
---
|
||||
|
||||
## 四、最终结论与可用操作路径
|
||||
|
||||
### 事实矩阵 (v11 实测修正: 2026-08-27 06:40)
|
||||
|
||||
| 想要做的事情 | 正确做法 | 实测 |
|
||||
|---|---|---|
|
||||
| App 在模拟器上正常启动/正常登录/验证码加载 | **完全不要 attach frida** | A 组: 100% 正常, 验证码完整加载 |
|
||||
| 仅在模拟器取证抓 dfpReport / hdid / 协议帧 | **stale 模式**: spawn 挂起中加载 art_callsite+mask → resume → 尽快挂 SSL hook | v11 stale: 存活~4.2s, 55事件+actionV 完整链路, 随后 EGL 崩属固有代价 |
|
||||
| 用 frida attach 主进程长时 hook | **当前不可行** (任何 attach 均死: 空attach静默_exit / attach+bypass=EGL崩) | E0~E7 全灭, 与挂起时长/时机无关 |
|
||||
| 纯 spawn 不 attach | 可稳定存活(无 hook 能力), 适合验证 App 本身状态 | B组/E3: 3/3+2/2 存活 |
|
||||
| 长时间 hook 主进程 | 未达成 | 需解决 masaoaidsec (RE 仓库 B-G2-0001 阻塞仍开放) 或换非 frida 注入 |
|
||||
|
||||
### 给登录/验证码流程的操作要求
|
||||
|
||||
1. **登录页面(UIs): 全程不启 frida。** 验证码走 `OakVerifyActivity` 原生 WebView, 与 hook 无关。
|
||||
2. **取证抓包: 用 stale 模式单独一次 spawn** (`emu_hook_zero_suspend.py --mode stale`), 死前窗口(约4s)即可抓到 dfpReport/actionV, 抓完 App 自然崩/被杀, 再正常启动走登录。**不要用 race/attach 方式**(静默死还抓不到关键帧)。
|
||||
3. 若见"验证码空白/不加载": 先 `am force-stop` + 正常重启 App 再试(清除 frida 痕迹); 若仍空白, 查 `logcat -b crash` 是否 EGL tombstone(挂起残留), 不是的话多半是服务端风控(行为上报带 hook 标记), 需要换干净设备身份。
|
||||
|
||||
### 复用脚本 (本次新增/更新)
|
||||
|
||||
- `scripts/diag_lifecycle.py` — 四组存活/闪退/崩溃对照诊断器 (A/B/C/D), 输出 JSON 证据
|
||||
- `scripts/diag_egl_strategy.py` — E0~E7 attach 归因实验 (空attach/mask/art_callsite/延迟attach)
|
||||
- `scripts/watch_proc_v2.py` — 主进程存活监控 (cmdline 精确匹配, 不用 frida enumerate)
|
||||
- `scripts/emu_hook_zero_suspend.py` — **v11 三策略抓包**: `--mode zero|race|stale` (推荐 stale 抓帧)
|
||||
- `scripts/emu_spawn_captcha_test.py` — spawn+bypass 下验证码加载对照 (结论: spawn 挂起偶发 EGL 崩, 登录勿用)
|
||||
- `scripts/emu_attach_captcha_test.py` — attach 三件套对照 (结论: attach 主进程必死, 勿用于登录)
|
||||
- `scripts/hook_emu_stable.py` — 既有: spawn+极短挂起抓 dfpReport (stale 模式同款, 可用)
|
||||
|
||||
### 下一步(按价值排序)
|
||||
|
||||
1. **P0 ✅完成** 抓包脚本 v11 三策略落地: stale=抓帧正解(zero存活不可抓, race被静默杀)
|
||||
2. **P1** 验证纯 Python 登录链再次打通 (不依赖模拟器 UI, 项目已有零设备链路) — 作为不碰 frida 的正路
|
||||
3. **P2** 若确需长时 hook: 深入 masaoaidsec (RE 仓库 G2 系列, 模拟器与真机版本差异), 或换用非 frida 注入 (ptrace-free/gdb), 或尝试 masaoaidsec 检测线程启动前的时间窗 (数据: msaoaid 检测在 App 启动 ~2s 内完成, attach 窗口<2s 理论存在但 E4 已证 1.6s attach 仍死, 需 <1s 超早 attach 验证)
|
||||
|
||||
---
|
||||
|
||||
## 附录 A: 本次使用的判定代码要点
|
||||
|
||||
```python
|
||||
def main_pid():
|
||||
# 唯一可信: /proc/<pid>/cmdline 精确等于包名
|
||||
for p in adb pidof 的所有 pid:
|
||||
if cat /proc/<p>/cmdline == "com.duowan.kiwi": return p
|
||||
|
||||
def focus_ok():
|
||||
m = re.search(r"mCurrentFocus=Window\{([^}]+)\}", dumpsys window)
|
||||
return PACKAGE in m.group(1) # 注意 u0/窗口hash在花括号内
|
||||
|
||||
# 崩溃: logcat -d -b crash | grep -E 'Fatal|signal|Abort message'
|
||||
```
|
||||
|
||||
## 附录 B: 相关既有文档
|
||||
|
||||
- `docs/dfpReport破解进度.md` 第十四~十六节 (模拟器探索/EGL 崩溃原始记录)
|
||||
- RE 仓库 `STATUS.md` B-G2-0001 (msaoaidsec EXIT_SELF 阻塞) / G2-0055 (maps/ELF 部分绕过)
|
||||
Reference in New Issue
Block a user