wx-cli/src/scanner/macos.rs

491 lines
16 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.

/// 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 密钥
///
/// v1.1: 支持扫描 WeChat 主进程 + WeChatAppEx Helper 进程。
/// 微信 3.8+ 将数据库操作分离到 WeChatAppEx两个进程都需要扫描。
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
/// 微信 3.8+ 将数据库操作分离到 WeChatAppEx Helper 进程,
/// 需要扫描主 WeChat 进程和所有 WeChatAppEx 进程才能找到密钥。
fn find_wechat_pids() -> Option<Vec<libc::pid_t>> {
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);
let pids: Vec<libc::pid_t> = s
.lines()
.filter_map(|l| l.trim().parse().ok())
.collect();
if pids.is_empty() {
None
} else {
Some(pids)
}
}
/// 判断字节是否是 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 相关进程
let pids = find_wechat_pids()
.context("找不到 WeChat 进程,请确认 WeChat 正在运行")?;
eprintln!("找到 {} 个 WeChat 相关进程: {:?}", pids.len(), pids);
// 2. 收集数据库 salt 映射
eprintln!("扫描数据库文件...");
let db_salts = collect_db_salts(db_dir);
eprintln!("找到 {} 个加密数据库", db_salts.len());
// 3. 扫描所有进程内存,收集所有候选密钥
let mut all_raw_keys: Vec<(String, String)> = Vec::new();
for pid in &pids {
eprintln!("扫描进程 {} 内存...", pid);
match scan_single_process(*pid) {
Ok(keys) => {
eprintln!(" 进程 {}: 找到 {} 个候选密钥", pid, keys.len());
all_raw_keys.extend(keys);
}
Err(e) => {
eprintln!(" 进程 {}: 跳过 ({})", pid, e);
}
}
}
// 去重(跨进程可能找到相同密钥)
all_raw_keys.sort_by(|a, b| a.0.cmp(&b.0).then_with(|| a.1.cmp(&b.1)));
all_raw_keys.dedup();
eprintln!("候选密钥去重后: {}", all_raw_keys.len());
// 4. 将密钥与数据库 salt 匹配
let mut entries = Vec::new();
for (key_hex, salt_hex) in &all_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(), all_raw_keys.len());
Ok(entries)
}
/// 获取单个进程的 task port
fn get_task_port(pid: libc::pid_t) -> Result<mach_port_t> {
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\
\n\
1. 对 WeChat 重新签名(只需做一次):\n\
codesign --force --deep --sign - /Applications/WeChat.app\n\
\n\
2. 重启 WeChat\n\
killall WeChat && open /Applications/WeChat.app\n\
\n\
3. 再次运行(需要 root\n\
sudo wx init\n\
\n\
如果 codesign 报 \"signature in use\",先执行:\n\
codesign --remove-signature /Applications/WeChat.app/Contents/Frameworks/vlc_plugins/librtp_mpeg4_plugin.dylib\n\
codesign --force --deep --sign - /Applications/WeChat.app",
kr
);
}
Ok(task)
}
}
/// 扫描单个进程的内存
fn scan_single_process(pid: libc::pid_t) -> Result<Vec<(String, String)>> {
let task = get_task_port(pid)?;
eprintln!(" Got task port: {}", task);
scan_memory(task)
}
/// 扫描进程内存,返回 (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 = 9来自 <mach/vm_region.h>,固定值,不能用 sizeof 计算)
let info_count_expected: mach_msg_type_number_t = 9;
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);
}
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 字节)
pub(crate) 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;
}
}
#[cfg(test)]
mod tests {
use super::*;
/// 构造一条合法的 x'<key><salt>' 模式字节串
fn make_pattern(key: &[u8; 64], salt: &[u8; 32]) -> Vec<u8> {
let mut v = vec![b'x', b'\''];
v.extend_from_slice(key);
v.extend_from_slice(salt);
v.push(b'\'');
v
}
#[test]
fn test_is_hex_char_valid() {
for c in b'0'..=b'9' { assert!(is_hex_char(c), "digit {}", c as char); }
for c in b'a'..=b'f' { assert!(is_hex_char(c), "lower {}", c as char); }
for c in b'A'..=b'F' { assert!(is_hex_char(c), "upper {}", c as char); }
}
#[test]
fn test_is_hex_char_invalid() {
for c in [b'g', b'G', b'x', b'\'', b' ', b'\0', b'z', b'Z'] {
assert!(!is_hex_char(c), "expected non-hex: {}", c as char);
}
}
#[test]
fn test_search_pattern_basic() {
let key = [b'a'; 64];
let salt = [b'b'; 32];
let buf = make_pattern(&key, &salt);
let mut results = Vec::new();
search_pattern(&buf, &mut results);
assert_eq!(results.len(), 1);
assert_eq!(results[0].0, "a".repeat(64));
assert_eq!(results[0].1, "b".repeat(32));
}
#[test]
fn test_search_pattern_uppercase_lowercased() {
// 大写十六进制字符应被统一转为小写
let key = [b'A'; 64];
let salt = [b'B'; 32];
let buf = make_pattern(&key, &salt);
let mut results = Vec::new();
search_pattern(&buf, &mut results);
assert_eq!(results.len(), 1);
assert_eq!(results[0].0, "a".repeat(64));
assert_eq!(results[0].1, "b".repeat(32));
}
#[test]
fn test_search_pattern_not_all_hex() {
// 96 个十六进制字符中有一个非法字符 → 不匹配
let mut buf = vec![b'x', b'\''];
buf.extend_from_slice(&[b'a'; 95]);
buf.push(b'g'); // 'g' 不是合法十六进制字符
buf.push(b'\'');
let mut results = Vec::new();
search_pattern(&buf, &mut results);
assert!(results.is_empty());
}
#[test]
fn test_search_pattern_wrong_closing_quote() {
// 结尾引号错误 → 不匹配
let mut buf = vec![b'x', b'\''];
buf.extend_from_slice(&[b'a'; 96]);
buf.push(b'"'); // 应为 b'\''
let mut results = Vec::new();
search_pattern(&buf, &mut results);
assert!(results.is_empty());
}
#[test]
fn test_search_pattern_dedup() {
// 相同模式出现两次 → 只保留一条
let key = [b'1'; 64];
let salt = [b'2'; 32];
let pattern = make_pattern(&key, &salt);
let mut buf = pattern.clone();
buf.extend_from_slice(&pattern);
let mut results = Vec::new();
search_pattern(&buf, &mut results);
assert_eq!(results.len(), 1);
}
#[test]
fn test_search_pattern_multiple_distinct() {
// 两个不同的合法模式 → 各自独立捕获
let key1 = [b'a'; 64]; let salt1 = [b'b'; 32];
let key2 = [b'c'; 64]; let salt2 = [b'd'; 32];
let mut buf = make_pattern(&key1, &salt1);
buf.extend_from_slice(&make_pattern(&key2, &salt2));
let mut results = Vec::new();
search_pattern(&buf, &mut results);
assert_eq!(results.len(), 2);
let keys: Vec<&str> = results.iter().map(|(k, _)| k.as_str()).collect();
assert!(keys.contains(&"a".repeat(64).as_str()));
assert!(keys.contains(&"c".repeat(64).as_str()));
}
#[test]
fn test_search_pattern_embedded_in_garbage() {
// 模式夹在垃圾字节中间,仍应找到
let mut buf = vec![0xFFu8; 50];
let key = [b'e'; 64];
let salt = [b'f'; 32];
buf.extend_from_slice(&make_pattern(&key, &salt));
buf.extend_from_slice(&[0x00u8; 50]);
let mut results = Vec::new();
search_pattern(&buf, &mut results);
assert_eq!(results.len(), 1);
}
#[test]
fn test_search_pattern_too_short() {
// 缓冲区太小,无法容纳完整模式
let buf = [b'x', b'\'', b'a', b'b'];
let mut results = Vec::new();
search_pattern(&buf, &mut results);
assert!(results.is_empty());
}
#[test]
fn test_search_pattern_empty_buf() {
let mut results = Vec::new();
search_pattern(&[], &mut results);
assert!(results.is_empty());
}
#[test]
fn test_search_pattern_real_hex_mix() {
// 合法的混合大小写十六进制0-9, a-f, A-F
let mut key = [b'0'; 64];
for (i, c) in b"0123456789abcdefABCDEF0123456789abcdef0123456789abcdef01234567".iter().enumerate() {
if i < 64 { key[i] = *c; }
}
let salt = [b'9'; 32];
let buf = make_pattern(&key, &salt);
let mut results = Vec::new();
search_pattern(&buf, &mut results);
assert_eq!(results.len(), 1);
// 结果应全小写
assert!(results[0].0.chars().all(|c| c.is_ascii_lowercase() || c.is_ascii_digit()));
}
}