diff --git a/electron/services/keyService.ts b/electron/services/keyService.ts index 34e7bd0..f3c2d2b 100644 --- a/electron/services/keyService.ts +++ b/electron/services/keyService.ts @@ -3,6 +3,7 @@ import { join, dirname, basename } from 'path' import { existsSync, readdirSync, readFileSync, statSync, copyFileSync, mkdirSync } from 'fs' import { execFile, spawn } from 'child_process' import { promisify } from 'util' +import { Worker } from 'worker_threads' import crypto from 'crypto' import os from 'os' @@ -26,12 +27,9 @@ export class KeyService { private user32: any = null private advapi32: any = null - // Kernel32 + // Kernel32 (已移除内存扫描相关的 API) private OpenProcess: any = null private CloseHandle: any = null - private VirtualQueryEx: any = null - private ReadProcessMemory: any = null - private MEMORY_BASIC_INFORMATION: any = null private TerminateProcess: any = null private QueryFullProcessImageNameW: any = null @@ -62,50 +60,33 @@ export class KeyService { private getDllPath(): string { const isPackaged = typeof app !== 'undefined' && app ? app.isPackaged : process.env.NODE_ENV === 'production' - - // 候选路径列表 const candidates: string[] = [] - // 1. 显式环境变量 (最高优先级) if (process.env.WX_KEY_DLL_PATH) { candidates.push(process.env.WX_KEY_DLL_PATH) } if (isPackaged) { - // 生产环境: 通常在 resources 目录下,但也可能直接在 resources 根目录 candidates.push(join(process.resourcesPath, 'resources', 'wx_key.dll')) candidates.push(join(process.resourcesPath, 'wx_key.dll')) } else { - // 开发环境 const cwd = process.cwd() candidates.push(join(cwd, 'resources', 'wx_key.dll')) candidates.push(join(app.getAppPath(), 'resources', 'wx_key.dll')) } - // 检查并返回第一个存在的路径 for (const path of candidates) { - if (existsSync(path)) { - return path - } + if (existsSync(path)) return path } - // 如果都没找到,返回最可能的路径以便报错信息有参考 return candidates[0] } - // 检查路径是否为 UNC 路径或网络路径 private isNetworkPath(path: string): boolean { - // UNC 路径以 \\ 开头 - if (path.startsWith('\\\\')) { - return true - } - // 检查是否为网络映射驱动器(简化检测:A: 表示驱动器) - // 注意:这是一个启发式检测,更准确的方式需要调用 GetDriveType Windows API - // 但对于大多数 VM 共享场景,UNC 路径检测已足够 + if (path.startsWith('\\\\')) return true return false } - // 将 DLL 复制到本地临时目录 private localizeNetworkDll(originalPath: string): string { try { const tempDir = join(os.tmpdir(), 'weflow_dll_cache') @@ -113,20 +94,12 @@ export class KeyService { mkdirSync(tempDir, { recursive: true }) } const localPath = join(tempDir, 'wx_key.dll') + if (existsSync(localPath)) return localPath - // 检查是否已经有本地副本,如果有就使用它 - if (existsSync(localPath)) { - - return localPath - } - - copyFileSync(originalPath, localPath) - return localPath } catch (e) { console.error('DLL 本地化失败:', e) - // 如果本地化失败,返回原路径 return originalPath } } @@ -144,9 +117,7 @@ export class KeyService { return false } - // 检查是否为网络路径,如果是则本地化 if (this.isNetworkPath(dllPath)) { - dllPath = this.localizeNetworkDll(dllPath) } @@ -161,13 +132,7 @@ export class KeyService { return true } catch (e) { const errorMsg = e instanceof Error ? e.message : String(e) - const errorStack = e instanceof Error ? e.stack : '' - console.error(`加载 wx_key.dll 失败`) - console.error(` 路径: ${dllPath}`) - console.error(` 错误: ${errorMsg}`) - if (errorStack) { - console.error(` 堆栈: ${errorStack}`) - } + console.error(`加载 wx_key.dll 失败\n 路径: ${dllPath}\n 错误: ${errorMsg}`) return false } } @@ -182,24 +147,10 @@ export class KeyService { this.koffi = require('koffi') this.kernel32 = this.koffi.load('kernel32.dll') - const HANDLE = this.koffi.pointer('HANDLE', this.koffi.opaque()) - this.MEMORY_BASIC_INFORMATION = this.koffi.struct('MEMORY_BASIC_INFORMATION', { - BaseAddress: 'uint64', - AllocationBase: 'uint64', - AllocationProtect: 'uint32', - RegionSize: 'uint64', - State: 'uint32', - Protect: 'uint32', - Type: 'uint32' - }) - - // Use explicit definitions to avoid parser issues this.OpenProcess = this.kernel32.func('OpenProcess', 'HANDLE', ['uint32', 'bool', 'uint32']) this.CloseHandle = this.kernel32.func('CloseHandle', 'bool', ['HANDLE']) this.TerminateProcess = this.kernel32.func('TerminateProcess', 'bool', ['HANDLE', 'uint32']) this.QueryFullProcessImageNameW = this.kernel32.func('QueryFullProcessImageNameW', 'bool', ['HANDLE', 'uint32', this.koffi.out('uint16*'), this.koffi.out('uint32*')]) - this.VirtualQueryEx = this.kernel32.func('VirtualQueryEx', 'uint64', ['HANDLE', 'uint64', this.koffi.out(this.koffi.pointer(this.MEMORY_BASIC_INFORMATION)), 'uint64']) - this.ReadProcessMemory = this.kernel32.func('ReadProcessMemory', 'bool', ['HANDLE', 'uint64', 'void*', 'uint64', this.koffi.out(this.koffi.pointer('uint64'))]) return true } catch (e) { @@ -219,15 +170,12 @@ export class KeyService { this.koffi = require('koffi') this.user32 = this.koffi.load('user32.dll') - // Callbacks - // Define the prototype and its pointer type const WNDENUMPROC = this.koffi.proto('bool __stdcall (void *hWnd, intptr_t lParam)') this.WNDENUMPROC_PTR = this.koffi.pointer(WNDENUMPROC) this.EnumWindows = this.user32.func('EnumWindows', 'bool', [this.WNDENUMPROC_PTR, 'intptr_t']) this.EnumChildWindows = this.user32.func('EnumChildWindows', 'bool', ['void*', this.WNDENUMPROC_PTR, 'intptr_t']) this.PostMessageW = this.user32.func('PostMessageW', 'bool', ['void*', 'uint32', 'uintptr_t', 'intptr_t']) - this.GetWindowTextW = this.user32.func('GetWindowTextW', 'int', ['void*', this.koffi.out('uint16*'), 'int']) this.GetWindowTextLengthW = this.user32.func('GetWindowTextLengthW', 'int', ['void*']) this.GetClassNameW = this.user32.func('GetClassNameW', 'int', ['void*', this.koffi.out('uint16*'), 'int']) @@ -247,8 +195,6 @@ export class KeyService { this.koffi = require('koffi') this.advapi32 = this.koffi.load('advapi32.dll') - // Types - // Use intptr_t for HKEY to match system architecture (64-bit safe) const HKEY = this.koffi.alias('HKEY', 'intptr_t') const HKEY_PTR = this.koffi.pointer(HKEY) @@ -274,27 +220,19 @@ export class KeyService { // --- WeChat Process & Path Finding --- - // Helper to read simple registry string private readRegistryString(rootKey: number, subKey: string, valueName: string): string | null { if (!this.ensureAdvapi32()) return null - - // Convert strings to UTF-16 buffers const subKeyBuf = Buffer.from(subKey + '\0', 'ucs2') const valueNameBuf = valueName ? Buffer.from(valueName + '\0', 'ucs2') : null + const phkResult = Buffer.alloc(8) - const phkResult = Buffer.alloc(8) // Pointer size (64-bit safe) - - if (this.RegOpenKeyExW(rootKey, subKeyBuf, 0, this.KEY_READ, phkResult) !== this.ERROR_SUCCESS) { - return null - } + if (this.RegOpenKeyExW(rootKey, subKeyBuf, 0, this.KEY_READ, phkResult) !== this.ERROR_SUCCESS) return null const hKey = this.koffi.decode(phkResult, 'uintptr_t') try { const lpcbData = Buffer.alloc(4) - lpcbData.writeUInt32LE(0, 0) // First call to get size? No, RegQueryValueExW expects initialized size or null to get size. - // Usually we call it twice or just provide a big buffer. - // Let's call twice. + lpcbData.writeUInt32LE(0, 0) let ret = this.RegQueryValueExW(hKey, valueNameBuf, null, null, null, lpcbData) if (ret !== this.ERROR_SUCCESS) return null @@ -306,7 +244,6 @@ export class KeyService { ret = this.RegQueryValueExW(hKey, valueNameBuf, null, null, dataBuf, lpcbData) if (ret !== this.ERROR_SUCCESS) return null - // Read UTF-16 string (remove null terminator) let str = dataBuf.toString('ucs2') if (str.endsWith('\0')) str = str.slice(0, -1) return str @@ -317,7 +254,6 @@ export class KeyService { private async getProcessExecutablePath(pid: number): Promise { if (!this.ensureKernel32()) return null - // 0x1000 = PROCESS_QUERY_LIMITED_INFORMATION const hProcess = this.OpenProcess(0x1000, false, pid) if (!hProcess) return null @@ -341,33 +277,21 @@ export class KeyService { } private async findWeChatInstallPath(): Promise { - // 0. 优先尝试获取正在运行的微信进程路径 try { const pid = await this.findWeChatPid() if (pid) { const runPath = await this.getProcessExecutablePath(pid) - if (runPath && existsSync(runPath)) { - - return runPath - } + if (runPath && existsSync(runPath)) return runPath } } catch (e) { console.error('尝试获取运行中微信路径失败:', e) } - // 1. Registry - Uninstall Keys const uninstallKeys = [ 'SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Uninstall', 'SOFTWARE\\WOW6432Node\\Microsoft\\Windows\\CurrentVersion\\Uninstall' ] const roots = [this.HKEY_LOCAL_MACHINE, this.HKEY_CURRENT_USER] - - // NOTE: Scanning subkeys in registry via Koffi is tedious (RegEnumKeyEx). - // Simplified strategy: Check common known registry keys first, then fallback to common paths. - // wx_key searches *all* subkeys of Uninstall, which is robust but complex to port quickly. - // Let's rely on specific Tencent keys first. - - // 2. Tencent specific keys const tencentKeys = [ 'Software\\Tencent\\WeChat', 'Software\\WOW6432Node\\Tencent\\WeChat', @@ -382,16 +306,13 @@ export class KeyService { } } - // 3. Uninstall key exact match (sometimes works) for (const root of roots) { for (const parent of uninstallKeys) { - // Try WeChat specific subkey const path = this.readRegistryString(root, parent + '\\WeChat', 'InstallLocation') if (path && existsSync(join(path, 'Weixin.exe'))) return join(path, 'Weixin.exe') } } - // 4. Common Paths const drives = ['C', 'D', 'E', 'F'] const commonPaths = [ 'Program Files\\Tencent\\WeChat\\WeChat.exe', @@ -424,7 +345,6 @@ export class KeyService { } return null } catch (e) { - console.error(`获取进程失败 (${imageName}):`, e) return null } } @@ -435,7 +355,6 @@ export class KeyService { const pid = await this.findPidByImageName(name) if (pid) return pid } - const fallbackPid = await this.waitForWeChatWindow(5000) return fallbackPid ?? null } @@ -486,14 +405,11 @@ export class KeyService { try { await execFileAsync('taskkill', ['/F', '/T', '/IM', 'Weixin.exe']) await execFileAsync('taskkill', ['/F', '/T', '/IM', 'WeChat.exe']) - } catch (e) { - // Ignore if not found - } + } catch (e) { } return await this.waitForWeChatExit(5000) } - // --- Window Detection --- private getWindowTitle(hWnd: any): string { @@ -574,17 +490,12 @@ export class KeyService { for (const child of children) { const normalizedTitle = child.title.replace(/\s+/g, '') if (normalizedTitle) { - if (readyTexts.some(marker => normalizedTitle.includes(marker))) { - return true - } + if (readyTexts.some(marker => normalizedTitle.includes(marker))) return true titleMatchCount += 1 } - const className = child.className if (className) { - if (readyClassMarkers.some(marker => className.includes(marker))) { - return true - } + if (readyClassMarkers.some(marker => className.includes(marker))) return true if (className.length > 5) { classMatchCount += 1 hasValidClassName = true @@ -630,11 +541,11 @@ export class KeyService { return true } - // --- Main Methods --- + // --- DB Key Logic (Unchanged core flow) --- async autoGetDbKey( - timeoutMs = 60_000, - onStatus?: (message: string, level: number) => void + timeoutMs = 60_000, + onStatus?: (message: string, level: number) => void ): Promise { if (!this.ensureWin32()) return { success: false, error: '仅支持 Windows' } if (!this.ensureLoaded()) return { success: false, error: 'wx_key.dll 未加载' } @@ -642,7 +553,6 @@ export class KeyService { const logs: string[] = [] - // 1. Find Path onStatus?.('正在定位微信安装路径...', 0) let wechatPath = await this.findWeChatInstallPath() if (!wechatPath) { @@ -651,7 +561,6 @@ export class KeyService { return { success: false, error: err } } - // 2. Restart WeChat onStatus?.('正在关闭微信以进行获取...', 0) const closed = await this.killWeChatProcesses() if (!closed) { @@ -660,7 +569,6 @@ export class KeyService { return { success: false, error: err } } -// 3. Launch onStatus?.('正在启动微信...', 0) const sub = spawn(wechatPath, { detached: true, @@ -669,23 +577,18 @@ export class KeyService { }) sub.unref() -// 4. Wait for Window & Get PID (Crucial change: discover PID from window) onStatus?.('等待微信界面就绪...', 0) const pid = await this.waitForWeChatWindow() - if (!pid) { - return { success: false, error: '启动微信失败或等待界面就绪超时' } - } + if (!pid) return { success: false, error: '启动微信失败或等待界面就绪超时' } onStatus?.(`检测到微信窗口 (PID: ${pid}),正在获取...`, 0) onStatus?.('正在检测微信界面组件...', 0) await this.waitForWeChatWindowComponents(pid, 15000) - // 5. Inject const ok = this.initHook(pid) if (!ok) { const error = this.getLastErrorMsg ? this.decodeCString(this.getLastErrorMsg()) : '' if (error) { - // 检测权限不足错误 (NTSTATUS 0xC0000022 = STATUS_ACCESS_DENIED) if (error.includes('0xC0000022') || error.includes('ACCESS_DENIED') || error.includes('打开目标进程失败')) { const friendlyError = '权限不足:无法访问微信进程。\n\n解决方法:\n1. 右键 WeFlow 图标,选择"以管理员身份运行"\n2. 关闭可能拦截的安全软件(如360、火绒等)\n3. 确保微信没有以管理员权限运行' return { success: false, error: friendlyError } @@ -695,8 +598,8 @@ export class KeyService { const statusBuffer = Buffer.alloc(256) const levelOut = [0] const status = this.getStatusMessage && this.getStatusMessage(statusBuffer, statusBuffer.length, levelOut) - ? this.decodeUtf8(statusBuffer) - : '' + ? this.decodeUtf8(statusBuffer) + : '' return { success: false, error: status || '初始化失败' } } @@ -716,9 +619,7 @@ export class KeyService { for (let i = 0; i < 5; i++) { const statusBuffer = Buffer.alloc(256) const levelOut = [0] - if (!this.getStatusMessage(statusBuffer, statusBuffer.length, levelOut)) { - break - } + if (!this.getStatusMessage(statusBuffer, statusBuffer.length, levelOut)) break const msg = this.decodeUtf8(statusBuffer) const level = levelOut[0] ?? 0 if (msg) { @@ -726,7 +627,6 @@ export class KeyService { onStatus?.(msg, level) } } - await new Promise((resolve) => setTimeout(resolve, 120)) } } finally { @@ -738,21 +638,19 @@ export class KeyService { return { success: false, error: '获取密钥超时', logs } } - // --- Image Key Stuff (Legacy but kept) --- + // --- Image Key Stuff (Refactored to Multi-core Crypto Brute Force) --- private isAccountDir(dirPath: string): boolean { return ( - existsSync(join(dirPath, 'db_storage')) || - existsSync(join(dirPath, 'FileStorage', 'Image')) || - existsSync(join(dirPath, 'FileStorage', 'Image2')) + existsSync(join(dirPath, 'db_storage')) || + existsSync(join(dirPath, 'FileStorage', 'Image')) || + existsSync(join(dirPath, 'FileStorage', 'Image2')) ) } private isPotentialAccountName(name: string): boolean { const lower = name.toLowerCase() - if (lower.startsWith('all') || lower.startsWith('applet') || lower.startsWith('backup') || lower.startsWith('wmpf')) { - return false - } + if (lower.startsWith('all') || lower.startsWith('applet') || lower.startsWith('backup') || lower.startsWith('wmpf')) return false if (lower.startsWith('wxid_')) return true if (/^\d+$/.test(name) && name.length >= 6) return true return name.length > 5 @@ -767,19 +665,12 @@ export class KeyService { const fullPath = join(rootDir, entry) try { if (!statSync(fullPath).isDirectory()) continue - } catch { - continue - } + } catch { continue } - if (!this.isPotentialAccountName(entry)) { - continue - } + if (!this.isPotentialAccountName(entry)) continue - if (this.isAccountDir(fullPath)) { - high.push(fullPath) - } else { - low.push(fullPath) - } + if (this.isAccountDir(fullPath)) high.push(fullPath) + else low.push(fullPath) } return high.length ? high.sort() : low.sort() } catch { @@ -792,9 +683,7 @@ export class KeyService { if (!existsSync(trimmed)) return null try { const stats = statSync(trimmed) - if (stats.isFile()) { - return dirname(trimmed) - } + if (stats.isFile()) return dirname(trimmed) } catch { return null } @@ -849,17 +738,13 @@ export class KeyService { let entries: string[] try { entries = readdirSync(dir) - } catch { - continue - } + } catch { continue } for (const entry of entries) { const fullPath = join(dir, entry) let stats: any try { stats = statSync(fullPath) - } catch { - continue - } + } catch { continue } if (stats.isDirectory()) { stack.push(fullPath) } else if (entry.endsWith('_t.dat')) { @@ -900,9 +785,7 @@ export class KeyService { break } } - if (valid) { - counts.set(xorKey, (counts.get(xorKey) ?? 0) + 1) - } + if (valid) counts.set(xorKey, (counts.get(xorKey) ?? 0) + 1) } } catch { } } @@ -918,180 +801,179 @@ export class KeyService { return bestKey } - private getCiphertextFromTemplate(templateFiles: string[]): Buffer | null { + // 改为返回 Buffer 数组,收集最多2个样本用于双重校验 + private getCiphertextsFromTemplate(templateFiles: string[]): Buffer[] { + const ciphertexts: Buffer[] = [] for (const file of templateFiles) { try { const bytes = readFileSync(file) if (bytes.length < 0x1f) continue + // 匹配微信 DAT 文件的特定头部特征 if ( - bytes[0] === 0x07 && - bytes[1] === 0x08 && - bytes[2] === 0x56 && - bytes[3] === 0x32 && - bytes[4] === 0x08 && - bytes[5] === 0x07 + bytes[0] === 0x07 && bytes[1] === 0x08 && bytes[2] === 0x56 && + bytes[3] === 0x32 && bytes[4] === 0x08 && bytes[5] === 0x07 ) { - return bytes.subarray(0x0f, 0x1f) + ciphertexts.push(bytes.subarray(0x0f, 0x1f)) + // 收集到 2 个样本就足够做双重校验了 + if (ciphertexts.length >= 2) break } } catch { } } - return null + return ciphertexts } - private isAlphaNumLower(byte: number): boolean { - // 只匹配小写字母 a-z 和数字 0-9(AES密钥格式) - return (byte >= 0x61 && byte <= 0x7a) || (byte >= 0x30 && byte <= 0x39) - } - - private isUtf16LowerKey(buf: Buffer, start: number): boolean { - if (start + 64 > buf.length) return false - for (let j = 0; j < 32; j++) { - const charByte = buf[start + j * 2] - const nullByte = buf[start + j * 2 + 1] - if (nullByte !== 0x00 || !this.isAlphaNumLower(charByte)) { - return false - } - } - return true - } - - private verifyKey(ciphertext: Buffer, keyBytes: Buffer): boolean { - try { - const key = keyBytes.subarray(0, 16) - const decipher = crypto.createDecipheriv('aes-128-ecb', key, null) - decipher.setAutoPadding(false) - const decrypted = Buffer.concat([decipher.update(ciphertext), decipher.final()]) - const isJpeg = decrypted.length >= 3 && decrypted[0] === 0xff && decrypted[1] === 0xd8 && decrypted[2] === 0xff - const isPng = decrypted.length >= 8 && - decrypted[0] === 0x89 && - decrypted[1] === 0x50 && - decrypted[2] === 0x4e && - decrypted[3] === 0x47 && - decrypted[4] === 0x0d && - decrypted[5] === 0x0a && - decrypted[6] === 0x1a && - decrypted[7] === 0x0a - return isJpeg || isPng - } catch { - return false - } - } - - private getMemoryRegions(hProcess: any): Array<[number, number]> { - const regions: Array<[number, number]> = [] - const MEM_COMMIT = 0x1000 - const MEM_PRIVATE = 0x20000 - const PAGE_NOACCESS = 0x01 - const PAGE_GUARD = 0x100 - - let address = 0 - const maxAddress = 0x7fffffffffff - while (address >= 0 && address < maxAddress) { - const info: any = {} - const result = this.VirtualQueryEx(hProcess, address, info, this.koffi.sizeof(this.MEMORY_BASIC_INFORMATION)) - if (!result) break - - const state = info.State - const protect = info.Protect - const type = info.Type - const regionSize = Number(info.RegionSize) - // 只收集已提交的私有内存(大幅减少扫描区域) - if (state === MEM_COMMIT && type === MEM_PRIVATE && (protect & PAGE_NOACCESS) === 0 && (protect & PAGE_GUARD) === 0) { - regions.push([Number(info.BaseAddress), regionSize]) - } - - const nextAddress = address + regionSize - if (nextAddress <= address) break - address = nextAddress - } - return regions - } - - private readProcessMemory(hProcess: any, address: number, size: number): Buffer | null { - const buffer = Buffer.alloc(size) - const bytesRead = [0] - const ok = this.ReadProcessMemory(hProcess, address, buffer, size, bytesRead) - if (!ok || bytesRead[0] === 0) return null - return buffer.subarray(0, bytesRead[0]) - } - - private async getAesKeyFromMemory( - pid: number, - ciphertext: Buffer, - onProgress?: (current: number, total: number, message: string) => void + private async bruteForceAesKey( + xorKey: number, + wxid: string, + ciphertexts: Buffer[], + onProgress?: (msg: string) => void ): Promise { - if (!this.ensureKernel32()) return null - const hProcess = this.OpenProcess(this.PROCESS_ALL_ACCESS, false, pid) - if (!hProcess) return null + const numCores = os.cpus().length || 4 + const totalCombinations = 1 << 24 // 16,777,216 种可能性 + const chunkSize = Math.ceil(totalCombinations / numCores) - try { - const allRegions = this.getMemoryRegions(hProcess) - const totalRegions = allRegions.length - let scannedCount = 0 - let skippedCount = 0 + onProgress?.(`准备启动 ${numCores} 个线程进行极速爆破...`) - for (const [baseAddress, regionSize] of allRegions) { - // 跳过太大的内存区域(> 100MB) - if (regionSize > 100 * 1024 * 1024) { - skippedCount++ - continue - } + const workerCode = ` + const { parentPort, workerData } = require('worker_threads'); + const crypto = require('crypto'); - scannedCount++ - if (scannedCount % 10 === 0) { - onProgress?.(scannedCount, totalRegions, `正在扫描微信内存... (${scannedCount}/${totalRegions})`) - await new Promise(resolve => setImmediate(resolve)) - } + const { start, end, xorKey, wxid, cipherHexList } = workerData; + const ciphertexts = cipherHexList.map(hex => Buffer.from(hex, 'hex')); - const memory = this.readProcessMemory(hProcess, baseAddress, regionSize) - if (!memory) continue - - // 直接在原始字节中搜索32字节的小写字母数字序列 - for (let i = 0; i < memory.length - 34; i++) { - // 检查前导字符(不是小写字母或数字) - if (this.isAlphaNumLower(memory[i])) continue - - // 检查接下来32个字节是否都是小写字母或数字 - let valid = true - for (let j = 1; j <= 32; j++) { - if (!this.isAlphaNumLower(memory[i + j])) { - valid = false - break - } - } - if (!valid) continue - - // 检查尾部字符(不是小写字母或数字) - if (i + 33 < memory.length && this.isAlphaNumLower(memory[i + 33])) { - continue - } - - const keyBytes = memory.subarray(i + 1, i + 33) - if (this.verifyKey(ciphertext, keyBytes)) { - return keyBytes.toString('ascii') - } + function verifyKey(cipher, keyStr) { + try { + const decipher = crypto.createDecipheriv('aes-128-ecb', keyStr, null); + decipher.setAutoPadding(false); + const decrypted = Buffer.concat([decipher.update(cipher), decipher.final()]); + const isJpeg = decrypted.length >= 3 && decrypted[0] === 0xff && decrypted[1] === 0xd8 && decrypted[2] === 0xff; + const isPng = decrypted.length >= 8 && decrypted[0] === 0x89 && decrypted[1] === 0x50 && decrypted[2] === 0x4e && decrypted[3] === 0x47; + return isJpeg || isPng; + } catch { + return false; } } - return null - } finally { - try { - this.CloseHandle(hProcess) - } catch { } - } + + let found = null; + for (let upper = end; upper > start; upper--) { + // 我就写 -- + if (upper % 100000 === 0 && upper !== start) { + parentPort.postMessage({ type: 'progress', scanned: 100000 }); + } + + const number = (upper * 256) + xorKey; + + // 1. 无符号整数校验 + const strUnsigned = number.toString(10) + wxid; + const md5Unsigned = crypto.createHash('md5').update(strUnsigned).digest('hex').slice(0, 16); + + let isValidUnsigned = true; + for (const cipher of ciphertexts) { + if (!verifyKey(cipher, md5Unsigned)) { + isValidUnsigned = false; + break; + } + } + if (isValidUnsigned) { + found = md5Unsigned; + break; + } + + // 2. 带符号整数校验 (溢出边界情况) + if (number > 0x7FFFFFFF) { + const strSigned = (number | 0).toString(10) + wxid; + const md5Signed = crypto.createHash('md5').update(strSigned).digest('hex').slice(0, 16); + + let isValidSigned = true; + for (const cipher of ciphertexts) { + if (!verifyKey(cipher, md5Signed)) { + isValidSigned = false; + break; + } + } + if (isValidSigned) { + found = md5Signed; + break; + } + } + } + + if (found) { + parentPort.postMessage({ type: 'success', key: found }); + } else { + parentPort.postMessage({ type: 'done' }); + } + ` + + return new Promise((resolve) => { + let activeWorkers = numCores + let resolved = false + let totalScanned = 0 // 总进度计数器 + const workers: Worker[] = [] + + const cleanup = () => { + for (const w of workers) w.terminate() + } + + for (let i = 0; i < numCores; i++) { + const start = i * chunkSize + const end = Math.min(start + chunkSize, totalCombinations) + + const worker = new Worker(workerCode, { + eval: true, + workerData: { + start, + end, + xorKey, + wxid, + cipherHexList: ciphertexts.map(c => c.toString('hex')) // 传入数组 + } + }) + workers.push(worker) + + worker.on('message', (msg) => { + if (!msg) return + if (msg.type === 'progress') { + totalScanned += msg.scanned + const percent = ((totalScanned / totalCombinations) * 100).toFixed(1) + onProgress?.(`多核爆破中: 已尝试 ${(totalScanned / 10000).toFixed(0)} 万次 (${percent}%)`) + } else if (msg.type === 'success' && !resolved) { + resolved = true + cleanup() + resolve(msg.key) + } else if (msg.type === 'done') { + // 单个 worker 跑完了没有找到 + activeWorkers-- + if (activeWorkers === 0 && !resolved) resolve(null) + } + }) + + worker.on('error', (err) => { + console.error('Worker error:', err) + activeWorkers-- + if (activeWorkers === 0 && !resolved) resolve(null) + }) + + worker.on('exit', () => { + activeWorkers-- + if (activeWorkers === 0 && !resolved) resolve(null) + }) + } + }) } async autoGetImageKey( - manualDir?: string, - onProgress?: (message: string) => void + manualDir?: string, + onProgress?: (message: string) => void ): Promise { - if (!this.ensureWin32()) return { success: false, error: '仅支持 Windows' } - if (!this.ensureLoaded()) return { success: false, error: 'wx_key.dll 未加载' } - if (!this.ensureKernel32()) return { success: false, error: '初始化系统 API 失败' } - onProgress?.('正在定位微信账号目录...') const accountDir = this.resolveAccountDir(manualDir) if (!accountDir) return { success: false, error: '未找到微信账号目录' } + // 精确提取 wxid,直接剥离 _f1c4 类似的 4 位十六进制校验码 + let wxid = basename(accountDir) + wxid = wxid.replace(/_[0-9a-fA-F]{4}$/, '') + onProgress?.('正在收集模板文件...') const templateFiles = this.findTemplateDatFiles(accountDir) if (!templateFiles.length) return { success: false, error: '未找到模板文件' } @@ -1101,23 +983,25 @@ export class KeyService { if (xorKey == null) return { success: false, error: '无法计算 XOR 密钥' } onProgress?.('正在读取加密模板数据...') - const ciphertext = this.getCiphertextFromTemplate(templateFiles) - if (!ciphertext) return { success: false, error: '无法读取加密模板数据' } + const ciphertexts = this.getCiphertextsFromTemplate(templateFiles) + if (ciphertexts.length === 0) return { success: false, error: '无法读取加密模板数据' } - const pid = await this.findWeChatPid() - if (!pid) return { success: false, error: '未检测到微信进程' } + // 提示收集到的样本数量 + onProgress?.(`提取到 ${ciphertexts.length} 个特征样本,开始进行交叉校验...`) - onProgress?.('正在扫描内存获取 AES 密钥...') - const aesKey = await this.getAesKeyFromMemory(pid, ciphertext, (current, total, msg) => { - onProgress?.(`${msg} (${current}/${total})`) + onProgress?.(`正在利用多核爆破 AES 密钥 (基于 wxid: ${wxid})...`) + // 注意这里传入的是 ciphertexts 数组 + const aesKey = await this.bruteForceAesKey(xorKey, wxid, ciphertexts, (msg) => { + onProgress?.(msg) }) + if (!aesKey) { return { success: false, - error: '未能从内存中获取 AES 密钥,请打开朋友圈图片后重试' + error: 'AES 密钥爆破失败,请确认该账号近期是否有接收过图片,或更换账号目录重试' } } - return { success: true, xorKey, aesKey: aesKey.slice(0, 16) } + return { success: true, xorKey, aesKey } } -} +} \ No newline at end of file diff --git a/src/pages/SettingsPage.tsx b/src/pages/SettingsPage.tsx index 4361e33..85dbee0 100644 --- a/src/pages/SettingsPage.tsx +++ b/src/pages/SettingsPage.tsx @@ -1351,8 +1351,13 @@ function SettingsPage() { - {imageKeyStatus &&
{imageKeyStatus}
} - {isFetchingImageKey &&
正在扫描内存,请稍候...
} + {isFetchingImageKey ? ( +
+ {imageKeyStatus || '正在启动多核爆破引擎...'} +
+ ) : ( + imageKeyStatus &&
{imageKeyStatus}
+ )}