Files
Spine_lab/rea-analysis/REA-Spine分析报告.md
T
tianmo 2e7ab7c33c 阶段四:Spine 3.8.99 编辑器里挂上自建视图面板
目标:在 Spine 编辑器窗口内部加一个自己的视图面板(按钮 + 文字显示),
点击读取场景对象,并像原生面板一样出现在 Views 菜单、可停靠可折叠。

本轮打通:
1. 编辑器在我们自己的进程里完整启动
   - 类加载器必须是 URLClassLoader 本身(自检要求)
   - Editor.main 必须在 AWT EDT 上调用
   - jvm.cfg 第 21 行要以两个空格结尾 + -DSpineJava
   - Editor.D:I 版本号、fonts/、/tzax 资源、Java8 版 launcher.Launcher 替身
2. 3.8.99 全量解密 → 可读源码
   - 字符串 17251/17461(伪造同名调用帧对齐 key)
   - 混淆动态派发还原 53871 处(调引导方法取 MemberName)
3. 自建视图
   - agent 在内存里给 us 枚举加 spineView 常量(不改磁盘,避开完整性自检)
   - 面板 Actor 由外部反射填进 us.spineView.D
   - 复用已有图标做别名(不新建图片资源)
   最终 "Spine View" 出现在 Views 菜单,可与 Tree 并排停靠、折叠展开

另外:
- ViewPatcher/ViewAgent:视图枚举与面板挂载的字节码插桩
- DecAll/ObMap/rewrite_source:字符串解密、派发还原、可读源码生成
- 阶段四-视图面板挂载.md:完整记录做法与踩过的坑

不纳入版本管理:rea/(独立仓库)、tools 下的 JDK/Ghidra 工具链、
Spine.app 与 spine-lab 副本、从载荷派生的批量产物。
2026-10-16 13:34:18 +08:00

162 lines
8.4 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.
# 用 REA 分析 Spine.app — 首轮结果
日期:2026-10-15
工具:REA (rea-agents 6.5.0),仓库来源 `https://github.com/morluto/rea.git`
目标:`/Users/tianmokeji/Desktop/临时/spine加功能/Spine.app`(Spine 4.3.06 启动器 / 4.3.26 Professional 编辑器)
## 0. 怎么跑起来的
| 步骤 | 结果 |
| --- | --- |
| `git clone https://github.com/morluto/rea.git` | 直连 **卡死在 72KB**;换 `ghfast.top` 镜像也卡在 4.1MB |
| `git -c http.version=HTTP/1.1 clone` | **成功**,68MB / 约 4 秒。沙箱代理下 HTTP/2 大包传输会中断,强制 HTTP/1.1 可绕过 |
| `npm install -g rea-agents` | 安装 rea 6.5.0,CLI 可用 |
| `rea doctor --client codex` | MCP 未注册;Hopper/Ghidra/IDA 均缺失(深度反汇编不可用) |
REA 的 MCP 注册需要重启客户端才生效,本轮全部走 **CLI**(REA 技能文档认可的同源路径)。
**踩坑**:REA 的 `inspect_plist` / `inspect_signature` / `observe-native-ui` 会现场用 `xcrun` 编译辅助进程,沙箱内会失败,必须放沙箱外执行。
## 1. 包结构(REA `inspect-artifact`)
- 图规模:185 节点 / 239 次出现 / 238 条边,`coverage.status = complete`
- Info.plist:`CFBundleShortVersionString 4.3.06`、`LSMinimumSystemVersion 10.13`、文档类型 `.spine`
- 签名(`inspect-signature`):**ad-hoc 签名**,`authorities: []`、`team_identifier: null`、`hardened_runtime: false`、无 entitlements
`/Applications/Spine.app` 与工作区副本的 `Contents/MacOS/Spine` SHA-256 完全一致(`fea6e69a…4340f2`),两个 libjvm 大小也一致 → 工作区副本即真实安装的等价副本。
## 2. 启动链路(REA `inspect-macho` + `trace-dylib-resolution`)
`Contents/MacOS/Spine` 只有 88KB,是纯原生启动器:
- 链接:Foundation / AppKit / Cocoa / libobjc / libSystem(**不含 libjvm**,JVM 是运行期 `dlopen` 进来的)
- 内置字符串给出完整启动参数:
```
-Djava.class.path=%@/Contents/MacOS/launcher/1/launcher.jar
%@/Contents/MacOS/launcher/1/lib/jli/libjli.dylib → 失败则 %@/Contents/MacOS/launcher/2/lib/libjli.dylib
%@/Contents/MacOS/launcher/1/lib/server/libjvm.dylib → 失败则 %@/Contents/MacOS/launcher/2/lib/server/libjvm.dylib
JNI_CreateJavaVM
主类候选:com/esotericsoftware/spine/editor/launcher/Launcher、com/esotericsoftware/spine/launcher/Launcher
```
`launcher.jar` 只有 72 个条目 / 2MB(`s/*.class` 混淆包 + 一个 374KB 的 `Launcher.class`)——**它只是引导壳,不是编辑器本体**。
## 3. 真正的加固点在 launcher/2 的 libjvm(已实测确认)
REA 对两个 libjvm 的段对比:
| | launcher/1/libjvm.dylib | launcher/2/libjvm.dylib |
| --- | --- | --- |
| 文件大小 | 13.3 MB | **58.4 MB** |
| `__TEXT` | 8.5 MB | 8.1 MB |
| `__DATA` | 313 KB | **48,896,800 B(≈48.9MB,文件偏移 8,933,376)** |
| HotSpot 调试段 `__dof_hotspot*` | 有 | **无** |
| `lib/modules`(jimage) | **46,549,519 B 存在** | **不存在** |
`launcher/2/libjvm` 的 `__DATA` 前 1MiB 熵 **7.59 bit/byte**,且扫描不到 `PK\x03\x04` / `PK\x05\x06` / jimage magic → 是**加密载荷**,不是明文 jar。
配合缺 `modules` 仍能正常启动这一事实,结论是:
> launcher/2 把 Java 模块镜像(jimage)连同编辑器类一起搬进了 libjvm 的 `__DATA` 并加密,
> 由打过补丁的 JVM 在加载时解密 —— 这就是「类不在 jar 里」的真正原因。
### 运行期到底加载了哪个?(零干扰实测)
对正在运行的 Spine(PID 91687)做 `lsof` / `vmmap` 只读采样:
```
…/launcher/2/lib/libjli.dylib
…/launcher/2/lib/server/libjvm.dylib ← 实际使用 launcher/2,不是 launcher/1
```
即:**`launcher/1` 是引导尝试路径,真正的运行时是 `launcher/2`**,与第 3 节吻合。
## 4. 新发现:编辑器原生核心在临时目录里
同一份 `lsof` 显示编辑器真正的原生库并不是从 app 包里加载的,而是运行期解包到临时目录:
```
$TMPDIR/.spine/tianmokeji/4.2.43/{libspine-editorarm64.dylib, libgdxarm64.dylib,
libgdx-freetypearm64.dylib, libjglfwarm64.dylib,
liblwjgl.dylib, openal.dylib}
$TMPDIR/.spine/tianmokeji/L4.3.06/libjglfwarm64.dylib
$TMPDIR/.spine/tianmokeji/4.3.26/libspine-editorarm64.dylib
```
REA `inspect-macho` 对 `libspine-editorarm64.dylib`(3,047,440 B)的结果:
- install name `target/libspine-editorarm64.dylib`,依赖仅 Cocoa/AppKit/libSystem/libobjc/libc++
- 导出符号(REA 导出列表)里最有价值的几条:
```
_Java_com_esotericsoftware_spine_editor_Editor_licCheck
_Java_com_esotericsoftware_spine_editor_Editor_register
_Java_com_esotericsoftware_spine_editor_Editor_expire
_Java_com_esotericsoftware_spine_editor_audio_Audio_pauseDeviceJni / resumeDeviceJni
```
另外导出大量 **Eigen** 符号(`Eigen::Matrix`、`squared_edge_lengths`),说明网格/几何解算是 Eigen 在跑。
**这条信息对「在编辑器内部加粒子面板」很关键**:编辑器原生核心是**磁盘上、未加密、可直接分析的 dylib**,
比拿 libjvm 里那 49MB 加密数据段要现实得多。
日志侧证据:`~/Library/Application Support/Spine/spine.log` →
`Spine Launcher 4.3.06 (macOS Apple Silicon)` / `Starting: Spine 4.3.26 Professional`。
## 5. REA 运行时 UI 观测:被一个具体阻塞点挡住
REA 有 `observe-native-ui` / `capture-native-ui-scenario`(读取窗口的无障碍树 + 截图),
本来最适合你现在的面板研究。实测报错:
```
process-identity-unavailable: Selected application does not expose a launch time
```
定位(读源码 + 实测):`rea/bridge/native/ReaNativeUI.swift` 用
`NSRunningApplication.launchDate` 做 PID 复用校验,而当前实例由命令行直接 exec 启动,
LaunchServices 没有登记它:
```
pid=91687 bundleIdentifier=nil bundleURL=…/spine-lab/Spine.app launchDate=nil
```
**解决办法**:让 Spine 由 LaunchServices 启动(`open -na <app> --args …` 或访达双击),
新进程才有 `launchDate`,REA 的 UI 树观测即可通过。
## 6. 本轮结论清单
| 问题 | 状态 | 依据 |
| --- | --- | --- |
| 编辑器类为什么不在 jar 里 | 已答 | launcher.jar 仅 72 条目;launcher/2 无 `lib/modules`,libjvm `__DATA` 48.9MB 高熵 |
| 实际运行哪个 libjvm | 已答(实测) | `lsof`/`vmmap`:launcher/2 |
| 编辑器原生代码在哪 | 已答 | `$TMPDIR/.spine/<user>/<ver>/libspine-editorarm64.dylib` |
| 编辑器原生库的 JNI 面 | 已答 | REA 导出:`Editor_licCheck/register/expire`、`Audio_*` |
| 包内清单 / 签名 / plist | 已答 | REA inventory / signature / plist |
| 深度反汇编(伪代码、函数、xrefs) | **未答** | 缺 Hopper/Ghidra/IDA 提供商 |
| 编辑器窗口内面板运行时结构 | **未答** | 需要先解决 launchDate(见第 5 节) |
## 7. 建议的下一步
1. **装 Ghidra(免费)**:`GHIDRA_INSTALL_DIR` 指过去,REA 立刻能对
`libspine-editorarm64.dylib`、`launcher/2/libjvm.dylib`、`libgdxarm64.dylib` 出伪代码与 xrefs。
2. **用 LaunchServices 启动 Spine**,然后跑
`rea capture-native-ui-scenario`,把编辑器面板/工具栏的无障碍树和可点击路径抓出来(可直接验证「窗口内面板是否可见、能否点」)。
3. 面板注入目标从 libjvm 改为 **`libspine-editorarm64.dylib`**:磁盘未加密、有完整 JNI 导出、可被 LLDB/Frida 附着。
4. 若要 REA 的 MCP 工具而非 CLI:`rea setup --client codex --dry-run --json` 看变更清单,
确认后 `--yes` 并重启 Codex(注册写入 `~/.codex/config.toml`)。
## 8. 证据文件
| 文件 | 内容 |
| --- | --- |
| `rea-analysis/01-inspect-artifact.json` | 应用清单图(185 节点,含 SHA-256) |
| `rea-analysis/02-macho-Spine.json` | 原生启动器 Mach-O(依赖、段、入口) |
| `rea-analysis/03-signature.json` | ad-hoc 签名详情 |
| `rea-analysis/04-dylib-resolution.json` | dyld 解析(8 个可执行根,含 ffmpeg/oxipng) |
| `rea-analysis/05-macho-libjvm2.json` / `06-macho-libjvm1.json` | 两个 libjvm 段对比 |
| `rea-analysis/07-native-ui.json` | 运行时 UI 观测尝试(记录阻塞原因) |
| `rea-analysis/08-macho-spine-editor.json` | 编辑器原生库(约 1MB 导出/导入明细) |
| `rea-analysis/tools/list-windows.swift` | 列出窗口 ID(配合 REA) |
| `rea-analysis/tools/check-app-identity.swift` | 检查启动时间/LaunchServices 身份(定位第 5 节问题) |