wx.setStorage 存的数据,没你以为的那么安全

微信小程序 Storage 是怎么存的,又是怎么被解密的

从「想看看数据存哪了」到「把所有小程序 Storage 全部解密出来」的完整过程。


前言

微信小程序提供了一套简洁的本地存储 API------wx.setStorage / wx.getStorage。对用户来说,这不过是个普通的键值存储;但你有没有好奇过:这些数据落盘后长什么样?能直接读出来吗?

本文以 macOS 上的微信 4.x(PC 端)为研究对象,记录我从 0 到 1 把本机所有小程序 Storage 解密出来的过程------包括加密方案分析、密钥捕获方法,以及一个意外发现的「快捷规律」。


一、数据存在哪

先找文件。微信 4.x 在 macOS 上的小程序数据统一放在:

javascript 复制代码
~/Library/Containers/com.tencent.xinWeChat/Data/Documents/
    app_data/radium/users/<user_hash>/applet/local/<wxid>/usrmmkvstorage0/

每个小程序(由 wxid 标识,形如 wx1a2b3c4d5e6f7a8b)在这个目录下有两个文件:

文件 作用
wx1a2b3c4d5e6f7a8b 加密主数据文件,预分配 128KB
wx1a2b3c4d5e6f7a8b.crc 元数据文件,存 CRC32、IV 等信息

打开主文件,全是乱码。显然是加密了的。

file 命令识别不出格式,但看着文件名和目录结构,搜一下关键词------这其实是 MMKV,腾讯开源的高性能键值存储库。微信把 Storage 的落盘层直接交给了它,并且开启了加密。


二、MMKV 文件格式拆解

主文件

结构极为紧凑:

arduino 复制代码
offset 0:   u32  actualSize      // 有效密文长度
offset 4:   byte[actualSize]     // AES-128-CFB 加密的 payload
(后续全为 0x00 padding)

元数据文件(.crc)

前 40 字节的 layout:

rust 复制代码
offset 0:   u32      m_crcDigest          // payload 的 CRC32
offset 4:   u32      m_version            // 实测值为 4
offset 8:   u32      m_sequence
offset 12:  byte[16] m_vector             // AES-CFB IV  ← 关键
offset 28:  u32      m_actualSize
offset 32:  u32      m_lastConfirmedMMKVFileSize
offset 36:  u32      m_lastActualUpdateCRCDigest

重点 :IV 存在 .crc 文件里,不在主文件里。这意味着只要拿到 IV 和 cryptKey,就能完整解密。

解密方式

MMKV 使用 AES-128-CFB128(全块反馈模式):

python 复制代码
def aes_cfb128_decrypt(key16: bytes, iv16: bytes, ct: bytes) -> bytes:
    return AES.new(key16, AES.MODE_CFB, iv=iv16, segment_size=128).decrypt(ct)

cryptKey 经过零填充变成 16 字节的 AES key(对应 MMKV 源码里的 memset + memcpy):

python 复制代码
def derive_aes_key(crypt_key: bytes) -> bytes:
    return crypt_key[:16].ljust(16, b"\x00")

明文结构

解密后的 payload 是一段 protobuf-like 的 append-only 日志:

scss 复制代码
[4字节 hash] (varint key_len)(key)(varint val_len)(value) ...

MMKV 每次 setObjectForKey: 不覆盖旧值,而是直接追加一条新记录。读取时取同一 key 最后出现的值为当前值。这也是 MMKV 高性能的来源------写入永远是 O(1) 的顺序追加。

微信的值包装格式

解密出来的 value 还套了一层微信自己的 JSON 包装:

json 复制代码
{"data": "true",           "dataType": "Boolean"}
{"data": "42.5",           "dataType": "Number"}
{"data": "{\"key\":\"v\"}", "dataType": "Object"}

Object 类型的 data 是再次 JSON 序列化的字符串,需要二次解析才能还原成对象。


三、问题核心:cryptKey 从哪里来

格式分析完了。解密唯一缺的就是 cryptKey

查了微信的通信协议,cryptKey 由后端服务(mmbizwxasyncappsvr.WxaSyncIssueDecryptKeyCmd)在小程序初始化时实时下发,不持久化到磁盘,每个 user × wxid 独立一份。

换句话说:静态分析文件系统是拿不到 cryptKey 的,必须从运行时进程里捞。

这也是为什么大多数人打开这个目录,看到一堆乱码就放弃了。


四、进阶方法:用 LLDB 实时捕获密钥

选哪个切入点

MMKV 的打开接口签名是:

cpp 复制代码
static MMKV* mmkvWithID(
    const std::string &mmapID,    // X0: wxid(小程序 ID)
    MMKVMode mode,                // W1: 1 = SINGLE_PROCESS
    std::string *cryptKey,        // X2: 密钥指针  ← 目标
    std::string *rootPath,        // X3
    size_t expectedCapacity       // X4
);

每次小程序启动,微信都会调用这个函数并把 cryptKey 作为第三个参数传入。在 ARM64 调用约定下,第三个参数就在 X2 寄存器里。

只要在这个函数入口设断点,在小程序打开的瞬间读出 X0(wxid)和 X2(cryptKey),就大功告成了。

跨版本自动定位函数偏移

微信每次更新,framework 的偏移都会变,不能硬编码。用静态特征来定位:

第一步 :MMKV 源码里有一条内部日志 MMKVInfo("mmkvWithID", ...),这个字符串字面量在 __cstring 段里全二进制只出现一次。先找到它的 vmaddr。

第二步 :扫描 __text 段,找所有 ADRP+ADD 指令对,筛出引用该 vmaddr 的位置------这就是函数内部引用这个字符串的位置。

第三步 :从引用点向前扫描,找最近的 SUB SP, SP, #imm(ARM64 函数 prologue)------这就是函数入口。

python 复制代码
s_vma    = find_string_in_cstring(binary, b"mmkvWithID\x00")
xrefs    = find_adrp_add_references(binary, s_vma)
entry    = find_prologue_before(xrefs[0])   # 向前扫描找 SUB SP

不依赖任何硬编码偏移,跨微信版本稳定可用。

绕过 hardened runtime

WeChat.app 以 Apple Team 签名的 hardened runtime 运行,直接 lldb -p <pid> 会被系统拒绝。

解决方案是对 WeChat.app 做一次 ad-hoc 重签,加上允许调试的 entitlements:

bash 复制代码
sudo codesign --force --deep --sign - \
    --entitlements wechat.entitlements \
    /Applications/WeChat.app

wechat.entitlements 里的关键项:

xml 复制代码
<key>com.apple.security.cs.debugger</key><true/>
<key>get-task-allow</key><true/>
<key>com.apple.security.cs.disable-library-validation</key><true/>

重签后 attach 到 WeChatAppEx 子进程,加载断点脚本,打开目标小程序------密钥立刻落到磁盘上,解密随即完成。

这套进阶方法能 100% 捕获密钥,但需要重签微信、需要管理员密码,用户还得手动打开小程序------体验不够优雅。能不能绕过这一步?


五、一个意外发现的规律

在用进阶方法采集了一批样本之后,我注意到一个规律:

对每个测试过的 wxid,其 usrmmkvstorage0 的 cryptKey 都满足:

ini 复制代码
cryptKey = "w" + wxid[2::2]

即:去掉 wx 前缀,取剩余 16 个十六进制字符中下标为偶数 (0、2、4......)的字符,前面加一个 "w",得到 9 字节的 ASCII 字符串。

举个例子:

makefile 复制代码
wxid:    w  x  1  a  2  b  3  c  4  d  5  e  6  f  7  a  8  b
                ↑     ↑     ↑     ↑     ↑     ↑     ↑     ↑
index:          0     2     4     6     8    10    12    14

cryptKey = "w" + "1" + "2" + "3" + "4" + "5" + "6" + "7" + "8"
         = "w12345678"    (9 字节)

这个规律在手上所有样本中全部验证通过

这意味着:只需要读磁盘上现有的文件,不需要 attach 任何进程,就能完成解密。 整个流程变成了:

python 复制代码
def derive_crypt_key(wxid: str) -> bytes:
    return ("w" + wxid[2::2]).encode("ascii")

一行代码,数秒完成。


为什么是这个规律?

我没有在微信的代码里找到明确的生成逻辑,猜测可能是后端在下发密钥时对 wxid 做了某种简单的字符选取,或者早期版本就是这样生成的并延续了下来。

但不管原因是什么,规律本身是实测有效的。这个规律可能随微信版本变化;如果哪天推导失败(解密后一条 key 都读不出来),回退到进阶方法即可。


六、完整解密流程

综合以上,最终的完整流程:

sql 复制代码
扫描 applet/local/ 下所有 wxid
        ↓
对每个 wxid 用规律推导 cryptKey(快速,纯离线,无需操作微信)
        ↓
读取 .crc 文件,提取 IV 和 actualSize
读取主文件,截取有效密文
        ↓
AES-128-CFB128 解密
        ↓
解析 MMKV append-only payload
取每个 key 最后一次出现的值
        ↓
展开微信的 JSON 包装层,还原原始类型
        ↓
输出 out/<user_hash>/<wxid>.json

(如有失败条目 且 在 macOS 上)
        ↓
提示用户确认 → 重签微信 → attach LLDB
→ 实时捕获真实 cryptKey → 再次解密

实测:在我的机器上,58 个小程序全部在 3 秒内解密完成,无需触碰微信进程。


七、开源

整套工具已整理开源,支持 macOS / Windows / Linux:

github.com/PeanutSplas...

bash 复制代码
# macOS / Linux
make setup
make run

# Windows
python -m pip install -r requirements.txt
python src\decrypt.py --offline-only

结果写入 out/ 目录,按账号和小程序 ID 分文件存放,格式为 JSON。


结语

整个研究过程里最有意思的部分,不是解密本身,而是那个推导规律------它把一个「必须 attach 进程」的问题变成了一个一行代码的纯函数。不知道腾讯是有意为之还是历史遗留,但结果是:你用 wx.setStorage 存的所有数据,其实并没有你以为的那么不透明。


免责声明 :本文及相关工具仅供对自己设备、自己账号下数据进行研究和备份,禁止用于任何非自有数据。解密结果可能包含 token 等敏感凭证,请妥善保管 out/ 目录。对 WeChat.app 重签会移除官方签名,请自行评估风险。

相关推荐
帅次4 小时前
讯飞与腾讯云:Android 实时语音识别服务对比选择
android·ios·微信小程序·小程序·android studio·android runtime
he___H7 小时前
微信小程序实现两行交错功能
微信小程序·小程序
前端小木屋1 天前
uniapp与蓝牙设备连接详细步骤
前端·微信小程序
huang_jimei1 天前
【无标题】
微信小程序
Brave & Real2 天前
小程序 const 在js中以及与同类的var和let之间的差异
javascript·微信小程序·小程序
silvia_Anne2 天前
微信小程序商品列表
微信小程序·小程序
ze^02 天前
Day05 APP应用&微信小程序&原生态开发&H5+Vue技术&封装打包&反编译抓包点
vue.js·微信小程序·小程序
用户8574824354803 天前
useList 通用列表管理hook
vue.js·微信小程序
陪小甜甜赏月3 天前
微信小程序分享onShareAppMessage
前端·微信小程序·小程序