diff --git a/electron/services/keyServiceMac.ts b/electron/services/keyServiceMac.ts index 4392c81..0c9cd02 100644 --- a/electron/services/keyServiceMac.ts +++ b/electron/services/keyServiceMac.ts @@ -23,6 +23,7 @@ export class KeyServiceMac { private machVmRegion: any = null private machVmReadOverwrite: any = null private machPortDeallocate: any = null + private _needsElevation = false private getHelperPath(): string { const isPackaged = app.isPackaged @@ -49,6 +50,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[] = [] @@ -614,6 +635,32 @@ export class KeyServiceMac { ciphertext: Buffer, onProgress?: (message: string) => void ): Promise { + // 优先通过 image_scan_helper 子进程调用 + try { + const helperPath = this.getImageScanHelperPath() + const ciphertextHex = ciphertext.toString('hex') + + // 1) 直接运行 helper(有正式签名的 debugger entitlement 时可用) + if (!this._needsElevation) { + const direct = await this._spawnScanHelper(helperPath, pid, ciphertextHex, false) + if (direct.key) return direct.key + if (direct.permissionError) { + console.warn('[KeyServiceMac] task_for_pid 权限不足,切换到 osascript 提权模式') + this._needsElevation = true + onProgress?.('需要管理员权限,请在弹出的对话框中输入密码...') + } + } + + // 2) 通过 osascript 以管理员权限运行 helper(SIP 下 ad-hoc 签名无法获取 task_for_pid) + if (this._needsElevation) { + const elevated = await this._spawnScanHelper(helperPath, pid, ciphertextHex, true) + if (elevated.key) return elevated.key + } + } catch (e: any) { + console.warn('[KeyServiceMac] image_scan_helper unavailable, fallback to Mach API:', e?.message) + } + + // fallback: 直接通过 Mach API 扫描内存(Electron 进程可能没有 task_for_pid 权限) if (!this.ensureMachApis()) return null const VM_PROT_READ = 0x1 @@ -708,6 +755,45 @@ export class KeyServiceMac { } } + private _spawnScanHelper( + helperPath: string, pid: number, ciphertextHex: string, elevated: boolean + ): Promise<{ key: string | null; permissionError: boolean }> { + return new Promise((resolve, reject) => { + let child: ReturnType + if (elevated) { + const shellCmd = `'${helperPath}' ${pid} ${ciphertextHex}` + child = spawn('osascript', ['-e', `do shell script ${JSON.stringify(shellCmd)} with administrator privileges`], + { stdio: ['ignore', 'pipe', 'pipe'] }) + } else { + child = spawn(helperPath, [String(pid), ciphertextHex], { stdio: ['ignore', 'pipe', 'pipe'] }) + } + const tag = elevated ? '[image_scan_helper:elevated]' : '[image_scan_helper]' + let stdout = '', stderr = '' + child.stdout.on('data', (chunk: Buffer) => { stdout += chunk.toString() }) + child.stderr.on('data', (chunk: Buffer) => { + stderr += chunk.toString() + console.log(tag, chunk.toString().trim()) + }) + child.on('error', reject) + child.on('close', () => { + const permissionError = !elevated && stderr.includes('task_for_pid failed') + try { + const lines = stdout.split(/\r?\n/).map(x => x.trim()).filter(Boolean) + const last = lines[lines.length - 1] + if (!last) { resolve({ key: null, permissionError }); return } + const payload = JSON.parse(last) + resolve({ + key: payload?.success && payload?.aesKey ? payload.aesKey : null, + permissionError + }) + } catch { + resolve({ key: null, permissionError }) + } + }) + setTimeout(() => { try { child.kill('SIGTERM') } catch {} }, elevated ? 60_000 : 30_000) + }) + } + private async findWeChatPid(): Promise { const { execSync } = await import('child_process') try { 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; +}