阶段四: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

File diff suppressed because it is too large. Load diff
+139
View File
@@ -0,0 +1,139 @@
// 在类定义入口抓字节:判断编辑器的类是在哪个环节被解密的。
//
// 用法(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);
Whitespace-only changes.
Whitespace-only changes.
+1
View File
@@ -0,0 +1 @@
{"A": {"name": "A", "super": "java.lang.Enum", "interfaces": [], "flags": 16433, "fields": [{"name": "D", "type": "A", "flags": 16409}, {"name": "B", "type": "A", "flags": 16409}, {"name": "L", "type": "A", "flags": 16394}, {"name": "I", "type": "A", "flags": 16394}, {"name": "S", "type": "A", "flags": 16394}, {"name": "C", "type": "A", "flags": 16394}, {"name": "Z", "type": "A", "flags": 16394}, {"name": "D", "type": "int", "flags": 17}, {"name": "D", "type": "A[]", "flags": 4122}], "methods": [{"name": "values", "params": [], "returns": "A[]", "flags": 9}, {"name": "valueOf", "params": ["java.lang.String"], "returns": "A", "flags": 9}, {"name": "<init>", "params": ["java.lang.String", "int", "int"], "returns": "void", "flags": 2}, {"name": "<clinit>", "params": [], "returns": "void", "flags": 8}], "path": "/Volumes/Spine398/A.class"}, "AA": {"name": "AA", "super": "java.lang.Enum", "interfaces": [], "flags": 16433, "fields": [{"name": "D", "type": "AA", "flags": 16409}, {"name": "B", "type": "AA", "flags": 16409}, {"name": "L", "type": "AA", "flags": 16409}, {"name": "I", "type": "AA", "flags": 16409}, {"name": "S", "type": "AA", "flags": 16409}, {"name": "D", "type": "AA[]", "flags": 4122}], "methods": [{"name": "values", "params": [], "returns": "AA[]", "flags": 9}, {"name": "valueOf", "params": ["java.lang.String"], "returns": "AA", "flags": 9}, {"name": "<init>", "params": ["java.lang.String", "int"], "returns": "void", "flags": 2}, {"name": "<clinit>", "params": [], "returns": "void", "flags": 8}], "path": "/Volumes/Spine398/AA.class"}, "AB": {"name": "AB", "super": "et", "interfaces": [], "flags": 1057, "fields": [{"name": "D", "type": "Ah", "flags": 17}, {"name": "D", "type": "JS", "flags": 1}, {"name": "B", "type": "boolean", "flags": 0}], "methods": [{"name": "<init>", "params": ["Ah", "JS"], "returns": "void", "flags": 1}, {"name": "L", "params": ["java.lang.String"], "returns": "void", "flags": 20}, {"name": "D", "params": ["AB"], "returns": "void", "flags": 1}, {"name": "B", "params": ["AB"], "returns": "void", "flags": 1}, {"name": "D", "params": [], "returns": "java.lang.Exception", "flags": 1025}, {"name": "D", "params": ["com.esotericsoftware.spine.editor.export.ExportSettings"], "returns": "void", "flags": 1025}, {"name": "D", "params": [], "returns": "com.esotericsoftware.spine.editor.export.ExportSettings", "flags": 1025}, {"name": "D", "params": ["java.lang.String"], "returns": "boolean", "flags": 17}, {"name": "D", "params": ["java.lang.Class", "java.lang.String"], "returns": "java.io.InputStream", "flags": 8}, {"name": "Nb", "params": ["java.lang.Object", "java.lang.Object", "java.lang.Object", "java.lang.Object"], "returns": "java.lang.Object", "flags": 10}], "path": "/Volumes/Spine398/AB.class"}, "AC": {"name": "AC", "super": "vv", "interfaces": [], "flags": 48, "fields": [{"name": "D", "type": "AB", "flags": 4098}], "methods": [{"name": "<init>", "params": ["AB", "java.lang.String"], "returns": "void", "flags": 0}, {"name": "L", "params": ["java.lang.Object"], "returns": "void", "flags": 17}, {"name": "xp", "params": ["java.lang.Object", "java.lang.Object", "java.lang.Object", "java.lang.Object"], "returns": "java.lang.Object", "flags": 10}], "path": "/Volumes/Spine398/AC.class"}, "AD": {"name": "AD", "super": "et", "interfaces": ["java.lang.Runnable"], "flags": 49, "fields": [{"name": "D", "type": "Ah", "flags": 16}, {"name": "D", "type": "s", "flags": 1}, {"name": "D", "type": "JX", "flags": 1}, {"name": "D", "type": "dl", "flags": 1}, {"name": "D", "type": "df", "flags": 16}, {"name": "B", "type": "df", "flags": 2}, {"name": "D", "type": "float[]", "flags": 16}, {"name": "B", "type": "int", "flags": 0}, {"name": "Code", "type": "float", "flags": 0}, {"name": "SourceFile", "type": "float", "flags": 0}, {"name": "StackMapTable", "type": "float", "flags": 0}, {"name": "init", "type": "float", "flags": 0}, {"name": "LineNumberTable", "type": "float", "flags": 0}, {"name": "String", "type": "float", "flags": 0}, {"name": "Class", "type": "float", "flags": 0}, {"name": "Ljava", "type": "float", "flags": 0}, {"name": "java", "type": "float", "flags": 0}, {"name": "lang", "type": "float", "flags": 0}, {"name": "Object", "type": "float", "flags": 0}, {"name": "invoke", "type": "float", "flags": 0}, {"name": "MethodHandle", "type": "float", "flags": 0}, {"name": "Lcom", "type": "float", "flags": 0}, {"name": "CallSite", "type": "float", "flags": 0}, {"name": "ConstantCallSite", "type": "float", "flags": 0}, {"name": "Exceptions", "type": "float", "flags": 0}, {"name": "util", "type": "float", "flags": 0}, {"name": "B", "type": "boolean", "flags": 0}, {"name": "L", "type": "boolean", "flags": 0}, {"name": "D", "type": "de", "flags": 2}, {"name": "D", "type": "long", "flags": 2}, {"name": "D", "type": "java.util.concurrent.ThreadPoolExecutor", "flags": 16}, {"name": "D", "type": "z", "flags": 0}, {"name": "I", "type": "boolean", "flags": 0}, {"name": "S", "type": "boolean", "flags": 0}, {Line truncated
@@ -0,0 +1 @@
{"evidence_id":"ev_e367b51c6019f1891f6453ad38ede6785bbe937bb983126847ece3b99ae05bd3","subject":{"name":"libspine-editorarm64.dylib","digest":{"sha256":"2549d0b07be0926995628e0e887e72cf0812c80438a7cfb9d1935c88cad28c57"},"local_path":"/private/var/folders/jl/m50cxy0d7zzfd0448wzhj2bc0000gn/T/.spine/tianmokeji/4.3.26/libspine-editorarm64.dylib","format":"mach-o","architecture":"arm64"},"provider":{"id":"rea-workflow","name":"REA composed investigation workflow","version":"1"},"analysis_profile":{"provider":{"id":"rea-workflow","name":"REA composed investigation workflow","version":"1"},"parameters":{"upstream_analysis_profile":{"provider":{"id":"ghidra","name":"Ghidra","version":"12.1.4"},"parameters":{"target_kind":"executable","target_format":"mach-o","architecture":"arm64","available_architectures":["arm64"],"import_mode":"ephemeral-source-immutable","annotation_policy":"atomic-function-entry-metadata-v1","load_image_observations":"source-mappings-entry-context-v2","function_body_evidence":"complete-inclusive-ranges-v1","function_references":"complete-body-and-entry-reference-manager-v2","call_relationship_coverage":"function-body-call-coverage-v5","function_dossier_coverage":"observed-facets-and-call-relationships-v1","string_inventory_evidence":"defined-data-coverage-v1","location_resolution":"explicit-address-exact-entry-symbol-first-v3","procedure_file_offset_resolution":"address-first-unique-target-source-mapping-v2","instruction_flow_evidence":"decoded-return-pcode-v1","instruction_references":"exact-address-incoming-v1","function_boundary_observations":"ghidra-function-boundary-limitations-v3","no_return_repair":"returning-imports-decoded-return-v2","process_launch":"inspected-jvm-launch-support-v1","jump_table_evidence":"typed-case-default-blocks-v1","decompiler_jump_loads":true,"loader":"auto-from-header","language_id":"auto-from-header","compiler_spec_id":"auto-default","analyzer_preset":"ghidra-default"},"digest":"966e6016724421ad282356f583a9d5bf24577db4ac77a75c7099d236b88bcc28"},"workflow_revision":3},"digest":"36e03fb12d9b7fb24826a311c442f39020ecdc72441154c33bf99b49256605a8"},"predicate_type":"rea.analysis","operation":"binary_overview","parameters":{},"raw_result":null,"normalized_result":{"document":"target-2549d0b07be0.dylib","image_base":"0x0","segments":[{"name":"__TEXT","start":"0x0","end":"0x1d70","length":7536,"image_base":"0x0","address_space":"ram"},{"name":"__text","start":"0x1d70","end":"0xb6f74","length":741892,"image_base":"0x0","address_space":"ram"},{"name":"__stubs","start":"0xb6f74","end":"0xc40f0","length":53628,"image_base":"0x0","address_space":"ram"},{"name":"__stub_helper","start":"0xc40f0","end":"0xc4420","length":816,"image_base":"0x0","address_space":"ram"},{"name":"__objc_stubs","start":"0xc4420","end":"0xc45a0","length":384,"image_base":"0x0","address_space":"ram"},{"name":"__gcc_except_tab","start":"0xc45a0","end":"0xc6ca0","length":9984,"image_base":"0x0","address_space":"ram"},{"name":"__const","start":"0xc6ca0","end":"0xc6dd8","length":312,"image_base":"0x0","address_space":"ram"},{"name":"__cstring","start":"0xc6dd8","end":"0xc739a","length":1474,"image_base":"0x0","address_space":"ram"},{"name":"__objc_methname","start":"0xc739a","end":"0xc7458","length":190,"image_base":"0x0","address_space":"ram"},{"name":"__unwind_info","start":"0xc7458","end":"0xcc1e0","length":19848,"image_base":"0x0","address_space":"ram"},{"name":"__eh_frame","start":"0xcc1e0","end":"0x118000","length":310816,"image_base":"0x0","address_space":"ram"},{"name":"__got","start":"0x118000","end":"0x118118","length":280,"image_base":"0x0","address_space":"ram"},{"name":"__la_symbol_ptr","start":"0x118118","end":"0x120cc0","length":35752,"image_base":"0x0","address_space":"ram"},{"name":"__const","start":"0x120cc0","end":"0x120e30","length":368,"image_base":"0x0","address_space":"ram"},{"name":"__objc_imageinfo","start":"0x120e30","end":"0x120e38","length":8,"image_base":"0x0","address_space":"ram"},{"name":"__objc_selrefs","start":"0x120e38","end":"0x120e98","length":96,"image_base":"0x0","address_space":"ram"},{"name":"__objc_classrefs","start":"0x120e98","end":"0x120ea0","length":8,"image_base":"0x0","address_space":"ram"},{"name":"__data","start":"0x120ea0","end":"0x120f18","length":120,"image_base":"0x0","address_space":"ram"},{"name":"__bss","start":"0x120f18","end":"0x120f1c","length":4,"image_base":"0x0","address_space":"ram"},{"name":"__DATA","start":"0x120f1c","end":"0x124000","length":12516,"image_base":"0x0","address_space":"ram"},{"name":"__LINKEDIT","start":"0x124000","end":"0x2e8010","length":1851408,"image_base":"0x0","address_space":"ram"},{"name":"__LINKEDIT","start":"0x2e8010","end":"0x2ec000","length":16368,"image_base":"0x0","address_space":"ram"},{"name":"EXTERNAL","start":"0x2ec000","end":"0x2edf00","length":7936,"image_base":"0x0","address_space":"ram"}],"segment_count":23,"procedure_count":17006,"string_count":16920},"confidence":"derived","authority":"shipped-artifact"Line truncated
View File
Whitespace-only changes.
@@ -0,0 +1 @@
{"binding":{"analysis_profile":{"digest":"966e6016724421ad282356f583a9d5bf24577db4ac77a75c7099d236b88bcc28","parameters":{"analyzer_preset":"ghidra-default","annotation_policy":"atomic-function-entry-metadata-v1","architecture":"arm64","available_architectures":["arm64"],"call_relationship_coverage":"function-body-call-coverage-v5","compiler_spec_id":"auto-default","decompiler_jump_loads":true,"function_body_evidence":"complete-inclusive-ranges-v1","function_boundary_observations":"ghidra-function-boundary-limitations-v3","function_dossier_coverage":"observed-facets-and-call-relationships-v1","function_references":"complete-body-and-entry-reference-manager-v2","import_mode":"ephemeral-source-immutable","instruction_flow_evidence":"decoded-return-pcode-v1","instruction_references":"exact-address-incoming-v1","jump_table_evidence":"typed-case-default-blocks-v1","language_id":"auto-from-header","load_image_observations":"source-mappings-entry-context-v2","loader":"auto-from-header","location_resolution":"explicit-address-exact-entry-symbol-first-v3","no_return_repair":"returning-imports-decoded-return-v2","procedure_file_offset_resolution":"address-first-unique-target-source-mapping-v2","process_launch":"inspected-jvm-launch-support-v1","string_inventory_evidence":"defined-data-coverage-v1","target_format":"mach-o","target_kind":"executable"},"provider":{"id":"ghidra","name":"Ghidra","version":"12.1.4"}},"provider":{"id":"ghidra","name":"Ghidra","version":"12.1.4"}},"entries":[],"evidence_bundle":{"artifacts":[{"architecture":"arm64","digest":{"sha256":"2549d0b07be0926995628e0e887e72cf0812c80438a7cfb9d1935c88cad28c57"},"format":"mach-o"}],"captures":[],"environments":[],"providers":[{"id":"rea-workflow","name":"REA composed investigation workflow","version":"1"}],"records":[{"analysis_profile":{"digest":"36e03fb12d9b7fb24826a311c442f39020ecdc72441154c33bf99b49256605a8","parameters":{"upstream_analysis_profile":{"digest":"966e6016724421ad282356f583a9d5bf24577db4ac77a75c7099d236b88bcc28","parameters":{"analyzer_preset":"ghidra-default","annotation_policy":"atomic-function-entry-metadata-v1","architecture":"arm64","available_architectures":["arm64"],"call_relationship_coverage":"function-body-call-coverage-v5","compiler_spec_id":"auto-default","decompiler_jump_loads":true,"function_body_evidence":"complete-inclusive-ranges-v1","function_boundary_observations":"ghidra-function-boundary-limitations-v3","function_dossier_coverage":"observed-facets-and-call-relationships-v1","function_references":"complete-body-and-entry-reference-manager-v2","import_mode":"ephemeral-source-immutable","instruction_flow_evidence":"decoded-return-pcode-v1","instruction_references":"exact-address-incoming-v1","jump_table_evidence":"typed-case-default-blocks-v1","language_id":"auto-from-header","load_image_observations":"source-mappings-entry-context-v2","loader":"auto-from-header","location_resolution":"explicit-address-exact-entry-symbol-first-v3","no_return_repair":"returning-imports-decoded-return-v2","procedure_file_offset_resolution":"address-first-unique-target-source-mapping-v2","process_launch":"inspected-jvm-launch-support-v1","string_inventory_evidence":"defined-data-coverage-v1","target_format":"mach-o","target_kind":"executable"},"provider":{"id":"ghidra","name":"Ghidra","version":"12.1.4"}},"workflow_revision":3},"provider":{"id":"rea-workflow","name":"REA composed investigation workflow","version":"1"}},"authority":"shipped-artifact","confidence":"derived","environment":null,"evidence_id":"ev_e367b51c6019f1891f6453ad38ede6785bbe937bb983126847ece3b99ae05bd3","evidence_links":[],"limitations":["Derived by an REA composed workflow.","A headless Ghidra session contains exactly one imported Program, unlike Hopper's multi-document GUI session.","Cancelling an active Ghidra request terminates its ephemeral database and discards session annotations. The selected target remains open; the next provider query imports the original artifact again.","Charset is reported, while a non-missing terminator cannot distinguish a present terminator from a fixed or Pascal layout.","Default-space addresses use lowercase 0x-prefixed hexadecimal; other address spaces use <percent-encoded-space>:0x<hex>.","External functions and local thunks are distinct; procedure metadata identifies both and preserves a thunk target when Ghidra resolves one.","Memory-block end addresses are exclusive; permissions come from Ghidra MemoryBlock flags rather than inference from section names.","Only Ghidra-defined string Data is observed; packed or undefined string bytes may be absent. Completeness applies to that inventory, and an empty search does not establish that text is absent from the artifact. If an address is known, use read_bytes and xrefs to inspect bytes and references.","The imported Program and temporary project are ephemeral and deleted on close. Annotation operations edit session database metadata; original executable bytes are never written.","The seLine truncated