macOS 微信数据库解密

通过 Frida hook CCCryptorCreate,在运行时拦截 WeChat 传入 SQLCipher 的 AES enc_key,无需逆向二进制、无需 root、不触碰 Keychain。

✓ 实测成功 WeChat 4.1.7 (macOS) SQLCipher v4 Frida 17.9.10 2026-05-16
解密数据库
3 / 30+
contact · message_0 · session
消息表数量
3,654
message_0.db 中的会话分表
联系人数量
26 表
contact.db 成功打开
关键工具
Frida + sqlcipher
brew install sqlcipher

⚡ 三分钟速览(核心结论)

微信运行时
WeChat.app
→
打开数据库
WCDB/SQLCipher
→
AES 密钥设置
CCCryptorCreate
→
Frida 拦截
args[3] = key
→
sqlcipher
x'' 打开
💡
核心发现:WeChat 4.x on macOS 使用 Apple CommonCrypto(而非 BoringSSL)做 SQLCipher 的 AES 加解密。 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
⚠️
必须先打开微信并登录,Frida 需要附加到运行中的进程。数据库在微信启动后即被打开,CCCryptorCreate 会在读取每一页时被调用。

步骤三:Hook CCCryptorCreate

/tmp/wechat_hook.js — Frida 注入脚本
/*
 * 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 驱动 + 自动验证

/tmp/wechat_frida.py — 驱动脚本
#!/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 打开数据库

Shell — 打开解密数据库
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 决定:

Python — 计算消息表名
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 / contact.db
1064e7dfb42cc3ea447da335ca848a2381e8738ad1ebc31e76e993456ba1fe2b
✓ sqlcipher 验证:26 张表
message / message_0.db
de0b8d3304e9940e96baa7c9ecfe33a226e68590fa0060d7d09ee088e1a104f8
✓ sqlcipher 验证:3654 张 Msg_* 表
session / session.db
f6b305477cda040af70a41f6321c3017b3400a3688e712bb1c9cf7aee50515b8
✓ sqlcipher 验证:14 张表
其他数据库(message_1~10, general, sns...)
e5364de757971b4ead1db0d667d9e78bd7a6612254f48a19b43ae01247c9d28f
待确认 — 滚动浏览对应聊天触发后再验证

真实查询结果

联系人列表(contact.db)

username | nick_name | remark ───────────────────────────────────────────────────────────── wxid_9802458023412 | Kaki | Kaki 青藤 93 荷兰 mali847944 | 马丽 | 您好,招行个贷马丽 wxid_kb6z85qjjrrv21| 1.26 5 | 我禧瑞春秋物业前台 wxid_0puuvbx9df1321| Qian | Qian 青藤 37 财务 yuanhanweibo | 幸运 | 幸运 lulu 青藤 90摩羯 ... 更多联系人 ...

最近会话(session.db)

username | 时间 | 最新消息预览 ────────────────────────────────────────────────────────────────── 3164323182@chatroom | 15:31:16 | [強][強][強] 44170630061@chatroom| 15:30:55 | @Letty 戈壁大神[呲牙][玫瑰] brandsessionholder | 15:31:26 | [38条] 新智元: 突发!OpenAI大规模重组... gh_8391a4921226 | 15:29:52 | MiniCPM-V4.6登顶 Hugging Face Trending! qqtech | 15:26:25 | 稚晖君抛出"三条曲线",讲了一套具身智能出海新逻辑 ... 共 20+ 活跃会话 ...

聊天消息(message_0.db · 特定对话)

type | sender | 时间 | 内容 ──────────────────────────────────────────────────────────── 1 | 543 | 2026-05-16 15:19 | 微信里"拿 DB 数据"之前其实分两类... 1 | 15 | 2026-05-15 12:39 | 生产力大于需求 1 | 15 | 2026-05-15 12:39 | 短剧卷是肯定的 1 | 543 | 2026-05-15 12:39 | 中国人干死中国人 1 | 543 | 2026-05-15 12:38 | 短剧现在好卷 1 | 15 | 2026-05-15 12:38 | 干不过 claude ... local_type=47 是图片/视频,local_type=10000 是系统消息 ...

群聊消息(44170630061@chatroom)

群消息格式:content = "sender_wxid:\n内容" ──────────────────────────────────────────── a36269329: @Letty 戈壁大神[呲牙][玫瑰][玫瑰] wxid_5194951949422: 一起共修 a36269329: Web3 大神 @Ivy [呲牙][玫瑰][玫瑰]

后续扩展

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 更新后需重验)
⚠️
注意事项:密钥为 运行时动态值,不存储在任何磁盘文件中(Keychain 无记录)。 WeChat 版本更新后 AES 密钥 可能 变化,需重新运行 Frida hook 提取。 本文档和密钥仅用于自己账号的个人数据访问,勿用于他人数据。