114 lines
4.8 KiB
PowerShell
114 lines
4.8 KiB
PowerShell
# extract-weixin-key.ps1
|
|
# 输入参数: -SeedDir <path> (可选, 默认自动找 Documents/xwechat_files/ 最近目录)
|
|
# 输出 (stdout): JSON { wxid: ..., dbKeys: { "message_0.db": "<hex>", ... } }
|
|
# 退出码: 0 OK / 1 无 Weixin 进程 / 2 seed 目录不存在 / 3 内存读取失败
|
|
|
|
param([string]$SeedDir = "")
|
|
|
|
$ErrorActionPreference = 'Stop'
|
|
|
|
# 1. 找 Weixin 主进程
|
|
$weixin = Get-Process Weixin -ErrorAction SilentlyContinue | Where-Object { $_.MainWindowHandle -ne 0 } | Select-Object -First 1
|
|
if (-not $weixin) { Write-Error "No Weixin process"; exit 1 }
|
|
|
|
# 2. 确定 seed 目录
|
|
if (-not $SeedDir) {
|
|
$root = Join-Path ([Environment]::GetFolderPath('MyDocuments')) 'xwechat_files'
|
|
if (-not (Test-Path $root)) { Write-Error "xwechat_files not found"; exit 2 }
|
|
$SeedDir = Get-ChildItem $root -Directory | Where-Object { $_.Name -ne 'all_users' -and $_.Name -ne 'Backup' } |
|
|
Sort-Object LastWriteTime -Descending | Select-Object -First 1 -ExpandProperty FullName
|
|
}
|
|
|
|
# 3. 收集各 DB 的 salt (前 16B)
|
|
$dbStorage = Join-Path $SeedDir 'db_storage'
|
|
$dbFiles = Get-ChildItem $dbStorage -Recurse -Filter '*.db' -File |
|
|
Where-Object { $_.Name -notmatch '-wal$|-shm$' }
|
|
$saltMap = @{}
|
|
foreach ($f in $dbFiles) {
|
|
$bytes = New-Object byte[] 16
|
|
try {
|
|
# Weixin 运行时持有 DB 句柄 (exclusive write), 必须 FileShare=ReadWrite 才能并发读
|
|
$fs = [System.IO.File]::Open($f.FullName, [System.IO.FileMode]::Open, [System.IO.FileAccess]::Read, [System.IO.FileShare]::ReadWrite)
|
|
try { $fs.Read($bytes, 0, 16) | Out-Null } finally { $fs.Close() }
|
|
$saltMap[$f.Name] = [BitConverter]::ToString($bytes).Replace('-', '').ToLowerInvariant()
|
|
} catch {
|
|
# 个别 DB 读不到不算致命 (例如临时锁), 跳过该 DB; 主流程仍可工作
|
|
continue
|
|
}
|
|
}
|
|
|
|
# 4. P/Invoke — 枚举 + 读内存
|
|
Add-Type -TypeDefinition @"
|
|
using System;
|
|
using System.Runtime.InteropServices;
|
|
public static class W {
|
|
[StructLayout(LayoutKind.Sequential)] public struct MBI {
|
|
public IntPtr BaseAddress; public IntPtr AllocationBase;
|
|
public uint AllocationProtect; public IntPtr RegionSize;
|
|
public uint State; public uint Protect; public uint Type;
|
|
}
|
|
[DllImport("kernel32.dll", SetLastError=true)] public static extern IntPtr OpenProcess(uint a, bool i, int p);
|
|
[DllImport("kernel32.dll", SetLastError=true)] public static extern bool CloseHandle(IntPtr h);
|
|
[DllImport("kernel32.dll", SetLastError=true)] public static extern int VirtualQueryEx(IntPtr h, IntPtr a, out MBI m, uint l);
|
|
[DllImport("kernel32.dll", SetLastError=true)] public static extern bool ReadProcessMemory(IntPtr h, IntPtr a, byte[] b, IntPtr s, out IntPtr r);
|
|
}
|
|
"@
|
|
|
|
$handle = [W]::OpenProcess(0x410, $false, $weixin.Id)
|
|
if ($handle -eq [IntPtr]::Zero) { Write-Error "OpenProcess failed"; exit 3 }
|
|
|
|
# 扫描 + 提取 96-char hex literal
|
|
$addr = [IntPtr]::Zero
|
|
$max = 0x7FFFFFFEFFFF
|
|
$regex = [regex]::new("x'([a-f0-9]{96})'", 'IgnoreCase')
|
|
$literals = New-Object System.Collections.Generic.HashSet[string]
|
|
$latin1 = [System.Text.Encoding]::GetEncoding('iso-8859-1')
|
|
try {
|
|
while ([Int64]$addr -lt $max) {
|
|
$mbi = New-Object W+MBI
|
|
if ([W]::VirtualQueryEx($handle, $addr, [ref]$mbi, 48) -eq 0) { break }
|
|
$isCommit = ($mbi.State -band 0x1000) -ne 0
|
|
$isPrivate = ($mbi.Type -band 0x20000) -ne 0
|
|
$isRW = ($mbi.Protect -eq 0x04) -or ($mbi.Protect -eq 0x40)
|
|
$noGuard = ($mbi.Protect -band 0x100) -eq 0
|
|
if ($isCommit -and $isPrivate -and $isRW -and $noGuard) {
|
|
$size = [Int64]$mbi.RegionSize
|
|
if ($size -gt 0 -and $size -lt 1GB) {
|
|
$buf = New-Object byte[] $size
|
|
$read = [IntPtr]::Zero
|
|
$ok = [W]::ReadProcessMemory($handle, $mbi.BaseAddress, $buf, [IntPtr]::new($size), [ref]$read)
|
|
if ($ok) {
|
|
$text = $latin1.GetString($buf, 0, [Int64]$read)
|
|
foreach ($m in $regex.Matches($text)) {
|
|
[void]$literals.Add($m.Groups[1].Value.ToLowerInvariant())
|
|
}
|
|
}
|
|
}
|
|
}
|
|
$addr = [IntPtr]::new([Int64]$mbi.BaseAddress + [Int64]$mbi.RegionSize)
|
|
}
|
|
} finally { [W]::CloseHandle($handle) | Out-Null }
|
|
|
|
# 5. 反向匹配 salt → key
|
|
$dbKeys = @{}
|
|
foreach ($lit in $literals) {
|
|
$keyHex = $lit.Substring(0, 64)
|
|
$saltHex = $lit.Substring(64)
|
|
foreach ($entry in $saltMap.GetEnumerator()) {
|
|
if ($entry.Value -eq $saltHex) {
|
|
$dbKeys[$entry.Key] = $keyHex
|
|
break
|
|
}
|
|
}
|
|
}
|
|
|
|
# 6. 输出 JSON
|
|
$result = @{
|
|
seedDir = $SeedDir
|
|
wxid = (Split-Path $SeedDir -Leaf)
|
|
wechatVersion = $weixin.MainModule.FileVersionInfo.FileVersion
|
|
pid = $weixin.Id
|
|
dbKeys = $dbKeys
|
|
}
|
|
$result | ConvertTo-Json -Compress
|