From 6898a065d7796046955caf57be2d7265a11dd55b Mon Sep 17 00:00:00 2001 From: PeanutSplash Date: Tue, 3 Mar 2026 22:20:12 +0800 Subject: [PATCH] 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()