Files
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

140 lines
4.3 KiB
JavaScript

// 在类定义入口抓字节:判断编辑器的类是在哪个环节被解密的。
//
// 用法(attach 到正在跑的编辑器,或 spawn):
// rea instrument-with-frida attach <pid> --script-file frida-dump-classes.js --duration-ms 60000
//
// 输出:
// /tmp/spine-classes/plain/*.class —— 在入口就已经是 cafebabe 的类(直接可用)
// /tmp/spine-classes/meta.txt —— 每次调用的名字 + 前 8 字节 + 长度
const OUT = "/tmp/spine-classes";
function ensureDirs() {
try { new File(OUT, "r"); } catch (e) { new File(OUT, "w").close(); }
const plain = OUT + "/plain";
try { new File(plain, "r"); } catch (e) { new File(plain, "w").close(); }
}
let metaFile = null;
function meta(line) {
try {
if (metaFile === null) metaFile = new File(OUT + "/meta.txt", "a");
metaFile.write(line + "\n");
metaFile.flush();
} catch (e) { }
}
let dumped = 0;
let seen = 0;
function hex(bytes, count) {
let s = "";
for (let i = 0; i < count && i < bytes.length; i++) {
s += ("0" + bytes[i].toString(16)).slice(-2);
}
return s;
}
function handle(namePtr, bufPtr, len, where) {
seen++;
if (bufPtr.isNull() || len <= 8 || len > 8 * 1024 * 1024) return;
let name = "";
try { name = namePtr.isNull() ? "(null)" : namePtr.readUtf8String(); } catch (e) { name = "(?)"; }
let head;
try { head = Memory.readByteArray(bufPtr, Math.min(len, 8)); } catch (e) { return; }
const bytes = new Uint8Array(head);
const tag = hex(bytes, 4);
if (seen < 40) send({ where: where, name: name, len: len, head: hex(bytes, 8) });
if (tag === "cafebabe") {
dumped++;
try {
const data = Memory.readByteArray(bufPtr, len);
const file = new File(OUT + "/plain/" + (name ? name.replace(/[\\/]/g, "_") : ("anon" + dumped)) + ".class", "wb");
file.write(data);
file.close();
} catch (e) { }
} else if (seen < 40) {
meta(where + " " + name + " len=" + len + " head=" + hex(bytes, 8));
}
}
// Frida 17 去掉了 Module.findExportByName,这里把新旧几套 API 都试一遍。
function exportOf(module, name) {
try {
if (typeof module.findExportByName === "function") return module.findExportByName(name);
} catch (e) { }
try {
if (typeof module.getExportByName === "function") return module.getExportByName(name);
} catch (e) { }
try {
if (typeof Module.findExportByName === "function") return Module.findExportByName(module.name, name);
} catch (e) { }
try {
if (typeof Module.getExportByName === "function") return Module.getExportByName(module.name, name);
} catch (e) { }
return null;
}
function hookModule() {
const libjvm = Process.findModuleByName("libjvm.dylib");
if (!libjvm) {
send({ error: "libjvm.dylib 未加载" });
return false;
}
send({ module: libjvm.name, base: libjvm.base.toString(), size: libjvm.size });
const targets = [
"JVM_DefineClassWithSource",
"JVM_DefineClass",
"JVM_LookupDefineClass",
"_JVM_DefineClassWithSource",
"_JVM_DefineClass",
"_JVM_LookupDefineClass",
];
let hooked = 0;
for (const name of targets) {
const addr = exportOf(libjvm, name);
if (!addr) continue;
hooked++;
send({ hooked: name, addr: addr.toString() });
Interceptor.attach(addr, {
onEnter(args) {
// (JNIEnv*, const char* name, jobject loader, const jbyte* buf, jsize len, jobject pd[, const char* source])
try {
handle(args[1], args[3], args[4].toInt32(), name);
} catch (e) { }
},
});
}
send({ hookedCount: hooked });
return hooked > 0;
}
ensureDirs();
if (!hookModule()) {
// libjvm 还没加载:用模块加载通知(拿不到就退回到高频轮询),
// 因为类定义紧接着 libjvm 加载就开始了,晚了就什么都抓不到。
let done = false;
try {
Process.attachModuleObserver({
onAdded(module) {
if (done || module.name !== "libjvm.dylib") return;
done = true;
hookModule();
},
});
} catch (e) { }
const waiter = setInterval(function () {
if (done) { clearInterval(waiter); return; }
if (Process.findModuleByName("libjvm.dylib")) {
done = true;
clearInterval(waiter);
hookModule();
}
}, 20);
}
setInterval(function () {
send({ seen: seen, dumped: dumped });
}, 10000);