646 lines
29 KiB
Markdown
646 lines
29 KiB
Markdown
---
|
||
title: Weixin 4.x SQLCipher 数据库破解进度日志
|
||
created: 2026-05-11
|
||
status: ✅ SOLVED
|
||
author: Claude Code
|
||
related:
|
||
- docs/superpowers/specs/2026-05-11-weixin-4x-automation-survey.md
|
||
- docs/superpowers/specs/2026-05-09-wechat-uia-channel-design.md
|
||
environment:
|
||
- Weixin 4.1.8.107
|
||
- Windows 11 26200
|
||
- Node v24.11.1
|
||
- PowerShell 5.1
|
||
---
|
||
|
||
# Weixin 4.x SQLCipher 数据库破解进度日志
|
||
|
||
> **✅ 2026-05-12 突破:破解成功!message_0.db 完整解密,可读真实群消息内容(含"123 测试")**
|
||
|
||
## 🎯 最终公式(直接复用)
|
||
|
||
Weixin 4.x 用 **WCDB + 标准 SQLCipher 4**,raw key 模式,**reserve_sz = 80**(关键!不是 SQLCipher 3 的 48):
|
||
|
||
```
|
||
encKey = 32 字节 raw key (从 Weixin 进程内存提取,见 §S9)
|
||
salt = DB page 1 前 16 字节
|
||
hmacKey = PBKDF2-HMAC-SHA512(encKey, salt XOR 0x3a, 2 rounds, 32 字节输出)
|
||
|
||
每 page (4096 字节):
|
||
encStart = 16 if page == 1 else 0
|
||
ciphertext = page[encStart : 4096 - 80] = 4000B (page 1) or 4016B (others)
|
||
iv = page[4096 - 80 : 4096 - 64] = 16B
|
||
storedHmac = page[4096 - 64 : 4096] = 64B HMAC-SHA512
|
||
|
||
HMAC 验证:
|
||
HMAC-SHA512(hmacKey, ciphertext || iv || pageNumber_LE_u32) == storedHmac
|
||
|
||
解密:
|
||
AES-256-CBC(encKey, iv).decrypt(ciphertext) → plaintext
|
||
|
||
输出明文 DB 时 page 1 前 16 字节替换为 "SQLite format 3\0"
|
||
```
|
||
|
||
**验证**: `poc-7f-reserve80.mjs` page 1 HMAC + AES 一次通过;`poc-10-decrypt-and-read.mjs`
|
||
解 10644 个 page 全部成功 (0 HMAC fail) 用时 0.2s
|
||
|
||
## TL;DR · 当前进度
|
||
|
||
| 阶段 | 状态 | 产出 |
|
||
|---|---|---|
|
||
| **S1 实测 Weixin 4.x 环境** | ✅ 完成 | 确认 UIA 路径不可行;定位到 `~\Documents\xwechat_files\<seed>\db_storage\` |
|
||
| **S2 DB 文件监控** | ✅ 完成 | `message_0.db-wal` 实时被 Weixin 写入(用户发消息后 mtime 立刻变) |
|
||
| **S3 定位 key 存储** | ✅ 完成 | `all_users\login\shin_seed\key_info.db` **明文 SQLite**,86 条 180B 记录 |
|
||
| **S4 分析 key_info 结构** | 🟡 部分 | protobuf 封装,固定头 17B + 变化 payload;**加密算法未破解** |
|
||
| **S5 内存 dump key 候选** | ✅ 完成 | 500K 候选,去重后 47K;PoC 2 **不需要管理员权限** |
|
||
| **S6 暴力 SQLCipher 解密** | ❌ 3 phase 全未命中 | Phase 1/2/3 跑完(raw / PBKDF2 4000 / 64000),说明 Weixin 4.x 非标准 SQLCipher |
|
||
| **S7 静态分析 Weixin.dll** | ✅ 完成 | 确认用 **WCDB**(Tencent fork of SQLCipher),`compatibility=4`、`page_size=4096` |
|
||
| **S8 内存全量 dump + 字符串扫描** | ✅ 完成 | 在 memdump 找到 `PRAGMA cipher_salt = ...` 和 **17 条 96-char hex literals**,每条对应一个 DB(末尾 16B = DB salt 完全匹配) |
|
||
| **S9 用 hex literal 当 raw key 解密** | ❌ 失败 | 17 条 hex × salt 一一对应非常强证据,但作为 raw key 时:AES 输出非 SQLite 格式 + HMAC 396 种派生组合全部不通过 |
|
||
| **S10 看 WCDB 源码** | ✅ **找到真正问题** | reserve_sz 不是 48 而是 **80**(HMAC-SHA512 全长 64B + IV 16B);之前误用 SQLCipher 3 的 48B 布局 |
|
||
| **S11 用正确公式解密** | ✅ **成功** | poc-7f reserve=80 → HMAC + AES 同时通过;poc-10 全 DB 10644 pages 全部解密 |
|
||
| **S12 读群消息** | ✅ **完成** | 用 node:sqlite 打开明文 DB,读到 `Msg_<sha256(room_id)>` 表,**找到 "123 测试" 那条**,真实 wxid/时间/内容全可见 |
|
||
|
||
**当前阻塞**: ✅ 已解决!key + 公式都对了,**reserve_sz = 80** 是关键。
|
||
|
||
**下一步**:
|
||
1. ✅ 已可全量解密 + 读取
|
||
2. 把 PoC 整理成 production 代码:
|
||
- 内存抽 key + 自动找各 DB 的 raw key
|
||
- WAL 增量解密(实时监听新消息)
|
||
- 配合 Win32 SendInput 实现回复
|
||
3. 集成进 `Neta.WeChatBridge` 替换 UIA 实现
|
||
|
||
---
|
||
|
||
## S1 · 环境实测与事实清单
|
||
|
||
### S1.1 进程与窗口
|
||
```powershell
|
||
Get-Process Weixin | ? MainWindowHandle -ne 0
|
||
# => PID 10888, Path: C:\Program Files\Tencent\Weixin\Weixin.exe, Version 4.1.8.107
|
||
```
|
||
|
||
### S1.2 UIA 树(证明原 spec 路径失效)
|
||
```powershell
|
||
Add-Type -AssemblyName UIAutomationClient
|
||
$root = [System.Windows.Automation.AutomationElement]::FromHandle($hwnd)
|
||
# 完整深度遍历结果:
|
||
# [Window] "微信" cls=Qt51514QWindowIcon
|
||
# [Pane] "Weixin" cls=Qt51514QWindowIcon
|
||
# [Pane] "MMUIRenderSubWindowHW" cls=MMUIRenderSubWindowHW
|
||
# 总共仅 3 节点 — 无列表、无消息气泡、无输入框
|
||
```
|
||
|
||
**结论**: Weixin 4.x 使用 Qt 5.15.14 自绘 UI,默认不发 UIAutomation 事件,UIA 树只有 3 个空壳。**不是改注册表能解决** —— 注册表(`EnableUIADesktopToggle` 等)只控制跨权限访问,不能让 Qt 自绘内容变成可读 UIA 元素。
|
||
|
||
### S1.3 监听端口
|
||
```
|
||
Weixin 主进程(PID 10888)监听: 127.0.0.1:14013, 14016, 14019, 14022, 14023
|
||
```
|
||
`curl` 返回 HTTP 000 —— 是私有 IPC 而非 HTTP/CDP。**CDP 注入路径不成立**。
|
||
|
||
### S1.4 文件存储布局
|
||
```
|
||
~\Documents\xwechat_files\
|
||
├── all_users\
|
||
│ └── login\shin_seed\
|
||
│ └── key_info.db ⭐ 明文 SQLite!
|
||
├── Backup\
|
||
└── <wxid_seed>_<suffix>\ (本机是 shin_seed_7861)
|
||
└── db_storage\
|
||
├── message\
|
||
│ ├── message_0.db ⭐ SQLCipher 加密,是本 PoC 目标
|
||
│ ├── message_0.db-wal (SQLite WAL journal)
|
||
│ ├── message_0.db-shm
|
||
│ ├── message_0.db-*.material (Tencent 自定义增量索引)
|
||
│ ├── media_0.db
|
||
│ └── biz_message_0.db
|
||
├── contact\contact.db (加密)
|
||
├── session\session.db (加密)
|
||
└── ...
|
||
```
|
||
|
||
**老 spec 假设的 `Tencent\WeChat\WeChat Files\` 在 4.x 不存在**。
|
||
|
||
---
|
||
|
||
## S2 · DB 文件实时性验证
|
||
|
||
### 脚本: `packages/backend/poc/weixin-4x/poc-1b-poll-all-db.mjs`
|
||
|
||
**验证方法**: 对 `db_storage/` 下所有 `.db/-wal/-shm/.material` 文件,每 500ms `fs.stat` 一次,
|
||
对比 size/mtime。
|
||
|
||
**实测结果**: 用户在"小云雀用户交流群90"发"123 测试"后,观测到:
|
||
- `message_0.db-wal` 的 mtime 实时变化(但 size `+0`)
|
||
- `message_fts.db-wal` 同步变化
|
||
- `head_image.db` 有 `+4096 bytes` 的真实增长
|
||
|
||
**关键发现** — SQLite WAL 模式是**预分配 + 就地覆盖**,写入不改变文件 size:
|
||
```
|
||
prev.size === curr.size, but mtime 变
|
||
```
|
||
|
||
**结论**: ✅ DB 被实时写入,信号可捕捉。正确做法:监听 mtime 或 `-wal` 文件内容变化(hash
|
||
前几 KB),不要依赖 size delta。
|
||
|
||
---
|
||
|
||
## S3 · 定位 key_info.db(明文!)
|
||
|
||
### 意外发现
|
||
|
||
```bash
|
||
xxd key_info.db | head -1
|
||
# 00000000: 5351 4c69 7465 2066 6f72 6d61 7420 3300 SQLite format 3.
|
||
```
|
||
|
||
Weixin 4.x 把**某种 key 信息存在明文 SQLite 里**,不是 DPAPI 加密文件。
|
||
|
||
### Schema (单表 `LoginKeyInfoTable`)
|
||
|
||
```sql
|
||
CREATE TABLE LoginKeyInfoTable (
|
||
user_name_md5 TEXT, -- 登录 wxid 的 md5,本机所有记录一致
|
||
key_md5 TEXT, -- 全为空字符串("")
|
||
key_info_md5 TEXT, -- 每条一个 md5 指纹(版本号?)
|
||
key_info_data BLOB -- 180 字节 payload,见下
|
||
);
|
||
```
|
||
|
||
### key_info_data 字节结构分析
|
||
|
||
86 条记录,每条 180 字节。逐字节对比同/异:
|
||
|
||
```
|
||
offset 0-16: 0a a8 01 00 0b b5 17 39 05 00 00 01 00 00 00 00 00 ← 所有记录相同 (17 字节固定头)
|
||
offset 17-30: XXXXXXXXXXXXXX ← 每条不同 (14 字节)
|
||
offset 31: 20 ← 固定
|
||
offset 32-34: 00 00 00 ← 固定
|
||
offset 35-66: 6e b0 01 aa 1e 57 b6 93 70 d0 71 8d 0f 53 e0 fd ← 所有记录相同!(32 字节共享 blob)
|
||
67 21 8d f5 ca 93 32 e8 64 59 66 5d ac f4 4f e9
|
||
offset 67-154: XXXXXXXXXXXX... ← 每条不同 (88 字节变化区)
|
||
offset 155-158: . X . . ← 部分变化
|
||
offset 159+: 混合 .X.X ← 混合
|
||
```
|
||
|
||
### Protobuf 解析(部分)
|
||
|
||
固定头首字节 `0a a8 01` 是标准 protobuf:
|
||
- `0a` = field 1, wire type 2 (length-delimited bytes)
|
||
- `a8 01` = varint length = 168 (0xa8 = 168)
|
||
|
||
所以 field 1 是 168 字节的 bytes(offset 3 到 170)。
|
||
后面 180 - 3 - 168 = 9 字节是其他 field(可能是时间戳、版本号)。
|
||
|
||
**field 1 内部结构(168 字节):**
|
||
```
|
||
offset 0-13 (of field): 前 14 字节每条不同 → 可能是 IV 或 nonce
|
||
offset 14-16: 20 00 00 00 固定
|
||
offset 17-48: 共享 32 字节 blob ⭐
|
||
offset 49+: 88 字节变化区 ⭐
|
||
```
|
||
|
||
### 假设(未验证)
|
||
|
||
**假设 A**: 共享 32 字节 blob 是 **wrap key(KEK)**,每条记录的"加密 key"用这个 KEK 解开。
|
||
但 KEK 本身存在明文文件里太危险,Tencent 应该不会这么做。
|
||
|
||
**假设 B**: 共享 32 字节是 **协议版本/算法标识符**,不是 key。每条的 88 字节变化区才是加密后的 AES key。
|
||
|
||
**假设 C**: key_info.db 只存"加密后的 key",真正的 wrap key 在 Weixin 二进制里硬编码,或通过某种 KDF
|
||
从系统信息(MachineGuid、用户 SID 等)派生。Tencent 常见做法。
|
||
|
||
### 下次可以做的实验
|
||
|
||
- **实验 E1**: 把 `user_name_md5` 反推 — 对 "shin_seed" "shin_seed_7861" 各种组合做 md5,看是否匹配
|
||
`3d25d0b45f678b1c29b99e44280e5aea`。如果匹配,说明 user_name_md5 是 wxid 本身的 md5,**说明这个
|
||
文件是按 wxid 索引的**(多账号场景下每个 wxid 一条或多条记录)
|
||
- **实验 E2**: 用 Weixin.exe 在 ProcMon 下跑,看它读 key_info.db 后紧接着读/写了哪些其他文件。
|
||
通常 KEK 派生会读 `SYSTEM\CurrentControlSet\Control\...` 之类注册表
|
||
- **实验 E3**: 用 API Monitor 看 Weixin 调 `CryptUnprotectData` (DPAPI) / `BCryptDecrypt` / `EVP_Decrypt`
|
||
等 API 时的参数。如果用 DPAPI,CryptUnprotectData 的 input 会泄露加密 blob,output 就是明文 key
|
||
|
||
---
|
||
|
||
## S4 · 内存 dump key 候选
|
||
|
||
### 脚本: `poc-2b-dump-candidates.ps1`
|
||
|
||
**原理**: 用 Win32 `OpenProcess(VMRead|QueryInfo)` + `VirtualQueryEx` + `ReadProcessMemory` 枚举
|
||
Weixin 进程的所有**私有可读写已提交**内存区域,每 4 字节步长扫描 32 字节窗口,用启发式
|
||
过滤器(排除全零/全 FF/全 ASCII/低熵)。
|
||
|
||
### 重要发现 🎯
|
||
|
||
**`OpenProcess` 不需要管理员权限** — 只要是同用户运行的脚本即可。
|
||
这极大降低了未来产品化的用户门槛(不用 UAC 弹窗)。
|
||
|
||
### 执行结果
|
||
|
||
```
|
||
[+] Target PID: 10888
|
||
[+] 内存区域 635 个, 合计 321.95 MB
|
||
[+] 扫描耗时: 30.8s
|
||
[+] 候选数 (上限): 500000 (16 MB bin 文件)
|
||
```
|
||
|
||
### 去重后分析
|
||
|
||
```
|
||
[+] 唯一候选: 47743
|
||
[+] 频度分布: >=100次:26 个, 10-99:97 个, 2-9:2016 个, 1次:45604 个
|
||
```
|
||
|
||
Top 10 高频候选都是噪声:
|
||
```
|
||
#1: freq=396020 aaaa...aaaa (Windows debug heap marker)
|
||
#2: freq=22983 x64 pointer pattern (e05b5f67f97f0000 重复)
|
||
#3: freq=22842 pointer tail (f97f0000 偏移的变形)
|
||
#4-10: 小整数 / 低熵
|
||
```
|
||
|
||
**结论**: 高频候选几乎全是噪声;真 key 应该在 `2-9` 频次区间(被 SQLite 连接池/每 DB 一份
|
||
引用,但不过多)。
|
||
|
||
### 坑与教训
|
||
|
||
1. **PowerShell FileStream 不 Flush 导致空文件**: `[System.IO.File]::Create().Close()` 在某些
|
||
PS 版本不可靠,必须 `Dispose()`。修正版见 `poc-2b-dump-candidates.ps1`
|
||
2. **Measure-Object 对 hashtable 的自定义属性不识别**: 改用手动 `foreach + +=`
|
||
3. **启发式过滤不够严**: 前 3 个候选看是堆指针,说明 "前 4 字节全零/全 ASCII" 过滤不足。
|
||
下次可加 "所有 32 字节的低字节熵 > 4.0" 条件
|
||
|
||
---
|
||
|
||
## S6 · SQLCipher 暴力解密尝试
|
||
|
||
### message_0.db 文件头
|
||
|
||
```
|
||
91 f3 c3 14 7a 62 cd 0f 9d 1b 96 e8 f5 00 04 f1
|
||
85 e5 81 fd 32 76 f2 d0 f1 08 20 0f 4c 35 55 8a
|
||
```
|
||
|
||
标准 SQLite 应是 `53 51 4C 69 74 65 20 66 6F 72 6D 61 74 20 33 00`(ASCII "SQLite format 3\0")。
|
||
前 16 字节按 SQLCipher 规范应是 **salt**(PBKDF2 用)。
|
||
|
||
### 脚本: `poc-3d-crack-key.mjs`(松验证)和 `poc-3e-crack-strict.mjs`(严格验证)
|
||
|
||
**测试候选来源**:
|
||
|
||
1. `key_info.db` 的 86 条记录 × 每条 180B 滑窗 = 约 12K 候选
|
||
2. 内存 dump 去重后 Top 5000 高频候选
|
||
3. 合并去重: **~17K 候选**
|
||
|
||
**SQLCipher 参数矩阵**:
|
||
|
||
- `reserved size`: 48 (v4 默认,16 IV+32 HMAC-SHA512) / 36 (v3, 16 IV+20 HMAC-SHA1) / 16 (仅 IV)
|
||
- `PBKDF2 iterations`: 0 (raw key) / 1 / 4000 (SQLCipher 老默认) / 64000 (SQLCipher 3) / 256000 (v4 默认)
|
||
|
||
**验证逻辑(严格版)**:
|
||
|
||
解密后的 plaintext 对应 DB 从 offset 16 开始,必须匹配 SQLite 文件头规范:
|
||
- `pt[0..1]` = page size,必须 ∈ {512, 1024, 2048, 4096, 8192, 16384, 32768}
|
||
- `pt[2]` (write version) ∈ {1, 2}
|
||
- `pt[3]` (read version) ∈ {1, 2}
|
||
- `pt[5..7]` = `0x40 0x20 0x20`(SQLite 固定值,对应 DB offset 21-23)
|
||
|
||
### 试过的组合与结果
|
||
|
||
| Phase | 参数 | 候选数 | 耗时 | 结果 |
|
||
|---|---|---|---|---|
|
||
| **松验证暴力**(PoC 3d) | reserved=48 iter=1 | 17K × 3 × 5 | 296s | 假阳性: pageSize=1 (65536) 不合理 |
|
||
| **严格验证 Phase 1** | reserved=[48/36/16] iter=0 (raw) | 22289 × 3 = 66867 | 0.5s | ❌ 未命中 |
|
||
| **严格验证 Phase 2** | reserved=[48/36/16] iter=4000 | 66867 | 51.3s | ❌ 未命中 |
|
||
| **严格验证 Phase 3** | reserved=[48/36/16] iter=64000 | 66867 | 808.8s (13.5min) | ❌ 未命中 |
|
||
| **严格验证 Phase 4** | iter=256000 | 66867 | ~4h 预计 | ⬜ 未跑(3 个 phase 都不中,256000 继续也不会中,说明 SQLCipher 参数不标准或 key 不在池里) |
|
||
|
||
**最终 verdict**: 标准 SQLCipher 所有常见 PBKDF2 参数 + 3 种 reserved 布局 × 22K 候选全部未命中 →
|
||
**Weixin 4.x 加密方案非标准 SQLCipher**,或 key 不是裸 32 字节 raw bytes。
|
||
|
||
---
|
||
|
||
## S7 · 静态分析 Weixin.dll(2026-05-12 新进展)
|
||
|
||
### 确认加密库
|
||
|
||
```bash
|
||
grep -ao "sqlcipher\|wcdb\|WCDB" "C:/Program Files/Tencent/Weixin/4.1.8.107/Weixin.dll"
|
||
# 命中: sqlcipher, wcdb, WCDB
|
||
```
|
||
|
||
**Weixin 4.x 使用 [WCDB](https://github.com/Tencent/wcdb)** —— Tencent 自己开源的 SQLCipher 衍生加密 SQLite 库。
|
||
|
||
### 字符串证据
|
||
|
||
```
|
||
com.Tencent.WCDB.Config.Cipher
|
||
sqlcipher_export
|
||
HMAC_SHA1 / HMAC_SHA256 / HMAC_SHA512
|
||
PBKDF2_HMAC_SHA1 / PBKDF2_HMAC_SHA256 / PBKDF2_HMAC_SHA512
|
||
|
||
PRAGMA cipher_compatibility = %d;
|
||
PRAGMA cipher_default_kdf_algorithm = %s;
|
||
PRAGMA cipher_default_kdf_iter = %d;
|
||
PRAGMA cipher_default_page_size = %d;
|
||
PRAGMA cipher_default_plaintext_header_size = %d;
|
||
PRAGMA cipher_default_hmac_algorithm = %s;
|
||
PRAGMA cipher_hmac_algorithm = %s;
|
||
PRAGMA cipher_kdf_algorithm = %s;
|
||
PRAGMA cipher_page_size = %d;
|
||
PRAGMA cipher_use_hmac = %d;
|
||
PRAGMA kdf_iter = %d;
|
||
```
|
||
|
||
WCDB 完整暴露了标准 SQLCipher 4 的 PRAGMA 接口。
|
||
|
||
---
|
||
|
||
## S8 · 内存全量 dump + PRAGMA 字符串扫描
|
||
|
||
### 思路
|
||
|
||
PowerShell 正则在 322MB 内存上太慢。改两步:
|
||
1. PowerShell 只做 `OpenProcess + VirtualQueryEx + ReadProcessMemory` 把所有 private RW 区域整块写到磁盘
|
||
2. Node `Buffer.toString('latin1')` + 高效正则扫描
|
||
|
||
### 结果
|
||
|
||
- 总 dump 大小: 239.6 MB(612 个内存区域)
|
||
- 64-char hex 字符串(可能是 raw key 候选): **10068 个唯一值**
|
||
- `x'<96-char hex>'` 形式(SQLCipher raw key 字面量): **20 个唯一值**
|
||
- `PRAGMA cipher_salt = ...` 出现 3 次
|
||
|
||
### 决定性发现
|
||
|
||
在 memdump 中搜索 `PRAGMA cipher_compatibility`:
|
||
|
||
```
|
||
compatibility = 4
|
||
page_size = 4096
|
||
```
|
||
|
||
**完全是 SQLCipher v4 默认参数**:
|
||
- KDF: PBKDF2-HMAC-SHA512, **256000 iterations**
|
||
- HMAC: HMAC-SHA512
|
||
- reserved per page: 48 字节(16 IV + 32 HMAC truncated)
|
||
- page size: 4096
|
||
|
||
---
|
||
|
||
## S9 · 找到候选 raw keys 一一对应(关键证据)
|
||
|
||
### PoC 8 思路
|
||
|
||
对 `db_storage/` 下所有 17 个 `.db` 文件,提取前 16 字节作为 salt。
|
||
在 memdump 的 20 个 `x'<96-char>'` 字面量里,找**末尾 16 字节恰好等于某 DB salt** 的。
|
||
|
||
### 结果(完美对应,17:17)
|
||
|
||
| DB 文件 | salt | 候选 raw key(32B) |
|
||
|---|---|---|
|
||
| message/**message_0.db** | `91f3c3147a62cd0f9d1b96e8f50004f1` | `374c4e1a2da5a7bee6e0de020a1ad24e9234c9db709e93e4c01dd1eda9e40b50` |
|
||
| message/biz_message_0.db | `30e6a2caee77a38578684980b8e7e1b9` | `5b1d42eefa14e7f050e41af8836a5fb9bb1f3698a67f10e958f450853066c5a3` |
|
||
| message/media_0.db | `07788b05e312eee3ba7133af2e19af1c` | `a194a326c5305bbe0808a47892f27b283079848748fe63a69f9d3f098575f231` |
|
||
| message/message_fts.db | `03b5195c66cf90ad5eb90abb27422a1c` | `f8f9b52cd299a4f723408f28849db8c113aacd12686c44afcf7898c0ed3a21e4` |
|
||
| message/message_resource.db | `5bd8155588a884b7e0153b244c744194` | `c0b004db73e9262fd407a428671d8bf48a7a7386b59e41516f3ca29ef392db70` |
|
||
| contact/contact.db | `6b9563aa1b2d5853a57c906040b3736c` | `bd93ce66be76d2efc88ec5347b954f1b9bc14a148eb5eae9667c19866ca51338` |
|
||
| contact/contact_fts.db | `57c24e569cba0fe37c81c8ffc6c0bc13` | `7da06127d8a0a560c8ae7d1bb479fdbd944ca3720cb651b87fd8c209fec2edde` |
|
||
| session/session.db | `60dcef0524c20d75d1a54b425ec0cf1a` | `e43bd85c5b509122d431c1512183f9e2da75fb8997677271bc1a6862c99dfbb3` |
|
||
| general/general.db | `690a391e6da19442d6feef3d3f381807` | `bfb2547c735713c88421537985b15e6867e2d87d9fb881e0beb3d145d014c3fc` |
|
||
| favorite/favorite.db | `4b1699953959192abaa6f8a4aa92b87a` | `2dcff10b76f756b248d064b35cf26d67d43f2b93f3da5f138dde1ee68049108d` |
|
||
| favorite/favorite_fts.db | `07a68944fba45ed5e3ff536d3619392e` | `d78287bb99b9404f53866bcc2b1344d8ff8edd520ecb6ffc36cb933dfcdce1c0` |
|
||
| emoticon/emoticon.db | `ca01076c55626bc4ebac1543e8f44d2a` | `85aa5fc40b53f8c2c42fd11818120f9c02645ac2d0cd2c56d9aafae4e869a459` |
|
||
| head_image/head_image.db | `76446b412632e8cf571b612450b98373` | `a5e1343be87076eb60e4780f77cf64d3af9131ac65ab95b4744d0854771224bd` |
|
||
| hardlink/hardlink.db | `b7a6afb6d478a483da8e77b1c76c66da` | `0b74bffe72d8e6fa646b9cbd7bcce4ad4731a6c928ebf637eefc7844e990948d` |
|
||
| bizchat/bizchat.db | `6662742c773b9316d9b27e1388265cac` | `4a6b1dcc417bc6ba4d56c7ffe9bd5998975836b13b7d1a5bf7f260e4b5dd6908` |
|
||
| (sns, solitaire — 类似,记录省略) | | |
|
||
|
||
**17 DB × 17 候选,每个 DB 恰好 1 个候选,salt 完全匹配**。这绝对不是巧合,
|
||
**这就是 SQLCipher PRAGMA key = "x'<32B raw key><16B salt>'"** 的 raw key 模式字面量,
|
||
Weixin 4.x 在运行时构造此字符串调用 sqlite3_key_v2()。
|
||
|
||
### 关键备注:这个证据是**当前会话最大突破**
|
||
|
||
下次接手时**直接信这些 key 就是真 key**,不要再回去暴力搜了——focus 在派生公式上。
|
||
|
||
### memdump 文件保留
|
||
|
||
- `weixin-memdump.bin` (240 MB) — 整个 Weixin 进程内存
|
||
- `weixin-memdump.idx` — 每个区域的 base address + offset
|
||
|
||
这俩可以离线反复扫描,无需重启微信。但**注意 Weixin 重启后 PID 变 / 内存布局变,这个 dump 仅对本次登录有效**。
|
||
|
||
---
|
||
|
||
## S10 · 用候选 raw key 解密失败(派生公式未破)
|
||
|
||
### 实验:把 `374c4e1a...` 当作 message_0.db 的 PRAGMA key raw 字节
|
||
|
||
按 SQLCipher 4 标准:
|
||
- `encKey` = raw key 32B(不派生)
|
||
- `hmacKey` = PBKDF2-HMAC-SHA512(raw_key, salt XOR 0x3a, **2 rounds**, 32B output)
|
||
- HMAC 计算: HMAC-SHA512(hmacKey, ciphertext || iv || pageNumber_LE_u32)[0..32]
|
||
- AES: AES-256-CBC(encKey, iv).decrypt(ciphertext)
|
||
|
||
### 测试矩阵(失败汇总)
|
||
|
||
| 实验 | KDF 算法 | HMAC 算法 | reserved | HMAC 输入 | 结果 |
|
||
|---|---|---|---|---|---|
|
||
| PoC 7 标准 SQLCipher 4 | PBKDF2-SHA512 × 2 | SHA512 | 48 | content+iv+pgLE | HMAC ❌ AES ❌ |
|
||
| PoC 7b 9 组合 | SHA1/256/512 × {std/256k/raw} | SHA1/512 | 36/48 | content+iv+pgLE | 全 ❌ |
|
||
| PoC 7c 7 派生 × 7 输入顺序 × 3 算法 | 全部 | 全部 | 48 | 全部排列 | 全 ❌ |
|
||
| PoC 7d 不验 HMAC,试 reserved=48/32/24/20/16 直接 AES | - | - | 5 种 | - | AES 解出非 SQLite 格式 |
|
||
| PoC 7e 396 组合(3 kdf × 7 iter × 3 hmac × 6 input) | 全部 | 全部 | 48 | 全部 | 全 ❌ |
|
||
|
||
### 结论(systematic-debugging Phase 4.5 触发)
|
||
|
||
3+ 次大规模尝试失败 → **不再继续猜测**,而是质疑前提:
|
||
|
||
- ✅ raw key 候选肯定正确(17:17 一一对应是不可能巧合的)
|
||
- ❌ HMAC/KDF 派生公式是**非标准**的——WCDB 改了 SQLCipher 默认行为
|
||
|
||
可能的非标准:
|
||
1. WCDB 把 `cipher_use_hmac = 0` 禁用 HMAC,reserved 48B 是别的用途
|
||
2. WCDB 改了 HMAC 输入(可能 include salt 或 page header 等)
|
||
3. raw key 在被用作 encKey 之前还要做一次变换(SHA256?XOR?)
|
||
4. WCDB 自定义的 cipher provider(`PRAGMA cipher_provider` 暴露但具体值未知)
|
||
|
||
### 下次接手必读
|
||
|
||
下次接手时,**不要重新做暴力**——key 已经找到。Focus 在以下:
|
||
|
||
1. **找现成的 WCDB 4.x decrypt 工具** —— GitHub 搜 `wxdump 4` / `WeChatMsg 4` / `wcdb-decrypt`
|
||
2. **直接读 WCDB 源码** —— GitHub `Tencent/wcdb` 的 `src/cpp/core/codec/` 目录
|
||
3. **API Monitor hook Weixin 调用 sqlite3_key_v2()** —— 看它传的真实 password buffer
|
||
|
||
可能还要做的:
|
||
- 把 17 个 raw key 用 17 个 DB 文件做 Python `pysqlcipher3` 库直接试解(如果能装上,会自动用 SQLCipher 库内的所有版本兼容模式)
|
||
- 试 `PRAGMA cipher_compatibility = 1/2/3/4` 各版本(已知 default=4,但可能存在 fallback)
|
||
|
||
---
|
||
|
||
## 当前所有候选 raw key(可直接复用)
|
||
|
||
> 下次接手保留这些 key 即可,不用重做内存 dump。
|
||
> **注意**: 这些 key 仅在**本机当前已登录用户**有效。如果用户重新登录或换号,key 会变。
|
||
|
||
| DB | raw key (32 bytes hex) |
|
||
|---|---|
|
||
| message_0 | `374c4e1a2da5a7bee6e0de020a1ad24e9234c9db709e93e4c01dd1eda9e40b50` |
|
||
| biz_message_0 | `5b1d42eefa14e7f050e41af8836a5fb9bb1f3698a67f10e958f450853066c5a3` |
|
||
| media_0 | `a194a326c5305bbe0808a47892f27b283079848748fe63a69f9d3f098575f231` |
|
||
| message_fts | `f8f9b52cd299a4f723408f28849db8c113aacd12686c44afcf7898c0ed3a21e4` |
|
||
| message_resource | `c0b004db73e9262fd407a428671d8bf48a7a7386b59e41516f3ca29ef392db70` |
|
||
| contact | `bd93ce66be76d2efc88ec5347b954f1b9bc14a148eb5eae9667c19866ca51338` |
|
||
| contact_fts | `7da06127d8a0a560c8ae7d1bb479fdbd944ca3720cb651b87fd8c209fec2edde` |
|
||
| session | `e43bd85c5b509122d431c1512183f9e2da75fb8997677271bc1a6862c99dfbb3` |
|
||
| general | `bfb2547c735713c88421537985b15e6867e2d87d9fb881e0beb3d145d014c3fc` |
|
||
| favorite | `2dcff10b76f756b248d064b35cf26d67d43f2b93f3da5f138dde1ee68049108d` |
|
||
| favorite_fts | `d78287bb99b9404f53866bcc2b1344d8ff8edd520ecb6ffc36cb933dfcdce1c0` |
|
||
| emoticon | `85aa5fc40b53f8c2c42fd11818120f9c02645ac2d0cd2c56d9aafae4e869a459` |
|
||
| head_image | `a5e1343be87076eb60e4780f77cf64d3af9131ac65ab95b4744d0854771224bd` |
|
||
| hardlink | `0b74bffe72d8e6fa646b9cbd7bcce4ad4731a6c928ebf637eefc7844e990948d` |
|
||
| bizchat | `4a6b1dcc417bc6ba4d56c7ffe9bd5998975836b13b7d1a5bf7f260e4b5dd6908` |
|
||
|
||
---
|
||
|
||
### 失败原因候选
|
||
|
||
1. **Key 根本不在我们的候选池里** — 可能 Weixin 4.x 的 key 不是 32 字节原始随机,而是某种派生结构
|
||
2. **SQLCipher 参数不标准** — Tencent 可能自定义了 HMAC 算法(BLAKE2? SHA3?)或者改了 reserved 布局
|
||
3. **Page 1 不是标准 SQLCipher 格式** — 可能 Weixin 在 SQLCipher 之上又套了一层加密(双层)
|
||
|
||
### 已尝试的坑
|
||
|
||
1. **PoC 3d 的验证条件太松**:只检查 pt[0..1] 是否是有效 page size,但 pageSize=1 虽有效但极少见,
|
||
其他字节也需验证。PoC 3e 改为严格验证 6 个字节
|
||
2. **PBKDF2 时间预算估算错误**: 初版想一次跑所有 iter × 所有候选,
|
||
实际 17K × 3 × 5 = 255K 组合,其中 256000 轮占主要时间,导致卡 4+ 小时
|
||
3. **Node stdout 在 bash 后台 buffer**: 进度输出在 task 结束前不 flush,误以为脚本挂了。
|
||
下次用 `node --no-stdout-buffering` 或显式 `process.stderr.write()` + flush
|
||
|
||
---
|
||
|
||
## S6 · 下次接手的建议路径
|
||
|
||
### 推荐优先级 1: ProcMon + API Monitor 黑盒分析
|
||
|
||
**动机**: 与其盲猜 Weixin 怎么用 key,不如**观察它怎么读**。
|
||
|
||
```
|
||
步骤:
|
||
1. 关 Weixin,清空 ProcMon
|
||
2. 启 ProcMon 过滤: Path contains "xwechat_files" OR "CryptUnprotect"
|
||
3. 启 Weixin 并登录
|
||
4. 分析 Weixin 启动后读了哪些文件、按什么顺序
|
||
5. 用 API Monitor (x64 版本) 在 Weixin.exe 启动前 attach,监控:
|
||
- advapi32!CryptUnprotectData
|
||
- bcrypt!BCryptDecrypt
|
||
- crypt32!CryptDecrypt
|
||
- sqlite3!sqlite3_key_v2 (如果有导出,否则看内存)
|
||
6. 检查 CryptUnprotectData 的 in/out 参数:in 应该是 key_info_data,out 应该是明文 key
|
||
```
|
||
|
||
**如果 Weixin 用 DPAPI**: 那我们可以用 `CryptUnprotectData` API 在同用户会话下直接解开 `key_info_data`,
|
||
**完全不需要破 SQLCipher**!这是最优路径。
|
||
|
||
### 推荐优先级 2: 找社区最新 Weixin 4.x 解密项目
|
||
|
||
已知可能相关的 GitHub 关键词(WebSearch 在当前环境失效,需另外找):
|
||
|
||
- `Weixin 4.0 sqlcipher decrypt`
|
||
- `xwechat_files LoginKeyInfoTable`
|
||
- `pywxdump Weixin 4`
|
||
- `wechat-dump-rs`
|
||
- `@0xlane WeChatMsg 4.0`
|
||
|
||
如果有现成项目,他们的 key 派生算法就是答案。
|
||
|
||
### 推荐优先级 3: 继续完成 Phase 3/4 暴力
|
||
|
||
如果没时间做逆向,把 `poc-3e-crack-strict.mjs` 的 Phase 3 (iter=64000) 和 Phase 4
|
||
(iter=256000) 跑完。**在 Node worker_threads 并行**可以把 4 小时压到 1 小时。
|
||
|
||
### 推荐优先级 4: 扩大候选池
|
||
|
||
- 把内存 dump 的候选从 `5000` 扩到 `47743`(全量 dedup 后)
|
||
- 加入 **相邻滑窗互异** (`diff(buf[i], buf[i+1]) > threshold`) 启发
|
||
- 考虑 key 不一定是 32 字节,也可能是 16 字节(AES-128)或 64 字节(key + hmac_key 合并)
|
||
|
||
### 推荐优先级 5: 放弃 SQLCipher 路径,用 DLL 注入
|
||
|
||
如果以上都不行,回退到调研报告 §4.2 "WeChatFerry 4.x 路线"。注入风险高但确定能做。
|
||
|
||
---
|
||
|
||
## 文件索引 (packages/backend/poc/weixin-4x/)
|
||
|
||
| 文件 | 作用 | 状态 |
|
||
|---|---|---|
|
||
| `poc-1-watch-db.mjs` | fs.watch 监听 DB 变化 | ✅ v1,在 Windows 漏事件 |
|
||
| `poc-1b-poll-all-db.mjs` | 轮询 stat 监听 | ✅ 确认了实时写入信号 |
|
||
| `poc-2-dump-candidates.ps1` | Win32 内存 dump v1 | ❌ 文件写入 bug |
|
||
| `poc-2b-dump-candidates.ps1` | Win32 内存 dump 修复版 | ✅ 生成 500K 候选 |
|
||
| `poc-3a-dedup.mjs` | 候选去重 + 频度排序 | ✅ 得到 47K 唯一候选 |
|
||
| `poc-3b-explore-keyinfo.mjs` | 打开 key_info.db 看 schema | ✅ 确认 LoginKeyInfoTable |
|
||
| `poc-3c-dump-keyinfo-raw.mjs` | 逐字节对比 86 条记录 | ✅ 识别出固定头/变化区 |
|
||
| `poc-3d-crack-key.mjs` | 宽验证暴力(已废) | ⚠️ 假阳性 |
|
||
| `poc-3e-crack-strict.mjs` | 严格验证暴力 (SHA1) | ❌ 3 phase 全未命中 |
|
||
| `poc-3f-wcdb-aware.mjs` | WCDB-aware (SHA512+256000+HMAC) | ❌ 仍未命中 |
|
||
| `poc-4-dpapi.ps1` | DPAPI 试解 key_info_data | ❌ 数据无效,不是 DPAPI |
|
||
| `poc-5-scan-hex-strings.ps1` | PS 扫内存找 hex 字符串(慢) | ⚠️ 被 poc-6 替代 |
|
||
| `poc-6a-memdump.ps1` | 整内存 dump 到磁盘(快) | ✅ 240MB memdump |
|
||
| `poc-6b-scan-memdump.mjs` | Node 高速扫 memdump | ✅ 找到 10068 hex + 20 xQuote |
|
||
| `poc-6c-find-pragma-context.mjs` | 找候选 key 上下文 | ✅ 找到 sqlcipher_export 调用点 |
|
||
| `poc-7-decrypt.mjs` | 用 raw key 解 message_0.db | ❌ HMAC 失败 |
|
||
| `poc-7b/7c/7d/7e` | 各种 HMAC/KDF 派生组合 | ❌ 400+ 组合全败 |
|
||
| `poc-8-collect-db-keys.mjs` | **每 DB 一一对应候选 key** ⭐ | ✅ 17:17 完美对应 |
|
||
| `key-candidates.bin` | 500K 候选 (16 MB) | 产物,可复用 |
|
||
| `key-candidates-dedup.bin` | 47K 去重候选 (1.5 MB) | 产物,可复用 |
|
||
| `key_info_copy.db` | key_info.db 拷贝 | 产物,可复用 |
|
||
| `weixin-memdump.bin` | 240MB 全内存 dump | ⭐ 关键产物 |
|
||
| `weixin-memdump.idx` | 内存区域索引 | ⭐ 关键产物 |
|
||
|
||
---
|
||
|
||
## 关键事实摘录(下次 5 分钟读懂)
|
||
|
||
1. **UIA 路径死**: Weixin 4.x 窗口类 `Qt51514QWindowIcon`,UIA 树仅 3 空壳节点
|
||
2. **DB 实时写**: message_0.db-wal 在用户发消息时 mtime 立刻变(但 size 不变,WAL 预分配)
|
||
3. **key_info.db 明文**: SQLite 标准文件,`LoginKeyInfoTable` 86 条 180B 记录,里面存加密的 key
|
||
4. **dump 内存不要管理员**: 同用户进程用 OpenProcess 就能读 Weixin 内存
|
||
5. **Weixin 用 WCDB**(Tencent fork of SQLCipher),`cipher_compatibility = 4`、`page_size = 4096`
|
||
6. **找到 17 个 raw key 候选**: 内存里有 17 条 `x'<32B key><16B salt>'` 字面量,17:17 与 DB salt 完美一一对应
|
||
7. **暴力失败**: 17 个 raw key + 400+ HMAC/KDF 派生组合全部不通过 SQLCipher 4 标准验证
|
||
8. **结论**: key 100% 正确,**WCDB 自定义了 KDF/HMAC 公式**,需查源码或 API hook 才知道
|
||
|
||
---
|
||
|
||
## 风险 / 合规性再次确认
|
||
|
||
按调研报告 `2026-05-11-weixin-4x-automation-survey.md`:
|
||
|
||
- 读本机 DB 文件 = 读用户自己电脑上的文件 → 零技术风控(Tencent 服务器无信号)
|
||
- 法律上属"处理用户个人数据",产品上线前务必过法务 + 用户明示授权
|
||
- 破解 key 的过程(即使是研究自己账号)严格意义触及 "反绕过 DRM",但因对象是**自己数据**且**非商用传播**,属低风险
|
||
- 本 PoC 所有产物(候选 bin、key_info 拷贝)**不可分发**,每台机器的 key 都不同,也无迁移价值
|
||
|
||
---
|
||
|
||
## 更新日志
|
||
|
||
- **2026-05-11 03:xx** (Claude): 初稿。完成 S1-S5。
|
||
- **2026-05-11 03:4x** (Claude): 补 S6 暴力解密 Phase 1/2/3 结果 — 全部未命中。
|
||
- **2026-05-12 10:xx** (Claude): 重大突破 + 新阻塞。S7-S9 完成,找到 17:17 一一对应 raw key 但派生失败。
|
||
- **2026-05-12 11:xx** (Claude): ✅ **完全攻破**。
|
||
- 下载 WCDB 源码到 `wcdb-master/`,阅读 `deprecated/android/sqlcipher/sqlite3.c` 的 codec 实现
|
||
- **关键发现**:`reserve_sz = iv_sz + hmac_sz` 中 `hmac_sz` 对 HMAC-SHA512 是 **64 字节**,
|
||
所以总 reserve = 80 字节,不是我之前以为的 48 字节(SQLCipher 3 时代的 SHA1 才 20)
|
||
- poc-7f-reserve80.mjs 用 reserve=80 一次性通过 HMAC + AES
|
||
- poc-10-decrypt-and-read.mjs 全量解密 10644 pages 用时 0.2s,0 HMAC failure
|
||
- poc-12-find-123.mjs 在 `Msg_0600e242d978ea1f90b85ac3fe2d22ba` 表找到 **"123 测试"** 那条
|
||
- **方案 1 (DB 读 + Win32 SendInput) 完全可行**,可以开始 production 集成
|