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

134 lines
5.0 KiB
C
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/* 内存扫描:找进程里已经解密成明文的 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);
}