郭立 (leeguoo)

# 微信本地数据的两条路——wechat-use 与 wechat-decrypt 的原理级对比(附实测订正)

两个微信解密工具经常被拿来比,但比错了方向。真正的区别不在功能多少,在怎么拿到那把 SQLCipher 的 key。这篇把两者拆到原理层——SQLCipher 的 per-DB 密钥模型、两种从内存抠 key 的机制(LLDB+entitlement vs sudo+mach_vm)——并且我实际在本机跑了一遍,顺手订正了自己写第一版时的两个错判。

2026年7月6日 · 文章 · 公开

本页目录

Snoopy 举着钥匙够那个上锁的聊天气泡

微信 4.x 把本地聊天库锁进了 SQLCipher。想让程序读到聊天记录、图片、语音,绕不开一件事:先拿到那把解密的 key。

围绕"拿 key 再读库"这件事,社区有两条成熟的路子。一条是 wechat-use——macOS 上发消息 + 读库 + 实时监听的那套。另一条是开源的 wechat-decrypt——跨平台解密 + 批量导出 + 语音转文本 + 原生 MCP。

它俩经常被拿来比,但比错了方向。真正的区别不在"谁功能多",在怎么拿到那把 key,以及拿到之后能干嘛。这篇把两者拆到原理层。写完我在本机把 wechat-use 实际跑了一遍——结果订正了自己两个错判,后面会讲。

先把加密模型讲透

微信 4.x 的本地库是标准 SQLCipher 4 加密的 SQLite,放在:

macOS: ~/Library/Containers/com.tencent.xinWeChat/Data/Documents/xwechat_files/<wxid>/db_storage

SQLCipher 解密模型:文件头 salt + 内存 raw key → SQLCipher 4 → 明文

SQLCipher 的密钥模型有三个点必须先搞清楚,不然后面看不懂两个工具在抠什么:

  • 一库一 key。 不是一把全局钥匙。contact.dbmessage_*.dbsns.db… 每个库各有自己的 32 字节 raw key。我在本机看 wechat-use 存下来的 ~/.wx-rs/keys.json,里面正好是 19 个 per-DB 条目,每条 {key_hex: 64 字符, salt_hex: 32 字符}
  • salt 藏在文件头。 每个加密 DB 文件的前 16 字节是随机 salt。这也是为什么加密的 SQLite 打开不是 SQLite format 3 开头——那 16 字节被 salt 占了。wechat-decrypt 的 C 代码里就有一句 memcmp(header, "SQLite format 3", 15) == 0 → return -1:撞上明文库就跳过。
  • raw key 只在内存里。 32 字节的 raw key 不落盘,微信运行时才把它派生出来、握在进程内存里,格式是 x'<64位 key hex><32位 salt hex>'(就是 SQLCipher PRAGMA key 的原始形式,96 个十六进制字符)。

所以解一个库,要凑齐两样:raw key(从进程内存抠)+ salt(从 DB 文件头前 16 字节读)wechat-decryptfind_all_keys_macos.cHEX_PATTERN_LEN 96key_hex[65]salt_hex[33] 这几个常量,对的就是这个模型。

"salt 在文件头"不是我瞎说,一次 hexdump 就能验。 我本机 contact.db 的前 16 字节:

$ xxd -l 16 contact/contact.db
00000000: 90f7 c71d 17b5 bfe6 24b1 76ac df40 df7f

明文 SQLite 该是 53 51 4c 69 74 65 …("SQLite format 3\000")开头;这里是一坨随机字节——这 16 字节就是 salt,加密把文件头顶掉了。wechat-decrypt 判断"是不是已明文库"用的就是这一句 memcmp(header, "SQLite format 3", 15)

一号一整套库,不是一个文件。 微信按用途拆了近 20 个加密库,各有各的 key。我本机 ~/.wx-rs/keys.json 里 19 条,对应:

装什么
message/message_0.dbmessage_1.db聊天记录正文(按量分片)
message/message_fts.db聊天全文搜索索引
message/media_0.dbmessage_resource.db消息里的媒体/资源
message/biz_message_0.db公众号/服务号消息
session/session.db会话列表(左边那一栏)
contact/contact.dbcontact_fts.db联系人 + 联系人搜索
sns/sns.db朋友圈
favorite/favorite.dbfavorite_fts.db收藏
emoticon/emoticon.db / head_image/head_image.db表情 / 头像
hardlink/hardlink.db文件硬链接索引
bizchatgeneralsolitaireweclaw企业号会话 / 通用配置 / 接龙 / 等

难点从来不是解密——SQLCipher 标准库给对 key 就开——难在从内存里把那把 key 抠出来。 两个工具真正拉开差距的,就是这一步。

原理核心:两条从内存抠 key 的路

两条抠 key 流程:wechat-use 走登录瞬间调试接口(不重签不sudo,直达);wechat-decrypt 先重签+sudo+扫内存找 key

要读微信进程的内存,macOS 有一道坎:微信带 hardened runtime 签名,系统默认不让别的进程 task_for_pid 去 attach 它——哪怕你是 root。两个工具过这道坎的方式,是最大的差异,也是我这篇改得最狼狈的地方(下面会讲我怎么错了两次)。

路线 A:wechat-decrypt —— replace-mode 重签 + sudo 扫内存

$ bash
sudo codesign --force --deep --sign - /Applications/WeChat.app   # ① replace-mode 重签成 ad-hoc
cc -O2 -o find_all_keys_macos find_all_keys_macos.c -framework Foundation
sudo ./find_all_keys_macos                                        # ② root 扫内存

codesign --force --deep --sign -replace-mode(替换式) 重签:把微信整个改成 ad-hoc 签名、连 hardened runtime 带微信自己的 entitlements 一起抹掉,然后靠 rootmach_vm_read 逐块(每块 2MB)扫内存,正则匹配那个 96-hex 的 key+salt 模式,命中写进 all_keys.json。salt 它另从 DB 文件头读。思路直白、逆向圈标准做法、全开源可审计。代价:要 root,而且替换式重签把微信原有权限一并抹了(极端情况会影响微信自己的 TCC)。

路线 B:wechat-use —— 读 key 不重签(登录瞬间调试接口)

这里是我一开始理解最拧巴的地方,拆两层说:

读 key(init)本身不重签、不 sudo。 它在微信登录那一瞬间用 macOS 公开调试接口抓一次解密材料,抓完立即 detach——不做 codesign --force --deep、不用 root。这跟路线 A 完全不同,路线 A 是先替换式重签再 root 硬扫。

要发消息才需要重签,而且是 merge-mode。 发送用的 slot_send 适配器需要微信 get-task-allow=true,这要跑一次 merge-mode(合并式) 重签——只加 get-task-allow 这一个 entitlement,保留微信自己原有的所有权限(SKILL 里明确警告不许用 replace-mode,因为那会把微信的 TCC 权限抹掉)。这是它比路线 A 克制的地方:同样动签名,它是外科式合并,不是一刀切替换。

我本机 wechat-use doctor 的输出:

✓ wechat_get_task_allow   get-task-allow = true (LLDB 可 attach)
✓ wechat_codesigned       signer: ad-hoc (本地重签,含 get-task-allow)
✓ wechat_dylib_fingerprint SHA-256=fc162db6…  raw_key ✓ (keymap)
✓ wechat_update_guard     ✓ disabled (Tencent 写不进 MacUpdate)

注意别被这个坑到(我就被坑了):这里显示"本地重签"是因为我这台机配过发送、做了那次 merge-mode 重签。它不是读 key 的必要条件——只读不发的话,init 的登录瞬间接口就够了。另外两个细节:wechat_dylib_fingerprint 说明它按微信 dylib 指纹选 keymap 定位 raw key(比硬扫固定 pattern 抗升级),wechat_update_guard: disabled 说明它把 MacUpdate 目录弄成写不进去,让重签不被升级冲掉。

一句话对比抠 key:wechat-decrypt 读 key = replace-mode 重签 + root 硬扫内存;wechat-use 读 key = 登录瞬间调试接口、不重签不 sudo(发消息才另做 merge-mode 重签)。 读 key 这一步,wechat-use 明显更轻、更克制;wechat-decrypt 更糙但全开源、跨平台一致。

一个值得抄的巧思:图片 key 不扫内存

wechat-decrypt 里有个细节挺聪明:图片的 key 不从进程内存拿,从磁盘的 kvcomm 缓存派生(find_image_key_macos.py)。微信把图片 .dat 的 key 落在了磁盘缓存里,解图片根本不用 attach 进程、不用 root。要自己做图片解密,这条路干净,可以直接抄。

解密本身:两边一模一样,手动也就几行

抠到 raw key、配上文件头的 salt,后面就没悬念了——都是标准 SQLCipher 4。拿到 key 之后,手动解一个库就这么几行(key 打码):

$ sql
$ sqlcipher contact/contact.db
sqlite> PRAGMA key = "x'<64位 key hex…>'";   -- 内存抠来的 raw key
sqlite> PRAGMA cipher_compatibility = 4;      -- 声明 SQLCipher 4
sqlite> .tables                               -- 能列出表 = 解开了

key 对,.tables 就把联系人表列出来;key 错,直接报 file is not a databasesalt 不用手动喂——它在文件头前 16 字节,SQLCipher 自己会读。两个工具做的就是把这几行自动化,前面重签/attach/扫内存所有功夫,都只为拿到那个 x'…'谁都没有独家解密算法,"谁解密更强"是伪命题,差异全在前面抠 key 那一步。

拿到库之后:能力就分叉了

发消息的邮差 vs 挖数据的侦探

抠 key 是入场券,拿到之后两个工具走向不同。这张表是我实测订正后的版本(打叉的地方就是第一版写错的):

维度wechat-usewechat-decrypt
发消息后台零 UI 闪,带投递状态不能发,只读
读库(会话/联系人/历史/图片)支持(本机实测 sessions 正常返回)全量解密成明文 SQLite
语音转文本✅ 也支持(v1.17:silk-decoder + whisper-cli + ggml-medium)✅ SILK→WAV→文本
抠 key 方式(macOS)读 key:登录瞬间调试接口,不重签、不 sudo(发消息才另做 merge-mode 重签)replace-mode 重签 + sudo mach_vm 硬扫
升级适配dylib 指纹 keymap + probe-build 自适应固定内存 pattern,等作者更新
平台仅 macOSWin / Mac / Linux
企业微信5.x 仅 Windows 实测
给 Claude 用(MCP)走 HTTP/gRPC,要自己包原生 MCP,一行接入,20+ 工具
实时监听daemon RPC → SSEWeb UI SSE / CLI / MCP
远程接入Cloudflare Tunnel 暴露 REST本地为主
成本付费激活码免费开源

这里就是实测的价值,而且我错了不止一次。 第一版凭读文档写:"wechat-use 不重签名、不转录语音"。跑起来看 doctor 显示"本地重签",我以为抓到错、改成"两边都重签才能读"——结果矫枉过正:那次重签是发消息配的,读 key 根本不重签。读全 SKILL + 源码才拼准:读 key 不重签,发消息才 merge-mode 重签;转录 v1.17 本来就有。 读文档错一次、看半个证据又错一次,把工具跑透、文档和源码读全,才敢下结论。

一句话读这张表:wechat-use 是"发 + 读 + 转录 + 实时"的全能 bot 平台(付费、闭源、只 macOS),wechat-decrypt 是"解密 + 导出 + 转录 + 原生 MCP"的开源数据工具(免费、跨平台、有企微,但不能发消息)。

各自的 workflow

wechat-use(发 + 读 + 实时)

① 订阅 TG 频道拿激活码
② wechat-use init          # 登录瞬间调试接口抠 key,不重签、不 sudo(发消息才另做 merge-mode 重签);daemon 按需起
   ├─ 读: wechat-use sessions / history / contacts / search / image  (YAML,低 token)
   ├─ 发: wechat-use send "文本" <名字或wxid>                         (后台零闪)
   └─ 实时: wechat-use listen  或  wechat-bridge → HTTP /messages/stream?since=
③ 远程: wechat-use tunnel setup → Cloudflare Tunnel 暴露 REST
④ 升级: wechat-use probe-build → 适配新 build

wechat-decrypt(解密 + 导出 + MCP,macOS)

① python venv + pip install -r requirements.txt + brew install whisper-cpp
② 退出微信 → sudo codesign 重签 → cc 编译 → sudo ./find_all_keys_macos   # root 扫内存抠 key
③ python decrypt_db.py            # 全量解密 → 明文 SQLite(-i 增量)
   ├─ 导出: python export_all_chats.py -t  # JSON/CSV/HTML + 语音转录 + 增量/日期窗口
   ├─ 实时: python main.py                 # Web UI localhost:5678 (SSE)
   └─ 给 Claude: claude mcp add wechat -- python mcp_server.py   # 20+ 查询工具

mcp_server.py 一行接进 Claude,get_chat_history / search_messages / decode_voice 直接查。它解析朋友圈等 XML 时还加了 XXE 防护(拒绝 DOCTYPE/ENTITY + 大小上限),这点比很多同类严谨。

组合起来(真实场景:agent 接微信)

发消息 / 实时响应 / 群@ bot ──────────→  wechat-use   (成熟,已在用)
批量拉历史 / 企微 / 给 Claude 原生查 ──→  wechat-decrypt MCP  (补 MCP + 跨平台)

两者不冲突,但别同时跑——都要 attach 微信、都动它的签名和 DB,一台机器上同时上两套,签名和 attach 会打架。

两个容易被忽略的点

语音怎么变成文本,两边一样。 微信语音不是 mp3,是 SILK(Skype 开源的窄带语音 codec)。所以转文本是三段流水线:SILK → 解成 WAV → 丢给 whisper 转文字wechat-decrypt 内置 silk decoder + whisper.cpp/OpenAI;wechat-use v1.17 也把 silk-decoder + whisper-cli + ggml-medium 打包进去了,读群历史时语音自动转。这条链跟做 TTS 的欢迎语其实是反的——那边文字→语音,这边语音→文字,但都靠 whisper。

个人微信和企业微信不是一把锁。 个人微信 4.x 用 SQLCipher 4(前面讲的那套:文件头 salt + 内存 raw key)。企业微信 5.x 是另一套——wxSQLite3 的 AES-128,加密参数、key 位置都不同,所以 wechat-decrypt 单独写了 find_wxwork_keys.py,而且只在 Windows 实测过。wechat-use 干脆不碰企微。想解企微,目前只有 wechat-decrypt + Windows 这一条路。

风险与边界(这是解密工具,得说清楚)

  • 都可能改微信签名,但方式和时机不同。 wechat-decrypt 为读 key 就做 replace-mode 重签(把微信原有权限一并抹掉)+ root;wechat-use 读 key 不重签,只有发消息才 merge-mode 加 get-task-allow(保留原权限),并主动关掉自动更新来保住它。不管哪种,ad-hoc 重签都让微信从官方签名变成本地签名,理论上有被检测、Gatekeeper 提示的可能。动手前知道自己在改什么。
  • 只解自己的号、自己的数据。 这类工具的正当用途是导出/备份自己的聊天,不是去读别人的。
  • key 和聊天内容不要外泄。 raw key 一旦泄露,谁都能解你的库。本文里所有真实 key、聊天内容全部脱敏——写这类文章、贴日志时都该这么做。
  • 需要开发者模式 / attach 权限。 wechat-use 要 macOS Developer Mode + get-task-allow;wechat-decrypt 要 root。都是"能读任意进程内存"级别的权限,给之前想清楚。

那么,他的实现比我们的好吗

分开看:

  • 抠 key 的手法:wechat-use 更轻更克制。 读 key 它压根不重签(登录瞬间调试接口、不 sudo);要发消息才 merge-mode 重签(只加 get-task-allow、保留微信原权限)。wechat-decrypt 读 key 就得 replace-mode 重签 + root 硬扫,更糙、还会连微信原有权限一起抹,但全开源、可审计、跨平台一套
  • 解密本身:一样。 都是标准 SQLCipher 4,没人有黑科技。
  • 能力覆盖:各有各的。 wechat-use 独占"发消息";wechat-decrypt 独占"原生 MCP + 跨平台 + 企微"。语音转录两边都有(这点我一开始搞错了)。

准确的说法:这不是"谁实现更好",是两条路线。 抠 key 的优雅度和"能发消息"上 wechat-use 赢;开源、免费、跨平台、原生 MCP 上 wechat-decrypt 赢。

选型结论

  • 要发消息,离不开 wechat-use wechat-decrypt 压根不能发,替代不了。
  • wechat-decrypt 值得引入的,是它补的两块: 免费的原生 MCP(让 Claude 一行接入、直接查聊天历史)、跨平台 + 企微。语音转录不再是它的独有卖点——两边都有。
  • 落地:wechat-use 继续当发送 + 实时 + 读库主力;需要"Claude 原生查微信数据"或"读企微/在 Linux 跑"时,加装 wechat-decrypt 的 MCP。别在同一台机器同时开两套。

技术上能从对方抄两样:图片 key 从磁盘 kvcomm 缓存派生(不用 attach 进程),x'key+salt' 96-hex 的内存格式配 keymap 定位(比硬扫 pattern 抗升级)。哪天要自研或加固,这两个 trick 现成。

最后留一句方法论:这篇最有价值的不是那张对比表,是我错了两次的过程——读文档写错一次,看半个证据(doctor 的重签)又矫枉过正一次,把工具跑透、SKILL 和源码读全,才敢下结论。写对比最危险的不是没读,是读了一半就下笔——而误导的源头,恰恰是工具自己文档里那句没分清"读 key"和"发消息"的 no re-signing

下一篇 →
Claude 封号看三件事:地区、IP 类型、请求指纹,以及怎么自查

评论

评论发布后会立即公开,如触发规则可能被审核下架。

最多 1000 字。