diff --git a/electron/services/keyServiceMac.ts b/electron/services/keyServiceMac.ts index 764337f..8e1e95b 100644 --- a/electron/services/keyServiceMac.ts +++ b/electron/services/keyServiceMac.ts @@ -43,6 +43,26 @@ export class KeyServiceMac { throw new Error('xkey_helper not found') } + private getImageScanHelperPath(): string { + const isPackaged = app.isPackaged + const candidates: string[] = [] + + if (isPackaged) { + candidates.push(join(process.resourcesPath, 'resources', 'image_scan_helper')) + candidates.push(join(process.resourcesPath, 'image_scan_helper')) + } else { + const cwd = process.cwd() + candidates.push(join(cwd, 'resources', 'image_scan_helper')) + candidates.push(join(app.getAppPath(), 'resources', 'image_scan_helper')) + } + + for (const path of candidates) { + if (existsSync(path)) return path + } + + throw new Error('image_scan_helper not found') + } + private getDylibPath(): string { const isPackaged = app.isPackaged const candidates: string[] = [] @@ -463,6 +483,36 @@ export class KeyServiceMac { private async _scanMemoryForAesKey(pid: number, ciphertext: Buffer): Promise { const ciphertextHex = ciphertext.toString('hex') + + // 优先通过 image_scan_helper 子进程调用(有 debugger entitlement) + try { + const helperPath = this.getImageScanHelperPath() + const result = await new Promise((resolve, reject) => { + const child = spawn(helperPath, [String(pid), ciphertextHex], { stdio: ['ignore', 'pipe', 'pipe'] }) + let stdout = '' + child.stdout.on('data', (chunk: Buffer) => { stdout += chunk.toString() }) + child.stderr.on('data', (chunk: Buffer) => { console.log('[image_scan_helper]', chunk.toString().trim()) }) + child.on('error', reject) + child.on('close', () => { + try { + const lines = stdout.split(/\r?\n/).map(x => x.trim()).filter(Boolean) + const last = lines[lines.length - 1] + if (!last) { resolve(null); return } + const payload = JSON.parse(last) + resolve(payload?.success && payload?.aesKey ? payload.aesKey : null) + } catch { + resolve(null) + } + }) + setTimeout(() => { try { child.kill('SIGTERM') } catch {} }, 30_000) + }) + return result + } catch (e: any) { + console.warn('[KeyServiceMac] image_scan_helper unavailable, fallback to dylib:', e?.message) + } + + // fallback: 直接调 dylib(Electron 进程可能没有 task_for_pid 权限) + if (!this.initialized) await this.initialize() const aesKey = this.ScanMemoryForImageKey(pid, ciphertextHex) return aesKey || null } diff --git a/resources/image_scan_entitlements.plist b/resources/image_scan_entitlements.plist new file mode 100644 index 0000000..023065e --- /dev/null +++ b/resources/image_scan_entitlements.plist @@ -0,0 +1,10 @@ + + + + + com.apple.security.cs.debugger + + com.apple.security.cs.allow-unsigned-executable-memory + + + diff --git a/resources/image_scan_helper b/resources/image_scan_helper new file mode 100755 index 0000000..b10856d Binary files /dev/null and b/resources/image_scan_helper differ diff --git a/resources/image_scan_helper.c b/resources/image_scan_helper.c new file mode 100644 index 0000000..39bcf27 --- /dev/null +++ b/resources/image_scan_helper.c @@ -0,0 +1,77 @@ +/* + * image_scan_helper - 轻量包装程序 + * 加载 libwx_key.dylib 并调用 ScanMemoryForImageKey + * 用法: image_scan_helper + * 输出: JSON {"success":true,"aesKey":"..."} 或 {"success":false,"error":"..."} + */ +#include +#include +#include +#include +#include +#include + +typedef const char* (*ScanMemoryForImageKeyFn)(int pid, const char* ciphertext); +typedef void (*FreeStringFn)(const char* str); + +int main(int argc, char* argv[]) { + if (argc != 3) { + fprintf(stderr, "Usage: %s \n", argv[0]); + printf("{\"success\":false,\"error\":\"invalid arguments\"}\n"); + return 1; + } + + int pid = atoi(argv[1]); + const char* ciphertext_hex = argv[2]; + + if (pid <= 0) { + printf("{\"success\":false,\"error\":\"invalid pid\"}\n"); + return 1; + } + + /* 定位 dylib: 与自身同目录下的 libwx_key.dylib */ + char exe_path[4096]; + uint32_t size = sizeof(exe_path); + if (_NSGetExecutablePath(exe_path, &size) != 0) { + printf("{\"success\":false,\"error\":\"cannot get executable path\"}\n"); + return 1; + } + + char* dir = dirname(exe_path); + char dylib_path[4096]; + snprintf(dylib_path, sizeof(dylib_path), "%s/libwx_key.dylib", dir); + + void* handle = dlopen(dylib_path, RTLD_LAZY); + if (!handle) { + printf("{\"success\":false,\"error\":\"dlopen failed: %s\"}\n", dlerror()); + return 1; + } + + ScanMemoryForImageKeyFn scan_fn = (ScanMemoryForImageKeyFn)dlsym(handle, "ScanMemoryForImageKey"); + if (!scan_fn) { + printf("{\"success\":false,\"error\":\"symbol not found: ScanMemoryForImageKey\"}\n"); + dlclose(handle); + return 1; + } + + FreeStringFn free_fn = (FreeStringFn)dlsym(handle, "FreeString"); + + fprintf(stderr, "[image_scan_helper] calling ScanMemoryForImageKey(pid=%d, ciphertext=%s)\n", pid, ciphertext_hex); + + const char* result = scan_fn(pid, ciphertext_hex); + + if (result && strlen(result) > 0) { + /* 检查是否是错误 */ + if (strncmp(result, "ERROR", 5) == 0) { + printf("{\"success\":false,\"error\":\"%s\"}\n", result); + } else { + printf("{\"success\":true,\"aesKey\":\"%s\"}\n", result); + } + if (free_fn) free_fn(result); + } else { + printf("{\"success\":false,\"error\":\"no key found\"}\n"); + } + + dlclose(handle); + return 0; +}