🏠

第二章:如何从内存中"偷"取密钥

想象一下,你面前有一个保险箱,里面装着微信的所有聊天记录。这个保险箱用的是军用级别的加密——AES-256,而且钥匙是通过一种叫 PBKDF2 的算法生成的,需要迭代 256,000 次才能算出来。如果你用普通电脑暴力破解这把钥匙,可能需要几百年。

但如果我们换个思路呢?如果保险箱的主人——也就是正在运行的微信程序——已经把钥匙放在桌上(内存里)了呢?

这就是 find_all_keys 模块的核心思想:与其硬撬保险箱,不如直接拿走主人随手放在桌上的钥匙

为什么传统方法行不通?

在深入技术细节之前,让我们先理解微信的数据库加密有多"变态"。

flowchart LR A[你的微信密码] -->|PBKDF2
256,000次迭代| B[派生密钥] B -->|+ salt| C[最终加密密钥] C -->|AES-256-CBC| D[加密数据库] style A fill:#e1f5ff style D fill:#ffe1e1

想象你有一把万能钥匙胚子(密码),但要让它能打开特定的门,你需要把它放进一台机器里磨 256,000 次——每次打磨都会改变钥匙的形状。最后,你还要往钥匙上撒一把特殊的盐(salt),才能得到真正能开锁的钥匙。

这个过程设计得如此复杂,就是为了防止有人偷走你的数据库文件后能快速破解。即使是最快的显卡,每秒也只能尝试几千次派生,想要试完所有可能性几乎是不可能的。

微信的"疏忽":WCDB 的密钥缓存

但微信团队自己也需要快速访问这些数据。他们不可能每次查询聊天记录都重新计算 256,000 次 PBKDF2——那会让微信卡成 PPT。

于是他们做了一个合理的工程妥协:在内存中缓存已经派生好的密钥

微信使用的数据库引擎叫 WCDB(WeChat DataBase),它是 SQLCipher 的一个封装。当微信第一次打开某个数据库时,WCDB 会完成完整的密钥派生过程,然后把结果以特定格式缓存在内存中:

x'1234567890abcdef...'  ← 96位十六进制字符串

这串字符就像一张贴在显示器边缘的便利贴,上面写着:"数据库 X 的钥匙是……"

我们的任务,就是找到这张便利贴。

内存扫描:在数十亿字节中寻找线索

现在想象一下:微信进程的内存空间就像一个巨大的图书馆,有几十亿本书(字节)。我们要找的便利贴就夹在某本书的某页里,但我们不知道具体位置。

sequenceDiagram participant User as 用户 participant Finder as find_all_keys participant WinAPI as Windows API participant WeChat as 微信进程 User->>Finder: 运行脚本 Finder->>WinAPI: tasklist 找微信PID WinAPI-->>Finder: 返回进程ID Finder->>WinAPI: OpenProcess(读取权限) WinAPI-->>Finder: 进程句柄 loop 遍历整个地址空间 Finder->>WinAPI: VirtualQueryEx(当前地址) WinAPI-->>Finder: 内存区域信息 alt 区域可读且已提交 Finder->>WinAPI: ReadProcessMemory WinAPI->>WeChat: 读取内存页 WeChat-->>WinAPI: 返回数据 WinAPI-->>Finder: 内存内容 Finder->>Finder: 正则匹配 x'...' 模式 end end Finder->>User: 输出 all_keys.json

第一步:定位目标

首先,我们需要找到微信的主进程。电脑上可能同时运行着多个 Weixin.exe(比如小程序、视频号等子进程),我们选择内存占用最大的那个——这通常是主进程,就像在一群人中找块头最大的那个一样简单直接。

第二步:绘制地图

拿到进程 ID 后,我们需要了解这个"图书馆"的布局。Windows 提供一个叫 VirtualQueryEx 的 API,可以告诉我们:

  • 这块内存从哪个地址开始(书架编号)
  • 有多大(多少本书)
  • 能不能读(是否对外开放)
  • 是什么状态(是否真的存放了东西)
graph TD subgraph "微信进程虚拟地址空间" A[0x00000000
不可访问] B[0x00400000
代码段
只读] C[0x10000000
堆内存
✓ 重点扫描] D[0x7FFE0000
系统保留
跳过] E[0x7FFFFFFFFFFF
64位上限] end style C fill:#90EE90 style A fill:#FFB6C1 style D fill:#FFB6C1

我们只关心那些已提交(MEM_COMMIT)可读的区域——这些地方才真正存放着数据。空书架和禁入区域我们直接跳过。

第三步:逐页翻阅

对于每个可读区域,我们使用 ReadProcessMemory 把内容读出来,然后用一个精心设计的正则表达式搜索:

pattern = rb"x'([0-9a-fA-F]{64,192})'"

这个模式是什么意思呢?

  • x' 是 WCDB 存储十六进制数据的独特前缀,就像便利贴上的公司 Logo
  • [0-9a-fA-F] 表示十六进制字符(0-9, a-f, A-F)
  • {64,192} 限定长度在 64 到 192 个字符之间

为什么是 64 到 192?因为我们实际发现了三种不同的存储格式:

格式 长度 含义
标准格式 96 32字节 enc_key + 16字节 salt
仅密钥 64 只有 32字节 enc_key
超长格式 >96 包含额外元数据

这就像便利贴有时写得详细,有时写得简略,但都有那个标志性的 Logo。

验证:如何确定找到的真的是钥匙?

假设你在图书馆里找到了一张写着 x'1234...' 的纸条,你怎么知道这是数据库的钥匙,而不是某个用户的聊天内容恰好匹配了这个模式?

这就是 verify_key_for_db 函数的工作——它是一把试金石

flowchart TD A[候选密钥] --> B{格式判断} B -->|96位| C[提取 enc_key + salt] B -->|64位| D[逐个数据库尝试] B -->|超长| E[截取前64位和后32位] C --> F[HMAC-SHA512 验证] D --> F E --> F F -->|验证通过| G[确认有效密钥] F -->|验证失败| H[丢弃] style G fill:#90EE90 style H fill:#FFB6C1

密码学的巧妙验证

SQLCipher 4 的设计给了我们一个捷径。每个加密数据库的第一页都包含:

  • Salt:16字节的随机数,用于派生密钥
  • IV:初始化向量,用于 AES 加密
  • 加密数据:实际的密文
  • HMAC:消息认证码,用于完整性校验

正常情况下,要验证一个密钥是否正确,你需要:

  1. 用 salt 和密钥通过 PBKDF2 派生 MAC 密钥(256,000 次迭代!)
  2. 计算 HMAC-SHA512
  3. 与存储的值比较

但我们发现,SQLCipher 的 HMAC 验证只需要 2 次迭代就能派生出足够的密钥材料!这是因为 HMAC 密钥不需要像加密密钥那样强的熵。

于是我们的验证流程变成了:

sequenceDiagram participant V as verify_key_for_db participant DB as 数据库第一页 V->>DB: 读取 page 1 DB-->>V: salt, IV, ciphertext, stored_hmac V->>V: salt XOR 0x3a = mac_salt Note over V: 只有2次迭代的PBKDF2! V->>V: PBKDF2(enc_key, mac_salt, 2) = mac_key V->>V: HMAC-SHA512(ciphertext, mac_key) = computed_hmac V->>V: compare(computed_hmac, stored_hmac) alt 匹配 V->>V: 密钥有效! else 不匹配 V->>V: 密钥无效 end

整个过程在微秒级完成,既快速又可靠——这是密码学级别的安全性保证,不是简单的猜测。

处理特殊情况:交叉验证的艺术

有时候,我们会遇到一些"顽固"的数据库——它们的 salt 没有直接出现在找到的密钥字符串里。这时候就需要一点推理能力了。

WCDB 有一个特性:如果多个数据库使用相同的密码,它们会派生出相同的 enc_key。这在微信中很常见,因为所有数据库通常都用同一个主密码保护。

所以当我们遇到未匹配的 salt 时,会尝试用已找到的其他密钥去验证它。这就像你有一串钥匙,虽然不知道哪把对应哪个锁,但可以一把一把试——而我们的"试"是微秒级的密码学验证,不是真的去开锁。

最终产出:all_keys.json

经过完整的扫描和验证流程,我们得到一个整洁的 JSON 文件:

{
  "contact.db": {
    "enc_key": "a1b2c3d4...",
    "salt": "e5f6g7h8..."
  },
  "message_0.db": {
    "enc_key": "i9j0k1l2...",
    "salt": "m3n4o5p6..."
  }
}

这个文件是整个 wechat-decrypt 工具链的基石。后续的实时监控、AI 查询等功能,都依赖这些从内存中提取的密钥。

安全与伦理的边界

读到这里,你可能会有一个担忧:这种技术会不会被滥用?

重要的是理解几个关键点:

  1. 需要管理员权限:读取其他进程的内存需要系统最高权限,这不是普通病毒能做到的
  2. 需要微信正在运行:钥匙只在微信运行时存在于内存中,关机或退出后就消失了
  3. 只能解密自己的数据:你只能读取自己电脑上、自己账号的微信数据

这就像你能打开自己家的保险箱,但不能因此说开锁技术是邪恶的。技术的价值在于使用者如何运用它。

动手实践

如果你想亲自体验这个过程,确保满足以下条件:

  • Windows 系统(目前只支持 Windows 版微信)
  • Python 3.10+
  • 以管理员身份运行 PowerShell 或 CMD
  • 微信 4.0 正在运行

然后执行:

python -m wechat_decrypt.find_all_keys

你会看到扫描进度和找到的密钥数量。最终生成的 all_keys.json 就可以用于后续的数据解密了。

小结

在这一章中,我们学习了:

  • PBKDF2 的高成本:为什么暴力破解不可行
  • WCDB 的密钥缓存:微信为了性能而在内存中留下的"破绽"
  • 内存扫描技术:如何在数十亿字节中找到特定的模式
  • 密码学验证:用 HMAC 快速确认密钥的正确性

下一章,我们将看看这些密钥如何被用来构建实时监控系统——让新消息像直播一样推送到你的浏览器。

On this page