mirror of
https://github.com/hicccc77/WeFlow.git
synced 2026-03-24 23:06:51 +00:00
Merge pull request #425 from xioFelix/fix/image-scan-helper-entitlement
fix: 图片密钥内存扫描通过 image_scan_helper 子进程解决 task_for_pid 权限问题
This commit is contained in:
@@ -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<string | null> {
|
||||
// 优先通过 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<typeof spawn>
|
||||
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<number | null> {
|
||||
const { execSync } = await import('child_process')
|
||||
try {
|
||||
|
||||
10
resources/image_scan_entitlements.plist
Normal file
10
resources/image_scan_entitlements.plist
Normal 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
BIN
resources/image_scan_helper
Executable file
Binary file not shown.
77
resources/image_scan_helper.c
Normal file
77
resources/image_scan_helper.c
Normal 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;
|
||||
}
|
||||
Reference in New Issue
Block a user