From 2fa95b283f3b3a87a8b67ede048b8dd2a84380d6 Mon Sep 17 00:00:00 2001 From: PeanutSplash Date: Tue, 3 Mar 2026 21:42:31 +0800 Subject: [PATCH 1/5] feat: Added automatic detection of WeChat data directories and optimized configuration loading process --- config.py | 100 ++++++++++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 89 insertions(+), 11 deletions(-) diff --git a/config.py b/config.py index a7055b4..444eb53 100644 --- a/config.py +++ b/config.py @@ -1,15 +1,18 @@ """ 配置加载器 - 从 config.json 读取路径配置 -首次运行时自动生成 config.json 模板 +首次运行时自动检测微信数据目录,检测失败则提示手动配置 """ +import glob import json import os import sys CONFIG_FILE = os.path.join(os.path.dirname(os.path.abspath(__file__)), "config.json") +_DEFAULT_TEMPLATE_DIR = r"D:\xwechat_files\your_wxid\db_storage" + _DEFAULT = { - "db_dir": r"D:\xwechat_files\your_wxid\db_storage", + "db_dir": _DEFAULT_TEMPLATE_DIR, "keys_file": "all_keys.json", "decrypted_dir": "decrypted", "decoded_image_dir": "decoded_images", @@ -17,16 +20,91 @@ _DEFAULT = { } -def load_config(): - if not os.path.exists(CONFIG_FILE): - with open(CONFIG_FILE, "w") as f: - json.dump(_DEFAULT, f, indent=4) - print(f"[!] 已生成配置文件: {CONFIG_FILE}") - print(" 请修改 config.json 中的路径后重新运行") - sys.exit(1) +def auto_detect_db_dir(): + """从微信本地配置自动检测 db_storage 路径。 - with open(CONFIG_FILE) as f: - cfg = json.load(f) + 读取 %APPDATA%\\Tencent\\xwechat\\config\\*.ini, + 找到数据存储根目录,然后匹配 xwechat_files\\*\\db_storage。 + """ + appdata = os.environ.get("APPDATA", "") + config_dir = os.path.join(appdata, "Tencent", "xwechat", "config") + if not os.path.isdir(config_dir): + return None + + # 从 ini 文件中找到有效的目录路径 + data_roots = [] + for ini_file in glob.glob(os.path.join(config_dir, "*.ini")): + try: + with open(ini_file, "r", encoding="utf-8") as f: + content = f.read(1024).strip() + if not content or "\n" in content or "\x00" in content: + continue + if os.path.isdir(content): + data_roots.append(content) + except (OSError, UnicodeDecodeError): + continue + + # 在每个根目录下搜索 xwechat_files\*\db_storage + seen = set() + candidates = [] + for root in data_roots: + pattern = os.path.join(root, "xwechat_files", "*", "db_storage") + for match in glob.glob(pattern): + normalized = os.path.normcase(os.path.normpath(match)) + if os.path.isdir(match) and normalized not in seen: + seen.add(normalized) + candidates.append(match) + + if len(candidates) == 1: + return candidates[0] + if len(candidates) > 1: + # 非交互环境(MCP、无 stdin 管道等)直接取第一个 + if not sys.stdin.isatty(): + return candidates[0] + print("[!] 检测到多个微信数据目录:") + for i, c in enumerate(candidates, 1): + print(f" {i}. {c}") + print(" 0. 跳过,稍后手动配置") + try: + while True: + choice = input("请选择 [0-{}]: ".format(len(candidates))).strip() + if choice == "0": + return None + if choice.isdigit() and 1 <= int(choice) <= len(candidates): + return candidates[int(choice) - 1] + print(" 无效输入,请重新选择") + except (EOFError, KeyboardInterrupt): + print() + return None + return None + + +def load_config(): + cfg = {} + if os.path.exists(CONFIG_FILE): + with open(CONFIG_FILE) as f: + cfg = json.load(f) + + # db_dir 缺失或仍为模板值时,尝试自动检测 + db_dir = cfg.get("db_dir", "") + if not db_dir or db_dir == _DEFAULT_TEMPLATE_DIR or "your_wxid" in db_dir: + detected = auto_detect_db_dir() + if detected: + print(f"[+] 自动检测到微信数据目录: {detected}") + cfg["db_dir"] = detected + # 合并默认值并保存 + cfg = {**_DEFAULT, **cfg, "db_dir": detected} + with open(CONFIG_FILE, "w") as f: + json.dump(cfg, f, indent=4, ensure_ascii=False) + print(f"[+] 已保存到: {CONFIG_FILE}") + else: + if not os.path.exists(CONFIG_FILE): + with open(CONFIG_FILE, "w") as f: + json.dump(_DEFAULT, f, indent=4) + print(f"[!] 未能自动检测微信数据目录") + print(f" 请手动编辑 {CONFIG_FILE} 中的 db_dir 字段") + print(f" 路径可在 微信设置 → 文件管理 中找到") + sys.exit(1) # 将相对路径转为绝对路径 base = os.path.dirname(os.path.abspath(__file__)) From bf68409c39364d65ed59989b0e2d40b8bbd4f892 Mon Sep 17 00:00:00 2001 From: PeanutSplash Date: Tue, 3 Mar 2026 21:43:40 +0800 Subject: [PATCH 2/5] docs: Updated configuration instructions to automatically detect the WeChat data directory and generate config.json. --- README.md | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index c955911..02ea249 100644 --- a/README.md +++ b/README.md @@ -40,13 +40,9 @@ pip install pycryptodome ### 1. 配置 -复制配置模板并修改: +首次运行任意脚本时,程序会自动检测微信数据目录并生成 `config.json`,无需手动配置。 -```bash -copy config.example.json config.json -``` - -编辑 `config.json`: +如果自动检测失败(例如微信安装在非默认位置),手动创建 `config.json`: ```json { "db_dir": "D:\\xwechat_files\\你的微信ID\\db_storage", From 6898a065d7796046955caf57be2d7265a11dd55b Mon Sep 17 00:00:00 2001 From: PeanutSplash Date: Tue, 3 Mar 2026 22:20:12 +0800 Subject: [PATCH 3/5] feat: add unified entry point and multi-process key extraction Add main.py as single entry point that auto-detects config, extracts keys, and launches Web UI or decrypts databases in one command. Refactor find_all_keys to scan all Weixin.exe processes instead of only the largest one, enabling multi=account support. --- README.md | 56 +++++----------- find_all_keys.py | 163 ++++++++++++++++++++++++----------------------- main.py | 99 ++++++++++++++++++++++++++++ 3 files changed, 197 insertions(+), 121 deletions(-) create mode 100644 main.py diff --git a/README.md b/README.md index 02ea249..c02eb35 100644 --- a/README.md +++ b/README.md @@ -38,9 +38,16 @@ WCDB (微信的 SQLCipher 封装) 会在进程内存中缓存派生后的 raw ke pip install pycryptodome ``` -### 1. 配置 +### 快速开始 -首次运行任意脚本时,程序会自动检测微信数据目录并生成 `config.json`,无需手动配置。 +确保微信正在运行,以**管理员权限**执行: + +```bash +python main.py # 实时消息监听 (Web UI) +python main.py decrypt # 解密全部数据库到 decrypted/ +``` + +程序会自动完成:配置检测 → 密钥提取 → 启动。首次运行会自动检测微信数据目录并生成 `config.json`。 如果自动检测失败(例如微信安装在非默认位置),手动创建 `config.json`: ```json @@ -54,33 +61,9 @@ pip install pycryptodome `db_dir` 路径可以在 微信设置 → 文件管理 中找到。 -### 2. 提取密钥 +### Web UI 说明 -确保微信正在运行,以**管理员权限**运行: - -```bash -python find_all_keys.py -``` - -密钥将保存到 `all_keys.json`。 - -### 3. 解密数据库 - -```bash -python decrypt_db.py -``` - -解密后的数据库保存在 `decrypted/` 目录,可以直接用 SQLite 工具打开。 - -### 4. 实时消息监听 - -#### Web UI (推荐) - -```bash -python monitor_web.py -``` - -打开 http://localhost:5678 查看实时消息流。 +`python main.py` 启动后打开 http://localhost:5678 查看实时消息流。 - 30ms 轮询 WAL 文件变化 (mtime) - 检测到变化后全量解密 + WAL patch (~70ms) @@ -88,15 +71,7 @@ python monitor_web.py - 总延迟约 100ms - **图片消息内联预览**(支持旧 XOR / V1 / V2 三种 .dat 加密格式) -#### 命令行 - -```bash -python monitor.py -``` - -每 3 秒轮询一次,在终端显示新消息。 - -### 5. MCP Server (Claude AI 集成) +### MCP Server (Claude AI 集成) 将微信数据查询能力接入 [Claude Code](https://claude.ai/claude-code),让 AI 直接读取你的微信消息。 @@ -134,11 +109,11 @@ claude mcp add wechat -- python C:\Users\你的用户名\wechat-decrypt\mcp_serv | `get_contacts(query, limit)` | 搜索/列出联系人 | | `get_new_messages()` | 获取自上次调用以来的新消息 | -前置条件:需要先完成步骤 1-2(配置 + 提取密钥)。 +前置条件:需要先运行 `python main.py` 或 `python find_all_keys.py` 完成密钥提取。 **[查看使用案例 →](USAGE.md)** -### 6. 图片解密 (V2 格式) +### 图片解密 (V2 格式) 微信 4.0 (2025-08+) 的 .dat 图片文件使用 AES-128-ECB + XOR 混合加密 (V2 格式)。AES 密钥需要从运行中的微信进程内存中提取: @@ -159,7 +134,8 @@ python find_image_key.py | 文件 | 说明 | |------|------| -| `config.py` | 配置加载器 | +| `main.py` | **一键启动入口** — 自动配置、提取密钥、启动服务 | +| `config.py` | 配置加载器(自动检测微信数据目录) | | `find_all_keys.py` | 从微信进程内存提取所有数据库密钥 | | `decrypt_db.py` | 全量解密所有数据库 | | `mcp_server.py` | MCP Server,让 Claude AI 查询微信数据 | diff --git a/find_all_keys.py b/find_all_keys.py index 7b1cec6..173049a 100644 --- a/find_all_keys.py +++ b/find_all_keys.py @@ -33,20 +33,23 @@ class MBI(ctypes.Structure): ("Protect", wt.DWORD), ("Type", wt.DWORD), ("_pad2", wt.DWORD), ] -def get_pid(): +def get_pids(): + """返回所有 Weixin.exe 进程的 (pid, mem_kb) 列表,按内存降序""" import subprocess r = subprocess.run(["tasklist","/FI","IMAGENAME eq Weixin.exe","/FO","CSV","/NH"], capture_output=True, text=True) - best = (0,0) + pids = [] for line in r.stdout.strip().split('\n'): if not line.strip(): continue p = line.strip('"').split('","') if len(p)>=5: pid=int(p[1]); mem=int(p[4].replace(',','').replace(' K','').strip() or '0') - if mem>best[1]: best=(pid,mem) - if not best[0]: print("[ERROR] Weixin.exe 未运行"); sys.exit(1) - print(f"[+] Weixin.exe PID={best[0]} ({best[1]//1024}MB)") - return best[0] + pids.append((pid, mem)) + if not pids: raise RuntimeError("Weixin.exe 未运行") + pids.sort(key=lambda x: x[1], reverse=True) + for pid, mem in pids: + print(f"[+] Weixin.exe PID={pid} ({mem//1024}MB)") + return pids def read_mem(h, addr, sz): buf = ctypes.create_string_buffer(sz) @@ -113,98 +116,97 @@ def main(): for salt_hex, dbs in sorted(salt_to_dbs.items(), key=lambda x: len(x[1]), reverse=True): print(f" salt {salt_hex}: {', '.join(dbs)}") - # 2. 打开进程 - pid = get_pid() - h = kernel32.OpenProcess(0x0010 | 0x0400, False, pid) - if not h: - print("[ERROR] 无法打开进程"); sys.exit(1) + # 2. 打开所有微信进程 + pids = get_pids() - regions = enum_regions(h) - total_mb = sum(s for _,s in regions)/1024/1024 - print(f"[+] 可读内存: {len(regions)} 区域, {total_mb:.0f}MB") - - # 3. 搜索所有 x'' 模式 - print(f"\n搜索 x'' 缓存密钥...") hex_re = re.compile(b"x'([0-9a-fA-F]{64,192})'") - - # 结果: salt_hex -> enc_key_hex - key_map = {} + key_map = {} # salt_hex -> enc_key_hex all_hex_matches = 0 t0 = time.time() - for reg_idx, (base, size) in enumerate(regions): - data = read_mem(h, base, size) - if not data: continue + for proc_idx, (pid, mem) in enumerate(pids): + h = kernel32.OpenProcess(0x0010 | 0x0400, False, pid) + if not h: + print(f"[WARN] 无法打开进程 PID={pid},跳过") + continue - for m in hex_re.finditer(data): - hex_str = m.group(1).decode() - addr = base + m.start() - all_hex_matches += 1 - hex_len = len(hex_str) + regions = enum_regions(h) + total_mb = sum(s for _,s in regions)/1024/1024 + print(f"\n[*] 扫描 PID={pid} ({total_mb:.0f}MB, {len(regions)} 区域)") - if hex_len == 96: - # enc_key(32bytes=64hex) + salt(16bytes=32hex) - enc_key_hex = hex_str[:64] - salt_hex = hex_str[64:] + for reg_idx, (base, size) in enumerate(regions): + data = read_mem(h, base, size) + if not data: continue - if salt_hex in salt_to_dbs and salt_hex not in key_map: - # 验证! + for m in hex_re.finditer(data): + hex_str = m.group(1).decode() + addr = base + m.start() + all_hex_matches += 1 + hex_len = len(hex_str) + + if hex_len == 96: + enc_key_hex = hex_str[:64] + salt_hex = hex_str[64:] + + if salt_hex in salt_to_dbs and salt_hex not in key_map: + enc_key = bytes.fromhex(enc_key_hex) + for rel, path, sz, s, page1 in db_files: + if s == salt_hex: + if verify_key_for_db(enc_key, page1): + key_map[salt_hex] = enc_key_hex + dbs = salt_to_dbs[salt_hex] + print(f"\n [FOUND] salt={salt_hex}") + print(f" enc_key={enc_key_hex}") + print(f" PID={pid} 地址: 0x{addr:016X}") + print(f" 数据库: {', '.join(dbs)}") + break + + elif hex_len == 64: + enc_key_hex = hex_str enc_key = bytes.fromhex(enc_key_hex) - # 找到对应的page1 - for rel, path, sz, s, page1 in db_files: - if s == salt_hex: + for rel, path, sz, salt_hex_db, page1 in db_files: + if salt_hex_db not in key_map: if verify_key_for_db(enc_key, page1): - key_map[salt_hex] = enc_key_hex - dbs = salt_to_dbs[salt_hex] - print(f"\n [FOUND] salt={salt_hex}") + key_map[salt_hex_db] = enc_key_hex + dbs = salt_to_dbs[salt_hex_db] + print(f"\n [FOUND] salt={salt_hex_db}") print(f" enc_key={enc_key_hex}") - print(f" 地址: 0x{addr:016X}") + print(f" PID={pid} 地址: 0x{addr:016X}") print(f" 数据库: {', '.join(dbs)}") - break + break - elif hex_len == 64: - # 只有enc_key, 没有salt - 需要逐个DB试 - enc_key_hex = hex_str - enc_key = bytes.fromhex(enc_key_hex) - for rel, path, sz, salt_hex_db, page1 in db_files: - if salt_hex_db not in key_map: - if verify_key_for_db(enc_key, page1): - key_map[salt_hex_db] = enc_key_hex - dbs = salt_to_dbs[salt_hex_db] - print(f"\n [FOUND] salt={salt_hex_db}") - print(f" enc_key={enc_key_hex}") - print(f" 地址: 0x{addr:016X}") - print(f" 数据库: {', '.join(dbs)}") - break + elif hex_len > 96 and hex_len % 2 == 0: + enc_key_hex = hex_str[:64] + salt_hex = hex_str[-32:] - elif hex_len > 96 and hex_len % 2 == 0: - # 可能是 enc_key + hmac_key + salt 或其他格式 - # 取前64作为enc_key, 后32作为salt - enc_key_hex = hex_str[:64] - salt_hex = hex_str[-32:] + if salt_hex in salt_to_dbs and salt_hex not in key_map: + enc_key = bytes.fromhex(enc_key_hex) + for rel, path, sz, s, page1 in db_files: + if s == salt_hex: + if verify_key_for_db(enc_key, page1): + key_map[salt_hex] = enc_key_hex + dbs = salt_to_dbs[salt_hex] + print(f"\n [FOUND] salt={salt_hex} (long hex {hex_len})") + print(f" enc_key={enc_key_hex}") + print(f" PID={pid} 地址: 0x{addr:016X}") + print(f" 数据库: {', '.join(dbs)}") + break - if salt_hex in salt_to_dbs and salt_hex not in key_map: - enc_key = bytes.fromhex(enc_key_hex) - for rel, path, sz, s, page1 in db_files: - if s == salt_hex: - if verify_key_for_db(enc_key, page1): - key_map[salt_hex] = enc_key_hex - dbs = salt_to_dbs[salt_hex] - print(f"\n [FOUND] salt={salt_hex} (long hex {hex_len})") - print(f" enc_key={enc_key_hex}") - print(f" 地址: 0x{addr:016X}") - print(f" 数据库: {', '.join(dbs)}") - break + if (reg_idx + 1) % 200 == 0: + elapsed = time.time() - t0 + progress = sum(s for b,s in regions[:reg_idx+1]) / sum(s for _,s in regions) * 100 + print(f" [{progress:.1f}%] {len(key_map)}/{len(salt_to_dbs)} salts matched, " + f"{all_hex_matches} hex patterns, {elapsed:.1f}s") - # 进度 - if (reg_idx + 1) % 200 == 0: - elapsed = time.time() - t0 - progress = sum(s for b,s in regions[:reg_idx+1]) / sum(s for _,s in regions) * 100 - print(f" [{progress:.1f}%] {len(key_map)}/{len(salt_to_dbs)} salts matched, " - f"{all_hex_matches} hex patterns, {elapsed:.1f}s") + kernel32.CloseHandle(h) + + # 所有 salt 都找到了就提前退出 + if len(key_map) == len(salt_to_dbs): + print(f"\n[+] 所有密钥已找到,跳过剩余进程") + break elapsed = time.time() - t0 - print(f"\n扫描完成: {elapsed:.1f}s, {all_hex_matches} hex模式") + print(f"\n扫描完成: {elapsed:.1f}s, {len(pids)} 个进程, {all_hex_matches} hex模式") # 4. 如果有未找到的salt,用已找到的key做交叉验证 # (WCDB有时对同一passphrase的不同DB用同一enc_key,如果salt相同) @@ -248,7 +250,6 @@ def main(): for rel in missing: print(f" {rel}") - kernel32.CloseHandle(h) if __name__ == '__main__': diff --git a/main.py b/main.py new file mode 100644 index 0000000..e03da3e --- /dev/null +++ b/main.py @@ -0,0 +1,99 @@ +""" +WeChat Decrypt 一键启动 + +python main.py # 提取密钥 + 启动 Web UI +python main.py decrypt # 提取密钥 + 解密全部数据库 +""" +import json +import os +import subprocess +import sys + +import functools +print = functools.partial(print, flush=True) + + +def check_wechat_running(): + """检查微信是否在运行,返回 True/False""" + r = subprocess.run( + ["tasklist", "/FI", "IMAGENAME eq Weixin.exe", "/FO", "CSV", "/NH"], + capture_output=True, text=True, + ) + for line in r.stdout.strip().split("\n"): + if "Weixin.exe" in line: + return True + return False + + +def ensure_keys(keys_file): + """确保密钥文件存在,不存在则自动提取""" + if os.path.exists(keys_file): + with open(keys_file) as f: + keys = json.load(f) + if keys: + print(f"[+] 已有 {len(keys)} 个数据库密钥") + return + + print("[*] 密钥文件不存在,正在从微信进程提取...") + print() + from find_all_keys import main as extract_keys + extract_keys() + print() + + # 提取后再次检查 + if not os.path.exists(keys_file): + print("[!] 密钥提取失败") + sys.exit(1) + with open(keys_file) as f: + keys = json.load(f) + if not keys: + print("[!] 未能提取到任何密钥") + print(" 可能原因:选择了错误的微信数据目录,或微信需要重启") + print(" 请检查 config.json 中的 db_dir 是否与当前登录的微信账号匹配") + sys.exit(1) + + +def main(): + print("=" * 60) + print(" WeChat Decrypt") + print("=" * 60) + print() + + # 1. 加载配置(自动检测 db_dir) + from config import load_config + cfg = load_config() + + # 2. 检查微信进程 + if not check_wechat_running(): + print("[!] 未检测到微信进程 (Weixin.exe)") + print(" 请先启动微信并登录,然后重新运行") + sys.exit(1) + print("[+] 微信进程运行中") + + # 3. 提取密钥 + ensure_keys(cfg["keys_file"]) + + # 4. 根据子命令执行 + cmd = sys.argv[1] if len(sys.argv) > 1 else "web" + + if cmd == "decrypt": + print("[*] 开始解密全部数据库...") + print() + from decrypt_db import main as decrypt_all + decrypt_all() + elif cmd == "web": + print("[*] 启动 Web UI...") + print() + from monitor_web import main as start_web + start_web() + else: + print(f"[!] 未知命令: {cmd}") + print() + print("用法:") + print(" python main.py 启动实时消息监听 (Web UI)") + print(" python main.py decrypt 解密全部数据库到 decrypted/") + sys.exit(1) + + +if __name__ == "__main__": + main() From eb6471d42c9c0fc6b171b7611530df85e9aa6338 Mon Sep 17 00:00:00 2001 From: PeanutSplash Date: Tue, 3 Mar 2026 22:23:41 +0800 Subject: [PATCH 4/5] fix: Updated prompt messages for detecting multiple data directories and added instructions for users to select their current WeChat account. --- config.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config.py b/config.py index 444eb53..1871fc5 100644 --- a/config.py +++ b/config.py @@ -61,7 +61,7 @@ def auto_detect_db_dir(): # 非交互环境(MCP、无 stdin 管道等)直接取第一个 if not sys.stdin.isatty(): return candidates[0] - print("[!] 检测到多个微信数据目录:") + print("[!] 检测到多个微信数据目录(请选择当前正在运行的微信账号):") for i, c in enumerate(candidates, 1): print(f" {i}. {c}") print(" 0. 跳过,稍后手动配置") From fd4a2fce313d5ee21db663c93d91ca53772720b2 Mon Sep 17 00:00:00 2001 From: PeanutSplash Date: Tue, 3 Mar 2026 22:49:03 +0800 Subject: [PATCH 5/5] fix(config): handle corrupted config file and improve encoding detection --- config.py | 24 +++++--- decrypt_db.py | 1 + find_all_keys.py | 139 +++++++++++++++++++++++++++-------------------- main.py | 48 ++++++++++------ 4 files changed, 128 insertions(+), 84 deletions(-) diff --git a/config.py b/config.py index 1871fc5..432273c 100644 --- a/config.py +++ b/config.py @@ -35,13 +35,20 @@ def auto_detect_db_dir(): data_roots = [] for ini_file in glob.glob(os.path.join(config_dir, "*.ini")): try: - with open(ini_file, "r", encoding="utf-8") as f: - content = f.read(1024).strip() - if not content or "\n" in content or "\x00" in content: + # 微信 ini 可能是 utf-8 或 gbk 编码(中文路径) + content = None + for enc in ("utf-8", "gbk"): + try: + with open(ini_file, "r", encoding=enc) as f: + content = f.read(1024).strip() + break + except UnicodeDecodeError: + continue + if not content or any(c in content for c in "\n\r\x00"): continue if os.path.isdir(content): data_roots.append(content) - except (OSError, UnicodeDecodeError): + except OSError: continue # 在每个根目录下搜索 xwechat_files\*\db_storage @@ -82,8 +89,12 @@ def auto_detect_db_dir(): def load_config(): cfg = {} if os.path.exists(CONFIG_FILE): - with open(CONFIG_FILE) as f: - cfg = json.load(f) + try: + with open(CONFIG_FILE) as f: + cfg = json.load(f) + except json.JSONDecodeError: + print(f"[!] {CONFIG_FILE} 格式损坏,将使用默认配置") + cfg = {} # db_dir 缺失或仍为模板值时,尝试自动检测 db_dir = cfg.get("db_dir", "") @@ -91,7 +102,6 @@ def load_config(): detected = auto_detect_db_dir() if detected: print(f"[+] 自动检测到微信数据目录: {detected}") - cfg["db_dir"] = detected # 合并默认值并保存 cfg = {**_DEFAULT, **cfg, "db_dir": detected} with open(CONFIG_FILE, "w") as f: diff --git a/decrypt_db.py b/decrypt_db.py index 9ac39e5..d1be29a 100644 --- a/decrypt_db.py +++ b/decrypt_db.py @@ -118,6 +118,7 @@ def main(): with open(KEYS_FILE) as f: keys = json.load(f) + keys.pop("_db_dir", None) print(f"\n加载 {len(keys)} 个数据库密钥") print(f"输出目录: {OUT_DIR}") os.makedirs(OUT_DIR, exist_ok=True) diff --git a/find_all_keys.py b/find_all_keys.py index 173049a..9933337 100644 --- a/find_all_keys.py +++ b/find_all_keys.py @@ -121,6 +121,7 @@ def main(): hex_re = re.compile(b"x'([0-9a-fA-F]{64,192})'") key_map = {} # salt_hex -> enc_key_hex + remaining_salts = set(salt_to_dbs.keys()) all_hex_matches = 0 t0 = time.time() @@ -130,78 +131,87 @@ def main(): print(f"[WARN] 无法打开进程 PID={pid},跳过") continue - regions = enum_regions(h) - total_mb = sum(s for _,s in regions)/1024/1024 - print(f"\n[*] 扫描 PID={pid} ({total_mb:.0f}MB, {len(regions)} 区域)") + try: + regions = enum_regions(h) + total_bytes = sum(s for _,s in regions) + total_mb = total_bytes/1024/1024 + print(f"\n[*] 扫描 PID={pid} ({total_mb:.0f}MB, {len(regions)} 区域)") - for reg_idx, (base, size) in enumerate(regions): - data = read_mem(h, base, size) - if not data: continue + scanned_bytes = 0 + for reg_idx, (base, size) in enumerate(regions): + data = read_mem(h, base, size) + scanned_bytes += size + if not data: continue - for m in hex_re.finditer(data): - hex_str = m.group(1).decode() - addr = base + m.start() - all_hex_matches += 1 - hex_len = len(hex_str) + for m in hex_re.finditer(data): + hex_str = m.group(1).decode() + addr = base + m.start() + all_hex_matches += 1 + hex_len = len(hex_str) - if hex_len == 96: - enc_key_hex = hex_str[:64] - salt_hex = hex_str[64:] + if hex_len == 96: + enc_key_hex = hex_str[:64] + salt_hex = hex_str[64:] - if salt_hex in salt_to_dbs and salt_hex not in key_map: + if salt_hex in remaining_salts: + enc_key = bytes.fromhex(enc_key_hex) + for rel, path, sz, s, page1 in db_files: + if s == salt_hex: + if verify_key_for_db(enc_key, page1): + key_map[salt_hex] = enc_key_hex + remaining_salts.discard(salt_hex) + dbs = salt_to_dbs[salt_hex] + print(f"\n [FOUND] salt={salt_hex}") + print(f" enc_key={enc_key_hex}") + print(f" PID={pid} 地址: 0x{addr:016X}") + print(f" 数据库: {', '.join(dbs)}") + break + + elif hex_len == 64: + if not remaining_salts: + continue + enc_key_hex = hex_str enc_key = bytes.fromhex(enc_key_hex) - for rel, path, sz, s, page1 in db_files: - if s == salt_hex: + for rel, path, sz, salt_hex_db, page1 in db_files: + if salt_hex_db in remaining_salts: if verify_key_for_db(enc_key, page1): - key_map[salt_hex] = enc_key_hex - dbs = salt_to_dbs[salt_hex] - print(f"\n [FOUND] salt={salt_hex}") + key_map[salt_hex_db] = enc_key_hex + remaining_salts.discard(salt_hex_db) + dbs = salt_to_dbs[salt_hex_db] + print(f"\n [FOUND] salt={salt_hex_db}") print(f" enc_key={enc_key_hex}") print(f" PID={pid} 地址: 0x{addr:016X}") print(f" 数据库: {', '.join(dbs)}") - break + break - elif hex_len == 64: - enc_key_hex = hex_str - enc_key = bytes.fromhex(enc_key_hex) - for rel, path, sz, salt_hex_db, page1 in db_files: - if salt_hex_db not in key_map: - if verify_key_for_db(enc_key, page1): - key_map[salt_hex_db] = enc_key_hex - dbs = salt_to_dbs[salt_hex_db] - print(f"\n [FOUND] salt={salt_hex_db}") - print(f" enc_key={enc_key_hex}") - print(f" PID={pid} 地址: 0x{addr:016X}") - print(f" 数据库: {', '.join(dbs)}") - break + elif hex_len > 96 and hex_len % 2 == 0: + enc_key_hex = hex_str[:64] + salt_hex = hex_str[-32:] - elif hex_len > 96 and hex_len % 2 == 0: - enc_key_hex = hex_str[:64] - salt_hex = hex_str[-32:] + if salt_hex in remaining_salts: + enc_key = bytes.fromhex(enc_key_hex) + for rel, path, sz, s, page1 in db_files: + if s == salt_hex: + if verify_key_for_db(enc_key, page1): + key_map[salt_hex] = enc_key_hex + remaining_salts.discard(salt_hex) + dbs = salt_to_dbs[salt_hex] + print(f"\n [FOUND] salt={salt_hex} (long hex {hex_len})") + print(f" enc_key={enc_key_hex}") + print(f" PID={pid} 地址: 0x{addr:016X}") + print(f" 数据库: {', '.join(dbs)}") + break - if salt_hex in salt_to_dbs and salt_hex not in key_map: - enc_key = bytes.fromhex(enc_key_hex) - for rel, path, sz, s, page1 in db_files: - if s == salt_hex: - if verify_key_for_db(enc_key, page1): - key_map[salt_hex] = enc_key_hex - dbs = salt_to_dbs[salt_hex] - print(f"\n [FOUND] salt={salt_hex} (long hex {hex_len})") - print(f" enc_key={enc_key_hex}") - print(f" PID={pid} 地址: 0x{addr:016X}") - print(f" 数据库: {', '.join(dbs)}") - break - - if (reg_idx + 1) % 200 == 0: - elapsed = time.time() - t0 - progress = sum(s for b,s in regions[:reg_idx+1]) / sum(s for _,s in regions) * 100 - print(f" [{progress:.1f}%] {len(key_map)}/{len(salt_to_dbs)} salts matched, " - f"{all_hex_matches} hex patterns, {elapsed:.1f}s") - - kernel32.CloseHandle(h) + if (reg_idx + 1) % 200 == 0: + elapsed = time.time() - t0 + progress = scanned_bytes / total_bytes * 100 if total_bytes else 100 + print(f" [{progress:.1f}%] {len(key_map)}/{len(salt_to_dbs)} salts matched, " + f"{all_hex_matches} hex patterns, {elapsed:.1f}s") + finally: + kernel32.CloseHandle(h) # 所有 salt 都找到了就提前退出 - if len(key_map) == len(salt_to_dbs): + if not remaining_salts: print(f"\n[+] 所有密钥已找到,跳过剩余进程") break @@ -240,6 +250,12 @@ def main(): else: print(f" MISSING: {rel} (salt={salt_hex})") + if not result: + print(f"\n[!] 未提取到任何密钥,保留已有的 {OUT_FILE}(如存在)") + raise RuntimeError("未能从任何微信进程中提取到密钥") + + # 写入密钥并记录对应的 db_dir,防止切换账号后误复用 + result["_db_dir"] = DB_DIR with open(OUT_FILE, 'w') as f: json.dump(result, f, indent=2) print(f"\n密钥保存到: {OUT_FILE}") @@ -251,6 +267,9 @@ def main(): print(f" {rel}") - if __name__ == '__main__': - main() + try: + main() + except RuntimeError as e: + print(f"\n[ERROR] {e}") + sys.exit(1) diff --git a/main.py b/main.py index e03da3e..a7884a5 100644 --- a/main.py +++ b/main.py @@ -6,7 +6,6 @@ python main.py decrypt # 提取密钥 + 解密全部数据库 """ import json import os -import subprocess import sys import functools @@ -15,21 +14,29 @@ print = functools.partial(print, flush=True) def check_wechat_running(): """检查微信是否在运行,返回 True/False""" - r = subprocess.run( - ["tasklist", "/FI", "IMAGENAME eq Weixin.exe", "/FO", "CSV", "/NH"], - capture_output=True, text=True, - ) - for line in r.stdout.strip().split("\n"): - if "Weixin.exe" in line: - return True - return False + from find_all_keys import get_pids + try: + get_pids() + return True + except RuntimeError: + return False -def ensure_keys(keys_file): - """确保密钥文件存在,不存在则自动提取""" +def ensure_keys(keys_file, db_dir): + """确保密钥文件存在且匹配当前 db_dir,否则重新提取""" if os.path.exists(keys_file): - with open(keys_file) as f: - keys = json.load(f) + try: + with open(keys_file) as f: + keys = json.load(f) + except (json.JSONDecodeError, ValueError): + keys = {} + # 检查密钥是否匹配当前 db_dir(防止切换账号后误复用旧密钥) + saved_dir = keys.pop("_db_dir", None) + if saved_dir and os.path.normcase(os.path.normpath(saved_dir)) != os.path.normcase(os.path.normpath(db_dir)): + print(f"[!] 密钥文件对应的目录已变更,需要重新提取") + print(f" 旧: {saved_dir}") + print(f" 新: {db_dir}") + keys = {} if keys: print(f"[+] 已有 {len(keys)} 个数据库密钥") return @@ -37,15 +44,22 @@ def ensure_keys(keys_file): print("[*] 密钥文件不存在,正在从微信进程提取...") print() from find_all_keys import main as extract_keys - extract_keys() + try: + extract_keys() + except RuntimeError as e: + print(f"\n[!] 密钥提取失败: {e}") + sys.exit(1) print() # 提取后再次检查 if not os.path.exists(keys_file): print("[!] 密钥提取失败") sys.exit(1) - with open(keys_file) as f: - keys = json.load(f) + try: + with open(keys_file) as f: + keys = json.load(f) + except (json.JSONDecodeError, ValueError): + keys = {} if not keys: print("[!] 未能提取到任何密钥") print(" 可能原因:选择了错误的微信数据目录,或微信需要重启") @@ -71,7 +85,7 @@ def main(): print("[+] 微信进程运行中") # 3. 提取密钥 - ensure_keys(cfg["keys_file"]) + ensure_keys(cfg["keys_file"], cfg["db_dir"]) # 4. 根据子命令执行 cmd = sys.argv[1] if len(sys.argv) > 1 else "web"