目标:在 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 副本、从载荷派生的批量产物。
134 lines
5.0 KiB
C
134 lines
5.0 KiB
C
/* 内存扫描:找进程里已经解密成明文的 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);
|
||
}
|