wx-cli/docs/macos-3x-vs-4x-decryption-g...

17 KiB
Raw Blame History

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/<account_md5_hash>/
      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/<account_id>/
    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. 安装 Fridapip3 install frida-toolsbrew 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)

# 在目标机器上执行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 权限完全指南

新手操作步骤

根据你的微信签名状态,选择对应方案:

# 首先检查你的微信签名状态
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 <pid> → "unable to access process"
  • lldb -p <pid> → "non-interactive debug session"
  • sudo gcore <pid> → "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

pgrep -x WeChat
# 输出例如: 51051

步骤 2: 准备 Frida 扫描脚本

创建 scan_keys.js

// 扫描内存中所有看起来像加密密钥的 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 注入

# 方法 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 解密参数

# 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 解密参数

# 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

#!/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('<I', 1)
    calc_hmac = hmac.new(mac_key, msg, hashlib.sha1).digest()

    return calc_hmac == stored_hmac


def decrypt_db(db_path, raw_key_hex, output_path):
    """
    解密单个数据库文件
    自动尝试多种 SQLCipher 参数组合
    """
    raw_key = bytes.fromhex(raw_key_hex)

    with open(db_path, 'rb') as f:
        data = f.read()

    # 检查是否已经是 SQLite (未加密)
    if data[:15] == b'SQLite format 3':
        shutil.copy2(db_path, output_path)
        return 'unencrypted'

    salt = data[:16]

    # 尝试的参数组合: (page_size, use_pbkdf2)
    configs = [
        (1024, False),   # 大部分 DB
        (4096, False),   # WebTemplate
        (1024, True),    # FTS 索引
        (4096, True),    # mediaData
    ]

    for page_size, use_pbkdf2 in configs:
        if use_pbkdf2:
            enc_key = hashlib.pbkdf2_hmac('sha1', raw_key, salt, 64000, dklen=32)
        else:
            enc_key = raw_key

        reserve = 48  # SQLCipher 3 固定

        if verify_hmac_page1(data, enc_key, page_size, reserve):
            # HMAC 验证通过,开始解密
            num_pages = len(data) // page_size
            output = b''
            for i in range(num_pages):
                page = data[i * page_size:(i + 1) * page_size]
                output += decrypt_page(page, enc_key, i + 1, page_size, reserve)

            with open(output_path, 'wb') as f:
                f.write(output)

            mode = 'pbkdf2' if use_pbkdf2 else 'direct'
            return f'ok (page={page_size}, {mode})'

    return 'failed'

依赖安装: pip3 install pycryptodome

4.x 的解密差异

4.x 的代码逻辑相同,只需改参数:

  • reserve = 80, HMAC 用 SHA512, mac_key 的 PBKDF2 也用 SHA512
  • verify_hmacstored_hmac 长度为 64 字节
  • 4.x 中所有 DB 使用统一的参数(不像 3.x 那样混用多种配置)

五、新手操作清单

你需要准备什么

  • macOS 电脑,微信已登录
  • Python 3 + pycryptodome (pip3 install pycryptodome)
  • Frida (pip3 install frida-tools)
  • 管理员密码sudo 权限)

一步步操作

# 1. 确认微信版本
ls ~/Library/Containers/com.tencent.xinWeChat/Data/Library/Application\ Support/com.tencent.xinWeChat/
# 如果看到 2.0b4.0.9 → 3.x 版本
# 如果看到其他 / Documents/xwechat_files → 4.x 版本

# 2. 找到你的账号目录
ls ~/Library/Containers/com.tencent.xinWeChat/Data/Library/Application\ Support/com.tencent.xinWeChat/2.0b4.0.9/
# 最大的那个目录就是你的主账号

# 3. 确认数据库是加密的
file ~/.../<account>/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_<hash> 表名                     不同表结构
密钥提取方式相同: Frida dump 32字节    密钥提取方式相同

核心经验: 密钥提取是最难的一步(受 macOS TCC 限制解密算法本身是确定的。3.x 比 4.x 更复杂,因为同一账号内的数据库使用了不同的加密参数组合。