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

+133
View File
@@ -0,0 +1,133 @@
/* 内存扫描:找进程里已经解密成明文的 class 字节。
*
* 为什么走这条路:实测 Frida 的内联挂钩(Interceptor.attach 到 JVM_DefineClass*)
* 会让这套运行时直接 SIGSEGV,attach 又太晚(类早加载完了)。
* 扫描内存不改任何代码,风险最低。
*/
#include <mach/mach.h>
#include <mach/vm_region.h>
#include <pthread.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
#include <unistd.h>
#define SCAN_LOG "/tmp/spine-viewpanel.log"
#define SCAN_DIR "/tmp/spine-classes/scan"
static void scan_log(const char *message) {
FILE *file = fopen(SCAN_LOG, "a");
if (file) { fprintf(file, "%s\n", message); fclose(file); }
}
/* 用常量池长度算出一个 class 文件到底多少字节;不是合法 class 就返回 -1。 */
static int class_file_length(const unsigned char *p, size_t avail) {
if (avail < 10) return -1;
if (!(p[0] == 0xCA && p[1] == 0xFE && p[2] == 0xBA && p[3] == 0xBE)) return -1;
uint16_t cp_count = (uint16_t)((p[8] << 8) | p[9]);
size_t i = 10;
for (uint16_t index = 1; index < cp_count; index++) {
if (i >= avail) return -1;
uint8_t tag = p[i++];
switch (tag) {
case 1: {
if (i + 2 > avail) return -1;
uint16_t len = (uint16_t)((p[i] << 8) | p[i + 1]);
i += 2 + len;
break;
}
case 3: case 4: i += 4; break;
case 5: case 6: i += 8; index++; break;
case 7: case 8: case 16: case 19: case 20: i += 2; break;
case 9: case 10: case 11: case 12: case 17: case 18: i += 4; break;
case 15: i += 3; break;
default: return -1;
}
if (i > avail) return -1;
}
if (i + 8 > avail) return -1;
uint16_t interfaces = (uint16_t)((p[i + 6] << 8) | p[i + 7]);
i += 8 + (size_t)interfaces * 2;
for (int section = 0; section < 3; section++) {
if (i + 2 > avail) return -1;
uint16_t count = (uint16_t)((p[i] << 8) | p[i + 1]);
i += 2;
for (uint16_t k = 0; k < count; k++) {
if (i + 8 > avail) return -1;
uint16_t attributes = (uint16_t)((p[i + 6] << 8) | p[i + 7]);
i += 8;
for (uint16_t a = 0; a < attributes; a++) {
if (i + 6 > avail) return -1;
uint32_t len = ((uint32_t)p[i + 2] << 24) | ((uint32_t)p[i + 3] << 16)
| ((uint32_t)p[i + 4] << 8) | p[i + 5];
i += 6 + len;
if (i > avail) return -1;
}
}
}
return (int)i;
}
static void scan_memory(void) {
mkdir("/tmp/spine-classes", 0755);
mkdir(SCAN_DIR, 0755);
int dumped = 0;
uint64_t scanned = 0;
vm_address_t address = 0;
while (dumped < 6000) {
vm_size_t size = 0;
vm_region_basic_info_data_64_t info;
mach_msg_type_number_t count = VM_REGION_BASIC_INFO_COUNT_64;
mach_port_t object = MACH_PORT_NULL;
kern_return_t kr = vm_region_64(mach_task_self(), &address, &size, VM_REGION_BASIC_INFO_64,
(vm_region_info_t)&info, &count, &object);
if (kr != KERN_SUCCESS) break;
int readable = (info.protection & VM_PROT_READ) != 0;
/* 只扫进程自己分配的区域,跳过文件映射和共享缓存,减少误报与耗时。 */
int is_shared = info.shared;
if (readable && !is_shared && size >= 4096 && size <= (vm_size_t)256 * 1024 * 1024) {
const unsigned char *base = (const unsigned char *)address;
const unsigned char *end = base + size;
scanned += size;
for (const unsigned char *p = base; p + 4 < end; p++) {
if (p[0] != 0xCA || p[1] != 0xFE || p[2] != 0xBA || p[3] != 0xBE) continue;
int length = class_file_length(p, (size_t)(end - p));
if (length < 100 || length > 4 * 1024 * 1024) continue;
char path[256];
snprintf(path, sizeof(path), SCAN_DIR "/mem_%05d.class", dumped);
FILE *file = fopen(path, "wb");
if (file) {
fwrite(p, 1, (size_t)length, file);
fclose(file);
dumped++;
}
if (dumped >= 6000) break;
}
}
address += size;
}
char message[256];
snprintf(message, sizeof(message), "内存扫描:扫了 %.0f MB,dump 出 %d 个 class",
(double)scanned / 1048576.0, dumped);
scan_log(message);
}
static void *scan_thread(void *unused) {
(void)unused;
sleep(20);
scan_memory();
return NULL;
}
__attribute__((constructor))
static void scan_install(void) {
const char *enable = getenv("SPINE_VIEWPANEL_DUMP");
if (!enable || enable[0] != '1') return;
scan_log("=== memscan.dylib 已载入,20 秒后开始扫描 ===");
pthread_t thread;
if (pthread_create(&thread, NULL, scan_thread, NULL) == 0) pthread_detach(thread);
}