/* 内存扫描:找进程里已经解密成明文的 class 字节。 * * 为什么走这条路:实测 Frida 的内联挂钩(Interceptor.attach 到 JVM_DefineClass*) * 会让这套运行时直接 SIGSEGV,attach 又太晚(类早加载完了)。 * 扫描内存不改任何代码,风险最低。 */ #include #include #include #include #include #include #include #include #include #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); }