From bff9e8709685a6d7b34ff3cb3d07f8f5895d2d14 Mon Sep 17 00:00:00 2001 From: Felix Date: Fri, 13 Mar 2026 15:18:52 +1100 Subject: [PATCH 1/2] =?UTF-8?q?fix:=20=E5=9B=BE=E7=89=87=E5=AF=86=E9=92=A5?= =?UTF-8?q?=E5=86=85=E5=AD=98=E6=89=AB=E6=8F=8F=E9=80=9A=E8=BF=87=E5=AD=90?= =?UTF-8?q?=E8=BF=9B=E7=A8=8B=E8=B0=83=E7=94=A8=E8=A7=A3=E5=86=B3=20task?= =?UTF-8?q?=5Ffor=5Fpid=20=E6=9D=83=E9=99=90=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Electron 进程缺少 com.apple.security.cs.debugger entitlement, 导致 ScanMemoryForImageKey 中的 task_for_pid 调用失败(kr=5)。 新增 image_scan_helper 子进程包装程序(与 xkey_helper 方案一致): - 新建 resources/image_scan_helper.c:dlopen libwx_key.dylib 并调用 ScanMemoryForImageKey,通过 JSON stdout 返回结果 - 新建 resources/image_scan_entitlements.plist:包含 debugger 和 allow-unsigned-executable-memory entitlements - 编译为 universal binary(x86_64 + arm64)并 ad-hoc 签名 - 修改 keyServiceMac.ts _scanMemoryForAesKey():优先 spawn image_scan_helper 子进程,失败时 fallback 到直接调 dylib Co-Authored-By: Claude Opus 4.6 --- electron/services/keyServiceMac.ts | 50 +++++++++++++++ resources/image_scan_entitlements.plist | 10 +++ resources/image_scan_helper | Bin 0 -> 101920 bytes resources/image_scan_helper.c | 77 ++++++++++++++++++++++++ 4 files changed, 137 insertions(+) create mode 100644 resources/image_scan_entitlements.plist create mode 100755 resources/image_scan_helper create mode 100644 resources/image_scan_helper.c 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 0000000000000000000000000000000000000000..b10856d9cb0b8599471fd1ac640dff381320b2f6 GIT binary patch literal 101920 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 literal 0 HcmV?d00001 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; +} From 5acd3d86c8f08c5bee5d666249f51038f234123c Mon Sep 17 00:00:00 2001 From: Felix Date: Fri, 13 Mar 2026 23:22:27 +1100 Subject: [PATCH 2/2] =?UTF-8?q?fix:=20image=5Fscan=5Fhelper=20=E8=87=AA?= =?UTF-8?q?=E5=8A=A8=E6=A3=80=E6=B5=8B=20task=5Ffor=5Fpid=20=E6=9D=83?= =?UTF-8?q?=E9=99=90=E4=B8=8D=E8=B6=B3=E6=97=B6=E9=80=9A=E8=BF=87=20osascr?= =?UTF-8?q?ipt=20=E6=8F=90=E6=9D=83=E8=BF=90=E8=A1=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit macOS SIP 下 ad-hoc 签名的 debugger entitlement 不被信任,导致 image_scan_helper 调用 task_for_pid 返回 kr=5。现在先尝试直接运行, 检测到权限错误后自动切换到 osascript with administrator privileges 方式运行 helper,后续调用跳过直接运行直接走提权路径。 Co-Authored-By: Claude Opus 4.6 --- electron/services/keyServiceMac.ts | 79 ++++++++++++++++++++++-------- 1 file changed, 58 insertions(+), 21 deletions(-) diff --git a/electron/services/keyServiceMac.ts b/electron/services/keyServiceMac.ts index 65394ee..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 @@ -634,30 +635,27 @@ export class KeyServiceMac { ciphertext: Buffer, onProgress?: (message: string) => void ): Promise { - // 优先通过 image_scan_helper 子进程调用(有 debugger entitlement) + // 优先通过 image_scan_helper 子进程调用 try { const helperPath = this.getImageScanHelperPath() const ciphertextHex = ciphertext.toString('hex') - 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) - }) - if (result) return result + + // 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) } @@ -757,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 {