逆向工程

把微信 4.x 的聊天记录导出来:LLDB 抓 passphrase、PBKDF2 解密与 wxecho 导出

聊天的截图是最贵的一种「省事」:一屏只能装二十来条消息,上下文被裁掉一半,结论常常建立在被裁掉的那一半上。要做任何真正靠谱的分析,第一步都不是「怎么回」,而是把整段聊天变成一份可以检索、可以统计的文本。这篇文章拆开 pickled-fish 里的导出管线 skills/wechat-mac-export:密钥从进程内存里怎么取、passphrase 为什么还要过一遍 PBKDF2、db_storage 里到底躺着什么,以及为什么导出成 chat.txt 才算真正「能看懂」。

为什么先解决「导出」这件事#

先说清楚 pickled-fish(泡菜鱼)是什么。它是跑在 Cursor Agent 上的技能包,仓库里写着三块已经落地的能力:把本人 Mac 微信的聊天导出成可读的 chat.txt、对整段聊天做时间线与话轮分析、再按问题从本地知识包里挑理论和话术。它的口号是「你听得到吗 · 先看懂,再开口」——注意顺序,是先看懂。

在这个顺序里,导出是绕不过去的第一环。理由很实际:

  • 截图是有损压缩。你想问的是「这段时间她为什么变淡了」,截图给你的却只有最后二十条消息。冷场发生在第 300 条之后还是第 3000 条之后,截图看不出来。
  • 截图不能统计。话轮数、回复间隔、谁先开口、哪一天之后语气变了——这些都是需要遍历全部消息才能算出来的量。
  • 截图不能检索。「上次她提过的那件事」,如果没有可搜索的文本,就只能靠翻聊天记录翻到手酸。

所以导出管线不是附属功能,而是整个技能包的地基:没有 chat.txt,后面的分析都只能退化成「看着几张图猜」。

版本前提:WeChat 4.1.10+ 与 db_storage#

微信从 4.x 开始,macOS 版的本地数据被组织进一个叫 db_storage 的目录,消息按业务拆成多个 SQLite 数据库,仓库 README 里写的就是 db_storage/*.db,其中消息主库是 db_storage/message_*.db。

关键点在于:这些 .db 不是能直接双击打开的明文库。它们经过加密,用普通的 SQLite 客户端打开只会得到一句「file is not a database」。这也是整条管线为什么要和密钥打交道的全部原因。

层次是什么为什么重要
版本前提WeChat 4.1.10+(macOS)目录结构与加密方式在 4.x 上才成立,低版本走的是另一套路径
数据载体db_storage/*.db加密后的 SQLite 库,消息在 message_*.db
密钥材料进程内存里的 passphrase微信运行时持有它,并在其基础上派生数据库密钥
输出~/Downloads/wxecho/<联系人>/chat.txt人能读、程序能搜、能被后面的分析管线消费

先确认版本,再动手。这条规则在逆向相关的工作里几乎总是成立:同一个思路换个版本号,成功率可能从「十分钟搞定」变成「一整天白干」。仓库里明确标注的支持范围是 WeChat 4.1.10+,超出这个范围就不是「照着做」的问题了。

密钥从哪来:用 LLDB 抓 passphrase#

数据库是加密的,那就得有密钥;密钥不能靠猜——一个正常强度的 passphrase 没有任何爆破价值。剩下的路只有一条:微信自己在运行时会用到它,那就去它用到的地方拿。

这正是 wcdb-key-tool 做的事。它是一个独立的小工具(不在 pickled-fish 仓库里,属于外部依赖),思路是:用 LLDB 这个调试器附加到微信进程上,在数据库密钥被派生出来的位置把 passphrase 读出来。工具名里的 WCDB 指的正是微信那套基于 SQLite 的数据库组件——加密就发生在这一层。

为什么是 LLDB 而不是别的?因为 macOS 上要观察一个正在运行的进程内部状态,调试器是最直接的手段:可以附加进程、可以下断点、可以在断点命中时读取寄存器和内存里的值。整个过程不需要修改微信的任何文件,只是「在旁边看着它把密钥算出来」。

做法可行性说明
爆破 passphrase不可行搜索空间过大,纯属浪费电
从磁盘文件里找不可行密钥不以明文形式躺在数据目录里
LLDB 附加进程读取本管线的做法在密钥派生处取值,拿到的就是微信自己用的那一份

需要提醒的是,这一步涉及系统层面的调试权限,具体需要在系统里开哪些设置、工具需要什么参数,以 wcdb-key-tool 自己的说明为准——本文只讲它在整条管线里的位置,不复述另一份会过期的文档。

passphrase 不是 key:PBKDF2 在中间做了什么#

拿到 passphrase 就以为万事大吉,是这条管线里最容易犯的错。真实情况是:passphrase 是原料,不是密钥。数据库解密需要的是从 passphrase 派生出来的 key,中间的派生函数就是 PBKDF2。

为什么要多这一步?因为人选的 passphrase 通常短、熵低。如果直接把 passphrase 当密钥用,攻击者每秒钟可以试几百万次。PBKDF2 这类密钥派生函数(KDF)的设计目标正好相反:让每一次尝试都变得昂贵——通过加盐(同样的 passphrase 在不同场景派生出不同 key)和大量迭代(把一次尝试的成本抬高几个数量级),把「每秒几百万次」压到「每秒几次」。

概念它是什么在本管线里的角色
passphrase微信持有的原始口令材料LLDB 抓取的目标,属于中间产物
KDF(PBKDF2)把口令材料扩展成密钥的函数必须按同一套参数重算,才能得到相同的 key
key真正参与数据库加解密的值交给解密步骤,用来打开 db_storage/*.db

这个区分在实际操作中有个很直接的后果:只抄走 passphrase 而不复现派生参数,是打不开库的。加盐值、迭代次数、输出长度,任何一个对不上,派生出来的 key 就是另一串字节。所以这部分工作最好交给已经封装好的工具去做,而不是自己手搓一遍——手搓的版本会在微信升级后静默失效,而且失效时表现出的症状(「库打不开」)和密钥错误完全一样,非常难排查。

解密 db_storage/*.db#

派生出的 key 交给解密步骤,逐个打开 db_storage 下的库,得到可以直接用 SQLite 查询的副本。这一段的工程要点不是算法,而是纪律:

  • 只读、不动原库。解密应该产出新的文件,而不是原地改写。原始数据是唯一的一份,一旦写坏就没有第二份可对照。
  • 先退微信再操作。库正在被写入时复制,很容易拿到一个字段不完整的快照;顺序上应当先让微信安静下来。
  • 把密钥当密码对待。仓库的边界说明里专门写了一句:密钥、chat.txt 和自定义隐私包都不要提交到公开仓库。这句话比任何技术细节都重要。
mermaid 图 1 · 从进程内存到 chat.txt:微信 4.x 导出管线
flowchart TD A["macOS 微信 4.1.10+(运行中)"] --> B["wcdb-key-tool:LLDB 附加到微信进程"] B --> C["passphrase:进程内存里的口令材料"] C --> D["PBKDF2:加盐 + 多次迭代"] D --> E["key"] E --> F["逐个解密 db_storage/*.db"] F --> G["明文 SQLite 副本"] G --> H["wxecho export 昵称"] H --> I["chat.txt / json / csv"]
整条管线里,只有密钥那一段是难点:passphrase 要从进程内存里取,再按同一套参数走一遍 PBKDF2 才是 key。解密只读原库、只产出新文件;导出只负责把数据还原成人能读的样子,不掺杂任何判断。

解出来的库就是标准的 SQLite:联系人、会话、消息各在自己的表里,用任意客户端都能查。到这一步,「可检索」已经实现了一半,但它仍然不方便给人看——你不可能指望自己每次都用 SQL 去回忆一段对话。所以还需要最后一步。

导出成 chat.txt#

最后一步由 @walkerch/wxecho 完成。仓库里给出的用法就一条命令:

bash
wxecho export "昵称"

执行之后,会在下载目录下按联系人分文件夹写出结果:

text
~/Downloads/wxecho/<联系人>/chat.txt

除了纯文本 chat.txt,管线里也支持 json 与 csv 形式——文本适合人和 Agent 直接读,结构化格式适合自己写脚本做统计。子命令和参数以工具自身的帮助信息为准,这里只固定这条最短路径。

把整条链路连起来看,数据是这样流动的:

text
Mac WeChat 4.x
  db_storage/message_*.db
        │  wechat-mac-export
        │  (wcdb-key-tool → 明文库 → wxecho)
        ▼
  chat.txt / json / csv
        │  local-chat-pipeline
        │  (时间线 · 话轮 · 转折)
        ▼
  kb-rag 读 REGISTRY.md
        │  选 ≤2 个 pack
        ▼
  1–2 个关键点 + 可发送原文或「先不发」
  packs_used: [...]

注意这条链路里有一个容易被忽略的设计:导出与分析是分开的两件事。导出只负责把数据还原成人能读的样子,不掺杂任何判断;判断留给后面的分析环节。这样做的好处是,当分析结论可疑时,你可以回到原文核对,而不是怀疑「是不是导出的时候就丢东西了」。

导出来之后:整段分析,而不是截图分析#

pickled-fish 的中间一环是 skills/qingsheng/references/local-chat-pipeline.md。它处理的对象是「上万行导出文本」,做的是时间线、话轮、冷场与转折点的统计。这一步的价值,用一句话概括就是:把「我感觉最近不太对」变成「第 N 天开始,回复间隔的中位数变了」。

举几个只有整段文本才能回答的问题:

  • 是谁先开的口?这个比例在这段时间里有没有变化?
  • 最长的一次冷场出现在哪里,前后发生了什么?
  • 话题是被谁终止的?有多少次是「对方回了一个字之后就没有下文」?
  • 转折点是渐变的,还是某一件具体的事之后突然出现的?

分析完再进入知识库环节:kb-rag 先读 knowledge/REGISTRY.md 这张路由表,选出不超过两个 pack,然后才展开具体内容,并在回答里强制标注 packs_used。也就是说,最终给你的不是「一堆理论」,而是一到两个关键点,外加一段可以直接发送的原文,或者一句「先不发」。

这套流程里最反直觉、但最值得学的一点是:它把「不下结论」也当成一个合法输出。「先不发」是可以被交付的答案之一——这在情感场景里比在技术场景里更需要被显式支持。

把它装起来#

整包装进 Cursor 只要两步:

bash
git clone https://github.com/lnuxe/pickled-fish.git ~/.cursor/skills/pickled-fish
bash ~/.cursor/skills/pickled-fish/scripts/install-skills.sh

install-skills.sh 会把四个入口软链到 ~/.cursor/skills/:pickled-fish、wechat-mac-export、qingsheng、kb-rag。仓库的目录结构大致是这样:

tree
pickled-fish/
├── SKILL.md                 # 总控:导出 → 选库 → 分析
├── knowledge/
│   ├── REGISTRY.md          # 路由表(意图 → pack id)
│   ├── index.md             # 分类索引
│   ├── packs/               # 已捆绑的 MIT 知识包
│   └── custom/              # 你的私有包(默认不进公开 git)
├── skills/
│   ├── wechat-mac-export/   # macOS 微信解密与导出
│   ├── kb-rag/              # 选包与 progressive disclosure
│   └── qingsheng/           # 中文恋爱实操 + local-chat 管线
└── scripts/
    ├── install-skills.sh    # 软链到 ~/.cursor/skills/
    └── add-pack.sh          # git/本地目录装进 custom/

导出那一步的本机依赖有三个:@walkerch/wxecho、wcdb-key-tool,以及 Python 3。前两个负责解密和导出,Python 负责把库里的内容整理成文本。想换一套自己的笔记,用 scripts/add-pack.sh 把它装进 knowledge/custom/<id> 即可。

边界:这件事能做什么,不能做什么#

仓库里有一份 DISCLAIMER.md,内容很短,但三条都必须先读:仅限本人数据、非诊疗、禁止骚扰。把它翻译成工程语言,就是下面三条约束:

约束含义落地做法
仅限本人数据导出的对象必须是自己的账号数据不导出、不转发他人的聊天记录
非诊疗它给的是沟通建议,不是心理诊断不把分析结论当成医学判断使用
禁止骚扰对方明确拒绝时,任何话术都失效「停联系」优先于任何 pack 里的技巧

工程纪律

还有一条工程纪律:密钥、chat.txt、自定义隐私包都不要提交公开仓库。knowledge/custom/ 之所以默认不进公开 git,就是因为它存放的是私有的东西——设计上已经替你把这条线划好了,别自己把它越过去。

小结#

这条导出管线的全部复杂度,都集中在「拿到能用的密钥」这一步:用 LLDB 从进程里取 passphrase,再按同一套参数走 PBKDF2 派生 key,最后逐个解密 db_storage/*.db,交给 wxecho 写出 chat.txt。密钥之后的每一步,其实都是普通的工程活。

如果只带走一句话,我希望是这句:任何严肃的分析都必须建立在完整的、可检索的原始文本之上。截图不是数据,截图是数据的缩略图——而缩略图上的判断,通常比没有判断更危险。下一篇会接着讲导出之后的那一半:为什么给个人知识库配一套文件系统检索,比接一个向量库更划算。