mirror of https://github.com/jackwener/wx-cli.git
217 lines
6.3 KiB
Rust
217 lines
6.3 KiB
Rust
/// 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;
|
||
}
|
||
}
|