/// 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> { 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 = 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> { // 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 { 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> { 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> { let mut results: Vec<(String, String)> = Vec::new(); let mut addr: mach_vm_address_t = 0; // VM_REGION_BASIC_INFO_COUNT_64 = 9(来自 ,固定值,不能用 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'' 模式字节串 fn make_pattern(key: &[u8; 64], salt: &[u8; 32]) -> Vec { 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())); } }