<# .SYNOPSIS Finds the LEA instruction and function RVA that reference the nt_sqlite3_key_v2 string in wrapper.node, and launches QQ with a debugger to extract the encryption key. .DESCRIPTION This script analyzes a wrapper.node PE64 file to find: 1. The RVA of the target string "nt_sqlite3_key_v2: db=%p zDb=%s" in .rdata 2. The LEA instruction in .text that references this string (RIP-relative addressing) 3. The function containing this LEA instruction (via exception directory) It can automatically detect installed QQ and locate the wrapper.node file. By default, it launches QQ with a debugger attached to extract the encryption key. Use -NoDebugForKey to skip debugging and perform only static analysis. Compatible with Windows PowerShell 5.0 and PowerShell Core 7.0. .PARAMETER WrapperNodePath Path to the wrapper.node file. If not specified, auto-detects from installed QQ. .PARAMETER NoDebugForKey If specified, skips launching QQ.exe with a debugger. Only performs static analysis. .EXAMPLE .\v2.ps1 # Auto-detects installed QQ, analyzes wrapper.node, and extracts the encryption key .EXAMPLE .\v2.ps1 -WrapperNodePath "C:\QQ\wrapper.node" # Analyzes a specific wrapper.node file and extracts the encryption key .EXAMPLE .\v2.ps1 -NoDebugForKey # Auto-detects QQ, analyzes wrapper.node, but skips debugging to extract the key .OUTPUTS PSCustomObject with FunctionRVA, LeaInstructionRVA, and optionally Key properties. .NOTES This script uses Write-Host intentionally for interactive colored output. #> [CmdletBinding()] [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidUsingWriteHost', '', Justification = 'Interactive script requiring colored console output')] param( [Parameter(Position = 0)] [string]$WrapperNodePath, [Parameter()] [switch]$NoDebugForKey ) Set-StrictMode -Version 2.0 $ErrorActionPreference = 'Stop' #region Reload Script as UTF-8, if running locally if ($PSVersionTable.PSVersion.Major -le 5) { [Console]::OutputEncoding = [System.Text.Encoding]::UTF8 } else { [Console]::OutputEncoding = [System.Text.Encoding]::UTF8 if ([Console]::InputEncoding) { [Console]::InputEncoding = [System.Text.Encoding]::UTF8 } } $currentCommand = $MyInvocation.MyCommand if ($PSVersionTable.PSVersion.Major -le 5 -and $currentCommand.CommandType -eq 'ExternalScript' -and $currentCommand.Path) { try { $scriptContent = [System.IO.File]::ReadAllText($currentCommand.Path, [System.Text.Encoding]::UTF8) $scriptBlock = [scriptblock]::Create($scriptContent) & $scriptBlock @PSBoundParameters exit $LASTEXITCODE } catch { Write-Warning "UTF-8 Auto-Reload Failed: $_" } } #endregion #region P/Invoke Definitions for Debugging $DebugApiCode = @' using System; using System.Collections.Generic; using System.Runtime.InteropServices; using System.Text; namespace DebugApi { [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)] internal struct STARTUPINFOW { public int cb; public IntPtr lpReserved; public IntPtr lpDesktop; public IntPtr lpTitle; public int dwX; public int dwY; public int dwXSize; public int dwYSize; public int dwXCountChars; public int dwYCountChars; public int dwFillAttribute; public int dwFlags; public short wShowWindow; public short cbReserved2; public IntPtr lpReserved2; public IntPtr hStdInput; public IntPtr hStdOutput; public IntPtr hStdError; } [StructLayout(LayoutKind.Sequential)] internal struct PROCESS_INFORMATION { public IntPtr hProcess; public IntPtr hThread; public int dwProcessId; public int dwThreadId; } // DEBUG_EVENT structure for x64 // Layout: dwDebugEventCode (4) + dwProcessId (4) + dwThreadId (4) + padding (4) + union (160) // Total: 176 bytes // We use a byte array for the union and manually parse it to avoid CLR alignment issues [StructLayout(LayoutKind.Sequential)] internal struct DEBUG_EVENT { public uint dwDebugEventCode; public uint dwProcessId; public uint dwThreadId; private uint _padding; // 4 bytes padding for 8-byte alignment of union // Union as byte array - we'll parse manually [MarshalAs(UnmanagedType.ByValArray, SizeConst = 160)] public byte[] u; } // Helper class for parsing DEBUG_EVENT union data internal static class DebugEventParser { // EXCEPTION_DEBUG_INFO: ExceptionRecord starts at offset 0 // EXCEPTION_RECORD layout on x64: // 0: ExceptionCode (4) // 4: ExceptionFlags (4) // 8: ExceptionRecord pointer (8) // 16: ExceptionAddress pointer (8) // 24: NumberParameters (4) // 28: padding (4) // 32: ExceptionInformation[15] (120) public static uint GetExceptionCode(byte[] u) { return BitConverter.ToUInt32(u, 0); } public static ulong GetExceptionAddress(byte[] u) { return BitConverter.ToUInt64(u, 16); } // CREATE_PROCESS_DEBUG_INFO / LOAD_DLL_DEBUG_INFO: hFile is at offset 0 public static IntPtr GetFileHandle(byte[] u) { return (IntPtr)BitConverter.ToInt64(u, 0); } // EXIT_PROCESS_DEBUG_INFO: dwExitCode is at offset 0 public static uint GetExitCode(byte[] u) { return BitConverter.ToUInt32(u, 0); } } [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)] internal struct MODULEENTRY32W { public uint dwSize; public uint th32ModuleID; public uint th32ProcessID; public uint GlblcntUsage; public uint ProccntUsage; public IntPtr modBaseAddr; public uint modBaseSize; public IntPtr hModule; [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 256)] public string szModule; [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 260)] public string szExePath; } [StructLayout(LayoutKind.Sequential, Pack = 16)] internal struct CONTEXT64 { public ulong P1Home; public ulong P2Home; public ulong P3Home; public ulong P4Home; public ulong P5Home; public ulong P6Home; public uint ContextFlags; public uint MxCsr; public ushort SegCs; public ushort SegDs; public ushort SegEs; public ushort SegFs; public ushort SegGs; public ushort SegSs; public uint EFlags; public ulong Dr0; public ulong Dr1; public ulong Dr2; public ulong Dr3; public ulong Dr6; public ulong Dr7; public ulong Rax; public ulong Rcx; public ulong Rdx; public ulong Rbx; public ulong Rsp; public ulong Rbp; public ulong Rsi; public ulong Rdi; public ulong R8; public ulong R9; public ulong R10; public ulong R11; public ulong R12; public ulong R13; public ulong R14; public ulong R15; public ulong Rip; [MarshalAs(UnmanagedType.ByValArray, SizeConst = 512)] public byte[] FltSave; [MarshalAs(UnmanagedType.ByValArray, SizeConst = 26)] public ulong[] VectorRegister; public ulong VectorControl; public ulong DebugControl; public ulong LastBranchToRip; public ulong LastBranchFromRip; public ulong LastExceptionToRip; public ulong LastExceptionFromRip; } internal static class Native { public const int DEBUG_ONLY_THIS_PROCESS = 0x00000002; public const uint INFINITE = 0xFFFFFFFF; public const uint EXCEPTION_DEBUG_EVENT = 1; public const uint CREATE_THREAD_DEBUG_EVENT = 2; public const uint CREATE_PROCESS_DEBUG_EVENT = 3; public const uint EXIT_THREAD_DEBUG_EVENT = 4; public const uint EXIT_PROCESS_DEBUG_EVENT = 5; public const uint LOAD_DLL_DEBUG_EVENT = 6; public const uint UNLOAD_DLL_DEBUG_EVENT = 7; public const uint OUTPUT_DEBUG_STRING_EVENT = 8; public const uint RIP_EVENT = 9; public const uint EXCEPTION_BREAKPOINT = 0x80000003; public const uint EXCEPTION_SINGLE_STEP = 0x80000004; public const uint DBG_CONTINUE = 0x00010002; public const uint DBG_EXCEPTION_NOT_HANDLED = 0x80010001; public const uint CONTEXT_AMD64 = 0x00100000; public const uint CONTEXT_CONTROL = CONTEXT_AMD64 | 0x0001; public const uint CONTEXT_INTEGER = CONTEXT_AMD64 | 0x0002; public const uint CONTEXT_FULL = CONTEXT_CONTROL | CONTEXT_INTEGER | (CONTEXT_AMD64 | 0x0008); public const uint CONTEXT_ALL = CONTEXT_FULL | (CONTEXT_AMD64 | 0x0004) | (CONTEXT_AMD64 | 0x0010); public const uint TH32CS_SNAPMODULE = 0x00000008; public const uint TH32CS_SNAPMODULE32 = 0x00000010; public const uint THREAD_ALL_ACCESS = 0x1FFFFF; [DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Unicode)] public static extern bool CreateProcessW( string lpApplicationName, IntPtr lpCommandLine, IntPtr lpProcessAttributes, IntPtr lpThreadAttributes, bool bInheritHandles, int dwCreationFlags, IntPtr lpEnvironment, string lpCurrentDirectory, ref STARTUPINFOW lpStartupInfo, out PROCESS_INFORMATION lpProcessInformation); [DllImport("kernel32.dll", SetLastError = true)] public static extern bool WaitForDebugEvent(out DEBUG_EVENT lpDebugEvent, uint dwMilliseconds); [DllImport("kernel32.dll", SetLastError = true)] public static extern bool ContinueDebugEvent(uint dwProcessId, uint dwThreadId, uint dwContinueStatus); [DllImport("kernel32.dll", SetLastError = true)] public static extern bool TerminateProcess(IntPtr hProcess, int uExitCode); [DllImport("kernel32.dll", SetLastError = true)] public static extern bool CloseHandle(IntPtr hObject); [DllImport("kernel32.dll", SetLastError = true)] public static extern bool ReadProcessMemory( IntPtr hProcess, IntPtr lpBaseAddress, byte[] lpBuffer, UIntPtr nSize, out UIntPtr lpNumberOfBytesRead); [DllImport("kernel32.dll", SetLastError = true)] public static extern bool WriteProcessMemory( IntPtr hProcess, IntPtr lpBaseAddress, byte[] lpBuffer, UIntPtr nSize, out UIntPtr lpNumberOfBytesWritten); [DllImport("kernel32.dll", SetLastError = true)] public static extern bool FlushInstructionCache(IntPtr hProcess, IntPtr lpBaseAddress, UIntPtr dwSize); [DllImport("kernel32.dll", SetLastError = true)] public static extern IntPtr OpenThread(uint dwDesiredAccess, bool bInheritHandle, uint dwThreadId); [DllImport("kernel32.dll", SetLastError = true)] public static extern bool GetThreadContext(IntPtr hThread, ref CONTEXT64 lpContext); [DllImport("kernel32.dll", SetLastError = true)] public static extern bool SetThreadContext(IntPtr hThread, ref CONTEXT64 lpContext); [DllImport("kernel32.dll", SetLastError = true)] public static extern IntPtr CreateToolhelp32Snapshot(uint dwFlags, uint th32ProcessID); [DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Unicode)] public static extern bool Module32FirstW(IntPtr hSnapshot, ref MODULEENTRY32W lpme); [DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Unicode)] public static extern bool Module32NextW(IntPtr hSnapshot, ref MODULEENTRY32W lpme); } /// /// High-level debugger class that extracts the encryption key from QQ process. /// All P/Invoke calls are handled internally to avoid PowerShell marshaling issues. /// public sealed class KeyExtractor { private readonly string _qqExePath; private readonly ulong _functionRva; private readonly Action _log; private readonly Action _logVerbose; private IntPtr _hProcess = IntPtr.Zero; private IntPtr _hThread = IntPtr.Zero; private uint _processId; private ulong _wrapperBase; private ulong _breakpointAddress; private byte _originalByte; private bool _breakpointActive; private Dictionary _steppingThreads = new Dictionary(); public KeyExtractor(string qqExePath, ulong functionRva, Action log, Action logVerbose) { _qqExePath = qqExePath; _functionRva = functionRva; _log = log ?? (s => { }); _logVerbose = logVerbose ?? (s => { }); } public string ExtractKey() { _log("正在启动 QQ 进程并附加调试器..."); _log("QQ.exe 路径: " + _qqExePath); _log("目标函数 RVA: 0x" + _functionRva.ToString("X")); if (!StartDebugProcess()) { throw new Exception("启动 QQ 进程并附加调试器失败。错误: " + Marshal.GetLastWin32Error()); } _log("QQ 进程已启动。PID: " + _processId); _log("等待 wrapper.node 加载..."); _log("请在 QQ 窗口中登录目标 QQ 账号。"); try { return DebugLoop(); } finally { Cleanup(); } } private bool StartDebugProcess() { STARTUPINFOW si = new STARTUPINFOW(); si.cb = Marshal.SizeOf(typeof(STARTUPINFOW)); PROCESS_INFORMATION pi; bool result = Native.CreateProcessW( _qqExePath, IntPtr.Zero, IntPtr.Zero, IntPtr.Zero, false, Native.DEBUG_ONLY_THIS_PROCESS, IntPtr.Zero, null, ref si, out pi); if (result) { _hProcess = pi.hProcess; _hThread = pi.hThread; _processId = (uint)pi.dwProcessId; } return result; } private ulong GetModuleBaseAddress(string moduleName) { // Use both flags like the Rust implementation IntPtr snapshot = Native.CreateToolhelp32Snapshot( Native.TH32CS_SNAPMODULE | Native.TH32CS_SNAPMODULE32, _processId); if (snapshot == IntPtr.Zero || snapshot == new IntPtr(-1)) return 0; try { MODULEENTRY32W entry = new MODULEENTRY32W(); entry.dwSize = (uint)Marshal.SizeOf(typeof(MODULEENTRY32W)); if (Native.Module32FirstW(snapshot, ref entry)) { do { if (string.Equals(entry.szModule, moduleName, StringComparison.OrdinalIgnoreCase)) { return (ulong)entry.modBaseAddr; } } while (Native.Module32NextW(snapshot, ref entry)); } } finally { Native.CloseHandle(snapshot); } return 0; } private bool SetBreakpoint() { _breakpointAddress = _wrapperBase + _functionRva; _log("在以下地址设置断点: 0x" + _breakpointAddress.ToString("X")); byte[] buffer = new byte[1]; UIntPtr bytesRead; if (!Native.ReadProcessMemory(_hProcess, (IntPtr)_breakpointAddress, buffer, new UIntPtr(1), out bytesRead) || bytesRead != new UIntPtr(1)) { _log("读取原始字节失败"); return false; } _originalByte = buffer[0]; byte[] int3 = new byte[] { 0xCC }; UIntPtr bytesWritten; if (!Native.WriteProcessMemory(_hProcess, (IntPtr)_breakpointAddress, int3, new UIntPtr(1), out bytesWritten) || bytesWritten != new UIntPtr(1)) { _log("写入断点失败"); return false; } Native.FlushInstructionCache(_hProcess, (IntPtr)_breakpointAddress, new UIntPtr(1)); _breakpointActive = true; _log("断点设置成功"); return true; } private void RestoreOriginalByte() { byte[] orig = new byte[] { _originalByte }; UIntPtr written; Native.WriteProcessMemory(_hProcess, (IntPtr)_breakpointAddress, orig, new UIntPtr(1), out written); Native.FlushInstructionCache(_hProcess, (IntPtr)_breakpointAddress, new UIntPtr(1)); } private void ReinstallBreakpoint() { byte[] int3 = new byte[] { 0xCC }; UIntPtr written; Native.WriteProcessMemory(_hProcess, (IntPtr)_breakpointAddress, int3, new UIntPtr(1), out written); Native.FlushInstructionCache(_hProcess, (IntPtr)_breakpointAddress, new UIntPtr(1)); _breakpointActive = true; } private string DebugLoop() { DEBUG_EVENT debugEvent; bool shouldContinue = true; string extractedKey = null; // Store key here instead of returning immediately while (shouldContinue) { // Wait for debug event with 10 second timeout (like Rust implementation) if (!Native.WaitForDebugEvent(out debugEvent, 10000)) { int error = Marshal.GetLastWin32Error(); // Timeout (ERROR_SEM_TIMEOUT = 121) is normal, continue waiting if (error == 121) continue; _logVerbose("WaitForDebugEvent failed with error: " + error); continue; } uint continueStatus = Native.DBG_CONTINUE; switch (debugEvent.dwDebugEventCode) { case Native.CREATE_PROCESS_DEBUG_EVENT: _logVerbose("进程已创建。PID: " + debugEvent.dwProcessId); // Close the file handle from CREATE_PROCESS_DEBUG_INFO IntPtr hFile = DebugEventParser.GetFileHandle(debugEvent.u); if (hFile != IntPtr.Zero && hFile != new IntPtr(-1)) Native.CloseHandle(hFile); break; case Native.CREATE_THREAD_DEBUG_EVENT: _logVerbose("线程已创建: " + debugEvent.dwThreadId); break; case Native.EXIT_THREAD_DEBUG_EVENT: _logVerbose("线程已退出: " + debugEvent.dwThreadId); break; case Native.LOAD_DLL_DEBUG_EVENT: // Close the file handle from LOAD_DLL_DEBUG_INFO IntPtr dllFileHandle = DebugEventParser.GetFileHandle(debugEvent.u); if (dllFileHandle != IntPtr.Zero && dllFileHandle != new IntPtr(-1)) Native.CloseHandle(dllFileHandle); // Check for wrapper.node if not found yet if (_wrapperBase == 0) { ulong baseAddr = GetModuleBaseAddress("wrapper.node"); if (baseAddr != 0) { _wrapperBase = baseAddr; _log("wrapper.node 已加载到: 0x" + _wrapperBase.ToString("X")); ulong targetAddr = _wrapperBase + _functionRva; _log("目标函数位于: 0x" + targetAddr.ToString("X")); SetBreakpoint(); } } break; case Native.UNLOAD_DLL_DEBUG_EVENT: _logVerbose("DLL 已卸载"); break; case Native.OUTPUT_DEBUG_STRING_EVENT: // Just continue, we don't care about debug strings break; case Native.EXCEPTION_DEBUG_EVENT: uint exceptionCode = DebugEventParser.GetExceptionCode(debugEvent.u); ulong exceptionAddress = DebugEventParser.GetExceptionAddress(debugEvent.u); if (exceptionCode == Native.EXCEPTION_BREAKPOINT) { // Check if this is our breakpoint if (_breakpointActive && exceptionAddress == _breakpointAddress) { _logVerbose("软件断点在 0x" + exceptionAddress.ToString("X") + " 处被触发"); // Restore original byte first RestoreOriginalByte(); string foundKey = HandleBreakpoint(debugEvent.dwThreadId); if (foundKey != null) { // Restore breakpoint before termination (like Rust) ReinstallBreakpoint(); // Store key and request termination extractedKey = foundKey; Native.TerminateProcess(_hProcess, 0); _log("已请求目标进程终止。等待进程退出..."); // Don't return here - continue processing to receive EXIT_PROCESS_DEBUG_EVENT } } // System breakpoint or other breakpoints - just continue } else if (exceptionCode == Native.EXCEPTION_SINGLE_STEP) { if (_steppingThreads.ContainsKey(debugEvent.dwThreadId)) { _logVerbose("线程 " + debugEvent.dwThreadId + " 中发生单步异常"); ClearTrapFlag(debugEvent.dwThreadId); ReinstallBreakpoint(); _steppingThreads.Remove(debugEvent.dwThreadId); _logVerbose("断点已恢复到 0x" + _breakpointAddress.ToString("X")); } } else { // Pass unhandled exceptions to the debuggee continueStatus = Native.DBG_EXCEPTION_NOT_HANDLED; } break; case Native.EXIT_PROCESS_DEBUG_EVENT: uint exitCode = DebugEventParser.GetExitCode(debugEvent.u); _log("进程已退出,退出代码: " + exitCode); shouldContinue = false; break; case Native.RIP_EVENT: _logVerbose("收到 RIP 事件"); break; default: _logVerbose("未知调试事件: " + debugEvent.dwDebugEventCode); break; } Native.ContinueDebugEvent(debugEvent.dwProcessId, debugEvent.dwThreadId, continueStatus); } if (extractedKey != null) { _log("任务完成。"); return extractedKey; } _log("调试循环已退出,未找到密钥"); return null; } private string HandleBreakpoint(uint threadId) { IntPtr hThread = Native.OpenThread(Native.THREAD_ALL_ACCESS, false, threadId); if (hThread == IntPtr.Zero) return null; try { CONTEXT64 ctx = new CONTEXT64(); ctx.ContextFlags = Native.CONTEXT_ALL; ctx.FltSave = new byte[512]; ctx.VectorRegister = new ulong[26]; if (!Native.GetThreadContext(hThread, ref ctx)) { _logVerbose("获取线程上下文失败"); return null; } // Decrement RIP to point back to original instruction ctx.Rip = ctx.Rip - 1; // R8 contains the key pointer ulong r8Value = ctx.R8; _logVerbose("R8 = 0x" + r8Value.ToString("X")); // Read string from R8 byte[] stringBuffer = new byte[256]; UIntPtr bytesRead; if (!Native.ReadProcessMemory(_hProcess, (IntPtr)r8Value, stringBuffer, new UIntPtr(256), out bytesRead) || (ulong)bytesRead == 0) { _logVerbose("从 R8 读取字符串失败"); SetSingleStep(hThread, ref ctx, threadId); return null; } int nullIndex = Array.IndexOf(stringBuffer, (byte)0); if (nullIndex < 0) nullIndex = (int)(ulong)bytesRead; string keyString = Encoding.ASCII.GetString(stringBuffer, 0, nullIndex); // Check if it's a valid 16-char ASCII key bool isValidKey = keyString.Length == 16; if (isValidKey) { foreach (char c in keyString) { if (c < 32 || c > 126) { isValidKey = false; break; } } } if (isValidKey) { _log(""); _log("========================================"); _log("找到密钥: " + keyString); _log("========================================"); return keyString; } else { _logVerbose("非目标调用,R8 字符串: " + keyString + " (长度: " + keyString.Length + ")"); SetSingleStep(hThread, ref ctx, threadId); return null; } } finally { Native.CloseHandle(hThread); } } private void SetSingleStep(IntPtr hThread, ref CONTEXT64 ctx, uint threadId) { ctx.EFlags = ctx.EFlags | 0x100; Native.SetThreadContext(hThread, ref ctx); _steppingThreads[threadId] = _breakpointAddress; _breakpointActive = false; } private void ClearTrapFlag(uint threadId) { IntPtr hThread = Native.OpenThread(Native.THREAD_ALL_ACCESS, false, threadId); if (hThread == IntPtr.Zero) return; try { CONTEXT64 ctx = new CONTEXT64(); ctx.ContextFlags = Native.CONTEXT_ALL; ctx.FltSave = new byte[512]; ctx.VectorRegister = new ulong[26]; if (Native.GetThreadContext(hThread, ref ctx)) { ctx.EFlags = ctx.EFlags & ~0x100u; Native.SetThreadContext(hThread, ref ctx); } } finally { Native.CloseHandle(hThread); } } private void Cleanup() { if (_hProcess != IntPtr.Zero) { Native.CloseHandle(_hProcess); _hProcess = IntPtr.Zero; } if (_hThread != IntPtr.Zero) { Native.CloseHandle(_hThread); _hThread = IntPtr.Zero; } } } } '@ #endregion #region Helper Functions function Read-UInt16 { param([byte[]]$Bytes, [int]$Offset) return [BitConverter]::ToUInt16($Bytes, $Offset) } function Read-UInt32 { param([byte[]]$Bytes, [int]$Offset) return [BitConverter]::ToUInt32($Bytes, $Offset) } function Read-UInt64 { param([byte[]]$Bytes, [int]$Offset) return [BitConverter]::ToUInt64($Bytes, $Offset) } function Read-Int32 { param([byte[]]$Bytes, [int]$Offset) return [BitConverter]::ToInt32($Bytes, $Offset) } function Find-BytePattern { param( [byte[]]$Data, [byte[]]$Pattern, [int]$StartOffset = 0, [int]$EndOffset = -1 ) if ($EndOffset -lt 0) { $EndOffset = $Data.Length } $patternLength = $Pattern.Length $searchEnd = $EndOffset - $patternLength for ($i = $StartOffset; $i -le $searchEnd; $i++) { $found = $true for ($j = 0; $j -lt $patternLength; $j++) { if ($Data[$i + $j] -ne $Pattern[$j]) { $found = $false break } } if ($found) { return $i } } return -1 } function Get-InstalledQQInfo { <# .SYNOPSIS Detects installed QQ and returns installation info. .OUTPUTS Hashtable with InstallDir, Version, QQExePath, WrapperNodePath #> [CmdletBinding()] [OutputType([hashtable])] param() $reg = $null # Try WOW6432Node first (32-bit or upgraded from 32-bit legacy QQ) try { $regPath = "HKLM:\SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall\QQ" if (Test-Path $regPath) { $reg = Get-ItemProperty -Path $regPath -ErrorAction SilentlyContinue } } catch { Write-Verbose "WOW6432Node QQ registry key not found" } # Try NTQQ registry key if (-not $reg -or -not $reg.UninstallString) { try { $regPath = "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\NTQQ" if (Test-Path $regPath) { $reg = Get-ItemProperty -Path $regPath -ErrorAction SilentlyContinue } } catch { Write-Verbose "NTQQ registry key not found" } } if (-not $reg -or -not $reg.UninstallString) { throw "QQ installation not found in registry" } $uninstallString = $reg.UninstallString Write-Verbose "UninstallString: $uninstallString" # Extract install directory from UninstallString # Handle quoted paths if ($uninstallString.StartsWith('"')) { $endQuote = $uninstallString.IndexOf('"', 1) if ($endQuote -gt 0) { $uninstallPath = $uninstallString.Substring(1, $endQuote - 1) } else { $uninstallPath = $uninstallString.Trim('"') } } else { # Unquoted path - find .exe extension to handle paths with spaces $exeIndex = $uninstallString.ToLower().IndexOf('.exe') if ($exeIndex -gt 0) { $uninstallPath = $uninstallString.Substring(0, $exeIndex + 4) } else { # Fallback: take everything before first space (legacy behavior) $uninstallPath = $uninstallString.Split(' ')[0] } } $installDir = Split-Path -Parent $uninstallPath Write-Verbose "Install directory: $installDir" if (-not (Test-Path $installDir)) { throw "QQ install directory not found: $installDir" } # Get version $version = $null try { $qqntRegPath = "HKCU:\Software\Tencent\QQNT" if (Test-Path $qqntRegPath) { $qqntReg = Get-ItemProperty -Path $qqntRegPath -ErrorAction SilentlyContinue if ($qqntReg -and $qqntReg.Version) { $version = $qqntReg.Version } } } catch { Write-Verbose "Could not read QQNT version from registry" } # Find version directory $versionsDir = Join-Path $installDir "versions" $versionDir = $null if ($version) { $versionDir = Join-Path $versionsDir $version if (-not (Test-Path $versionDir)) { Write-Verbose "Version directory $versionDir not found, trying alternatives" $versionDir = $null } } if (-not $versionDir) { # Try config.json $configJson = Join-Path $installDir "config.json" if (Test-Path $configJson) { try { $configContent = Get-Content $configJson -Raw if ($configContent -match '"curVersion"\s*:\s*"([^"]+)"') { $version = $Matches[1] $versionDir = Join-Path $versionsDir $version Write-Verbose "Found version from config.json: $version" } } catch { Write-Verbose "Could not parse config.json" } } } if (-not $versionDir -or -not (Test-Path $versionDir)) { # Fall back to single directory in versions folder if (Test-Path $versionsDir) { $dirs = @(Get-ChildItem -Path $versionsDir -Directory -ErrorAction SilentlyContinue) if ($dirs.Count -eq 1) { $versionDir = $dirs[0].FullName $version = $dirs[0].Name Write-Verbose "Using single version directory: $versionDir" } elseif ($dirs.Count -gt 1) { throw "Multiple version directories found in $versionsDir. Please specify WrapperNodePath manually." } else { throw "No version directories found in $versionsDir" } } else { throw "Versions directory not found: $versionsDir" } } $wrapperNode = Join-Path $versionDir "resources\app\wrapper.node" if (-not (Test-Path $wrapperNode)) { throw "wrapper.node not found at: $wrapperNode" } $qqExe = Join-Path $installDir "QQ.exe" if (-not (Test-Path $qqExe)) { throw "QQ.exe not found at: $qqExe" } return @{ InstallDir = $installDir Version = $version QQExePath = $qqExe WrapperNodePath = $wrapperNode } } #endregion #region Main Script # Detect installed QQ if no path specified $qqInfo = $null if (-not $WrapperNodePath) { Write-Host "正在自动检测已安装的QQ..." -ForegroundColor Yellow $qqInfo = Get-InstalledQQInfo $WrapperNodePath = $qqInfo.WrapperNodePath Write-Host "找到QQ安装信息:" -ForegroundColor Green Write-Host " 安装目录: $($qqInfo.InstallDir)" -ForegroundColor Cyan Write-Host " 版本: $($qqInfo.Version)" -ForegroundColor Cyan Write-Host " wrapper.node: $WrapperNodePath" -ForegroundColor Cyan Write-Host "" } # Resolve the path $resolvedPath = Resolve-Path -Path $WrapperNodePath -ErrorAction Stop Write-Verbose "Analyzing file: $resolvedPath" # Read the entire file $FileBytes = [System.IO.File]::ReadAllBytes($resolvedPath.Path) Write-Verbose "File size: $($FileBytes.Length) bytes" # Target pattern to search for $TargetPattern = [System.Text.Encoding]::ASCII.GetBytes("nt_sqlite3_key_v2: db=%p zDb=%s") #region Parse DOS Header $e_magic = Read-UInt16 $FileBytes 0 if ($e_magic -ne 0x5A4D) { throw "Not a valid PE file (invalid DOS header, expected MZ)" } $e_lfanew = Read-UInt32 $FileBytes 0x3C Write-Verbose "PE header offset: 0x$($e_lfanew.ToString('X'))" #endregion #region Parse PE Header $peSignature = Read-UInt32 $FileBytes $e_lfanew if ($peSignature -ne 0x00004550) { throw "Not a valid PE file (invalid PE signature)" } $coffHeaderOffset = $e_lfanew + 4 $machine = Read-UInt16 $FileBytes $coffHeaderOffset $numberOfSections = Read-UInt16 $FileBytes ($coffHeaderOffset + 2) $sizeOfOptionalHeader = Read-UInt16 $FileBytes ($coffHeaderOffset + 16) Write-Verbose "Machine: 0x$($machine.ToString('X'))" Write-Verbose "Number of sections: $numberOfSections" $optionalHeaderOffset = $coffHeaderOffset + 20 $magic = Read-UInt16 $FileBytes $optionalHeaderOffset if ($magic -ne 0x20B) { throw "Only PE32+ (64-bit) files are supported. Found magic: 0x$($magic.ToString('X'))" } #endregion #region Parse Optional Header (PE32+) $imageBase = Read-UInt64 $FileBytes ($optionalHeaderOffset + 24) Write-Verbose "Image base: 0x$($imageBase.ToString('X'))" # Data directories start at offset 112 in PE32+ optional header $dataDirectoriesOffset = $optionalHeaderOffset + 112 # Exception directory is at index 3 (0-indexed) $exceptionDirRVA = Read-UInt32 $FileBytes ($dataDirectoriesOffset + 3 * 8) $exceptionDirSize = Read-UInt32 $FileBytes ($dataDirectoriesOffset + 3 * 8 + 4) Write-Verbose "Exception directory RVA: 0x$($exceptionDirRVA.ToString('X')), Size: 0x$($exceptionDirSize.ToString('X'))" #endregion #region Parse Section Headers $sectionHeadersOffset = $optionalHeaderOffset + $sizeOfOptionalHeader $sections = @{} for ($i = 0; $i -lt $numberOfSections; $i++) { $sectionOffset = $sectionHeadersOffset + ($i * 40) # Read section name (8 bytes, null-padded) $nameBytes = New-Object byte[] 8 [Array]::Copy($FileBytes, $sectionOffset, $nameBytes, 0, 8) $name = [System.Text.Encoding]::ASCII.GetString($nameBytes).TrimEnd([char]0) $virtualSize = Read-UInt32 $FileBytes ($sectionOffset + 8) $virtualAddress = Read-UInt32 $FileBytes ($sectionOffset + 12) $sizeOfRawData = Read-UInt32 $FileBytes ($sectionOffset + 16) $pointerToRawData = Read-UInt32 $FileBytes ($sectionOffset + 20) $sections[$name] = @{ Name = $name VirtualAddress = $virtualAddress VirtualSize = $virtualSize PointerToRawData = $pointerToRawData SizeOfRawData = $sizeOfRawData } Write-Verbose ("Section {0}: VA=0x{1:X}, VSize=0x{2:X}, RawPtr=0x{3:X}, RawSize=0x{4:X}" -f ` $name, $virtualAddress, $virtualSize, $pointerToRawData, $sizeOfRawData) } #endregion #region Find target string in .rdata section $rdata = $sections[".rdata"] if (-not $rdata) { throw ".rdata section not found" } $rdataStart = [int]$rdata.PointerToRawData $rdataEnd = $rdataStart + [int]$rdata.SizeOfRawData Write-Verbose "Searching for target pattern in .rdata section..." $patternFileOffset = Find-BytePattern -Data $FileBytes -Pattern $TargetPattern -StartOffset $rdataStart -EndOffset $rdataEnd if ($patternFileOffset -lt 0) { throw "Target pattern 'nt_sqlite3_key_v2: db=%p zDb=%s' not found in .rdata section" } # Calculate RVA of the string $stringRVA = [uint64]$rdata.VirtualAddress + ([uint64]$patternFileOffset - [uint64]$rdata.PointerToRawData) Write-Verbose "Target string found at file offset: 0x$($patternFileOffset.ToString('X'))" Write-Host "目标字符串 RVA: 0x$($stringRVA.ToString('X'))" -ForegroundColor Cyan #endregion #region Search .text section for LEA instruction $text = $sections[".text"] if (-not $text) { throw ".text section not found" } $textStart = [int]$text.PointerToRawData $textSize = [int]$text.SizeOfRawData $textRVA = [uint64]$text.VirtualAddress Write-Verbose "Searching for LEA instruction in .text section..." Write-Verbose ".text section: fileOffset=0x$($textStart.ToString('X')), size=0x$($textSize.ToString('X')), RVA=0x$($textRVA.ToString('X'))" $leaInstructionRVA = $null for ($i = 1; $i -lt ($textSize - 6); $i++) { $fileOffset = $textStart + $i if ($FileBytes[$fileOffset] -ne 0x8D) { continue } $rex = $FileBytes[$fileOffset - 1] if (($rex -band 0xF8) -ne 0x48) { continue } $modrm = $FileBytes[$fileOffset + 1] if (($modrm -band 0xC7) -ne 0x05) { continue } $disp = Read-Int32 $FileBytes ($fileOffset + 2) $instrRVA = $textRVA + ($i - 1) $instrLen = 7 $targetRVA = $instrRVA + $instrLen + $disp if ($targetRVA -eq $stringRVA) { $leaInstructionRVA = $instrRVA Write-Verbose "Found LEA instruction at file offset: 0x$(($textStart + $i - 1).ToString('X'))" break } } if ($null -eq $leaInstructionRVA) { throw "LEA instruction referencing target string not found in .text section" } Write-Host "LEA 指令 RVA: 0x$($leaInstructionRVA.ToString('X'))" -ForegroundColor Cyan #endregion #region Find function via exception directory $exceptionSection = $null foreach ($section in $sections.Values) { if ($exceptionDirRVA -ge $section.VirtualAddress -and $exceptionDirRVA -lt ($section.VirtualAddress + $section.VirtualSize)) { $exceptionSection = $section break } } if ($null -eq $exceptionSection) { throw "Could not find section containing exception directory" } Write-Verbose "Exception directory is in section: $($exceptionSection.Name)" $exceptionDirFileOffset = [int]$exceptionSection.PointerToRawData + ([int]$exceptionDirRVA - [int]$exceptionSection.VirtualAddress) $entrySize = 12 $numEntries = [int]($exceptionDirSize / $entrySize) Write-Verbose "Exception directory: $numEntries entries at file offset 0x$($exceptionDirFileOffset.ToString('X'))" $left = 0 $right = $numEntries - 1 $functionBeginRVA = $null $targetRVA_uint32 = [uint32]$leaInstructionRVA while ($left -le $right) { $mid = [int][Math]::Floor(($left + $right) / 2) $entryOffset = $exceptionDirFileOffset + ($mid * $entrySize) $beginAddr = Read-UInt32 $FileBytes $entryOffset $endAddr = Read-UInt32 $FileBytes ($entryOffset + 4) if ($targetRVA_uint32 -lt $beginAddr) { $right = $mid - 1 } elseif ($targetRVA_uint32 -ge $endAddr) { $left = $mid + 1 } else { $functionBeginRVA = $beginAddr Write-Verbose "Found function at entry index $mid : begin=0x$($beginAddr.ToString('X')), end=0x$($endAddr.ToString('X'))" break } } if ($null -eq $functionBeginRVA) { throw "Could not find function containing LEA instruction in exception directory" } Write-Host "函数 RVA: 0x$($functionBeginRVA.ToString('X'))" -ForegroundColor Green #endregion #region Output results $result = [PSCustomObject]@{ FunctionRVA = $functionBeginRVA LeaInstructionRVA = [uint64]$leaInstructionRVA Key = $null } Write-Host "" Write-Host "=== 静态分析结果 ===" -ForegroundColor Yellow Write-Host "函数 RVA: 0x$($result.FunctionRVA.ToString('X'))" -ForegroundColor Green Write-Host "LEA 指令 RVA: 0x$($result.LeaInstructionRVA.ToString('X'))" -ForegroundColor Green #endregion #region Debug for key (default behavior, skip with -NoDebugForKey) if ($NoDebugForKey) { return $result } Write-Host "" Write-Host "=== 动态调试QQ进程 ===" -ForegroundColor Yellow # Add the debug API type if not already added if (-not ([System.Management.Automation.PSTypeName]'DebugApi.KeyExtractor').Type) { Add-Type -TypeDefinition $DebugApiCode -Language CSharp } if (-not $qqInfo) { $qqInfo = Get-InstalledQQInfo } # Create log delegates that write to host with appropriate colors $logAction = [Action[string]] { param($msg) Write-Host $msg -ForegroundColor Cyan } $logVerboseAction = [Action[string]] { param($msg) Write-Verbose $msg } # Create and run the key extractor $extractor = New-Object DebugApi.KeyExtractor( $qqInfo.QQExePath, [uint64]$functionBeginRVA, $logAction, $logVerboseAction ) try { $key = $extractor.ExtractKey() if ($key) { $result.Key = $key Write-Host "" Write-Host "=== 最终结果 ===" -ForegroundColor Yellow Write-Host "函数 RVA: 0x$($result.FunctionRVA.ToString('X'))" -ForegroundColor Green Write-Host "LEA 指令 RVA: 0x$($result.LeaInstructionRVA.ToString('X'))" -ForegroundColor Green Write-Host "加密密钥: $($result.Key)" -ForegroundColor Green } else { Write-Host "提取加密密钥失败" -ForegroundColor Red } } catch { Write-Host "密钥提取过程中出错: $_" -ForegroundColor Red } #endregion return $result