# WeChat macOS 数据库解密指南:3.x vs 4.x 完整对比 ## 一、背景 微信 macOS 版使用 SQLCipher 加密本地数据库。不同大版本的加密参数完全不同,解密方法不能混用。 | 项目 | WeChat 3.x (≤3.8.x) | WeChat 4.x (≥4.0.x) | |------|---------------------|---------------------| | SQLCipher 版本 | **3** | **4** | | 默认 page_size | **1024** | **4096** | | HMAC 算法 | HMAC-**SHA1** (20 bytes) | HMAC-**SHA512** (64 bytes) | | Reserve 区大小 | **48** bytes (IV16 + HMAC20 + pad12) | **80** bytes (IV16 + HMAC64) | | KDF 迭代次数 | **64,000** | **256,000** | | KDF 算法 | PBKDF2-SHA1 | PBKDF2-SHA512 | | 密钥使用方式 | 32字节 raw key **直接使用** | 32字节 raw key **直接使用** | --- ## 二、数据存放位置 ### WeChat 3.x ``` ~/Library/Containers/com.tencent.xinWeChat/Data/ Library/Application Support/com.tencent.xinWeChat/ 2.0b4.0.9// Message/msg_0.db ~ msg_9.db ← 聊天消息 (按hash分片) Contact/wccontact_new2.db ← 联系人 Session/session_new.db ← 会话列表 Group/group_new.db ← 群信息 Favorites/favorites.db ← 收藏 ...共约 34 个 DB ``` ### WeChat 4.x ``` ~/Library/Containers/com.tencent.xinWeChat/Data/ Documents/xwechat_files// db_storage/ message/message_0.db ~ message_5.db ← 聊天消息 contact/contact.db ← 联系人 session/session.db ← 会话列表 ... ``` **关键区别**: 3.x 用 MD5 hash 做账号目录名(看不出是谁),4.x 用微信ID做目录名。 --- ## 三、密钥提取(核心步骤) 两个版本的密钥提取方式完全一样:**从微信进程内存中读取 32 字节 raw key**。 ### 前提条件 1. 微信已登录且正在运行 2. 安装 Frida:`pip3 install frida-tools` 或 `brew install frida` 3. 管理员密码(sudo 权限) ### macOS 权限模型详解 Frida 附加到微信进程需要调用 `task_for_pid()` 这个 Mach 内核接口。macOS 的 `taskgated` 守护进程控制谁能调用它,核心逻辑取决于**目标进程的代码签名状态**: ``` taskgated 授权检查流程: 调用者 sudo task_for_pid(target_pid) │ ▼ ┌─ 目标进程有 Hardened Runtime 吗?──────────────────────────┐ │ │ │ YES (flags 包含 0x10000 runtime) │ │ → Apple 官方签名的 App, 如 App Store 下载的微信 │ │ → taskgated 严格模式: │ │ 1. 调用者必须是 root (sudo) │ │ 2. 调用者的"负责应用"必须有 TCC DeveloperTool 授权 │ │ 3. SSH 上下文的负责应用是 sshd → 无法获得 TCC 授权 │ │ 4. 本机 Terminal 可以弹窗获得 TCC 授权 ✅ │ │ → SSH 永远失败,本机 Terminal + sudo 可以 │ │ │ │ NO (Ad-hoc 签名, 或无 hardened runtime) │ │ → 自签名/防撤回补丁修改过的 App │ │ → taskgated 宽松模式: │ │ 1. 调用者是 root (sudo) 即可 ✅ │ │ 2. 不检查 TCC DeveloperTool │ │ 3. SSH sudo 也能成功! ✅ │ │ │ └────────────────────────────────────────────────────────────┘ ``` #### 🔑 关键发现:决定因素是目标 App 的签名,不是终端的权限! 我们实测发现: | 场景 | WeChat 签名 | codesign flags | sudo task_for_pid | SSH 可行? | |------|------------|----------------|-------------------|-----------| | MacBook (macOS 15.x) | **Ad-hoc** (防撤回补丁自签名) | `0x2(adhoc)` | ✅ 直接成功 | ✅ **可行** | | Mac mini (Catalina) | Apple 官方签名 | `runtime` | ✅ 本机 Terminal | ❌ SSH 失败 | | MacBook Pro (Big Sur) | Apple 官方签名 | `runtime` | ✅ 本机 Terminal | ❌ SSH 失败 | 本机 WeChat 的签名信息(安装了防撤回补丁后): ``` Signature=adhoc TeamIdentifier=not set flags=0x2(adhoc) ← 没有 hardened runtime! Internal requirements=0 ← 没有签名要求 Entitlements=无 ← 没有 entitlements ``` 正常 Apple 签名的 WeChat: ``` Signature=Apple Developer TeamIdentifier=5A4RE8SF68 flags=0x10002(adhoc,runtime) ← 有 hardened runtime! ``` #### 这意味着什么? 如果你想通过 SSH 远程提取密钥,有两条路: **方法 1: 对微信 App 重新签名 (推荐,不需要关 SIP)** ```bash # 在目标机器上执行(SSH 即可): # 1. 去掉 hardened runtime,用 ad-hoc 重签名 sudo codesign --force --deep --sign - /Applications/WeChat.app # 2. 重启微信 (需要用户在 GUI 操作,或用 kill + open) kill $(pgrep -x WeChat) # 用户需要在 GUI 上重新打开微信并登录 # 3. 现在 SSH sudo 就能 task_for_pid 了! sudo ./find_all_keys_macos ``` ⚠️ **副作用**: - 重签名后微信可能无法自动更新 - 某些 iCloud/Keychain 功能可能受影响 - 微信小程序可能报安全错误 - 需要重新登录微信 **方法 2: 关闭 SIP (不推荐)** - 需要重启进入恢复模式 - 安全风险大,影响整个系统 #### 常见误区 | 误区 | 真相 | |------|------| | "需要给终端完全磁盘访问才能调试" | ❌ FDA 控制文件访问,不控制进程调试。但 SSH 重签名 App 时需要 FDA | | "需要给终端开发者工具权限" | ⚠️ 仅当目标 App 有 hardened runtime 时才需要 | | "SSH 下永远无法 task_for_pid" | ❌ 如果目标 App 是 ad-hoc 签名的,SSH sudo 可以 | | "macOS 版本决定了能否 SSH 调试" | ❌ 主要取决于目标 App 的签名状态 | | "SIP 阻止了调试微信" | ❌ SIP 只保护系统进程,微信不受 SIP 保护 | | "加了 sshd 到 FDA 就行" | ❌ 还需要加 `/usr/libexec/sshd-keygen-wrapper`,且要重连 SSH | | "微信开着也能重签名" | ❌ 运行中的 binary/dylib 被占用,codesign 会失败 | > 📖 详细权限指南见 [macOS 权限完全指南](macos-permission-guide.md) ### 新手操作步骤 根据你的微信签名状态,选择对应方案: ```bash # 首先检查你的微信签名状态 codesign -dv /Applications/WeChat.app 2>&1 | grep -E "Signature|flags" # 如果显示 Signature=adhoc, flags=0x2(adhoc) # → 恭喜!直接 sudo 即可,SSH 也行 sudo ./find_all_keys_macos # 如果显示 Authority=..Apple.., flags 包含 runtime # → 需要本机 Terminal 操作,或者先重签名: sudo codesign --force --deep --sign - /Applications/WeChat.app # 然后重启微信,再用 sudo 提取密钥 ``` ### SSH 远程提取方案(需 ad-hoc 签名) 以下方法全部在 Apple 官方签名的微信上失败(经多台机器穷举验证): - `sudo frida -p ` → "unable to access process" - `lldb -p ` → "non-interactive debug session" - `sudo gcore ` → "insufficient privilege" - 自编译带 `com.apple.security.cs.debugger` entitlement 的 C 程序 → KERN_FAILURE=5 - `vmmap`/`heap` → 只能看元数据,无法读内存内容 - LaunchDaemon (root) / LaunchAgent (Aqua) / `launchctl asuser` → 全部失败 - 修改 TCC.db → SIP 保护,`restricted` 标志,只读 ### 实际操作步骤 #### 步骤 1: 找到微信进程 PID ```bash pgrep -x WeChat # 输出例如: 51051 ``` #### 步骤 2: 准备 Frida 扫描脚本 创建 `scan_keys.js`: ```javascript // 扫描内存中所有看起来像加密密钥的 hex 字符串 // WeChat 3.x: 64字符hex (32字节key) // WeChat 4.x: 也可能是64字符hex,或96字符hex (48字节) var ranges = Process.enumerateRanges('r--'); var pattern_64 = /^[0-9a-f]{64}$/; ranges.forEach(function(range) { try { var buf = range.base.readByteArray(range.size); // 实际扫描逻辑...扫描连续的hex字符 } catch(e) {} }); ``` #### 步骤 3: 在本机 Terminal 用 Frida 注入 ```bash # 方法 A: 用自己的脚本 (如果你已知内存地址) sudo frida -p $(pgrep -x WeChat) --debug -l debug.js # 方法 B: 用扫描脚本自动搜索 sudo frida -p $(pgrep -x WeChat) -l scan_keys.js ``` 输出示例(3.x 实际结果): ``` 600000d8d930 72 8e 8e dd 26 68 48 37 92 89 2c 7b 24 10 58 9d r...&hH7..,{$.X. 600000d8d940 3e 64 1e e7 ef b3 47 c9 9f 17 3d 58 bf 9d 38 05 >d....G...=X..8. ``` 这 32 字节就是密钥:`728e8edd2668483792892c7b2410589d3e641ee7efb347c99f173d58bf9d3805` --- ## 四、解密实现 ### 核心原理 SQLCipher 加密的每一页(page)结构: ``` ┌─────────────────────────────────────────────────────┐ │ 第 1 页 (特殊) │ ├──────────┬──────────────────────┬───────────────────┤ │ Salt │ 加密的数据 │ Reserve区 │ │ 16 bytes │ (page_size-16-rsv) │ IV+HMAC+padding │ ├──────────┴──────────────────────┴───────────────────┤ │ │ │ 第 2~N 页 (普通页) │ ├────────────────────────────────┬────────────────────┤ │ 加密的数据 │ Reserve区 │ │ (page_size - reserve) │ IV + HMAC + padding │ └────────────────────────────────┴────────────────────┘ ``` **第 1 页特殊处理**:前 16 字节是明文 salt(不加密),解密后需要拼回 `SQLite format 3\0` 头。 ### WeChat 3.x 解密参数 ```python # SQLCipher 3 参数 PAGE_SIZE = 1024 RESERVE = 48 # 16(IV) + 20(HMAC-SHA1) + 12(padding) KDF_ITER = 64000 HMAC_ALGO = 'sha1' HMAC_LEN = 20 ``` ### WeChat 4.x 解密参数 ```python # SQLCipher 4 参数 PAGE_SIZE = 4096 RESERVE = 80 # 16(IV) + 64(HMAC-SHA512) KDF_ITER = 256000 HMAC_ALGO = 'sha512' HMAC_LEN = 64 ``` ### 3.x 的特殊陷阱:同一账号的 DB 使用不同参数! 这是 3.x 最坑的地方。我们实测发现同一个账号的 34 个 DB 居然用了 **4 种不同的 SQLCipher 配置**: | DB 类别 | page_size | key 模式 | |---------|-----------|---------| | 大部分 DB (msg, contact, session...) | 1024 | raw key **直接使用** | | WebTemplate/webtemplate.db | 4096 | raw key **直接使用** | | FTS 索引 (ftsmessage, ftsfilemessage) | 1024 | PBKDF2(raw_key, salt, 64000) | | mediaData.db | 4096 | PBKDF2(raw_key, salt, 64000) | 还有 3 个 DB 根本没加密(kv_config, solitaire_chat, multiTalk),直接复制即可。 所以解密脚本必须自动判断并尝试多种组合。 ### 完整解密代码(Python, 3.x) ```python #!/usr/bin/env python3 """WeChat 3.x macOS 数据库解密器""" import hashlib, hmac, struct, os, shutil from Crypto.Cipher import AES def decrypt_page(page_data, enc_key, page_no, page_size, reserve): """解密单个 page""" if page_no == 1: # 第1页: 前16字节是salt(明文), 后面才是加密数据 salt = page_data[:16] encrypted = page_data[16:page_size - reserve] iv = page_data[page_size - reserve:page_size - reserve + 16] else: encrypted = page_data[:page_size - reserve] iv = page_data[page_size - reserve:page_size - reserve + 16] cipher = AES.new(enc_key, AES.MODE_CBC, iv) decrypted = cipher.decrypt(encrypted) if page_no == 1: # 拼回 SQLite 头: "SQLite format 3\0" + 解密内容 + reserve填零 return b'SQLite format 3\x00' + decrypted + b'\x00' * reserve else: return decrypted + page_data[page_size - reserve:] def verify_hmac_page1(page_data, enc_key, page_size, reserve): """验证第1页的 HMAC-SHA1 (SQLCipher 3)""" salt = page_data[:16] mac_salt = bytes([b ^ 0x3a for b in salt]) mac_key = hashlib.pbkdf2_hmac('sha1', enc_key, mac_salt, 2, dklen=32) content = page_data[16:page_size - reserve] iv = page_data[page_size - reserve:page_size - reserve + 16] stored_hmac = page_data[page_size - reserve + 16:page_size - reserve + 36] msg = content + iv + struct.pack('/Message/msg_0.db # 应该显示 "data" 而不是 "SQLite 3.x database" # 4. 提取密钥 (必须在本机 Terminal!) sudo frida -p $(pgrep -x WeChat) -l scan_keys.js # 记下输出的 64 字符 hex 字符串 # 5. 运行解密 python3 decrypt_db.py # 6. 验证 file decrypted/Message/msg_0.db # 应该显示 "SQLite 3.x database" sqlite3 decrypted/Message/msg_0.db "SELECT COUNT(*) FROM (SELECT name FROM sqlite_master WHERE type='table')" ``` ### 常见问题 | 问题 | 原因 | 解决 | |------|------|------| | Frida 报 "unable to access process" | SSH 下运行 / TCC 未授权 | 必须在本机 Terminal 运行 | | 解密后文件打不开 | 参数不匹配 | 脚本会自动尝试4种配置 | | 部分 DB 用不同密钥 | ChatSync.db 等特殊 DB | 非关键数据,可跳过 | | "No module named Crypto" | 未安装 pycryptodome | `pip3 install pycryptodome` | | 3.x 和 4.x 混用参数 | 版本判断错误 | 先确认微信版本号 | --- ## 六、总结对比 ``` WeChat 3.x WeChat 4.x ────────── ────────── SQLCipher 3 SQLCipher 4 page 1024 (混用4096) page 4096 (统一) HMAC-SHA1, reserve 48 HMAC-SHA512, reserve 80 KDF 64000 迭代 KDF 256000 迭代 4种参数组合混用 (坑!) 统一参数 (简单) msg_0~msg_9.db message_0~message_5.db Chat_ 表名 不同表结构 密钥提取方式相同: Frida dump 32字节 密钥提取方式相同 ``` **核心经验**: 密钥提取是最难的一步(受 macOS TCC 限制),解密算法本身是确定的。3.x 比 4.x 更复杂,因为同一账号内的数据库使用了不同的加密参数组合。