mirror of
https://github.com/jackwener/wx-cli.git
synced 2026-10-08 16:45:45 +00:00
feat: Rust 完整重写 wx-cli(单一二进制,支持 macOS/Linux/Windows)
实现所有核心模块: - src/crypto/: SQLCipher 4 页解密 + WAL 应用(AES-256-CBC) - src/scanner/: 三平台内存扫描(macOS Mach VM / Linux /proc/mem / Windows ReadProcessMemory) - src/daemon/: tokio 异步 daemon,Unix socket IPC,mtime-aware DB 缓存,WAL 监听推送 - src/cli/: clap CLI,自动启动 daemon,完整命令实现 - src/config.rs: 跨平台配置加载,兼容 Python 版 config.json 格式 - src/ipc.rs: 换行符分隔 JSON 协议,与 Python 版兼容 - .github/workflows/release.yml: 四平台自动构建发布 cargo build --release 验证通过,生成 4.8MB macOS arm64 单一二进制
This commit is contained in:
@@ -0,0 +1,187 @@
|
||||
/// Linux WeChat 进程内存密钥扫描器
|
||||
///
|
||||
/// 通过 /proc/<pid>/maps 枚举内存区域,
|
||||
/// 通过 /proc/<pid>/mem 读取内存内容,
|
||||
/// 搜索 x'<64hex><32hex>' 格式的 SQLCipher 密钥
|
||||
use anyhow::{bail, Context, Result};
|
||||
use std::io::{Read, Seek, SeekFrom};
|
||||
use std::path::Path;
|
||||
|
||||
use super::{collect_db_salts, KeyEntry};
|
||||
|
||||
const HEX_PATTERN_LEN: usize = 96;
|
||||
const CHUNK_SIZE: usize = 2 * 1024 * 1024;
|
||||
|
||||
/// 查找 WeChat 进程 PID
|
||||
fn find_wechat_pid() -> Option<u32> {
|
||||
let proc_dir = std::fs::read_dir("/proc").ok()?;
|
||||
for entry in proc_dir.flatten() {
|
||||
let name = entry.file_name();
|
||||
let name_str = name.to_string_lossy();
|
||||
// 只处理数字目录(PID)
|
||||
if !name_str.chars().all(|c| c.is_ascii_digit()) {
|
||||
continue;
|
||||
}
|
||||
let comm_path = format!("/proc/{}/comm", name_str);
|
||||
if let Ok(comm) = std::fs::read_to_string(&comm_path) {
|
||||
let comm = comm.trim().to_lowercase();
|
||||
if comm == "wechat" || comm == "weixin" {
|
||||
if let Ok(pid) = name_str.parse::<u32>() {
|
||||
return Some(pid);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
/// 解析 /proc/<pid>/maps 文件,返回可读的内存区域 (start, end)
|
||||
fn parse_maps(pid: u32) -> Result<Vec<(u64, u64)>> {
|
||||
let maps_path = format!("/proc/{}/maps", pid);
|
||||
let content = std::fs::read_to_string(&maps_path)
|
||||
.with_context(|| format!("读取 {} 失败", maps_path))?;
|
||||
|
||||
let mut regions = Vec::new();
|
||||
for line in content.lines() {
|
||||
// 格式: start-end perms offset dev inode pathname
|
||||
let parts: Vec<&str> = line.splitn(2, ' ').collect();
|
||||
if parts.len() < 2 {
|
||||
continue;
|
||||
}
|
||||
let perms = parts[1].trim_start();
|
||||
// 只选取 r 和 w 权限的区域
|
||||
if !perms.starts_with("rw") {
|
||||
continue;
|
||||
}
|
||||
let addr_parts: Vec<&str> = parts[0].splitn(2, '-').collect();
|
||||
if addr_parts.len() != 2 {
|
||||
continue;
|
||||
}
|
||||
if let (Ok(start), Ok(end)) = (
|
||||
u64::from_str_radix(addr_parts[0], 16),
|
||||
u64::from_str_radix(addr_parts[1], 16),
|
||||
) {
|
||||
regions.push((start, end));
|
||||
}
|
||||
}
|
||||
Ok(regions)
|
||||
}
|
||||
|
||||
pub fn scan_keys(db_dir: &Path) -> Result<Vec<KeyEntry>> {
|
||||
let pid = find_wechat_pid()
|
||||
.context("找不到 WeChat 进程,请确认 WeChat 正在运行")?;
|
||||
eprintln!("WeChat PID: {}", pid);
|
||||
|
||||
let db_salts = collect_db_salts(db_dir);
|
||||
eprintln!("找到 {} 个加密数据库", db_salts.len());
|
||||
|
||||
eprintln!("扫描进程内存...");
|
||||
let regions = parse_maps(pid)?;
|
||||
eprintln!("找到 {} 个可读写内存区域", regions.len());
|
||||
|
||||
let mem_path = format!("/proc/{}/mem", pid);
|
||||
let mut mem_file = std::fs::File::open(&mem_path)
|
||||
.with_context(|| format!("打开 {} 失败,请以 root 权限运行", mem_path))?;
|
||||
|
||||
let mut raw_keys: Vec<(String, String)> = Vec::new();
|
||||
for (start, end) in ®ions {
|
||||
scan_region(&mut mem_file, *start, *end, &mut raw_keys);
|
||||
}
|
||||
// 去重
|
||||
raw_keys.dedup_by(|a, b| a.0 == b.0 && a.1 == b.1);
|
||||
eprintln!("找到 {} 个候选密钥", raw_keys.len());
|
||||
|
||||
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_region(
|
||||
mem: &mut std::fs::File,
|
||||
start: u64,
|
||||
end: u64,
|
||||
results: &mut Vec<(String, String)>,
|
||||
) {
|
||||
let total_len = (end - start) as usize;
|
||||
let overlap = HEX_PATTERN_LEN + 3;
|
||||
let mut offset = 0usize;
|
||||
|
||||
loop {
|
||||
if offset >= total_len {
|
||||
break;
|
||||
}
|
||||
let chunk_size = std::cmp::min(CHUNK_SIZE, total_len - offset);
|
||||
let addr = start + offset as u64;
|
||||
|
||||
if mem.seek(SeekFrom::Start(addr)).is_err() {
|
||||
break;
|
||||
}
|
||||
let mut buf = vec![0u8; chunk_size];
|
||||
match mem.read(&mut buf) {
|
||||
Ok(n) if n > 0 => {
|
||||
buf.truncate(n);
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,296 @@
|
||||
/// macOS WeChat 进程内存密钥扫描器
|
||||
///
|
||||
/// 翻译自 find_all_keys_macos.c,使用 Mach VM API:
|
||||
/// - task_for_pid: 获取目标进程的 task port(需要 root 权限)
|
||||
/// - mach_vm_region: 枚举内存区域
|
||||
/// - mach_vm_read: 读取内存块
|
||||
///
|
||||
/// 注意:
|
||||
/// 1. 需要以 root (sudo) 运行
|
||||
/// 2. WeChat 需要进行 ad-hoc 签名
|
||||
/// 3. 在内存中搜索 x'<64hex><32hex>' 格式的 SQLCipher 密钥
|
||||
use anyhow::{bail, Context, Result};
|
||||
use std::path::Path;
|
||||
|
||||
use super::{collect_db_salts, KeyEntry};
|
||||
|
||||
// Mach 相关常量
|
||||
const KERN_SUCCESS: i32 = 0;
|
||||
const VM_PROT_READ: i32 = 1;
|
||||
const VM_PROT_WRITE: i32 = 2;
|
||||
const VM_REGION_BASIC_INFO_64: i32 = 9;
|
||||
const CHUNK_SIZE: usize = 2 * 1024 * 1024; // 2MB
|
||||
const HEX_PATTERN_LEN: usize = 96; // 64(key) + 32(salt)
|
||||
|
||||
// vm_region_basic_info_64 结构体
|
||||
#[repr(C)]
|
||||
struct VmRegionBasicInfo64 {
|
||||
protection: i32,
|
||||
max_protection: i32,
|
||||
inheritance: u32,
|
||||
shared: u32,
|
||||
reserved: u32,
|
||||
_offset: u64,
|
||||
behavior: i32,
|
||||
user_wired_count: u16,
|
||||
}
|
||||
|
||||
// Mach FFI 声明
|
||||
#[allow(non_camel_case_types)]
|
||||
type kern_return_t = i32;
|
||||
#[allow(non_camel_case_types)]
|
||||
type mach_port_t = u32;
|
||||
#[allow(non_camel_case_types)]
|
||||
type mach_vm_address_t = u64;
|
||||
#[allow(non_camel_case_types)]
|
||||
type mach_vm_size_t = u64;
|
||||
#[allow(non_camel_case_types)]
|
||||
type mach_msg_type_number_t = u32;
|
||||
#[allow(non_camel_case_types)]
|
||||
type vm_offset_t = usize;
|
||||
#[allow(non_camel_case_types, dead_code)]
|
||||
type vm_prot_t = i32;
|
||||
|
||||
extern "C" {
|
||||
fn mach_task_self() -> mach_port_t;
|
||||
fn task_for_pid(host: mach_port_t, pid: libc::pid_t, task: *mut mach_port_t) -> kern_return_t;
|
||||
fn mach_vm_region(
|
||||
task: mach_port_t,
|
||||
address: *mut mach_vm_address_t,
|
||||
size: *mut mach_vm_size_t,
|
||||
flavor: i32,
|
||||
info: *mut VmRegionBasicInfo64,
|
||||
info_count: *mut mach_msg_type_number_t,
|
||||
obj_name: *mut mach_port_t,
|
||||
) -> kern_return_t;
|
||||
fn mach_vm_read(
|
||||
task: mach_port_t,
|
||||
addr: mach_vm_address_t,
|
||||
size: mach_vm_size_t,
|
||||
data: *mut vm_offset_t,
|
||||
data_cnt: *mut mach_msg_type_number_t,
|
||||
) -> kern_return_t;
|
||||
fn mach_vm_deallocate(
|
||||
task: mach_port_t,
|
||||
addr: mach_vm_address_t,
|
||||
size: mach_vm_size_t,
|
||||
) -> kern_return_t;
|
||||
}
|
||||
|
||||
/// 查找 WeChat 进程的 PID
|
||||
fn find_wechat_pid() -> Option<libc::pid_t> {
|
||||
// 使用 pgrep -x WeChat 查找(与 C 版本一致)
|
||||
let output = std::process::Command::new("pgrep")
|
||||
.args(["-x", "WeChat"])
|
||||
.output()
|
||||
.ok()?;
|
||||
if !output.status.success() {
|
||||
return None;
|
||||
}
|
||||
let s = String::from_utf8_lossy(&output.stdout);
|
||||
s.trim().parse().ok()
|
||||
}
|
||||
|
||||
/// 判断字节是否是 ASCII 十六进制字符
|
||||
#[inline]
|
||||
fn is_hex_char(c: u8) -> bool {
|
||||
c.is_ascii_hexdigit()
|
||||
}
|
||||
|
||||
pub fn scan_keys(db_dir: &Path) -> Result<Vec<KeyEntry>> {
|
||||
// 1. 查找 WeChat PID
|
||||
let pid = find_wechat_pid()
|
||||
.context("找不到 WeChat 进程,请确认 WeChat 正在运行")?;
|
||||
eprintln!("WeChat PID: {}", pid);
|
||||
|
||||
// 2. 获取 task port
|
||||
// SAFETY: task_for_pid 是标准 Mach API,参数合法
|
||||
let task = unsafe {
|
||||
let mut task: mach_port_t = 0;
|
||||
let kr = task_for_pid(mach_task_self(), pid, &mut task);
|
||||
if kr != KERN_SUCCESS {
|
||||
bail!(
|
||||
"task_for_pid 失败 (kr={})\n请确认:(1) 以 root 运行 (2) WeChat 已 ad-hoc 签名",
|
||||
kr
|
||||
);
|
||||
}
|
||||
task
|
||||
};
|
||||
eprintln!("Got task port: {}", task);
|
||||
|
||||
// 3. 收集数据库 salt 映射
|
||||
eprintln!("扫描数据库文件...");
|
||||
let db_salts = collect_db_salts(db_dir);
|
||||
eprintln!("找到 {} 个加密数据库", db_salts.len());
|
||||
|
||||
// 4. 扫描进程内存
|
||||
eprintln!("扫描进程内存寻找密钥...");
|
||||
let raw_keys = scan_memory(task)?;
|
||||
eprintln!("找到 {} 个候选密钥", raw_keys.len());
|
||||
|
||||
// 5. 将密钥与数据库 salt 匹配
|
||||
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)
|
||||
}
|
||||
|
||||
/// 扫描进程内存,返回 (key_hex, salt_hex) 列表
|
||||
fn scan_memory(task: mach_port_t) -> Result<Vec<(String, String)>> {
|
||||
let mut results: Vec<(String, String)> = Vec::new();
|
||||
let mut addr: mach_vm_address_t = 0;
|
||||
|
||||
// VM_REGION_BASIC_INFO_COUNT_64 = sizeof(vm_region_basic_info_64) / sizeof(int32_t)
|
||||
let info_count_expected: mach_msg_type_number_t =
|
||||
(std::mem::size_of::<VmRegionBasicInfo64>() / 4) as u32;
|
||||
|
||||
loop {
|
||||
let mut size: mach_vm_size_t = 0;
|
||||
let mut info = VmRegionBasicInfo64 {
|
||||
protection: 0, max_protection: 0, inheritance: 0,
|
||||
shared: 0, reserved: 0, _offset: 0, behavior: 0, user_wired_count: 0,
|
||||
};
|
||||
let mut info_count: mach_msg_type_number_t = info_count_expected;
|
||||
let mut obj_name: mach_port_t = 0;
|
||||
|
||||
// SAFETY: mach_vm_region 枚举虚拟内存区域,所有参数合法
|
||||
let kr = unsafe {
|
||||
mach_vm_region(
|
||||
task,
|
||||
&mut addr,
|
||||
&mut size,
|
||||
VM_REGION_BASIC_INFO_64,
|
||||
&mut info,
|
||||
&mut info_count,
|
||||
&mut obj_name,
|
||||
)
|
||||
};
|
||||
|
||||
if kr != KERN_SUCCESS {
|
||||
break;
|
||||
}
|
||||
if size == 0 {
|
||||
addr = addr.saturating_add(1);
|
||||
continue;
|
||||
}
|
||||
|
||||
// 只扫描可读可写区域(密钥通常存在于堆内存)
|
||||
if (info.protection & (VM_PROT_READ | VM_PROT_WRITE)) == (VM_PROT_READ | VM_PROT_WRITE) {
|
||||
scan_region(task, addr, size, &mut results);
|
||||
}
|
||||
|
||||
addr = addr.saturating_add(size);
|
||||
}
|
||||
|
||||
// 去重
|
||||
results.dedup_by(|a, b| a.0 == b.0 && a.1 == b.1);
|
||||
Ok(results)
|
||||
}
|
||||
|
||||
/// 扫描单个内存区域,按 CHUNK_SIZE 分块读取
|
||||
fn scan_region(
|
||||
task: mach_port_t,
|
||||
addr: mach_vm_address_t,
|
||||
size: mach_vm_size_t,
|
||||
results: &mut Vec<(String, String)>,
|
||||
) {
|
||||
let end = addr + size;
|
||||
let mut ca = addr;
|
||||
|
||||
while ca < end {
|
||||
let cs = std::cmp::min(end - ca, CHUNK_SIZE as u64);
|
||||
|
||||
let mut data: vm_offset_t = 0;
|
||||
let mut dc: mach_msg_type_number_t = 0;
|
||||
|
||||
// SAFETY: mach_vm_read 读取目标进程内存到内核缓冲区,
|
||||
// 返回的 data 指针指向通过 vm_allocate 分配的内存,
|
||||
// 必须用 mach_vm_deallocate 释放
|
||||
let kr = unsafe {
|
||||
mach_vm_read(task, ca, cs, &mut data, &mut dc)
|
||||
};
|
||||
|
||||
if kr == KERN_SUCCESS {
|
||||
// SAFETY: data 是 mach_vm_read 返回的有效指针,dc 是字节数
|
||||
let buf: &[u8] = unsafe {
|
||||
std::slice::from_raw_parts(data as *const u8, dc as usize)
|
||||
};
|
||||
|
||||
search_pattern(buf, results);
|
||||
|
||||
// SAFETY: 释放 mach_vm_read 分配的内核内存
|
||||
unsafe {
|
||||
mach_vm_deallocate(mach_task_self(), data as u64, dc as u64);
|
||||
}
|
||||
}
|
||||
|
||||
// 保留 (HEX_PATTERN_LEN + 3) 字节重叠以处理跨块边界的模式
|
||||
let overlap = HEX_PATTERN_LEN + 3;
|
||||
if cs as usize > overlap {
|
||||
ca += cs - overlap as u64;
|
||||
} else {
|
||||
ca += cs;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 在缓冲区中搜索 x'<96个十六进制字符>' 模式
|
||||
///
|
||||
/// 格式:x'<64hex(key)><32hex(salt)>'(总计 99 字节)
|
||||
fn search_pattern(buf: &[u8], results: &mut Vec<(String, String)>) {
|
||||
let total = HEX_PATTERN_LEN + 3; // x' + 96 hex + '
|
||||
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;
|
||||
}
|
||||
|
||||
// 验证后续 96 字节都是十六进制字符
|
||||
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;
|
||||
}
|
||||
|
||||
// 提取 key_hex 和 salt_hex,统一转小写
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
use anyhow::Result;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::path::Path;
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
mod macos;
|
||||
#[cfg(target_os = "linux")]
|
||||
mod linux;
|
||||
#[cfg(target_os = "windows")]
|
||||
mod windows;
|
||||
|
||||
/// 扫描到的一条密钥记录
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct KeyEntry {
|
||||
/// 相对路径,如 "message/message_0.db"
|
||||
pub db_name: String,
|
||||
/// 32字节 AES 密钥(hex)
|
||||
pub enc_key: String,
|
||||
/// 16字节 salt(hex,来自数据库文件头)
|
||||
pub salt: String,
|
||||
}
|
||||
|
||||
/// 从进程内存中扫描所有 SQLCipher 密钥
|
||||
///
|
||||
/// 需要以 root/Administrator 权限运行
|
||||
pub fn scan_keys(db_dir: &Path) -> Result<Vec<KeyEntry>> {
|
||||
#[cfg(target_os = "macos")]
|
||||
return macos::scan_keys(db_dir);
|
||||
#[cfg(target_os = "linux")]
|
||||
return linux::scan_keys(db_dir);
|
||||
#[cfg(target_os = "windows")]
|
||||
return windows::scan_keys(db_dir);
|
||||
#[cfg(not(any(target_os = "macos", target_os = "linux", target_os = "windows")))]
|
||||
{
|
||||
anyhow::bail!("当前平台不支持自动密钥扫描")
|
||||
}
|
||||
}
|
||||
|
||||
/// 读取 DB 文件前 16 字节作为 salt(hex),如果是明文 SQLite 则返回 None
|
||||
pub fn read_db_salt(path: &Path) -> Option<String> {
|
||||
let mut buf = [0u8; 16];
|
||||
let mut f = std::fs::File::open(path).ok()?;
|
||||
use std::io::Read;
|
||||
f.read_exact(&mut buf).ok()?;
|
||||
// 明文 SQLite:头部是 "SQLite format 3"
|
||||
if &buf[..15] == b"SQLite format 3" {
|
||||
return None;
|
||||
}
|
||||
Some(hex::encode(&buf))
|
||||
}
|
||||
|
||||
/// 遍历 db_dir,收集所有 .db 文件的 salt -> 相对路径 映射
|
||||
pub fn collect_db_salts(db_dir: &Path) -> Vec<(String, String)> {
|
||||
let mut result = Vec::new();
|
||||
collect_recursive(db_dir, db_dir, &mut result);
|
||||
result
|
||||
}
|
||||
|
||||
fn collect_recursive(base: &Path, dir: &Path, out: &mut Vec<(String, String)>) {
|
||||
let entries = match std::fs::read_dir(dir) {
|
||||
Ok(e) => e,
|
||||
Err(_) => return,
|
||||
};
|
||||
for entry in entries.flatten() {
|
||||
let path = entry.path();
|
||||
if path.is_dir() {
|
||||
collect_recursive(base, &path, out);
|
||||
} else if path.extension().map(|e| e == "db").unwrap_or(false) {
|
||||
if let Some(salt) = read_db_salt(&path) {
|
||||
if let Ok(rel) = path.strip_prefix(base) {
|
||||
let rel_str = rel.to_string_lossy().replace('\\', "/");
|
||||
out.push((salt, rel_str));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// hex encoding helper (avoid adding hex crate by implementing inline)
|
||||
mod hex {
|
||||
pub fn encode(bytes: &[u8]) -> String {
|
||||
bytes.iter().map(|b| format!("{:02x}", b)).collect()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,217 @@
|
||||
/// 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
|
||||
}
|
||||
}
|
||||
|
||||
results.dedup_by(|a, b| a.0 == b.0 && a.1 == b.1);
|
||||
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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user