fix(daemon,scanner,crypto): harden lifecycle, widen Windows page scan, fix SQLCipher short read (#54)

- daemon: write pid file only after IPC bound; clean sock+pid on normal return
- transport: PidFile JSON metadata + identity verification (ps/QueryFullProcessImageNameW); SIGTERM with poll-timeout; backward-compat read for plain-text pid
- daemon_cmd: status/stop work with both new JSON and legacy plain-text pid file
- config: cwd → exe_dir → ~/.wx-cli config precedence matches `wx init` write order; Windows DB auto-detect picks newest by latest mtime
- crypto: full_decrypt uses read_exact for intermediate pages, zero-pads only the final partial page; tests cover short-chunk reads and early EOF
- scanner/windows: page protect check covers PAGE_READWRITE / PAGE_WRITECOPY / PAGE_EXECUTE_*WRITE* with modifier-bit stripping

Cross-reviewed by @wx-cli-coder. Windows verified via `cargo check --target x86_64-pc-windows-gnu` (no Windows runtime test).
This commit is contained in:
jakevin
2026-05-14 17:11:42 +08:00
committed by GitHub
parent d4587b1c68
commit 70aa3a44e3
6 changed files with 601 additions and 206 deletions
+32 -27
View File
@@ -5,19 +5,19 @@
/// - OpenProcess: 获取进程句柄(需要 PROCESS_VM_READ | PROCESS_QUERY_INFORMATION)
/// - VirtualQueryEx: 枚举内存区域
/// - ReadProcessMemory: 读取内存内容
use anyhow::{bail, Context, Result};
use anyhow::{Context, Result};
use std::path::Path;
use windows::Win32::Foundation::{CloseHandle, HANDLE};
use windows::Win32::System::Diagnostics::Debug::ReadProcessMemory;
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,
VirtualQueryEx, MEMORY_BASIC_INFORMATION, MEM_COMMIT, PAGE_EXECUTE_READWRITE,
PAGE_EXECUTE_WRITECOPY, PAGE_GUARD, PAGE_NOCACHE, PAGE_READWRITE, PAGE_WRITECOMBINE,
PAGE_WRITECOPY,
};
use windows::Win32::System::Threading::{
OpenProcess, PROCESS_QUERY_INFORMATION, PROCESS_VM_READ,
};
use windows::Win32::System::Diagnostics::Debug::ReadProcessMemory;
use windows::Win32::System::Threading::{OpenProcess, PROCESS_QUERY_INFORMATION, PROCESS_VM_READ};
use super::{collect_db_salts, KeyEntry};
@@ -27,9 +27,7 @@ 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 snap = unsafe { CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0).ok()? };
let mut entry = PROCESSENTRY32 {
dwSize: std::mem::size_of::<PROCESSENTRY32>() as u32,
@@ -43,8 +41,8 @@ fn find_wechat_pid() -> Option<u32> {
return None;
}
loop {
let name = std::ffi::CStr::from_ptr(entry.szExeFile.as_ptr() as *const i8)
.to_string_lossy();
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);
@@ -60,8 +58,7 @@ fn find_wechat_pid() -> Option<u32> {
}
pub fn scan_keys(db_dir: &Path) -> Result<Vec<KeyEntry>> {
let pid = find_wechat_pid()
.context("找不到 Weixin.exe 进程,请确认微信正在运行")?;
let pid = find_wechat_pid().context("找不到 Weixin.exe 进程,请确认微信正在运行")?;
eprintln!("WeChat PID: {}", pid);
// SAFETY: OpenProcess 请求读取权限
@@ -78,7 +75,9 @@ pub fn scan_keys(db_dir: &Path) -> Result<Vec<KeyEntry>> {
eprintln!("找到 {} 个候选密钥", raw_keys.len());
// SAFETY: 关闭进程句柄
unsafe { let _ = CloseHandle(process); }
unsafe {
let _ = CloseHandle(process);
}
let mut entries = Vec::new();
for (key_hex, salt_hex) in &raw_keys {
@@ -119,8 +118,9 @@ fn scan_memory(process: HANDLE) -> Result<Vec<(String, String)>> {
let region_size = mbi.RegionSize;
let base = mbi.BaseAddress as usize;
// 只扫描已提交的可读写页面
if mbi.State == MEM_COMMIT && mbi.Protect == PAGE_READWRITE {
// 只扫描已提交的可读可写页面。Windows 的保护位可能带 modifier bits,
// 也可能是 WRITECOPY / EXECUTE_READWRITE 这种同样可读可写的保护类型。
if mbi.State == MEM_COMMIT && is_writable_readable_page(mbi.Protect.0) {
scan_region(process, base, region_size, &mut results);
}
@@ -133,12 +133,18 @@ fn scan_memory(process: HANDLE) -> Result<Vec<(String, String)>> {
Ok(results)
}
fn scan_region(
process: HANDLE,
base: usize,
size: usize,
results: &mut Vec<(String, String)>,
) {
fn is_writable_readable_page(protect: u32) -> bool {
let base = protect & !(PAGE_GUARD.0 | PAGE_NOCACHE.0 | PAGE_WRITECOMBINE.0);
matches!(
base,
x if x == PAGE_READWRITE.0
|| x == PAGE_WRITECOPY.0
|| x == PAGE_EXECUTE_READWRITE.0
|| x == PAGE_EXECUTE_WRITECOPY.0
)
}
fn scan_region(process: HANDLE, base: usize, size: usize, results: &mut Vec<(String, String)>) {
let overlap = HEX_PATTERN_LEN + 3;
let mut offset = 0usize;
@@ -159,7 +165,8 @@ fn scan_region(
buf.as_mut_ptr() as *mut _,
chunk_size,
Some(&mut bytes_read),
).is_ok()
)
.is_ok()
};
if ok && bytes_read > 0 {
@@ -203,10 +210,8 @@ fn search_pattern(buf: &[u8], results: &mut Vec<(String, String)>) {
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 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));