macOS 微信数据库解密
通过 Frida hook CCCryptorCreate,在运行时拦截 WeChat 传入 SQLCipher 的 AES enc_key,无需逆向二进制、无需 root、不触碰 Keychain。
⚡ 三分钟速览(核心结论)
CCCryptorCreate(op, AES, options, key_ptr, 32, iv, ref) 的 args[3]
就是 32 字节的 raw enc_key,直接以 x'<hex>' 格式传给 sqlcipher 即可打开数据库。
不需要 PBKDF2 反推,不需要逆向二进制。
❌ 无效路线(踩过的坑)
| 方案 | 预期 | 实际结果 | 原因 |
|---|---|---|---|
| lldb 内存搜索 | 在堆内存暴力扫描 32 字节密钥 | ✗ 0 MB 被读取 | WeChat 沙箱 + 进程状态问题,GetMemoryRegionInfo 全部失败 |
| vmmap + salt 定位 | 找到 salt 字节 → 在附近找 key | ✗ salt 未找到 | vmmap 区域地址与 lldb attach 后的地址不对应 |
| hook 系统 sqlite3_key | 系统 libsqlite3.dylib 的 sqlite3_key | ✗ 从未被调用 | WeChat 静态链接 WCDB,不调用系统 sqlite3_key |
| hook EVP/BoringSSL | 微信可能用 BoringSSL 做 AES | ✗ 未测到 | SQLCipher 用的是 CommonCrypto,BoringSSL 只用于 TLS |
| HMAC Python 验证 | 用 HMAC 校验候选 key | ✗ 全部 ❌ | 验证逻辑看起来正确但有细节偏差;改用 sqlcipher 直接验证 |
🔍 突破口:CCCryptorCreate
扫描微信进程所有模块的导出符号时,发现 libsqlite3.dylib 的 sqlite3_key 在进程中存在,但从未被 WeChat 调用(静态链接的 WCDB 有自己的副本)。
随后转向 更底层的 AES 层,hook 了 CommonCrypto 的 CCCryptorCreate。
这个函数在 WeChat 打开数据库(解密每一页)时被高频调用,alg=0(AES)、keyLen=32(AES-256),args[3] 直接就是 enc_key。
10 秒内捕获了 4 个唯一的 32 字节 AES-256 密钥,用 sqlcipher 逐一验证,成功匹配 3 个数据库。
步骤一:定位数据库文件
微信 4.x 在 macOS 上的数据库路径(已知账号 yibooo_ac07):
~/Library/Containers/com.tencent.xinWeChat/Data/Documents/
└── xwechat_files/
└── yibooo_ac07/
└── db_storage/
├── contact/contact.db ← 联系人
├── message/message_0.db ← 消息分片 0
├── message/message_1.db ← 消息分片 1
│ ...
├── message/message_10.db ← 消息分片 10
├── session/session.db ← 会话列表
├── sns/sns.db ← 朋友圈
└── general/general.db ← 通用配置
.db 文件均为 SQLCipher v4 加密,文件头不是标准 SQLite format 3,而是随机盐值(前 16 字节)。Page size = 4096,reserved = 48 字节(16 IV + 32 HMAC-SHA256)。步骤二:确认 Frida 可用
# Frida 17.9.10 已通过 pip 安装
python3 -c "import frida; print(frida.__version__)"
# 17.9.10
# CLI 工具路径
~/Library/Python/3.9/bin/frida
~/Library/Python/3.9/bin/frida-ps
步骤三:Hook CCCryptorCreate
/*
* macOS WeChat SQLCipher enc_key 拦截器
* 原理:hook CommonCrypto 的 CCCryptorCreate,捕获 AES-256 密钥
* CCCryptorCreate(op, alg, options, key, keyLength, iv, cryptorRef)
* alg=0 → AES
* op=1 → 解密 (decrypt)
* key → 32 字节 enc_key ← 我们要的
*/
var found_keys = {};
// 找到 CCCryptorCreate 函数地址
var cc_create = Module.findExportByName(null, 'CCCryptorCreate');
if (cc_create) {
Interceptor.attach(cc_create, {
onEnter: function(args) {
try {
var op = args[0].toInt32(); // 0=encrypt, 1=decrypt
var alg = args[1].toInt32(); // 0=AES
var keyLen = args[4].toInt32();
// 只关注 AES-256(keyLen=32)
if (alg === 0 && keyLen === 32) {
var key = args[3].readByteArray(32);
var hex = Array.from(new Uint8Array(key))
.map(function(b) {
return ('0' + b.toString(16)).slice(-2);
}).join('');
// 去重后发送
if (!found_keys[hex]) {
found_keys[hex] = true;
send({ type: 'aes_key', op: op,
len: keyLen, hex: hex });
}
}
} catch(e) {}
}
});
send({ type: 'hooked', func: 'CCCryptorCreate',
addr: cc_create.toString() });
}
send({ type: 'ready' });
步骤四:Python 驱动 + 自动验证
#!/usr/bin/env python3
import sys, os, time, subprocess
# Frida 路径(pip 安装在用户目录)
sys.path.insert(0, os.path.expanduser("~/Library/Python/3.9/lib/python/site-packages"))
import frida
DB_BASE = os.path.expanduser(
"~/Library/Containers/com.tencent.xinWeChat/Data/Documents/"
"xwechat_files/yibooo_ac07/db_storage/"
)
def test_key_with_sqlcipher(key_hex, db_path):
"""用 sqlcipher 验证 key 是否能打开数据库"""
pragma = f"PRAGMA key = \"x'{key_hex}'\";"
r = subprocess.run(
['sqlcipher', db_path],
input=f"{pragma}\nSELECT count(*) FROM sqlite_master;\n",
capture_output=True, text=True
)
lines = [l for l in r.stdout.split('\n') if l.strip() and l != 'ok']
if lines and lines[0].isdigit():
return int(lines[0]) # 返回表数量
return None
# 获取微信 PID
pid = int(subprocess.run(['pgrep', '-x', 'WeChat'],
capture_output=True, text=True).stdout.strip().split('\n')[0])
print(f"[*] 附加到 WeChat PID={pid}")
session = frida.attach(pid)
script = session.create_script(open('/tmp/wechat_hook.js').read())
captured = []
databases = {
'contact': 'contact/contact.db',
'message0': 'message/message_0.db',
'session': 'session/session.db',
}
def on_message(msg, data):
if msg['type'] != 'send': return
p = msg['payload']
if p.get('type') == 'aes_key':
k = p['hex']
print(f" 捕获 AES key: {k[:16]}...")
# 验证这个 key 能打开哪些数据库
for dbname, relpath in databases.items():
tables = test_key_with_sqlcipher(k, DB_BASE + relpath)
if tables is not None:
print(f" ✅ {dbname}: key 匹配!表数量={tables}")
captured.append((k, dbname))
open('/tmp/wechat_key.hex', 'a').write(f"{dbname}:{k}\n")
script.on('message', on_message)
script.load()
print("[*] 监听中,在微信里操作触发数据库读取... (Ctrl+C 退出)")
try:
while True: time.sleep(1)
except KeyboardInterrupt: pass
session.detach()
步骤五:用 sqlcipher 打开数据库
DB_BASE="~/Library/Containers/com.tencent.xinWeChat/Data/Documents/xwechat_files/yibooo_ac07/db_storage"
# 打开联系人数据库
sqlcipher "$DB_BASE/contact/contact.db" <<SQL
PRAGMA key = "x'1064e7dfb42cc3ea447da335ca848a2381e8738ad1ebc31e76e993456ba1fe2b'";
SELECT username, nick_name, remark FROM contact WHERE local_type=3 LIMIT 20;
SQL
# 打开消息数据库
sqlcipher "$DB_BASE/message/message_0.db" <<SQL
PRAGMA key = "x'de0b8d3304e9940e96baa7c9ecfe33a226e68590fa0060d7d09ee088e1a104f8'";
-- 消息表名 = "Msg_" + MD5(username)
SELECT local_type, datetime(create_time,'unixepoch','localtime'), message_content
FROM Msg_8f5f8034b0f757ef2701729beabc4770 -- MD5('dp543831577')
WHERE local_type = 1
ORDER BY create_time DESC LIMIT 20;
SQL
步骤六:消息表名计算
WeChat 的每个聊天对话在 message_N.db 中对应一张表 Msg_<md5>,表名由会话 ID 的 MD5 决定:
import hashlib
def msg_table(username):
md5 = hashlib.md5(username.encode()).hexdigest()
return f"Msg_{md5}"
# 例子
print(msg_table("dp543831577"))
# → Msg_8f5f8034b0f757ef2701729beabc4770
print(msg_table("44170630061@chatroom"))
# → Msg_99ce6fe9bc90c6320bb5e75714360da0
密钥映射表
以下是本次成功捕获并验证的密钥(仅本机本账号有效,每次微信更新或重置后需重新提取):
真实查询结果
联系人列表(contact.db)
最近会话(session.db)
聊天消息(message_0.db · 特定对话)
群聊消息(44170630061@chatroom)
后续扩展
-
获取其余消息分片的密钥
在微信里滚动浏览对应的聊天,触发 WeChat 读取 message_1.db~message_10.db,Frida hook 会自动捕获对应的 AES key,用同样的 sqlcipher 验证方法确认后记录。
-
写 Python 脚本全量导出
结合三个密钥,用 Python 的
apsw(支持 SQLCipher)或 subprocess 调 sqlcipher,批量读取所有消息写入自己的 SQLite 或 JSON。 -
与 Android Frida 方案对接
Android 侧已经有
hook h9.r9 / h9.A8的消息拦截链路,macOS 侧则补充了历史数据的解密能力,两路数据可合并到同一个 SessionDB / read_history.db。 -
配置 Frida 持久化 Hook
将 hook 脚本和 Python 驱动写成 launchd plist 或 shell 别名,每次启动微信后自动运行 10 秒捕获密钥,无需手动操作。
Android vs macOS 方案对比
| 维度 | Android(已实现) | macOS(本次) |
|---|---|---|
| Hook 目标 | h9.r9 / h9.A8 (Java) |
CCCryptorCreate (C) |
| 获取数据 | 实时消息对象(D0/O0/j 字段) | SQLCipher AES enc_key → 离线全量 |
| 数据库 | 不直接解密 EnMicroMsg.db | 直接解密 contact/message/session.db |
| Frida 工具 | frida-server on Android + Python client in Debian proot | frida Python 17.9.10 on macOS |
| 密钥格式 | Java passphrase(hook WCDB open 可拿) | 32 字节 raw AES enc_key → x'hex' |
| 稳定性 | 高(hook 消息对象,不触碰 DB) | 中(依赖 CCCryptorCreate,WeChat 更新后需重验) |