Files
live-hub-py/docs/模拟器存活闪退诊断报告.md
T
yml2213 49c5c36c05 docs(huya): 模拟器存活闪退诊断报告与 Frida 探测脚本证据
- 诊断报告: attach 主进程静默退出/EGL 崩溃, 仅约 4s 窗口可抓帧
- scripts: attach/spawn/hook/emu 系列 Frida 脚本与抓帧/验证工具
- evidence: identity/reqchain/frame/inputbuf/magic_buf/propedge 抓取样本,
  emu_* 存活对比, diag_* 策略实验, baseline 裸测基准
2026-08-27 17:58:32 +08:00

207 lines
15 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 模拟器闪退/卡启动/验证码不加载 —— 存活与崩溃诊断报告
> 日期: 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 部分绕过)