mirror of
https://github.com/hicccc77/WeFlow.git
synced 2026-03-24 23:06:51 +00:00
feat: macOS 接入 xkey_helper 并完善密钥获取与诊断
This commit is contained in:
@@ -1,9 +1,12 @@
|
||||
import { app } from 'electron'
|
||||
import { app, shell } from 'electron'
|
||||
import { join } from 'path'
|
||||
import { existsSync, readdirSync, readFileSync, statSync } from 'fs'
|
||||
import { execFile, spawn } from 'child_process'
|
||||
import { promisify } from 'util'
|
||||
|
||||
type DbKeyResult = { success: boolean; key?: string; error?: string; logs?: string[] }
|
||||
type ImageKeyResult = { success: boolean; xorKey?: number; aesKey?: string; error?: string }
|
||||
const execFileAsync = promisify(execFile)
|
||||
|
||||
export class KeyServiceMac {
|
||||
private koffi: any = null
|
||||
@@ -15,6 +18,31 @@ export class KeyServiceMac {
|
||||
private FreeString: any = null
|
||||
private ListWeChatProcesses: any = null
|
||||
|
||||
private getHelperPath(): string {
|
||||
const isPackaged = app.isPackaged
|
||||
const candidates: string[] = []
|
||||
|
||||
if (process.env.WX_KEY_HELPER_PATH) {
|
||||
candidates.push(process.env.WX_KEY_HELPER_PATH)
|
||||
}
|
||||
|
||||
if (isPackaged) {
|
||||
candidates.push(join(process.resourcesPath, 'resources', 'xkey_helper'))
|
||||
candidates.push(join(process.resourcesPath, 'xkey_helper'))
|
||||
} else {
|
||||
const cwd = process.cwd()
|
||||
candidates.push(join(cwd, 'resources', 'xkey_helper'))
|
||||
candidates.push(join(cwd, 'Xkey', 'build', 'xkey_helper'))
|
||||
candidates.push(join(app.getAppPath(), 'resources', 'xkey_helper'))
|
||||
}
|
||||
|
||||
for (const path of candidates) {
|
||||
if (existsSync(path)) return path
|
||||
}
|
||||
|
||||
throw new Error('xkey_helper not found')
|
||||
}
|
||||
|
||||
private getDylibPath(): string {
|
||||
const isPackaged = app.isPackaged
|
||||
const candidates: string[] = []
|
||||
@@ -67,40 +95,42 @@ export class KeyServiceMac {
|
||||
timeoutMs = 60_000,
|
||||
onStatus?: (message: string, level: number) => void
|
||||
): Promise<DbKeyResult> {
|
||||
if (!this.initialized) {
|
||||
await this.initialize()
|
||||
}
|
||||
|
||||
try {
|
||||
onStatus?.('正在获取数据库密钥...', 0)
|
||||
|
||||
const result = this.GetDbKey()
|
||||
console.log('[KeyServiceMac] GetDbKey returned:', result)
|
||||
|
||||
if (!result) {
|
||||
onStatus?.('获取失败:未知错误', 2)
|
||||
return { success: false, error: '未知错误' }
|
||||
let parsed = await this.getDbKeyParsed(timeoutMs, onStatus)
|
||||
console.log('[KeyServiceMac] GetDbKey returned:', parsed.raw)
|
||||
|
||||
// ATTACH_FAILED 时自动走图形化授权,再重试一次
|
||||
if (!parsed.success && parsed.code === 'ATTACH_FAILED') {
|
||||
onStatus?.('检测到调试权限不足,正在请求系统授权...', 0)
|
||||
const permissionOk = await this.enableDebugPermissionWithPrompt()
|
||||
if (permissionOk) {
|
||||
onStatus?.('授权完成,正在重试获取密钥...', 0)
|
||||
parsed = await this.getDbKeyParsed(timeoutMs, onStatus)
|
||||
console.log('[KeyServiceMac] GetDbKey retry returned:', parsed.raw)
|
||||
} else {
|
||||
onStatus?.('已取消系统授权', 2)
|
||||
return { success: false, error: '已取消系统授权' }
|
||||
}
|
||||
}
|
||||
|
||||
// 检查是否是错误信息
|
||||
if (result.startsWith('ERROR:')) {
|
||||
const parts = result.split(':')
|
||||
let errorMsg = '未知错误'
|
||||
|
||||
if (parts[1] === 'PROCESS_NOT_FOUND') {
|
||||
errorMsg = '微信进程未运行'
|
||||
} else if (parts[1] === 'ATTACH_FAILED') {
|
||||
errorMsg = `无法附加到进程 (${parts[2] || ''})\n可能需要授予调试权限:sudo /usr/sbin/DevToolsSecurity -enable`
|
||||
} else if (parts[1] === 'SCAN_FAILED') {
|
||||
errorMsg = '内存扫描失败'
|
||||
}
|
||||
|
||||
if (!parsed.success && parsed.code === 'ATTACH_FAILED') {
|
||||
// DevToolsSecurity 仍不足时,自动拉起开发者工具权限页面
|
||||
await this.openDeveloperToolsPrivacySettings()
|
||||
await this.revealCurrentExecutableInFinder()
|
||||
const msg = `无法附加到微信进程。已打开“开发者工具”设置,并在访达中定位当前运行程序。\n请在“隐私与安全性 -> 开发者工具”点击“+”添加并允许:${process.execPath}`
|
||||
onStatus?.(msg, 2)
|
||||
return { success: false, error: msg }
|
||||
}
|
||||
|
||||
if (!parsed.success) {
|
||||
const errorMsg = this.mapDbKeyErrorMessage(parsed.code, parsed.detail)
|
||||
onStatus?.(errorMsg, 2)
|
||||
return { success: false, error: errorMsg }
|
||||
}
|
||||
|
||||
onStatus?.('密钥获取成功', 1)
|
||||
return { success: true, key: result }
|
||||
return { success: true, key: parsed.key }
|
||||
} catch (e: any) {
|
||||
console.error('[KeyServiceMac] Error:', e)
|
||||
console.error('[KeyServiceMac] Stack:', e.stack)
|
||||
@@ -109,6 +139,213 @@ export class KeyServiceMac {
|
||||
}
|
||||
}
|
||||
|
||||
private parseDbKeyResult(raw: any): { success: boolean; key?: string; code?: string; detail?: string; raw: string } {
|
||||
const text = typeof raw === 'string' ? raw : ''
|
||||
if (!text) return { success: false, code: 'UNKNOWN', raw: text }
|
||||
if (!text.startsWith('ERROR:')) return { success: true, key: text, raw: text }
|
||||
|
||||
const parts = text.split(':')
|
||||
return {
|
||||
success: false,
|
||||
code: parts[1] || 'UNKNOWN',
|
||||
detail: parts.slice(2).join(':') || undefined,
|
||||
raw: text
|
||||
}
|
||||
}
|
||||
|
||||
private async getDbKeyParsed(
|
||||
timeoutMs: number,
|
||||
onStatus?: (message: string, level: number) => void
|
||||
): Promise<{ success: boolean; key?: string; code?: string; detail?: string; raw: string }> {
|
||||
try {
|
||||
const helperResult = await this.getDbKeyByHelper(timeoutMs, onStatus)
|
||||
return this.parseDbKeyResult(helperResult)
|
||||
} catch (e: any) {
|
||||
console.warn('[KeyServiceMac] helper unavailable, fallback to dylib:', e?.message || e)
|
||||
if (!this.initialized) {
|
||||
await this.initialize()
|
||||
}
|
||||
return this.parseDbKeyResult(this.GetDbKey())
|
||||
}
|
||||
}
|
||||
|
||||
private async getDbKeyByHelper(
|
||||
timeoutMs: number,
|
||||
onStatus?: (message: string, level: number) => void
|
||||
): Promise<string> {
|
||||
const helperPath = this.getHelperPath()
|
||||
const waitMs = Math.max(timeoutMs, 30_000)
|
||||
return await new Promise<string>((resolve, reject) => {
|
||||
const child = spawn(helperPath, [String(waitMs)], { stdio: ['ignore', 'pipe', 'pipe'] })
|
||||
let stdout = ''
|
||||
let stderr = ''
|
||||
let stdoutBuf = ''
|
||||
let stderrBuf = ''
|
||||
let settled = false
|
||||
let killTimer: ReturnType<typeof setTimeout> | null = null
|
||||
let pidNotified = false
|
||||
let locatedNotified = false
|
||||
let hookNotified = false
|
||||
|
||||
const done = (fn: () => void) => {
|
||||
if (settled) return
|
||||
settled = true
|
||||
if (killTimer) clearTimeout(killTimer)
|
||||
fn()
|
||||
}
|
||||
|
||||
const processHelperLine = (line: string) => {
|
||||
if (!line) return
|
||||
console.log('[KeyServiceMac][helper][stderr]', line)
|
||||
const pidMatch = line.match(/Selected PID=(\d+)/)
|
||||
if (pidMatch && !pidNotified) {
|
||||
pidNotified = true
|
||||
onStatus?.(`已找到微信进程 PID=${pidMatch[1]},正在定位目标函数...`, 0)
|
||||
}
|
||||
if (!locatedNotified && (line.includes('strict hit=') || line.includes('sink matched by strict semantic signature'))) {
|
||||
locatedNotified = true
|
||||
onStatus?.('已定位到目标函数,正在安装 Hook...', 0)
|
||||
}
|
||||
if (line.includes('hook installed @')) {
|
||||
hookNotified = true
|
||||
onStatus?.('Hook 已安装,等待微信触发密钥调用...', 0)
|
||||
}
|
||||
if (line.includes('[MASTER] hex64=')) {
|
||||
onStatus?.('检测到密钥回调,正在回填...', 0)
|
||||
}
|
||||
}
|
||||
|
||||
child.stdout.on('data', (chunk: Buffer | string) => {
|
||||
const data = chunk.toString()
|
||||
stdout += data
|
||||
stdoutBuf += data
|
||||
const parts = stdoutBuf.split(/\r?\n/)
|
||||
stdoutBuf = parts.pop() || ''
|
||||
})
|
||||
|
||||
child.stderr.on('data', (chunk: Buffer | string) => {
|
||||
const data = chunk.toString()
|
||||
stderr += data
|
||||
stderrBuf += data
|
||||
const parts = stderrBuf.split(/\r?\n/)
|
||||
stderrBuf = parts.pop() || ''
|
||||
for (const line of parts) processHelperLine(line.trim())
|
||||
})
|
||||
|
||||
child.on('error', (err) => {
|
||||
done(() => reject(err))
|
||||
})
|
||||
|
||||
child.on('close', () => {
|
||||
if (stderrBuf.trim()) processHelperLine(stderrBuf.trim())
|
||||
|
||||
const lines = stdout.split(/\r?\n/).map(x => x.trim()).filter(Boolean)
|
||||
const last = lines[lines.length - 1]
|
||||
if (!last) {
|
||||
done(() => reject(new Error(stderr.trim() || 'helper returned empty output')))
|
||||
return
|
||||
}
|
||||
|
||||
let payload: any
|
||||
try {
|
||||
payload = JSON.parse(last)
|
||||
} catch {
|
||||
done(() => reject(new Error('helper returned invalid json: ' + last)))
|
||||
return
|
||||
}
|
||||
|
||||
if (payload?.success === true && typeof payload?.key === 'string') {
|
||||
if (!hookNotified) {
|
||||
onStatus?.('Hook 已触发,正在回填密钥...', 0)
|
||||
}
|
||||
done(() => resolve(payload.key))
|
||||
return
|
||||
}
|
||||
if (typeof payload?.result === 'string') {
|
||||
done(() => resolve(payload.result))
|
||||
return
|
||||
}
|
||||
done(() => reject(new Error('helper json missing key/result')))
|
||||
})
|
||||
|
||||
killTimer = setTimeout(() => {
|
||||
try { child.kill('SIGTERM') } catch { }
|
||||
done(() => reject(new Error(`helper timeout after ${waitMs}ms`)))
|
||||
}, waitMs + 10_000)
|
||||
})
|
||||
}
|
||||
|
||||
private mapDbKeyErrorMessage(code?: string, detail?: string): string {
|
||||
if (code === 'PROCESS_NOT_FOUND') return '微信进程未运行'
|
||||
if (code === 'ATTACH_FAILED') {
|
||||
const isDevElectron = process.execPath.includes('/node_modules/electron/')
|
||||
if ((detail || '').includes('task_for_pid:5')) {
|
||||
if (isDevElectron) {
|
||||
return `无法附加到微信进程(task_for_pid 被拒绝)。当前为开发环境 Electron:${process.execPath}\n建议使用打包后的 WeFlow.app(已携带调试 entitlements)再重试。`
|
||||
}
|
||||
return '无法附加到微信进程(task_for_pid 被系统拒绝)。请确认当前运行程序已正确签名并包含调试 entitlements。'
|
||||
}
|
||||
return `无法附加到进程 (${detail || ''})`
|
||||
}
|
||||
if (code === 'FRIDA_FAILED') {
|
||||
if ((detail || '').includes('FRIDA_TIMEOUT')) {
|
||||
return '定位已成功但在等待时间内未捕获到密钥调用。请保持微信前台并进行一次会话/数据库访问后重试。'
|
||||
}
|
||||
return `Frida 语义定位失败 (${detail || ''})`
|
||||
}
|
||||
if (code === 'HOOK_FAILED') {
|
||||
if ((detail || '').includes('HOOK_TIMEOUT')) {
|
||||
return 'Hook 已安装,但在等待时间内未触发目标函数。请保持微信前台并执行一次会话/数据库访问后重试。'
|
||||
}
|
||||
if ((detail || '').includes('attach_wait_timeout')) {
|
||||
return '附加调试器超时,未能进入 Hook 阶段。请确认微信处于可交互状态并重试。'
|
||||
}
|
||||
return `原生 Hook 失败 (${detail || ''})`
|
||||
}
|
||||
if (code === 'HOOK_TARGET_ONLY') {
|
||||
return `已定位到目标函数地址(${detail || ''}),但当前原生 C++ 仅完成定位,尚未完成远程 Hook 回调取 key 流程。`
|
||||
}
|
||||
if (code === 'SCAN_FAILED') return '内存扫描失败'
|
||||
return '未知错误'
|
||||
}
|
||||
|
||||
private async enableDebugPermissionWithPrompt(): Promise<boolean> {
|
||||
const script = [
|
||||
'do shell script "/usr/sbin/DevToolsSecurity -enable" with administrator privileges'
|
||||
]
|
||||
|
||||
try {
|
||||
await execFileAsync('osascript', script.flatMap(line => ['-e', line]), {
|
||||
timeout: 30_000
|
||||
})
|
||||
return true
|
||||
} catch (e: any) {
|
||||
const msg = `${e?.stderr || ''}\n${e?.message || ''}`
|
||||
const cancelled = msg.includes('User canceled') || msg.includes('(-128)')
|
||||
if (!cancelled) {
|
||||
console.error('[KeyServiceMac] enableDebugPermissionWithPrompt failed:', msg)
|
||||
}
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
private async openDeveloperToolsPrivacySettings(): Promise<void> {
|
||||
const url = 'x-apple.systempreferences:com.apple.preference.security?Privacy_DevTools'
|
||||
try {
|
||||
await shell.openExternal(url)
|
||||
} catch (e) {
|
||||
console.error('[KeyServiceMac] Failed to open settings page:', e)
|
||||
}
|
||||
}
|
||||
|
||||
private async revealCurrentExecutableInFinder(): Promise<void> {
|
||||
try {
|
||||
shell.showItemInFolder(process.execPath)
|
||||
} catch (e) {
|
||||
console.error('[KeyServiceMac] Failed to reveal executable in Finder:', e)
|
||||
}
|
||||
}
|
||||
|
||||
async autoGetImageKey(
|
||||
accountPath?: string,
|
||||
onStatus?: (message: string) => void,
|
||||
|
||||
Reference in New Issue
Block a user