人机协作逆向:用 AI + Frida 打通微信 4.1.8 macOS 数据库密钥提取
2026年4月18日 22:37
<blockquote><p>当内存特征扫描失效、标准 Hook 无功而返时,一个开发者与 AI 如何通过多轮迭代,最终找到微信 4.1.8 的密钥派生入口。</p></blockquote><hr><h2 id="一、背景:旧方案突然失效"><a href="#一、背景:旧方案突然失效" class="headerlink" title="一、背景:旧方案突然失效"></a>一、背景:旧方案突然失效</h2><p><code>chatlog</code> 项目长期依赖一套相对稳定的 macOS 密钥提取逻辑:</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">vmmap 定位内存区域 → lldb dump 内存 → 搜索 fts5(% 特征字节 → 固定偏移取 32B Key → DB 验证</span><br></pre></td></tr></table></figure><p>但在用户升级到 <strong>微信 4.1.8</strong> 后,这条链路彻底断裂。运行 <code>./bin/chatlog key --debug</code> 时,程序能正常读完 94 个内存区域,然后陷入漫长的静默,最终超时退出——<strong>Key 一个都没找到</strong>。</p><hr><h2 id="二、第一回合:AI-做诊断,人类给环境"><a href="#二、第一回合:AI-做诊断,人类给环境" class="headerlink" title="二、第一回合:AI 做诊断,人类给环境"></a>二、第一回合:AI 做诊断,人类给环境</h2><p>用户把问题抛给 AI:</p><blockquote><p>“我发现现在微信版本升级了,无法获取到 key。”</p></blockquote><p>AI 的第一反应是排查代码路径。通过阅读 <code>chatlog</code> 源码,AI 迅速定位到 V4 Extractor 的两个 Pattern:</p><ul><li>Pattern 1: <code> fts5(%</code>(<code>20 66 74 73 35 28 25 00</code>)</li><li>Pattern 2: 16 个零字节(<code>00 00 ...</code>)</li></ul><p>AI 列出可能的故障点,但用户没有手动去贴日志,而是直接说:</p><blockquote><p><strong>“你直接运行看看吧。”</strong></p></blockquote><p>AI 执行 <code>./bin/chatlog key --debug</code>,观察到内存读取正常、但匹配全部失败。接着 AI 检查微信二进制:</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">strings /Applications/WeChat.app/Contents/MacOS/WeChat | grep -c <span class="string">"fts5"</span></span><br><span class="line"><span class="comment"># 输出: 0</span></span><br></pre></td></tr></table></figure><p><strong>关键发现:<code>fts5</code> 在微信 4.1.8 里完全消失了。</strong> 这是旧方案失效的根因。</p><hr><h2 id="三、第二回合:人类抛出新线索,AI-转换思路"><a href="#三、第二回合:人类抛出新线索,AI-转换思路" class="headerlink" title="三、第二回合:人类抛出新线索,AI 转换思路"></a>三、第二回合:人类抛出新线索,AI 转换思路</h2><p>AI 解释了问题的本质:内存布局变了,需要找新的 Pattern 或换更稳定的 Hook 思路。此时用户提供了一个关键的外部信息:</p><blockquote><p><strong>“wx_key 你参考看看这个项目是 windows 的,但是你看看他是怎么找 key 的”</strong></p></blockquote><p>AI 读取 <code>wx_key</code> 的源码后,核心思路被点亮:</p><blockquote><p><strong>Windows 版不扫描数据,而是 Hook 函数。</strong> 它在 <code>Weixin.dll</code> 代码段里搜索机器码 pattern,定位到处理 Key 的函数,安装 Inline Hook,等微信调用时直接拦截参数。</p></blockquote><p>这是一个比内存扫描稳定得多的方案。用户接着问:</p><blockquote><p><strong>“frida hook 你能做吗”</strong></p></blockquote><p>AI 回答:能。于是进入下一轮技术攻坚。</p><hr><h2 id="四、第三回合:AI-搭建-Frida-环境,踩坑与修正"><a href="#四、第三回合:AI-搭建-Frida-环境,踩坑与修正" class="headerlink" title="四、第三回合:AI 搭建 Frida 环境,踩坑与修正"></a>四、第三回合:AI 搭建 Frida 环境,踩坑与修正</h2><p>AI 开始动手:</p><ol><li><strong>安装 Frida</strong>:<code>pip3 install frida-tools</code></li><li><strong>寻找目标函数</strong>:先尝试 Hook <code>libsqlite3.dylib</code> 的 <code>sqlite3_key</code> / <code>sqlite3_key_v2</code></li></ol><h3 id="坑-1:Frida-17-API-变更"><a href="#坑-1:Frida-17-API-变更" class="headerlink" title="坑 1:Frida 17 API 变更"></a>坑 1:Frida 17 API 变更</h3><p>写脚本时踩了第一个坑——旧教程里的 <code>Module.findExportByName</code> 在 Frida 17 中报错:</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">TypeError: not a function</span><br></pre></td></tr></table></figure><p>AI 通过快速测试定位到新 API:</p><figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// 正确写法</span></span><br><span class="line"><span class="title class_">Process</span>.<span class="title function_">getModuleByName</span>(<span class="string">"libsqlite3.dylib"</span>).<span class="title function_">findExportByName</span>(<span class="string">"sqlite3_key"</span>)</span><br><span class="line"><span class="comment">// 或</span></span><br><span class="line"><span class="title class_">Module</span>.<span class="title function_">getGlobalExportByName</span>(<span class="string">"sqlite3_key"</span>)</span><br></pre></td></tr></table></figure><h3 id="坑-2:sqlite3-key-根本没被调用"><a href="#坑-2:sqlite3-key-根本没被调用" class="headerlink" title="坑 2:sqlite3_key 根本没被调用"></a>坑 2:sqlite3_key 根本没被调用</h3><p>修正 API 后,Hook 安装成功。AI 让用户关闭微信,用 <code>frida.spawn</code> 重新启动并注入脚本。用户登录微信,AI 监控输出——<strong>60 秒内一个 Key 都没捕获到</strong>。</p><p>这说明 <strong>微信 4.1.8 macOS 并未使用标准 SQLCipher 的 <code>sqlite3_key</code> 接口</strong>。加密逻辑完全内嵌在 140MB 的 <code>wechat.dylib</code> 中,自己管理密钥派生和 VFS 层加解密。</p><hr><h2 id="五、第四回合:转移战场到-PBKDF2"><a href="#五、第四回合:转移战场到-PBKDF2" class="headerlink" title="五、第四回合:转移战场到 PBKDF2"></a>五、第四回合:转移战场到 PBKDF2</h2><p>既然 <code>chatlog</code> 的解密代码明确告诉我们:</p><ul><li><strong>算法</strong>:PBKDF2-HMAC-SHA512</li><li><strong>迭代次数</strong>:256000</li><li><strong>输出长度</strong>:32 字节</li></ul><p>那么微信启动数据库时,<strong>一定会在某个时刻做密钥派生</strong>。macOS 上最可能的系统入口是 <code>CommonCrypto</code> 中的 <code>CCKeyDerivationPBKDF</code>。</p><p>AI 更新 Frida 脚本,新增对 <code>CCKeyDerivationPBKDF</code> 的 Hook,并加上过滤条件:</p><figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">var</span> pbkdf2 = <span class="title class_">Module</span>.<span class="title function_">getGlobalExportByName</span>(<span class="string">"CCKeyDerivationPBKDF"</span>);</span><br><span class="line"></span><br><span class="line"><span class="title class_">Interceptor</span>.<span class="title function_">attach</span>(pbkdf2, {</span><br><span class="line"> <span class="attr">onEnter</span>: <span class="keyword">function</span>(<span class="params">args</span>) {</span><br><span class="line"> <span class="keyword">var</span> algo = args[<span class="number">0</span>].<span class="title function_">toInt32</span>(); <span class="comment">// 2 = kCCPBKDF2</span></span><br><span class="line"> <span class="keyword">var</span> prf = args[<span class="number">5</span>].<span class="title function_">toInt32</span>(); <span class="comment">// 5 = kCCPRFHmacAlgSHA512</span></span><br><span class="line"> <span class="keyword">var</span> rounds = args[<span class="number">6</span>].<span class="title function_">toInt32</span>();</span><br><span class="line"> <span class="keyword">var</span> pwdLen = args[<span class="number">2</span>].<span class="title function_">toInt32</span>();</span><br><span class="line"></span><br><span class="line"> <span class="keyword">if</span> (algo === <span class="number">2</span> && prf === <span class="number">5</span> && rounds > <span class="number">1000</span>) {</span><br><span class="line"> <span class="keyword">var</span> hex = <span class="title function_">toHex</span>(args[<span class="number">1</span>], pwdLen);</span><br><span class="line"> <span class="title function_">send</span>({<span class="attr">type</span>: <span class="string">"key"</span>, <span class="attr">key</span>: hex, <span class="attr">rounds</span>: rounds, <span class="attr">len</span>: pwdLen});</span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line">});</span><br></pre></td></tr></table></figure><hr><h2 id="六、第五回合:捕获成功,人类验证"><a href="#六、第五回合:捕获成功,人类验证" class="headerlink" title="六、第五回合:捕获成功,人类验证"></a>六、第五回合:捕获成功,人类验证</h2><p>脚本就绪后,用户让AI 关闭微信,运行脚本</p><p>几秒钟后,终端输出:</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">[KEY] via CCKeyDerivationPBKDF len=32 rounds=256000 dkLen=32</span><br><span class="line"> 24f52f003edd470e97e73fc63f4b89bb0cb9efa09b8a40d685c75c47df21fc11</span><br></pre></td></tr></table></figure><p><strong>Key 捕获成功。</strong></p><p>但还没完。用户紧接着提出验证要求:</p><blockquote><p><strong>“你根据这个 key 测试一下是否能 chatlog decrypt”</strong></p></blockquote><p>AI 立刻用捕获到的 Key 执行解密:</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">./bin/chatlog decrypt \</span><br><span class="line"> --data-dir ~/Library/Containers/com.tencent.xinWeChat/Data/Documents/xwechat_files/... \</span><br><span class="line"> --data-key 24f52f003edd470e97e73fc63f4b89bb0cb9efa09b8a40d685c75c47df21fc11 \</span><br><span class="line"> --work-dir /tmp/chatlog_test</span><br></pre></td></tr></table></figure><p>输出:</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">decrypt success</span><br></pre></td></tr></table></figure><p><strong>Key 有效,数据库成功解密。</strong></p><hr><h2 id="七、核心方法论:人机协作的分工"><a href="#七、核心方法论:人机协作的分工" class="headerlink" title="七、核心方法论:人机协作的分工"></a>七、核心方法论:人机协作的分工</h2><p>回顾整个过程,人类与 AI 的分工非常清晰:</p><table><thead><tr><th>角色</th><th>贡献</th></tr></thead><tbody><tr><td><strong>人类</strong></td><td>提供运行环境、触发问题、给出关键外部线索(<code>wx_key</code>)、决定尝试 Frida、执行微信登录操作、提出最终验证需求</td></tr><tr><td><strong>AI</strong></td><td>快速阅读大量源码做根因分析、搭建 Frida 环境、编写/调试 Hook 脚本、处理 API 兼容性问题、执行最终验证</td></tr></tbody></table><p>如果没有人类抛出的 <code>wx_key</code> 项目,AI 可能会继续在内存扫描或 sqlite3 Hook 的思路上打转;如果没有 AI 的快速代码阅读和脚本编写能力,从诊断到验证可能需要数小时甚至数天。</p><hr><h2 id="八、关键踩坑清单"><a href="#八、关键踩坑清单" class="headerlink" title="八、关键踩坑清单"></a>八、关键踩坑清单</h2><h3 id="1-特征字节依赖生命周期极短"><a href="#1-特征字节依赖生命周期极短" class="headerlink" title="1. 特征字节依赖生命周期极短"></a>1. 特征字节依赖生命周期极短</h3><p>微信 4.1.8 直接从二进制中移除了 <code>fts5</code> 字符串,导致基于内存布局的扫描方案瞬间破产。</p><h3 id="2-标准-SQLCipher-API-是个假目标"><a href="#2-标准-SQLCipher-API-是个假目标" class="headerlink" title="2. 标准 SQLCipher API 是个假目标"></a>2. 标准 SQLCipher API 是个假目标</h3><p>macOS 系统库里有 <code>sqlite3_key</code>,但微信根本不调用。Hook 之前必须做实际验证,不能凭假设行事。</p><h3 id="3-Frida-版本陷阱"><a href="#3-Frida-版本陷阱" class="headerlink" title="3. Frida 版本陷阱"></a>3. Frida 版本陷阱</h3><p>Frida 17 废弃了 <code>Module.findExportByName</code>,大量网络教程已过时。实际动手测试才能发现。</p><h3 id="4-必须在-spawn-模式下拦截"><a href="#4-必须在-spawn-模式下拦截" class="headerlink" title="4. 必须在 spawn 模式下拦截"></a>4. 必须在 <code>spawn</code> 模式下拦截</h3><p>attach 到已运行的微信会错过所有启动时的密钥派生。先 <code>pkill WeChat</code>,再用 <code>frida.spawn()</code> 启动,是捕获 Key 的必要条件。</p><hr><h2 id="九、完整可用脚本"><a href="#九、完整可用脚本" class="headerlink" title="九、完整可用脚本"></a>九、完整可用脚本</h2><p>以下脚本已保存至项目根目录 <code>wechat_key_hook.py</code>,可直接使用:</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">#!/usr/bin/env python3</span></span><br><span class="line"><span class="keyword">import</span> frida</span><br><span class="line"><span class="keyword">import</span> sys</span><br><span class="line"><span class="keyword">import</span> time</span><br><span class="line"></span><br><span class="line">JS = <span class="string">r"""</span></span><br><span class="line"><span class="string">function toHex(ptr, len) {</span></span><br><span class="line"><span class="string"> try {</span></span><br><span class="line"><span class="string"> var arr = new Uint8Array(ptr.readByteArray(len));</span></span><br><span class="line"><span class="string"> return arr.map(b => b.toString(16).padStart(2, '0')).join('');</span></span><br><span class="line"><span class="string"> } catch(e) { return null; }</span></span><br><span class="line"><span class="string">}</span></span><br><span class="line"><span class="string"></span></span><br><span class="line"><span class="string">var pbkdf2 = Module.getGlobalExportByName("CCKeyDerivationPBKDF");</span></span><br><span class="line"><span class="string">if (pbkdf2) {</span></span><br><span class="line"><span class="string"> Interceptor.attach(pbkdf2, {</span></span><br><span class="line"><span class="string"> onEnter: function(args) {</span></span><br><span class="line"><span class="string"> var algo = args[0].toInt32();</span></span><br><span class="line"><span class="string"> var prf = args[5].toInt32();</span></span><br><span class="line"><span class="string"> var rounds = args[6].toInt32();</span></span><br><span class="line"><span class="string"> var pwdLen = args[2].toInt32();</span></span><br><span class="line"><span class="string"> if (algo === 2 && prf === 5 && rounds > 1000) {</span></span><br><span class="line"><span class="string"> var hex = toHex(args[1], pwdLen);</span></span><br><span class="line"><span class="string"> if (hex) send({type:"key", key:hex, rounds:rounds, len:pwdLen});</span></span><br><span class="line"><span class="string"> }</span></span><br><span class="line"><span class="string"> }</span></span><br><span class="line"><span class="string"> });</span></span><br><span class="line"><span class="string"> send("[+] CCKeyDerivationPBKDF hooked @ " + pbkdf2);</span></span><br><span class="line"><span class="string">}</span></span><br><span class="line"><span class="string">"""</span></span><br><span class="line"></span><br><span class="line"><span class="built_in">print</span>(<span class="string">"[*] Spawning WeChat..."</span>)</span><br><span class="line">pid = frida.spawn(<span class="string">"/Applications/WeChat.app/Contents/MacOS/WeChat"</span>)</span><br><span class="line">session = frida.attach(pid)</span><br><span class="line">script = session.create_script(JS)</span><br><span class="line"></span><br><span class="line"><span class="keyword">def</span> <span class="title function_">on_msg</span>(<span class="params">message, data</span>):</span><br><span class="line"> <span class="keyword">if</span> message[<span class="string">'type'</span>] == <span class="string">'send'</span>:</span><br><span class="line"> payload = message[<span class="string">'payload'</span>]</span><br><span class="line"> <span class="keyword">if</span> <span class="built_in">isinstance</span>(payload, <span class="built_in">dict</span>) <span class="keyword">and</span> payload.get(<span class="string">'type'</span>) == <span class="string">'key'</span>:</span><br><span class="line"> <span class="built_in">print</span>(<span class="string">f"\n[!!!] KEY CAPTURED: <span class="subst">{payload[<span class="string">'key'</span>]}</span>"</span>)</span><br><span class="line"> <span class="keyword">else</span>:</span><br><span class="line"> <span class="built_in">print</span>(payload)</span><br><span class="line"></span><br><span class="line">script.on(<span class="string">'message'</span>, on_msg)</span><br><span class="line">script.load()</span><br><span class="line">frida.resume(pid)</span><br><span class="line"></span><br><span class="line">time.sleep(<span class="number">60</span>)</span><br><span class="line">session.detach()</span><br></pre></td></tr></table></figure><hr><h2 id="十、后续展望"><a href="#十、后续展望" class="headerlink" title="十、后续展望"></a>十、后续展望</h2><p>这次实践验证了 <strong>Hook 加密算法入口</strong> 比 <strong>内存特征扫描</strong> 更稳定、更跨版本。下一步可以将 Frida Hook 封装为 <code>chatlog key</code> 的新后端(例如 <code>--frida</code> 模式),让 macOS 用户不再需要关闭 SIP 或依赖 <code>lldb</code>,大幅降低使用门槛。</p><blockquote><p><strong>一个人类开发者 + 一个能读代码、写脚本、跑验证的 AI,可以在一个小时内完成传统上需要数天的逆向工程闭环。</strong></p></blockquote><p><br /><strong>本文作者</strong>:高金<br /><strong>本文地址</strong>: <a href="https://igaojin.me/2026/04/18/%E4%BA%BA%E6%9C%BA%E5%8D%8F%E4%BD%9C%E9%80%86%E5%90%91%EF%BC%9A%E7%94%A8-AI-Frida-%E6%89%93%E9%80%9A%E5%BE%AE%E4%BF%A1-4-1-8-macOS-%E6%95%B0%E6%8D%AE%E5%BA%93%E5%AF%86%E9%92%A5%E6%8F%90%E5%8F%96/">https://igaojin.me/2026/04/18/人机协作逆向:用-AI-Frida-打通微信-4-1-8-macOS-数据库密钥提取/</a> <br /><strong>版权声明</strong>:转载请注明出处!</p><div id="gitalk-container"></div><script src="https://cdn.bootcss.com/blueimp-md5/2.12.0/js/md5.min.js"></script><link rel="stylesheet" href="https://unpkg.com/gitalk/dist/gitalk.css"><script src="https://unpkg.com/gitalk/dist/gitalk.min.js"></script><script>var gitalkConfig = {"clientID":"935e92a5333436856348","clientSecret":"e655566eaf920d216ec6283978d67874bf0850a6","repo":"jin10086.github.io","owner":"jin10086","admin":["jin10086"],"distractionFreeMode":false,"id":"page.date","createIssueManually":false}; gitalkConfig.id = md5(location.pathname);var gitalk = new Gitalk(gitalkConfig); gitalk.render("gitalk-container"); </script>