diff --git a/electron/services/keyService.ts b/electron/services/keyService.ts index 34e7bd0..538e8cc 100644 --- a/electron/services/keyService.ts +++ b/electron/services/keyService.ts @@ -3,7 +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 crypto from 'crypto' +import { Worker } from 'worker_threads' import os from 'os' const execFileAsync = promisify(execFile) @@ -26,12 +26,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 +59,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 +93,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 +116,7 @@ export class KeyService { return false } - // 检查是否为网络路径,如果是则本地化 if (this.isNetworkPath(dllPath)) { - dllPath = this.localizeNetworkDll(dllPath) } @@ -161,13 +131,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 +146,11 @@ 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'))]) + // 直接使用原生支持的 'void*' 替换 'HANDLE',绝对不会再报类型错误 + this.OpenProcess = this.kernel32.func('OpenProcess', 'void*', ['uint32', 'bool', 'uint32']) + this.CloseHandle = this.kernel32.func('CloseHandle', 'bool', ['void*']) + this.TerminateProcess = this.kernel32.func('TerminateProcess', 'bool', ['void*', 'uint32']) + this.QueryFullProcessImageNameW = this.kernel32.func('QueryFullProcessImageNameW', 'bool', ['void*', 'uint32', this.koffi.out('uint16*'), this.koffi.out('uint32*')]) 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,206 +801,205 @@ 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) + // 优化文案,并确保包含 (xx.x%) 供前端解析 + 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?.('正在定位微信账号目录...') + onProgress?.('正在定位微信账号数据目录...') const accountDir = this.resolveAccountDir(manualDir) if (!accountDir) return { success: false, error: '未找到微信账号目录' } - onProgress?.('正在收集模板文件...') + 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: '未找到模板文件' } - onProgress?.('正在计算 XOR 密钥...') + onProgress?.('正在计算特征 XOR 密钥...') const xorKey = this.getXorKey(templateFiles) if (xorKey == null) return { success: false, error: '无法计算 XOR 密钥' } - onProgress?.('正在读取加密模板数据...') - const ciphertext = this.getCiphertextFromTemplate(templateFiles) - if (!ciphertext) return { success: false, error: '无法读取加密模板数据' } + onProgress?.('正在读取加密模板区块...') + 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?.(`准备启动 ${os.cpus().length || 4} 线程并发爆破引擎 (基于 wxid: ${wxid})...`) - onProgress?.('正在扫描内存获取 AES 密钥...') - const aesKey = await this.getAesKeyFromMemory(pid, ciphertext, (current, total, msg) => { - onProgress?.(`${msg} (${current}/${total})`) + 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.scss b/src/pages/SettingsPage.scss index eb54f95..a27d74d 100644 --- a/src/pages/SettingsPage.scss +++ b/src/pages/SettingsPage.scss @@ -2172,4 +2172,71 @@ width: 100%; margin-top: 12px; } +} + +.brute-force-progress { + margin-top: 12px; + padding: 14px 16px; + background: var(--bg-tertiary); + border: 1px solid var(--border-color); + border-radius: 12px; + animation: slideUp 0.3s ease; + + .status-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 10px; + + .status-text { + font-size: 13px; + color: var(--text-primary); + font-weight: 500; + margin: 0; + // 增加文字呼吸灯效果,表明正在运行 + animation: pulse 2s ease-in-out infinite; + } + + .percent { + font-size: 14px; + color: var(--primary); + font-weight: 700; + font-family: var(--font-mono); + } + } + + .progress-bar-container { + width: 100%; + height: 8px; + background: var(--bg-primary); + border-radius: 4px; + overflow: hidden; + border: 1px solid var(--border-color); + box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.05); + + .fill { + height: 100%; + background: linear-gradient(90deg, var(--primary) 0%, color-mix(in srgb, var(--primary) 60%, white) 100%); + border-radius: 4px; + transition: width 0.3s cubic-bezier(0.4, 0, 0.2, 1); + position: relative; + + // 流光扫过的高亮特效 + &::after { + content: ''; + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: linear-gradient( + 90deg, + transparent, + rgba(255, 255, 255, 0.3), + transparent + ); + animation: progress-shimmer 1.5s infinite linear; + } + } + } } \ No newline at end of file diff --git a/src/pages/SettingsPage.tsx b/src/pages/SettingsPage.tsx index 4361e33..faa558f 100644 --- a/src/pages/SettingsPage.tsx +++ b/src/pages/SettingsPage.tsx @@ -82,6 +82,8 @@ function SettingsPage() { const exportExcelColumnsDropdownRef = useRef(null) const exportConcurrencyDropdownRef = useRef(null) const [cachePath, setCachePath] = useState('') + const [imageKeyProgress, setImageKeyProgress] = useState(0) + const [imageKeyPercent, setImageKeyPercent] = useState(null) const [logEnabled, setLogEnabled] = useState(false) const [whisperModelName, setWhisperModelName] = useState('base') @@ -222,8 +224,28 @@ function SettingsPage() { const removeDb = window.electronAPI.key.onDbKeyStatus((payload: { message: string; level: number }) => { setDbKeyStatus(payload.message) }) - const removeImage = window.electronAPI.key.onImageKeyStatus((payload: { message: string }) => { - setImageKeyStatus(payload.message) + + const removeImage = window.electronAPI.key.onImageKeyStatus((payload: { message: string, percent?: number }) => { + let msg = payload.message; + let pct = payload.percent; + + // 如果后端没有显式传 percent,则用正则从字符串中提取如 "(12.5%)" + if (pct === undefined) { + const match = msg.match(/\(([\d.]+)%\)/); + if (match) { + pct = parseFloat(match[1]); + // 将百分比从文本中剥离,让 UI 更清爽 + msg = msg.replace(/\s*\([\d.]+%\)/, ''); + } + } + + setImageKeyStatus(msg); + if (pct !== undefined) { + setImageKeyPercent(pct); + } else if (msg.includes('启动多核') || msg.includes('定位') || msg.includes('准备')) { + // 预热阶段 + setImageKeyPercent(0); + } }) return () => { removeDb?.() @@ -745,15 +767,18 @@ function SettingsPage() { } const handleAutoGetImageKey = async () => { - if (isFetchingImageKey) return + if (isFetchingImageKey) return; if (!dbPath) { - showMessage('请先选择数据库目录', false) - return + showMessage('请先选择数据库目录', false); + return; } - setIsFetchingImageKey(true) - setImageKeyStatus('正在准备获取图片密钥...') + setIsFetchingImageKey(true); + setImageKeyPercent(0) + setImageKeyStatus('正在初始化多核爆破引擎...'); + setImageKeyProgress(0); // 重置进度 + try { - const accountPath = wxid ? `${dbPath}/${wxid}` : dbPath + const accountPath = wxid ? `${dbPath}/${wxid}` : dbPath; const result = await window.electronAPI.key.autoGetImageKey(accountPath) if (result.success && result.aesKey) { if (typeof result.xorKey === 'number') { @@ -1351,8 +1376,21 @@ function SettingsPage() { - {imageKeyStatus &&
{imageKeyStatus}
} - {isFetchingImageKey &&
正在扫描内存,请稍候...
} + {isFetchingImageKey ? ( +
+
+ {imageKeyStatus || '正在启动多核爆破引擎...'} + {imageKeyPercent !== null && {imageKeyPercent.toFixed(1)}%} +
+ {imageKeyPercent !== null && ( +
+
+
+ )} +
+ ) : ( + imageKeyStatus &&
{imageKeyStatus}
+ )}
diff --git a/src/pages/WelcomePage.scss b/src/pages/WelcomePage.scss index 395b24e..3890db4 100644 --- a/src/pages/WelcomePage.scss +++ b/src/pages/WelcomePage.scss @@ -803,3 +803,79 @@ opacity: 1; } } + + +.brute-force-progress { + margin-top: 16px; + padding: 14px 16px; + background: var(--bg-tertiary); + border: 1px solid var(--border-color); + border-radius: 12px; + animation: slideUp 0.3s ease; + + .status-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 10px; + + .status-text { + font-size: 13px; + color: var(--text-primary); + font-weight: 500; + margin: 0; + animation: pulse 2s ease-in-out infinite; + } + + .percent { + font-size: 14px; + color: var(--primary); + font-weight: 700; + font-family: var(--font-mono); + } + } + + .progress-bar-container { + width: 100%; + height: 8px; + background: var(--bg-primary); + border-radius: 4px; + overflow: hidden; + border: 1px solid var(--border-color); + box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.05); + + .fill { + height: 100%; + background: linear-gradient(90deg, var(--primary) 0%, color-mix(in srgb, var(--primary) 60%, white) 100%); + border-radius: 4px; + transition: width 0.3s cubic-bezier(0.4, 0, 0.2, 1); + position: relative; + + &::after { + content: ''; + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: linear-gradient( + 90deg, + transparent, + rgba(255, 255, 255, 0.3), + transparent + ); + animation: progress-shimmer 1.5s infinite linear; + } + } + } +} + +@keyframes pulse { + 0%, 100% { opacity: 1; } + 50% { opacity: 0.7; } +} + +@keyframes progress-shimmer { + 0% { transform: translateX(-100%); } + 100% { transform: translateX(100%); } +} \ No newline at end of file diff --git a/src/pages/WelcomePage.tsx b/src/pages/WelcomePage.tsx index a1e556b..4623d2f 100644 --- a/src/pages/WelcomePage.tsx +++ b/src/pages/WelcomePage.tsx @@ -48,6 +48,7 @@ function WelcomePage({ standalone = false }: WelcomePageProps) { const [dbKeyStatus, setDbKeyStatus] = useState('') const [imageKeyStatus, setImageKeyStatus] = useState('') const [isManualStartPrompt, setIsManualStartPrompt] = useState(false) + const [imageKeyPercent, setImageKeyPercent] = useState(null) // 安全相关 state const [enableAuth, setEnableAuth] = useState(false) @@ -111,8 +112,25 @@ function WelcomePage({ standalone = false }: WelcomePageProps) { const removeDb = window.electronAPI.key.onDbKeyStatus((payload: { message: string; level: number }) => { setDbKeyStatus(payload.message) }) - const removeImage = window.electronAPI.key.onImageKeyStatus((payload: { message: string }) => { - setImageKeyStatus(payload.message) + const removeImage = window.electronAPI.key.onImageKeyStatus((payload: { message: string, percent?: number }) => { + let msg = payload.message; + let pct = payload.percent; + + // 解析文本中的百分比 + if (pct === undefined) { + const match = msg.match(/\(([\d.]+)%\)/); + if (match) { + pct = parseFloat(match[1]); + msg = msg.replace(/\s*\([\d.]+%\)/, ''); + } + } + + setImageKeyStatus(msg); + if (pct !== undefined) { + setImageKeyPercent(pct); + } else if (msg.includes('启动多核') || msg.includes('定位') || msg.includes('准备')) { + setImageKeyPercent(0); + } }) return () => { removeDb?.() @@ -297,6 +315,7 @@ function WelcomePage({ standalone = false }: WelcomePageProps) { } setIsFetchingImageKey(true) setError('') + setImageKeyPercent(0) setImageKeyStatus('正在准备获取图片密钥...') try { // 拼接完整的账号目录,确保 KeyService 能准确找到模板文件 @@ -727,37 +746,52 @@ function WelcomePage({ standalone = false }: WelcomePageProps) { )} {currentStep.id === 'image' && ( -
-
-
- - setImageXorKey(e.target.value)} - /> -
-
- - setImageAesKey(e.target.value)} - /> +
+
+
+ + setImageXorKey(e.target.value)} + /> +
+
+ + setImageAesKey(e.target.value)} + /> +
+ + + + {isFetchingImageKey ? ( +
+
+ {imageKeyStatus || '正在启动多核爆破引擎...'} + {imageKeyPercent !== null && {imageKeyPercent.toFixed(1)}%} +
+ {imageKeyPercent !== null && ( +
+
+
+ )} +
+ ) : ( + imageKeyStatus &&
{imageKeyStatus}
+ )} + +
请在微信中打开几张图片后再点击获取
- - - - {imageKeyStatus &&
{imageKeyStatus}
} -
请在微信中打开几张图片后再点击获取
-
)}