wx-cli/src/scanner/windows.rs

217 lines
6.3 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
/// - CreateToolhelp32Snapshot + Process32Next: 枚举进程找 Weixin.exe
/// - OpenProcess: 获取进程句柄(需要 PROCESS_VM_READ | PROCESS_QUERY_INFORMATION
/// - VirtualQueryEx: 枚举内存区域
/// - ReadProcessMemory: 读取内存内容
use anyhow::{bail, Context, Result};
use std::path::Path;
use windows::Win32::Foundation::{CloseHandle, HANDLE};
use windows::Win32::System::Diagnostics::ToolHelp::{
CreateToolhelp32Snapshot, Process32First, Process32Next, PROCESSENTRY32, TH32CS_SNAPPROCESS,
};
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};
const HEX_PATTERN_LEN: usize = 96;
const CHUNK_SIZE: usize = 2 * 1024 * 1024;
/// 查找 Weixin.exe 进程 PID
fn find_wechat_pid() -> Option<u32> {
// SAFETY: CreateToolhelp32Snapshot 标准 Windows API
let snap = unsafe {
CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0).ok()?
};
let mut entry = PROCESSENTRY32 {
dwSize: std::mem::size_of::<PROCESSENTRY32>() as u32,
..Default::default()
};
// SAFETY: Process32First/Process32Next 标准快照遍历
unsafe {
if Process32First(snap, &mut entry).is_err() {
let _ = CloseHandle(snap);
return None;
}
loop {
let name = std::ffi::CStr::from_ptr(entry.szExeFile.as_ptr() as *const i8)
.to_string_lossy();
if name.eq_ignore_ascii_case("Weixin.exe") {
let pid = entry.th32ProcessID;
let _ = CloseHandle(snap);
return Some(pid);
}
if Process32Next(snap, &mut entry).is_err() {
break;
}
}
let _ = CloseHandle(snap);
}
None
}
pub fn scan_keys(db_dir: &Path) -> Result<Vec<KeyEntry>> {
let pid = find_wechat_pid()
.context("找不到 Weixin.exe 进程,请确认微信正在运行")?;
eprintln!("WeChat PID: {}", pid);
// SAFETY: OpenProcess 请求读取权限
let process = unsafe {
OpenProcess(PROCESS_VM_READ | PROCESS_QUERY_INFORMATION, false, pid)
.context("OpenProcess 失败,请以管理员权限运行")?
};
let db_salts = collect_db_salts(db_dir);
eprintln!("找到 {} 个加密数据库", db_salts.len());
eprintln!("扫描进程内存...");
let raw_keys = scan_memory(process)?;
eprintln!("找到 {} 个候选密钥", raw_keys.len());
// SAFETY: 关闭进程句柄
unsafe { let _ = CloseHandle(process); }
let mut entries = Vec::new();
for (key_hex, salt_hex) in &raw_keys {
for (db_salt, db_name) in &db_salts {
if salt_hex == db_salt {
entries.push(KeyEntry {
db_name: db_name.clone(),
enc_key: key_hex.clone(),
salt: salt_hex.clone(),
});
break;
}
}
}
eprintln!("匹配到 {}/{} 个密钥", entries.len(), raw_keys.len());
Ok(entries)
}
fn scan_memory(process: HANDLE) -> Result<Vec<(String, String)>> {
let mut results: Vec<(String, String)> = Vec::new();
let mut addr: usize = 0;
loop {
let mut mbi = MEMORY_BASIC_INFORMATION::default();
// SAFETY: VirtualQueryEx 枚举进程内存区域
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;
let base = mbi.BaseAddress as usize;
// 只扫描已提交的可读写页面
if mbi.State == MEM_COMMIT && mbi.Protect == PAGE_READWRITE {
scan_region(process, base, region_size, &mut results);
}
addr = base.saturating_add(region_size);
if addr == 0 {
break; // overflow
}
}
Ok(results)
}
fn scan_region(
process: HANDLE,
base: usize,
size: usize,
results: &mut Vec<(String, String)>,
) {
let overlap = HEX_PATTERN_LEN + 3;
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;
// SAFETY: ReadProcessMemory 读取目标进程内存
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 search_pattern(buf: &[u8], results: &mut Vec<(String, String)>) {
let total = HEX_PATTERN_LEN + 3;
if buf.len() < total {
return;
}
let mut i = 0;
while i + total <= buf.len() {
if buf[i] != b'x' || buf[i + 1] != b'\'' {
i += 1;
continue;
}
let hex_start = i + 2;
let all_hex = buf[hex_start..hex_start + HEX_PATTERN_LEN]
.iter()
.all(|&c| is_hex_char(c));
if !all_hex {
i += 1;
continue;
}
if buf[hex_start + HEX_PATTERN_LEN] != b'\'' {
i += 1;
continue;
}
let key_hex = String::from_utf8_lossy(&buf[hex_start..hex_start + 64])
.to_lowercase();
let salt_hex = String::from_utf8_lossy(&buf[hex_start + 64..hex_start + 96])
.to_lowercase();
let is_dup = results.iter().any(|(k, s)| k == &key_hex && s == &salt_hex);
if !is_dup {
results.push((key_hex, salt_hex));
}
i += total;
}
}