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.
feat/daemon-cli
PeanutSplash 2026-03-03 22:20:12 +08:00
parent bf68409c39
commit 6898a065d7
No known key found for this signature in database
GPG Key ID: DD6888629D611322
3 changed files with 197 additions and 121 deletions

View File

@ -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 查询微信数据 |

View File

@ -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,24 +116,23 @@ 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()
# 2. 打开所有微信进程
pids = get_pids()
hex_re = re.compile(b"x'([0-9a-fA-F]{64,192})'")
key_map = {} # salt_hex -> enc_key_hex
all_hex_matches = 0
t0 = time.time()
for proc_idx, (pid, mem) in enumerate(pids):
h = kernel32.OpenProcess(0x0010 | 0x0400, False, pid)
if not h:
print("[ERROR] 无法打开进程"); sys.exit(1)
print(f"[WARN] 无法打开进程 PID={pid},跳过")
continue
regions = enum_regions(h)
total_mb = sum(s for _,s in regions)/1024/1024
print(f"[+] 可读内存: {len(regions)} 区域, {total_mb:.0f}MB")
# 3. 搜索所有 x'<hex>' 模式
print(f"\n搜索 x'<hex>' 缓存密钥...")
hex_re = re.compile(b"x'([0-9a-fA-F]{64,192})'")
# 结果: salt_hex -> enc_key_hex
key_map = {}
all_hex_matches = 0
t0 = time.time()
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)
@ -143,14 +145,11 @@ def main():
hex_len = len(hex_str)
if hex_len == 96:
# enc_key(32bytes=64hex) + salt(16bytes=32hex)
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)
# 找到对应的page1
for rel, path, sz, s, page1 in db_files:
if s == salt_hex:
if verify_key_for_db(enc_key, page1):
@ -158,12 +157,11 @@ def main():
dbs = salt_to_dbs[salt_hex]
print(f"\n [FOUND] salt={salt_hex}")
print(f" enc_key={enc_key_hex}")
print(f" 地址: 0x{addr:016X}")
print(f" PID={pid} 地址: 0x{addr:016X}")
print(f" 数据库: {', '.join(dbs)}")
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:
@ -173,13 +171,11 @@ def main():
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
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:]
@ -192,19 +188,25 @@ def main():
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" 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)
# 所有 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__':

99
main.py 100644
View File

@ -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()