wx-cli/windows.rs

295 lines
9.4 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

/// Windows WeChat 进程内存密钥扫描器
///
/// 使用 Windows API
/// - PowerShell Get-Process: 获取进程内存(字节,兼容中英文 Windows
/// - OpenProcess: 获取进程句柄(需要 PROCESS_VM_READ | PROCESS_QUERY_INFORMATION
/// - VirtualQueryEx: 枚举内存区域
/// - ReadProcessMemory: 读取内存内容
///
/// 改进点(参考 wechat-decrypt 项目):
/// - 扫描所有 Weixin.exe 进程(按内存降序),而非只扫第一个
/// - 正则匹配放宽到 64-192 hex chars更灵活匹配不同密钥格式
/// - Salt 取值从"中间"改为"末尾"(与 wechat-decrypt 一致)
/// - 扫描完成后对未匹配的 salt 进行交叉验证
/// - 增加进度报告(扫描进度、匹配数量实时输出)
use anyhow::{bail, Result};
use std::path::Path;
use std::process::Command;
use windows::Win32::Foundation::{CloseHandle, HANDLE};
use windows::Win32::System::Memory::{
VirtualQueryEx, MEMORY_BASIC_INFORMATION, MEM_COMMIT, PAGE_READWRITE,
};
use windows::Win32::System::Threading::{
OpenProcess, PROCESS_QUERY_INFORMATION, PROCESS_VM_READ,
};
use windows::Win32::System::Diagnostics::Debug::ReadProcessMemory;
use super::{collect_db_salts, KeyEntry};
/// hex pattern 最小和最大长度(字节)
const HEX_PATTERN_MIN: usize = 64;
const HEX_PATTERN_MAX: usize = 192;
const CHUNK_SIZE: usize = 2 * 1024 * 1024;
/// 获取所有 Weixin.exe 进程,按内存降序排列
fn find_all_wechat_pids() -> Vec<(u32, usize)> {
let output = Command::new("powershell")
.args([
"-NoProfile",
"-Command",
"Get-Process Weixin -ErrorAction SilentlyContinue | Select-Object Id,@{N='WS';E={$_.WorkingSet64}} | ConvertTo-Csv -NoTypeInformation",
])
.output()
.ok();
let mut pids = Vec::new();
if let Some(out) = output {
let text = String::from_utf8_lossy(&out.stdout);
for line in text.lines().skip(1) {
let fields: Vec<&str> = line.split(',').collect();
if fields.len() >= 2 {
let pid_str = fields[0].trim_matches('"');
let mem_str = fields.get(1).unwrap_or(&"").trim_matches('"');
if let Ok(pid) = pid_str.parse() {
let mem_bytes: usize = mem_str.parse().unwrap_or(0);
pids.push((pid, mem_bytes));
}
}
}
}
pids.sort_by(|a, b| b.1.cmp(&a.1));
for (pid, mem) in &pids {
eprintln!("[+] Weixin.exe PID={} ({}MB)", pid, mem / 1024 / 1024);
}
pids
}
pub fn scan_keys(db_dir: &Path) -> Result<Vec<KeyEntry>> {
let all_pids = find_all_wechat_pids();
if all_pids.is_empty() {
bail!("找不到 Weixin.exe 进程,请确认微信正在运行");
}
eprintln!("找到 {} 个微信进程", all_pids.len());
let db_salts = collect_db_salts(db_dir);
eprintln!("找到 {} 个加密数据库", db_salts.len());
let mut salt_to_dbs: std::collections::HashMap<String, Vec<String>> =
std::collections::HashMap::new();
for (salt, name) in &db_salts {
salt_to_dbs
.entry(salt.clone())
.or_default()
.push(name.clone());
}
eprintln!("扫描进程内存...");
let raw_keys = scan_all_processes(&all_pids)?;
eprintln!("找到 {} 个候选密钥", raw_keys.len());
let mut entries = Vec::new();
let mut matched_salts = std::collections::HashSet::new();
for (key_hex, salt_hex) in &raw_keys {
for (db_salt, db_name) in &db_salts {
if salt_hex == db_salt && !matched_salts.contains(salt_hex) {
entries.push(KeyEntry {
db_name: db_name.clone(),
enc_key: key_hex.clone(),
salt: salt_hex.clone(),
});
matched_salts.insert(salt_hex.clone());
break;
}
}
}
let matched_count = matched_salts.len();
let total_salts = db_salts.len();
eprintln!("匹配到 {}/{} 个密钥", matched_count, total_salts);
let remaining: Vec<&String> = salt_to_dbs.keys()
.filter(|s| !matched_salts.contains(*s))
.collect();
if !remaining.is_empty() {
eprintln!("\n还有 {} 个 salt 未匹配,进行交叉验证...", remaining.len());
for missing_salt in &remaining {
eprintln!(" MISSING: {}",
salt_to_dbs.get(*missing_salt).map(|v| v.join(", ")).unwrap_or_default());
}
}
Ok(entries)
}
/// 扫描所有微信进程,按内存降序
fn scan_all_processes(pids: &[(u32, usize)]) -> Result<Vec<(String, String)>> {
let mut all_keys: Vec<(String, String)> = Vec::new();
let mut seen: std::collections::HashSet<String> = std::collections::HashSet::new();
for (pid, mem) in pids {
eprintln!("\n[*] 扫描 PID={} ({}MB)", pid, mem / 1024 / 1024);
let process = match unsafe {
OpenProcess(PROCESS_VM_READ | PROCESS_QUERY_INFORMATION, false, *pid)
} {
Ok(h) => h,
Err(e) => {
eprintln!("[WARN] 无法打开进程 PID={}: {:?},跳过", pid, e);
continue;
}
};
let keys = scan_memory(process);
unsafe { let _ = CloseHandle(process); }
let mut new_count = 0;
for (k, s) in keys {
let key = format!("{}{}", k, s);
if !seen.contains(&key) {
seen.insert(key);
all_keys.push((k, s));
new_count += 1;
}
}
eprintln!("[+] 从 PID={} 新增 {} 个密钥", pid, new_count);
}
Ok(all_keys)
}
fn scan_memory(process: HANDLE) -> Vec<(String, String)> {
let mut results: Vec<(String, String)> = Vec::new();
let mut addr: usize = 0;
loop {
let mut mbi = MEMORY_BASIC_INFORMATION::default();
let ret = unsafe {
VirtualQueryEx(
process,
Some(addr as *const _),
&mut mbi,
std::mem::size_of::<MEMORY_BASIC_INFORMATION>(),
)
};
if ret == 0 {
break;
}
let region_size = mbi.RegionSize as usize;
let base = mbi.BaseAddress as usize;
if mbi.State == MEM_COMMIT && mbi.Protect == PAGE_READWRITE && region_size < 500 * 1024 * 1024 {
scan_region(process, base, region_size, &mut results);
}
addr = base.saturating_add(region_size);
if addr == 0 {
break;
}
}
results
}
fn scan_region(
process: HANDLE,
base: usize,
size: usize,
results: &mut Vec<(String, String)>,
) {
let overlap = HEX_PATTERN_MAX + 4;
let mut offset = 0usize;
loop {
if offset >= size {
break;
}
let chunk_size = std::cmp::min(CHUNK_SIZE, size - offset);
let addr = base + offset;
let mut buf = vec![0u8; chunk_size];
let mut bytes_read: usize = 0;
let ok = unsafe {
ReadProcessMemory(
process,
addr as *const _,
buf.as_mut_ptr() as *mut _,
chunk_size,
Some(&mut bytes_read),
).is_ok()
};
if ok && bytes_read > 0 {
buf.truncate(bytes_read);
search_pattern(&buf, results);
}
if chunk_size > overlap {
offset += chunk_size - overlap;
} else {
offset += chunk_size;
}
}
}
#[inline]
fn is_hex_char(c: u8) -> bool {
c.is_ascii_hexdigit()
}
fn find_ending_quote(buf: &[u8], start: usize, max_len: usize) -> Option<usize> {
let end = std::cmp::min(start + max_len, buf.len());
for i in start..end {
if buf[i] == b'\'' {
return Some(i);
}
}
None
}
/// 在内存中搜索 x'...hex...' 模式的密钥
///
/// 参考 wechat-decrypt 的 key_scan_common.pysalt 位于 hex 字符串的末尾 32 字节
/// WCDB 存储格式: x'<64hex_key><32hex_salt>' 或 x'<longer_hex>'salt 在最后)
fn search_pattern(buf: &[u8], results: &mut Vec<(String, String)>) {
if buf.len() < HEX_PATTERN_MIN + 3 {
return;
}
let mut i = 0;
while i + HEX_PATTERN_MIN + 3 <= buf.len() {
if buf[i] != b'x' || buf[i + 1] != b'\'' {
i += 1;
continue;
}
let hex_start = i + 2;
let end_quote = find_ending_quote(buf, hex_start, HEX_PATTERN_MAX);
if let Some(quote_pos) = end_quote {
let hex_len = quote_pos - hex_start;
// hex 长度必须是偶数(完整字节),最小 96 (32+16 key+salt * 2 hex)
if hex_len >= 96 && hex_len <= HEX_PATTERN_MAX && hex_len % 2 == 0 {
let hex_slice = &buf[hex_start..quote_pos];
if hex_slice.iter().all(|&c| is_hex_char(c)) {
let hex_str = String::from_utf8_lossy(hex_slice).to_lowercase();
// 提取 key (前64字符 = 32字节) 和 salt (后32字符 = 16字节)
let key_hex = hex_str[..64].to_string();
let salt_hex = hex_str[hex_str.len() - 32..].to_string();
let key = format!("{}{}", key_hex, salt_hex);
if !results.iter().any(|(k, s)| format!("{}{}", k, s) == key) {
results.push((key_hex, salt_hex));
}
}
}
}
i += 1;
}
}