你打开 Mac 上的微信,聊天记录、联系人、图片,全在本地几十个 .db 文件里。这些文件不是明文,每一个都被 SQLCipher 加密过,直接用普通 SQLite 打开只会看到一堆乱码。
想读自己这些数据,只有一件事要做:拿到那把 32 字节的密钥。
密钥不在磁盘上。微信运行时会把它加载进内存去解密数据库,所以整个游戏就变成一句话,从正在运行的微信进程内存里,把那把 key 抠出来。听起来简单,真做起来,微信每换一个版本就给你换一种藏法。这篇把我们试过的几种抠法一条条摊开,从最省事的到最狠的,顺带讲一次真实的逆向:4.1.10 把 key 藏进了乱码里,我们怎么一步步把它挖回来的。
全程只针对本机、自己账号的数据。下面的密钥、账号名都做了脱敏,真值不会出现。这是讲原理、讲逆向思路,不是给谁去解别人的号。

先把锁看明白:SQLCipher 到底怎么加密
SQLCipher 是 SQLite 上包的一层加密。微信 4.1.9 之后,每个 .db 文件用自己独立的一把 32 字节 key + 16 字节盐(salt)。盐就明晃晃写在数据库文件头前 16 个字节里,不是秘密;key 才是要命的东西。
这里有个对逆向特别有用的机制,值得单独讲,你不需要真的解开数据库,也能判断一把 key 对不对。
SQLCipher 每一页数据的末尾都挂了一段 HMAC(可以理解成这一页的防伪签名)。这个签名是拿 key 派生出来的子密钥算的。所以流程是这样:你手上有一把 key 的候选值,拿它按 SQLCipher 的规则算一遍第一页的 HMAC,再跟文件里存的 HMAC 比一下。对上了,这把 key 就是真的;对不上,换下一个。
整个过程不碰任何明文数据,纯算 HMAC。这就叫验证 oracle,一个能回答"我猜对了吗"的黑盒。后面好几种抠法都靠它兜底。记住这个词,它是这篇里最重要的一个概念。
还有一层派生关系,4.1.9 上是这样的:并不是每个库的 key 各自随机,而是有一把账号级的总钥匙(master),每个库的 key 都是 PBKDF2-HMAC-SHA512(master, 这个库的盐, 迭代 256000 次) 算出来的。

PBKDF2 你可以理解成一台单向绞肉机:master 和盐塞进去,转 25.6 万圈,出来一把这个库专用的 key。反着推不回去。这个关系后面会变成一招很漂亮的抠法,找到一把 master,就能派生出所有库的 key。
铺垫到这就够了。下面开始一条条讲手法,从最懒的开始。

手法一:钥匙有时就明晃晃摆在内存里
最省事的一招,连算都不用算。
微信 4.1.9 给 SQLCipher 递 key 的时候,是拼成一句 SQL 明文命令递过去的,长这样:PRAGMA key = "x'<64位key的十六进制><32位盐的十六进制>'"。这句字符串会在内存里待着。
那还客气什么,直接在微信进程的内存里搜 x' 开头、后面跟着 96 个十六进制字符再一个引号的串,一抓一个准。抓到把里面的 key 和盐拆出来,按盐匹配到对应的 .db 文件,收工。
这招快、不用下断点、不用重启微信,只要能读到目标进程内存就行。缺点也明显:它赌的是微信恰好用这种字符串格式递 key。哪天微信改成直接调底层 API 传裸字节,内存里就没有这个 x'...' 串了,这招当场失效。
在 4.1.9 上它好使,是这条链子里最先试的一招。
手法二:字符串没了,就大海捞针,但得先有把尺子
假设微信不再把 key 拼成字符串了。内存里没有那个方便的 x'...',但 key 本身(那 32 个字节)总还在某个地方待着,总得有个地方存它才能拿去解密。
那就暴力扫:把整个进程的可读内存,每 4 个字节一挪,当成一把候选 key,一个个试。
问题来了,内存几百兆,32 字节的窗口有几千万个,你怎么知道哪个是真 key?
这就是手法一里那把"尺子"派上用场的地方。每个候选,拿去算第一页的 HMAC,跟数据库文件里存的对一下。对上就是它。几千万个候选里,只有真 key 能让 HMAC 对上,几乎不可能撞车。
这一招不依赖 key 长什么样、也不依赖版本,只要那 32 字节以连续明文的形式待在内存里,就能捞出来。真正的门槛是慢,几百兆内存全扫一遍要几十分钟。
它的死穴在 4.1.10 露出来了:那个版本把 key 加密之后再往内存里放,.db 的 key 根本不以连续明文存在,你扫穿整个内存也一个都对不上。暴力扫在这条路上撞墙了,得换招。
手法三:与其满地找,不如蹲在它出生的那一刻
换个思路。key 迟早要被微信自己写进某块内存去用,那我不满内存找,我蹲在"写 key 的那一行代码"上,它一执行,我就把值捞走。
具体做法:用调试器(LLDB)在微信内部那条"把 raw key 写下去"的指令上下个断点。断点一命中,读那个寄存器,里面就是 key。
这招很直接,但有个硬伤:断点是下在一个具体内存地址上的,而这个地址每次微信重新编译都会变。腾讯发一个新版本,那行代码挪了个位置,你原来记的地址就作废了,得重新逆向定位。所以这招要配一套"每个版本重新校准地址"的机制,维护起来烦。
一句话:它抓得准,但跟版本绑得太死。
手法四:换个不会跑的蹲点,系统的加密函数
断点会漂,是因为下在微信自己的代码里,而微信的代码天天变。那把断点挪到一个永远不变的地方呢?
macOS 上做 AES 加密,绕不开系统自带的 CommonCrypto,里面有个导出函数叫 CCCryptorCreate。微信要用 key 解密数据库,那把 key 一定会作为参数传进这个系统函数。而系统函数的名字和签名是稳定的,不管微信怎么改版本,CCCryptorCreate 还在那,参数顺序也不变。
所以断点下在 CCCryptorCreate 上,按名字挂,微信一调用它做数据库解密,我就从参数里把 key 读出来。这一招天生版本无关,4.1.10 就是靠它兜底。
代价是重。它得在 key 传进加密函数的那一刻才截得到,而那个时刻通常是"数据库刚被打开"的瞬间。所以实际操作要把微信退掉、重开、卡在启动那一下挂上断点,才能抓到。重启微信、打断用户,是它最不优雅的地方。
这里有个可以带走的逆向套路:要挂钩,就挂在稳定的边界上(系统 API),别挂在会漂的内部地址上。前者一劳永逸,后者天天返工。
手法五:最优雅的一招,找到那把"总钥匙"
回到前面埋的伏笔:每个库的 key 都是从一把账号 master 派生的。
那我何必一个个库去抠 key?找到那一把 master,剩下的全用 PBKDF2(master, 每个库的盐) 算出来就行。一把钥匙,开所有门。
master 存在哪?存在微信内存里的一个账号对象里,这个对象在逆向里的类名叫 LoginContextPrivate,是微信登录态里专门管密钥的结构。定位到它,能顺手摸到的不只是数据库那把 master:除了你的 wxid、昵称、别名,还挂着一把媒体 key,聊天图片、表情包都用它解。有意思的是这把 key 算不上秘密,它就是 MD5(uin + wxid),两样都半公开,LoginContextPrivate 只是把这个 MD5 缓存了一份。数据库 master 只是其中一把。在 4.1.9 上,master 就是其中一个明文的 32 字节字段。
于是这招是这样:先找到账号对象(顺着 wxid 这个锚点找),把附近那把高熵的 32 字节候选捞出来,当成 master,按第一个库的盐 PBKDF2 派生一下,拿手法一那把 HMAC 尺子验证。对上了,这就是 master;然后用它把每个库的 key 全派生出来。
这条路全程被动读内存,不下断点、不 hook、不重启微信。比手法一那种"逐库找字符串"更省,一次派生出全部,而且 master 只要一直在内存里,随时能拿。我们把它做成了原生实现,在 4.1.9 上端到端跑通,派生出来的十几把库 key 跟已知的真值逐字节一致。
看起来这招又稳又优雅。然后 4.1.10 来了。
4.1.10 的反击:钥匙被藏起来了
在 4.1.10 上重跑手法五,扑了个空。
账号对象还在,wxid 还是明文,昵称、别名都读得到,但那把 master 不在了。准确说,内存里搜不到 master 的明文字节。手法二的暴力扫也白搭,因为 key 根本不以明文连续存在。
有意思的是,同一个账号,4.1.9 上 master 是明文躺在账号对象里,4.1.10 上就没了。腾讯在这个版本给敏感 key 在内存里上了一层混淆。手法五直接被墙住。
按理说到这该认栽,换回手法四那条重启 + 断点的路。但有个东西卡在心里:市面上有个别人写的小工具,它在 4.1.10 上确实能把 key 抠出来,而且是纯读内存、不 hook、不重启的那种。也就是说,key 一定还在内存里,只是被藏起来了。既然它能挖出来,我们就能把它是怎么挖的搞明白。
于是有了下面这段逆向。
一次真实的逆向:把藏起来的钥匙挖回来
那个工具是编译好的二进制,还上了 OLLVM 混淆(控制流被打散、字符串被加密),直接反编译看不出人话。但没关系,我不需要读懂它的代码,我只需要看它对系统干了什么。
它读微信内存,靠的是一个系统调用 mach_vm_read_overwrite。那我就用 LLDB 挂着它跑,在这个系统调用上下断点,把它每一次读内存的地址、长度、读回来的字节全记下来。工具跑完,我手上就有一份它读了哪些地方、读到了什么的完整流水账。
拿到流水账,开始破案。真值我全部脱敏,下面用占位符讲结构。
第一步是排除法。我先假设 key 是明文,直接在内存里搜工具输出的那把 key,搜不到。那假设它被简单加密了:工具导入了哪些加密函数?一看,它一个 AES 都没导入,只导入了一个 PBKDF2。那 key 不是 AES 解出来的。再假设它靠 PBKDF2 现算,我在 PBKDF2 函数上下断点,工具跑完,断点一次没响。那也不是。

排到这,常规猜法基本用光了。我又试了各种简单 XOR:把工具读到的两块字节两两异或,看能不能凑出 key,凑不出。黑盒猜到这份上,该停了。
逆向卡住的时候,别继续瞎猜变换,回头去读数据结构。 这是这次最大的收获。
我换了个动作:直接用 LLDB 把账号对象那块内存原样 dump 出来,一个字节一个字节地看。看着看着,结构就浮出来了,账号对象里有几个 libc++ 的字符串字段,每个字段是一个指针,指向一段乱码;另外在内存别处,有一个"密钥流池",里面是几块看着完全随机的 32 字节。
把这两样凑到一起试,一下就中了:把某个字段指向的那段乱码,跟池子里对应的那块密钥流做异或,出来的就是 key。

拆开来看,它的混淆其实一点都不玄:每把敏感 key 不是明文存,而是拆成两半,一半是"密文",一半是"密钥流",分别放在内存两个不同的地方。要用的时候读两半、异或一下,还原成明文。有的 key 甚至是先转成十六进制文本再异或的。
就这么回事。你顺着黑盒猜"是不是 AES、是不是某种花哨算法",猜一辈子也猜不到;可你一旦把两块内存摆到一起,肉眼就能看出 A XOR B 这么简单的关系。
破了之后,原生复刻就顺理成章:找到账号对象(靠"两个相邻的 32 字节字符串字段"这种结构特征定位,不靠账号名),把它指向的密文读出来,再扫内存里的密钥流池,两两异或当 master,拿 HMAC 尺子验证一下。对上的那一对,异或出来就是 master,再 PBKDF2 派生所有库 key。整条链子回到手法五那种"纯读内存、不 hook、不重启"的形态,只是前面多了一步解混淆。
几种手法横着摆一排看
| 手法 | 怎么拿 key | 要下断点/hook 吗 | 要重启微信吗 | 跟版本绑得死吗 |
|---|---|---|---|---|
| 一 · 扫字符串 | 内存里搜 x'<hex>' 明文串 | 不用 | 不用 | 死(赌字符串格式) |
| 二 · 暴力扫 + HMAC | 每 32 字节当候选,HMAC 验 | 不用 | 不用 | 不死,但要 key 是明文 |
| 三 · 内部断点 | 断在"写 key"那行,读寄存器 | 要断点 | 不用 | 很死(地址每版本漂) |
| 四 · 系统函数 hook | 断在 CCCryptorCreate,读参数 | 要断点 | 要 | 不死(系统 API 稳定) |
| 五 · 总钥匙派生 | 账号对象读 master,PBKDF2 派生 | 不用 | 不用 | 不死(靠结构 + HMAC) |
| 五 + 解混淆 | 4.1.10 上先异或解出 master,再派生 | 不用 | 不用 | 不死 |
排在一起看就清楚了:越往下越不打扰微信本身(不 hook、不重启),但对"key 到底怎么存的"理解要求越高。手法一什么都不用懂,手法五得摸清账号对象的结构,4.1.10 还得把混淆逆出来。这也是逆向的常态,省事的招脆,稳的招要你真的看懂它。
小白也能带走的五条逆向思路
这次从头到尾,真正值钱的不是某一把 key,是几条能复用到别的逆向上的思路。挑五条最实用的。
先找一把"验证的尺子",比直接猜出答案更重要。 SQLCipher 那个 HMAC 就是尺子,它让你能判断"猜对了吗",于是暴力扫、异或凑对、派生验证全都有了兜底。逆向任何东西之前,先问一句:我怎么知道自己对了?能回答这个,后面全好办。
从最蠢的假设开始,一个个往上排。 明文 → 简单加密 → 花哨算法。别一上来就假设对方用了什么高深玩意儿,大部分时候答案蠢得让你意外。我们排到"AES?PBKDF2?"全否掉,最后是个小学生都会的异或。
要挂钩,挂在不会动的边界上。 挂微信自己的内部地址,它一改版你就返工;挂系统的 CCCryptorCreate,一劳永逸。找那条"数据非过不可、但又不归对方管"的关口。
黑盒 trace 卡住了,就去读结构。 一直猜"是什么变换"猜不出来,是因为你在盲人摸象。把内存 dump 出来一个字节一个字节看,结构自己会浮出来。异或那层混淆,黑盒猜一星期,肉眼看两块内存两分钟。
别被"混淆"吓住。 OLLVM、字符串加密听着唬人,但底下藏的往往是 split-XOR、ASCII 转码这种"把东西拆两半藏"的老套路。混淆是让你懒得看,不是真的看不懂。
最后一句正经的:上面这些只用来碰你自己设备上、你自己账号的数据。原理和思路可以学,别拿去解别人的号,那是另一码事,也不在这篇的范围里。

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