阶段四: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 副本、从载荷派生的批量产物。
This commit is contained in:
tianmo committed 2026-10-16 13:34:18 +08:00
commit 2e7ab7c33c
113 files changed
+25327

No files matched your search

+161
View File
@@ -0,0 +1,161 @@
# 用 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 节问题) |