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

+481
View File
@@ -0,0 +1,481 @@
/* ViewPanel —— 最小注入器
*
* 依据(REA 对 Contents/MacOS/Spine 的实测):
* · 该二进制导入 _dlopen/_dlsym/_dlerror,__la_symbol_ptr 里有 _dlsym 槽位,
* 且没有直接链接 _JNI_CreateJavaVM —— JNI 入口是运行期 dlopen+dlsym 拿的,
* 所以重绑定 _dlsym 是有效钩子点。
* · ad-hoc 签名、无 hardened runtime、无 entitlements —— DYLD_INSERT_LIBRARIES 可用。
*
* 本文件只做三件事:
* 1) 重绑定 _dlsym(含后加载的 libjvm)
* 2) 拦 JNI_CreateJavaVM
* 3) JVM 起来后,把我们的类 DefineClass 进编辑器自己的类加载器,再调用它的 main
*
* 这里没有任何 UI 代码:面板是纯 Java / Scene2D 实现的。
*/
#include <dirent.h>
#include <dlfcn.h>
#include <jni.h>
#include <mach-o/dyld.h>
#include <mach-o/loader.h>
#include <mach-o/nlist.h>
#include <pthread.h>
#include <stdarg.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/mman.h>
#include <sys/stat.h>
#include <unistd.h>
#define VP_LOG "/tmp/spine-viewpanel.log"
#define VP_DEFAULT_CLASSES "/tmp/spine-viewpanel/classes"
#define VP_DEFAULT_MAIN "com/spineview/SceneScan"
static void vp_log(const char *format, ...) {
FILE *file = fopen(VP_LOG, "a");
if (!file) return;
fprintf(file, "[pid %d] ", (int)getpid());
va_list arguments;
va_start(arguments, format);
vfprintf(file, format, arguments);
va_end(arguments);
fputc('\n', file);
fclose(file);
}
typedef void *(*dlsym_fn)(void *, const char *);
typedef jint (*create_vm_fn)(JavaVM **, void **, void *);
static dlsym_fn real_dlsym = NULL;
static create_vm_fn real_create_vm = NULL;
static JavaVM *g_vm = NULL;
static jobject g_editor_loader = NULL; /* 编辑器自己的 ClassLoader 实例 */
static jclass g_panel_class = NULL; /* com.spineview.SceneScan 的全局引用 */
static jmethodID g_on_render = NULL; /* SceneScan.onRenderThread() */
static char g_classes_dir[4096];
static char g_main_class[512];
static pthread_mutex_t g_lock = PTHREAD_MUTEX_INITIALIZER;
typedef void (*swap_fn)(JNIEnv *, jclass, jlong);
static swap_fn real_swap = NULL;
static int g_swap_calls = 0;
/* 编辑器用它自己的类加载器加载了这些保名的 libGDX 控件类(REA 运行时证据),
* 拿它们的 Class 对象就能反推出编辑器类加载器。 */
static const char *kEditorAnchorClasses[] = {
"com/badlogic/gdx/scenes/scene2d/ui/TextButton",
"com/badlogic/gdx/scenes/scene2d/ui/Label",
"com/badlogic/gdx/scenes/scene2d/ui/ImageTextButton",
"com/badlogic/gdx/scenes/scene2d/ui/Window",
"com/badlogic/jglfw/Glfw",
};
/* ---------- 读取我们的 .class ---------- */
static jclass vp_define_file(JNIEnv *env, jobject loader, const char *path) {
FILE *file = fopen(path, "rb");
if (!file) return NULL;
fseek(file, 0, SEEK_END);
long size = ftell(file);
fseek(file, 0, SEEK_SET);
if (size <= 0) { fclose(file); return NULL; }
unsigned char *buffer = malloc((size_t)size);
if (!buffer) { fclose(file); return NULL; }
size_t read = fread(buffer, 1, (size_t)size, file);
fclose(file);
if (read != (size_t)size) { free(buffer); return NULL; }
jclass defined = (*env)->DefineClass(env, NULL, loader, (const jbyte *)buffer, (jsize)size);
free(buffer);
if ((*env)->ExceptionCheck(env)) {
vp_log("DefineClass 抛出异常: %s", path);
(*env)->ExceptionDescribe(env);
(*env)->ExceptionClear(env);
return NULL;
}
return defined;
}
static jclass vp_define_directory(JNIEnv *env, jobject loader, const char *root,
const char *relative, const char *main_internal) {
jclass found = NULL;
char path[4096];
if (relative[0]) snprintf(path, sizeof(path), "%s/%s", root, relative);
else snprintf(path, sizeof(path), "%s", root);
DIR *directory = opendir(path);
if (!directory) return NULL;
struct dirent *entry;
while ((entry = readdir(directory)) != NULL) {
if (strcmp(entry->d_name, ".") == 0 || strcmp(entry->d_name, "..") == 0) continue;
char child_relative[4096];
if (relative[0]) snprintf(child_relative, sizeof(child_relative), "%s/%s", relative, entry->d_name);
else snprintf(child_relative, sizeof(child_relative), "%s", entry->d_name);
char child_path[4096];
snprintf(child_path, sizeof(child_path), "%s/%s", root, child_relative);
struct stat info;
if (stat(child_path, &info) != 0) continue;
if (S_ISDIR(info.st_mode)) {
jclass nested = vp_define_directory(env, loader, root, child_relative, main_internal);
if (nested) found = nested;
continue;
}
size_t length = strlen(child_relative);
if (length <= 6 || strcmp(child_relative + length - 6, ".class") != 0) continue;
char internal_name[4096];
snprintf(internal_name, sizeof(internal_name), "%s", child_relative);
internal_name[length - 6] = '\0';
jclass defined = vp_define_file(env, loader, child_path);
vp_log("define %s -> %s", internal_name, defined ? "ok" : "failed");
if (defined && main_internal && strcmp(internal_name, main_internal) == 0) found = defined;
}
closedir(directory);
return found;
}
/* ---------- 在编辑器自己的类加载器里加载并执行我们的类 ---------- */
/* 把我们的类定义进调用方传入的加载器(也就是编辑器自己的那个),返回入口类。
* 目的是让我们的代码和编辑器处在同一个类命名空间里,能直接按名字找它的类。 */
static jclass VPC_defineIntoLoader(JNIEnv *env, jclass clazz, jobject loader) {
(void)clazz;
if (!env || !loader) return NULL;
jclass defined = vp_define_directory(env, loader, g_classes_dir, "", g_main_class);
vp_log("defineIntoLoader: %s", defined ? "已定义" : "失败");
return defined;
}
static void *vp_run_thread(void *unused) {
(void)unused;
/* 等编辑器把保名的控件类加载进来(启动后几百毫秒到几秒)。 */
jobject loader = NULL;
for (int attempt = 0; attempt < 120 && !loader; attempt++) {
pthread_mutex_lock(&g_lock);
loader = g_editor_loader;
pthread_mutex_unlock(&g_lock);
if (!loader) usleep(200000);
}
JNIEnv *env = NULL;
if (!g_vm || (*g_vm)->AttachCurrentThread(g_vm, (void **)&env, NULL) != JNI_OK || !env) {
vp_log("run: AttachCurrentThread 失败");
return NULL;
}
if (!loader) {
/* 有些进程不触发 dlsym("Java_..."),抓不到编辑器加载器。
* 退一步用系统类加载器:我们的类定义进去照样能跑,只是看不到编辑器的私有类。 */
jclass class_loader_class = (*env)->FindClass(env, "java/lang/ClassLoader");
jmethodID get_system = class_loader_class
? (*env)->GetStaticMethodID(env, class_loader_class, "getSystemClassLoader",
"()Ljava/lang/ClassLoader;") : NULL;
if (get_system) loader = (*env)->CallStaticObjectMethod(env, class_loader_class, get_system);
if ((*env)->ExceptionCheck(env)) { (*env)->ExceptionDescribe(env); (*env)->ExceptionClear(env); loader = NULL; }
vp_log("run: 没抓到编辑器加载器,改用系统类加载器 -> %p", loader);
}
if (!loader) { vp_log("run: 拿不到任何类加载器,放弃"); return NULL; }
{
jclass object_class = (*env)->FindClass(env, "java/lang/Object");
jmethodID to_string = object_class
? (*env)->GetMethodID(env, object_class, "toString", "()Ljava/lang/String;") : NULL;
jstring text = to_string ? (jstring)(*env)->CallObjectMethod(env, loader, to_string) : NULL;
const char *chars = text ? (*env)->GetStringUTFChars(env, text, NULL) : NULL;
vp_log("run: 编辑器类加载器 = %s", chars ? chars : "(未知)");
if (chars) (*env)->ReleaseStringUTFChars(env, text, chars);
}
jclass main_class = vp_define_directory(env, loader, g_classes_dir, "", g_main_class);
if (!main_class) { vp_log("run: 没有定义到入口类 %s", g_main_class); return NULL; }
{
JNINativeMethod methods[] = {
{ "defineIntoLoader", "(Ljava/lang/ClassLoader;)Ljava/lang/Class;", (void *)VPC_defineIntoLoader },
};
if ((*env)->RegisterNatives(env, main_class, methods, 1) != JNI_OK) {
vp_log("run: 注册 defineIntoLoader 失败");
if ((*env)->ExceptionCheck(env)) { (*env)->ExceptionDescribe(env); (*env)->ExceptionClear(env); }
} else {
vp_log("run: 已注册 defineIntoLoader");
}
}
g_panel_class = (jclass)(*env)->NewGlobalRef(env, main_class);
g_on_render = (*env)->GetStaticMethodID(env, main_class, "onRenderThread", "()V");
if (!g_on_render) {
vp_log("run: 找不到 onRenderThread");
if ((*env)->ExceptionCheck(env)) { (*env)->ExceptionDescribe(env); (*env)->ExceptionClear(env); }
}
/* 把进程号写进去:日志里要能区分启动器进程和编辑器进程。 */
{
jfieldID pid_field = (*env)->GetStaticFieldID(env, main_class, "pid", "J");
if (pid_field) {
(*env)->SetStaticLongField(env, main_class, pid_field, (jlong)getpid());
vp_log("run: 已把 pid=%d 写进 Java", (int)getpid());
} else {
if ((*env)->ExceptionCheck(env)) (*env)->ExceptionClear(env);
vp_log("run: 没找到 pid 静态字段");
}
}
jclass string_class = (*env)->FindClass(env, "java/lang/String");
jobjectArray arguments = (*env)->NewObjectArray(env, 0, string_class, NULL);
jmethodID main_method = (*env)->GetStaticMethodID(env, main_class, "main", "([Ljava/lang/String;)V");
if (!main_method) {
vp_log("run: 找不到 main([Ljava/lang/String;)V");
if ((*env)->ExceptionCheck(env)) { (*env)->ExceptionDescribe(env); (*env)->ExceptionClear(env); }
return NULL;
}
vp_log("run: 调用 %s.main", g_main_class);
(*env)->CallStaticVoidMethod(env, main_class, main_method, arguments);
if ((*env)->ExceptionCheck(env)) {
vp_log("run: main 抛出异常");
(*env)->ExceptionDescribe(env);
(*env)->ExceptionClear(env);
}
return NULL;
}
static void vp_start_thread(void) {
pthread_t thread;
if (pthread_create(&thread, NULL, vp_run_thread, NULL) == 0) {
pthread_detach(thread);
vp_log("已启动注入线程");
} else {
vp_log("线程创建失败");
}
}
typedef jint (*get_created_vms_fn)(JavaVM **, jsize, jsize *);
/* 有些进程(例如 3.8.99 的 x86 编辑器)是从启动器进程继承 JVM 的,
* 根本不会调用 JNI_CreateJavaVM —— 钩子自然不会触发。
* 这里改用 JNI_GetCreatedJavaVMs 去取已经存在的 VM。 */
static void *vp_find_existing_vm_thread(void *unused) {
(void)unused;
for (int attempt = 0; attempt < 1200; attempt++) {
if (g_vm) return NULL;
get_created_vms_fn get_vms = real_dlsym
? (get_created_vms_fn)real_dlsym(RTLD_DEFAULT, "JNI_GetCreatedJavaVMs") : NULL;
if (get_vms) {
JavaVM *vm = NULL;
jsize count = 0;
if (get_vms(&vm, 1, &count) == JNI_OK && count > 0 && vm) {
g_vm = vm;
vp_log("用 JNI_GetCreatedJavaVMs 拿到已存在的 JVM,开始注入");
vp_start_thread();
return NULL;
}
}
usleep(500000);
}
return NULL;
}
static void vp_start_vm_watch(void) {
pthread_t thread;
if (pthread_create(&thread, NULL, vp_find_existing_vm_thread, NULL) == 0) pthread_detach(thread);
}
/* ---------- 抓编辑器类加载器 ---------- */
/* 编辑器在给自己绑定原生方法时会 dlsym("Java_...");此刻调用 FindClass,
* 正好落在编辑器自己的类加载上下文里,用它反推编辑器类加载器。只观察,不改返回值。 */
static void vp_try_capture(const char *symbol) {
pthread_mutex_lock(&g_lock);
int need = (g_editor_loader == NULL);
pthread_mutex_unlock(&g_lock);
if (!need || !g_vm) return;
JNIEnv *env = NULL;
if ((*g_vm)->GetEnv(g_vm, (void **)&env, JNI_VERSION_1_6) != JNI_OK || !env) return;
jclass class_class = (*env)->FindClass(env, "java/lang/Class");
jmethodID get_class_loader = class_class
? (*env)->GetMethodID(env, class_class, "getClassLoader", "()Ljava/lang/ClassLoader;") : NULL;
for (size_t index = 0; index < sizeof(kEditorAnchorClasses) / sizeof(kEditorAnchorClasses[0]); index++) {
jclass found = (*env)->FindClass(env, kEditorAnchorClasses[index]);
if (!found) { (*env)->ExceptionClear(env); continue; }
jobject loader = get_class_loader ? (*env)->CallObjectMethod(env, found, get_class_loader) : NULL;
if (!loader || (*env)->ExceptionCheck(env)) {
(*env)->ExceptionClear(env);
/* FindClass 落在 bootstrap/系统加载器上时 getClassLoader() 会是 null,
* 这时改用当前 Java 线程的上下文加载器(编辑器线程的上下文就是编辑器加载器)。 */
jclass thread_class = (*env)->FindClass(env, "java/lang/Thread");
jmethodID current = thread_class
? (*env)->GetStaticMethodID(env, thread_class, "currentThread", "()Ljava/lang/Thread;") : NULL;
jmethodID context = thread_class
? (*env)->GetMethodID(env, thread_class, "getContextClassLoader", "()Ljava/lang/ClassLoader;") : NULL;
if (current && context) {
jobject thread = (*env)->CallStaticObjectMethod(env, thread_class, current);
if (thread) loader = (*env)->CallObjectMethod(env, thread, context);
if ((*env)->ExceptionCheck(env)) { (*env)->ExceptionClear(env); loader = NULL; }
}
}
if (!loader) {
vp_log("锚点 %s 的加载器为空,继续试下一个", kEditorAnchorClasses[index]);
continue;
}
pthread_mutex_lock(&g_lock);
if (!g_editor_loader) {
g_editor_loader = (*env)->NewGlobalRef(env, loader);
char message[512];
snprintf(message, sizeof(message), "捕获编辑器类加载器(锚点 %s,触发符号 %s)",
kEditorAnchorClasses[index], symbol ? symbol : "(null)");
pthread_mutex_unlock(&g_lock);
vp_log("%s", message);
return;
}
pthread_mutex_unlock(&g_lock);
return;
}
}
/* ---------- JVM 创建拦截 ---------- */
static jint vp_create_vm(JavaVM **pvm, void **penv, void *args) {
vp_log("拦截到 JNI_CreateJavaVM");
jint result = real_create_vm ? real_create_vm(pvm, penv, args) : JNI_ERR;
if (result != JNI_OK || !pvm || !*pvm) {
vp_log("JNI_CreateJavaVM 失败: %d", (int)result);
return result;
}
g_vm = *pvm;
vp_log("JVM 已创建,等编辑器类加载器");
vp_start_thread();
return result;
}
/* ---------- _dlsym 重绑定 ---------- */
/* 渲染线程上的钩子:编辑器每帧调 glfwSwapBuffers 时,先问一下 Java 侧要不要装配面板。
* 只在前若干帧回调,之后彻底安静。 */
static void vp_swap(JNIEnv *env, jclass clazz, jlong window) {
const char *no_call = getenv("SPINE_VIEWPANEL_NO_JCALL");
if (g_panel_class && g_on_render && env && g_swap_calls < 20000
&& !(no_call && no_call[0] == '1')) {
g_swap_calls++;
(*env)->CallStaticVoidMethod(env, g_panel_class, g_on_render);
if ((*env)->ExceptionCheck(env)) {
(*env)->ExceptionDescribe(env);
(*env)->ExceptionClear(env);
}
}
if (real_swap) real_swap(env, clazz, window);
}
static void *vp_dlsym(void *handle, const char *name) {
if (name && strcmp(name, "JNI_CreateJavaVM") == 0) {
if (!real_create_vm && real_dlsym) real_create_vm = (create_vm_fn)real_dlsym(handle, name);
vp_log("dlsym: 返回注入版 JNI_CreateJavaVM");
return (void *)vp_create_vm;
}
/* 只截 JNI 方法,不碰 C 函数 glfwSwapBuffers。
* libjglfw 里两个符号都有:C 函数的签名是 (GLFWwindow*),JNI 方法是 (JNIEnv*,jclass,jlong)。
* 截错那个会让包装函数用错签名,直接把渲染搞坏(实测窗口全黑)。 */
if (name && strncmp(name, "Java_", 5) == 0 && strstr(name, "glfwSwapBuffers") != NULL) {
void *real = real_dlsym ? real_dlsym(handle, name) : NULL;
if (!real) {
vp_log("dlsym: %s 拿不到真实实现,不挂钩(否则会没有交换缓冲 → 黑屏)", name);
return NULL;
}
/* 实测:替换这个 JNI 方法会让编辑器整窗黑屏(启动器没事),所以默认不挂。
* 需要时用 SPINE_VIEWPANEL_SWAP=1 显式打开。 */
const char *enable = getenv("SPINE_VIEWPANEL_SWAP");
if (!enable || enable[0] != '1') return real;
if (getenv("SPINE_VIEWPANEL_NO_SWAP")) return real;
if (!real_swap) {
real_swap = (swap_fn)real;
void *via_default = real_dlsym(RTLD_DEFAULT, name);
void *via_next = real_dlsym(RTLD_NEXT, name);
vp_log("dlsym: 截住 %s 真实=%p RTLD_DEFAULT=%p RTLD_NEXT=%p",
name, real, via_default, via_next);
}
return (void *)vp_swap;
}
if (name && strncmp(name, "Java_", 5) == 0) vp_try_capture(name);
return real_dlsym ? real_dlsym(handle, name) : NULL;
}
static void vp_make_writable(void *address, size_t size) {
uintptr_t page = (uintptr_t)address & ~((uintptr_t)getpagesize() - 1);
uintptr_t end = ((uintptr_t)address + size + getpagesize() - 1) & ~((uintptr_t)getpagesize() - 1);
mprotect((void *)page, end - page, PROT_READ | PROT_WRITE);
}
static void vp_rebind(const struct mach_header *header, intptr_t slide) {
if (!header || header->magic != MH_MAGIC_64) return;
const uint8_t *image = (const uint8_t *)header;
const struct load_command *cmd = (const struct load_command *)(image + sizeof(struct mach_header_64));
const struct symtab_command *symtab = NULL;
const struct dysymtab_command *dysymtab = NULL;
const struct segment_command_64 *linkedit = NULL;
for (uint32_t index = 0; index < header->ncmds; index++) {
if (cmd->cmd == LC_SYMTAB) symtab = (const struct symtab_command *)cmd;
else if (cmd->cmd == LC_DYSYMTAB) dysymtab = (const struct dysymtab_command *)cmd;
else if (cmd->cmd == LC_SEGMENT_64) {
const struct segment_command_64 *segment = (const struct segment_command_64 *)cmd;
if (strcmp(segment->segname, "__LINKEDIT") == 0) linkedit = segment;
}
cmd = (const struct load_command *)((const uint8_t *)cmd + cmd->cmdsize);
}
if (!symtab || !dysymtab || !linkedit) return;
const uintptr_t linkedit_base = (uintptr_t)(linkedit->vmaddr - linkedit->fileoff + slide);
const struct nlist_64 *symbols = (const struct nlist_64 *)(linkedit_base + symtab->symoff);
const char *strings = (const char *)(linkedit_base + symtab->stroff);
const uint32_t *indirect = (const uint32_t *)(linkedit_base + dysymtab->indirectsymoff);
cmd = (const struct load_command *)(image + sizeof(struct mach_header_64));
for (uint32_t index = 0; index < header->ncmds; index++) {
if (cmd->cmd != LC_SEGMENT_64) {
cmd = (const struct load_command *)((const uint8_t *)cmd + cmd->cmdsize);
continue;
}
const struct segment_command_64 *segment = (const struct segment_command_64 *)cmd;
const struct section_64 *sections =
(const struct section_64 *)((const uint8_t *)segment + sizeof(struct segment_command_64));
for (uint32_t section_index = 0; section_index < segment->nsects; section_index++) {
const struct section_64 *section = &sections[section_index];
if (strcmp(section->sectname, "__la_symbol_ptr") != 0 &&
strcmp(section->sectname, "__got") != 0) continue;
if (section->reserved1 == 0) continue;
const size_t count = section->size / sizeof(void *);
for (size_t entry = 0; entry < count; entry++) {
uint32_t symbol_index = indirect[section->reserved1 + entry];
if (symbol_index >= symtab->nsyms) continue;
const char *name = strings + symbols[symbol_index].n_un.n_strx;
if (name && strcmp(name, "_dlsym") == 0) {
void **slot = (void **)((uintptr_t)section->addr + slide + entry * sizeof(void *));
vp_make_writable(slot, sizeof(void *));
*slot = vp_dlsym;
}
}
}
cmd = (const struct load_command *)((const uint8_t *)cmd + cmd->cmdsize);
}
}
static void vp_image_added(const struct mach_header *header, intptr_t slide) {
vp_rebind(header, slide);
}
__attribute__((constructor))
static void vp_install(void) {
real_dlsym = (dlsym_fn)dlsym(RTLD_DEFAULT, "dlsym");
const char *dir = getenv("SPINE_VIEWPANEL_CLASSES");
snprintf(g_classes_dir, sizeof(g_classes_dir), "%s",
(dir && dir[0]) ? dir : VP_DEFAULT_CLASSES);
const char *main_class = getenv("SPINE_VIEWPANEL_MAIN");
snprintf(g_main_class, sizeof(g_main_class), "%s",
(main_class && main_class[0]) ? main_class : VP_DEFAULT_MAIN);
vp_log("=== inject.dylib 已载入 pid=%d classes=%s main=%s ===",
(int)getpid(), g_classes_dir, g_main_class);
uint32_t count = _dyld_image_count();
for (uint32_t index = 0; index < count; index++) {
vp_rebind(_dyld_get_image_header(index), _dyld_get_image_vmaddr_slide(index));
}
_dyld_register_func_for_add_image(vp_image_added);
/* 万一这个进程不创建 JVM(继承来的),也要能注入。 */
vp_start_vm_watch();
}
+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);
}