fix: 图片密钥内存扫描通过子进程调用解决 task_for_pid 权限问题

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 <noreply@anthropic.com>
This commit is contained in:
Felix
2026-03-13 15:18:52 +11:00
parent 19ab4409a3
commit bff9e87096
4 changed files with 137 additions and 0 deletions

View File

@@ -43,6 +43,26 @@ export class KeyServiceMac {
throw new Error('xkey_helper not found') 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 { private getDylibPath(): string {
const isPackaged = app.isPackaged const isPackaged = app.isPackaged
const candidates: string[] = [] const candidates: string[] = []
@@ -463,6 +483,36 @@ export class KeyServiceMac {
private async _scanMemoryForAesKey(pid: number, ciphertext: Buffer): Promise<string | null> { private async _scanMemoryForAesKey(pid: number, ciphertext: Buffer): Promise<string | null> {
const ciphertextHex = ciphertext.toString('hex') const ciphertextHex = ciphertext.toString('hex')
// 优先通过 image_scan_helper 子进程调用(有 debugger entitlement
try {
const helperPath = this.getImageScanHelperPath()
const result = await new Promise<string | null>((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: 直接调 dylibElectron 进程可能没有 task_for_pid 权限)
if (!this.initialized) await this.initialize()
const aesKey = this.ScanMemoryForImageKey(pid, ciphertextHex) const aesKey = this.ScanMemoryForImageKey(pid, ciphertextHex)
return aesKey || null return aesKey || null
} }

View File

@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.security.cs.debugger</key>
<true/>
<key>com.apple.security.cs.allow-unsigned-executable-memory</key>
<true/>
</dict>
</plist>

BIN
resources/image_scan_helper Executable file

Binary file not shown.

View File

@@ -0,0 +1,77 @@
/*
* image_scan_helper - 轻量包装程序
* 加载 libwx_key.dylib 并调用 ScanMemoryForImageKey
* 用法: image_scan_helper <pid> <ciphertext_hex>
* 输出: JSON {"success":true,"aesKey":"..."} 或 {"success":false,"error":"..."}
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <dlfcn.h>
#include <libgen.h>
#include <mach-o/dyld.h>
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 <pid> <ciphertext_hex>\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;
}