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/<salt_hex>` 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/<salt>`.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
pull/83/head
Yanick Landry 2026-05-19 22:26:07 -03:00
parent 08af894594
commit 42169de2f0
4 changed files with 88 additions and 39 deletions

View File

@ -200,11 +200,6 @@ impl DbCache {
}
pub async fn get_with_mode(&self, rel_key: &str) -> Result<Option<CacheResolve>> {
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/<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() {

View File

@ -4,6 +4,7 @@
/// 通过 /proc/<pid>/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<Vec<KeyEntry>> {
}
eprintln!("找到 {} 个候选密钥", raw_keys.len());
// 无法匹配 DB 文件的密钥用 _by_salt/<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)
}

View File

@ -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<Vec<KeyEntry>> {
let raw_keys = scan_memory(task)?;
eprintln!("找到 {} 个候选密钥", raw_keys.len());
// 5. 将密钥与数据库 salt 匹配
// 5. 将密钥与数据库 salt 匹配,无法匹配的用 _by_salt/<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)
}

View File

@ -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<Vec<KeyEntry>> {
let _ = CloseHandle(process);
}
// 无法匹配 DB 文件的密钥用 _by_salt/<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)
}