From 42169de2f096da925255462904e999b42af0c6e8 Mon Sep 17 00:00:00 2001 From: Yanick Landry Date: Tue, 19 May 2026 22:26:07 -0300 Subject: [PATCH] fix(scanner): preserve all memory keys with _by_salt fallback Previously, scan_keys discarded any key found in WeChat's process memory that didn't match a known DB file's salt. If DB files were inaccessible during `wx init` (permission issues, path resolution failures), zero keys were saved and decryption silently failed. Apply the same logic as khipuchat's wechat-key-extract.c: save all (key, salt) pairs found in memory unconditionally. Keys that match a DB file by salt are stored under the relative DB path as before. Keys with no match are stored under `_by_salt/` so they are never lost. The daemon's DbCache::get_with_mode gains a fallback: if a DB's rel_key is not in all_keys, it reads the DB file's first 16 bytes to get the salt and retries the lookup as `_by_salt/`. Co-Authored-By: Claude Sonnet 4.6 --- src/daemon/cache.rs | 20 +++++++++++++++----- src/scanner/linux.rs | 35 ++++++++++++++++++++++++----------- src/scanner/macos.rs | 36 ++++++++++++++++++++++++------------ src/scanner/windows.rs | 36 +++++++++++++++++++++++++----------- 4 files changed, 88 insertions(+), 39 deletions(-) diff --git a/src/daemon/cache.rs b/src/daemon/cache.rs index 561df51..d0086da 100644 --- a/src/daemon/cache.rs +++ b/src/daemon/cache.rs @@ -200,11 +200,6 @@ impl DbCache { } pub async fn get_with_mode(&self, rel_key: &str) -> Result> { - let enc_key_hex = match self.all_keys.get(rel_key) { - Some(k) => k.clone(), - None => return Ok(None), - }; - let db_path = self.db_dir.join( rel_key .replace('\\', std::path::MAIN_SEPARATOR_STR) @@ -214,6 +209,21 @@ impl DbCache { return Ok(None); } + let enc_key_hex = match self.all_keys.get(rel_key) { + Some(k) => k.clone(), + None => { + // Fallback: look up by DB salt for keys saved as "_by_salt/" + // when the DB file wasn't accessible during `wx init`. + let salt = crate::scanner::read_db_salt(&db_path); + match salt.and_then(|s| { + self.all_keys.get(&format!("_by_salt/{}", s)).cloned() + }) { + Some(k) => k, + None => return Ok(None), + } + } + }; + let wal_path = wal_path_for(&db_path); let db_mt = mtime_nanos(&db_path); let wal_mt = if wal_path.exists() { diff --git a/src/scanner/linux.rs b/src/scanner/linux.rs index d6f4ee9..8c55d63 100644 --- a/src/scanner/linux.rs +++ b/src/scanner/linux.rs @@ -4,6 +4,7 @@ /// 通过 /proc//mem 读取内存内容, /// 搜索 x'<64hex><32hex>' 格式的 SQLCipher 密钥 use anyhow::{Context, Result}; +use std::collections::HashMap; use std::io::{Read, Seek, SeekFrom}; use std::path::Path; @@ -89,21 +90,33 @@ pub fn scan_keys(db_dir: &Path) -> Result> { } eprintln!("找到 {} 个候选密钥", raw_keys.len()); + // 无法匹配 DB 文件的密钥用 _by_salt/ 保存,daemon 会做 salt 回退查找, + // 确保所有在内存中找到的密钥都不会因 DB 文件权限或路径问题而丢失。 + let db_salt_map: HashMap<&str, &str> = db_salts + .iter() + .map(|(salt, name)| (salt.as_str(), name.as_str())) + .collect(); + 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; - } - } + let db_name = db_salt_map + .get(salt_hex.as_str()) + .map(|s| s.to_string()) + .unwrap_or_else(|| format!("_by_salt/{}", salt_hex)); + entries.push(KeyEntry { + db_name, + enc_key: key_hex.clone(), + salt: salt_hex.clone(), + }); } - eprintln!("匹配到 {}/{} 个密钥", entries.len(), raw_keys.len()); + let matched = entries.iter().filter(|e| !e.db_name.starts_with("_by_salt/")).count(); + eprintln!( + "匹配到 {}/{} 个密钥(另有 {} 个按 salt 保存)", + matched, + raw_keys.len(), + entries.len() - matched + ); Ok(entries) } diff --git a/src/scanner/macos.rs b/src/scanner/macos.rs index c22d3bb..6cb9f6b 100644 --- a/src/scanner/macos.rs +++ b/src/scanner/macos.rs @@ -10,6 +10,7 @@ /// 2. WeChat 需要进行 ad-hoc 签名 /// 3. 在内存中搜索 x'<64hex><32hex>' 格式的 SQLCipher 密钥 use anyhow::{bail, Context, Result}; +use std::collections::HashMap; use std::path::Path; use super::{collect_db_salts, KeyEntry}; @@ -141,22 +142,33 @@ pub fn scan_keys(db_dir: &Path) -> Result> { let raw_keys = scan_memory(task)?; eprintln!("找到 {} 个候选密钥", raw_keys.len()); - // 5. 将密钥与数据库 salt 匹配 + // 5. 将密钥与数据库 salt 匹配,无法匹配的用 _by_salt/ 保存, + // 确保所有在内存中找到的密钥都不会因 DB 文件权限或路径问题而丢失。 + let db_salt_map: HashMap<&str, &str> = db_salts + .iter() + .map(|(salt, name)| (salt.as_str(), name.as_str())) + .collect(); + 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; - } - } + let db_name = db_salt_map + .get(salt_hex.as_str()) + .map(|s| s.to_string()) + .unwrap_or_else(|| format!("_by_salt/{}", salt_hex)); + entries.push(KeyEntry { + db_name, + enc_key: key_hex.clone(), + salt: salt_hex.clone(), + }); } - eprintln!("匹配到 {}/{} 个密钥", entries.len(), raw_keys.len()); + let matched = entries.iter().filter(|e| !e.db_name.starts_with("_by_salt/")).count(); + eprintln!( + "匹配到 {}/{} 个密钥(另有 {} 个按 salt 保存)", + matched, + raw_keys.len(), + entries.len() - matched + ); Ok(entries) } diff --git a/src/scanner/windows.rs b/src/scanner/windows.rs index 391ba33..9f5198c 100644 --- a/src/scanner/windows.rs +++ b/src/scanner/windows.rs @@ -6,6 +6,7 @@ /// - VirtualQueryEx: 枚举内存区域 /// - ReadProcessMemory: 读取内存内容 use anyhow::{Context, Result}; +use std::collections::HashMap; use std::path::Path; use windows::Win32::Foundation::{CloseHandle, HANDLE}; use windows::Win32::System::Diagnostics::Debug::ReadProcessMemory; @@ -79,20 +80,33 @@ pub fn scan_keys(db_dir: &Path) -> Result> { let _ = CloseHandle(process); } + // 无法匹配 DB 文件的密钥用 _by_salt/ 保存,daemon 会做 salt 回退查找, + // 确保所有在内存中找到的密钥都不会因 DB 文件权限或路径问题而丢失。 + let db_salt_map: HashMap<&str, &str> = db_salts + .iter() + .map(|(salt, name)| (salt.as_str(), name.as_str())) + .collect(); + 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; - } - } + let db_name = db_salt_map + .get(salt_hex.as_str()) + .map(|s| s.to_string()) + .unwrap_or_else(|| format!("_by_salt/{}", salt_hex)); + entries.push(KeyEntry { + db_name, + enc_key: key_hex.clone(), + salt: salt_hex.clone(), + }); } - eprintln!("匹配到 {}/{} 个密钥", entries.len(), raw_keys.len()); + + let matched = entries.iter().filter(|e| !e.db_name.starts_with("_by_salt/")).count(); + eprintln!( + "匹配到 {}/{} 个密钥(另有 {} 个按 salt 保存)", + matched, + raw_keys.len(), + entries.len() - matched + ); Ok(entries) }