目标:在 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 副本、从载荷派生的批量产物。
140 lines
4.3 KiB
JavaScript
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);
|