/// 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 { // SAFETY: CreateToolhelp32Snapshot 标准 Windows API let snap = unsafe { CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0).ok()? }; let mut entry = PROCESSENTRY32 { dwSize: std::mem::size_of::() 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> { 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> { 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::(), ) }; 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; } }