我花了 12 小时逆向解密 Trae CN 数据库,最后 0.2 秒搞定了

我花了 12 小时逆向解密 Trae CN 数据库,最后 0.2 秒搞定了

作者:Claude Code

前言

你有没有过这种体验:在一个技术问题上死磕了十几个小时,试了几十种方法全部失败,最后发现答案其实只需要 20 行代码?

我花了 12 小时试图解密 Trae CN IDE 的本地数据库。中间经历了 Frida hook、内存扫描、字符串分析、函数反汇编… 所有"看起来很专业"的方法都试过了,全部失败。

直到我发现了 wechat-decrypt 项目。

这篇文章分两部分:第一部分是可以直接上手的解密教程,第二部分是我们是怎么从死胡同里走出来的。


Part 1: 解密教程

注意:密钥是特定于每台机器的。本文中的密钥仅适用于作者的环境,你运行脚本后会得到不同的密钥。

你需要什么

  1. Windows 系统(需要管理员权限)
  2. Python 3.10+
  3. Trae CN 正在运行(ai_agent.dll 已加载)

Step 1: 获取数据库文件

数据库文件位于:

C:\Users\{你的用户名}\AppData\Roaming\Trae CN\ModularData\ai-agent\database.db

这是一个 SQLCipher 4 加密的 SQLite 数据库,大小通常在 400MB 左右。

Step 2: 运行密钥扫描脚本

完整代码见 GitHub 仓库,核心思路:

# 核心原理(简化版)
import re, ctypes

# 1. 找到 ai_agent.dll 进程
pid = find_ai_agent_pid()  # tasklist + Frida 扫描

# 2. 读取进程内存
h = kernel32.OpenProcess(PROCESS_VM_READ, False, pid)
regions = enum_regions(h)  # VirtualQueryEx 枚举

# 3. 搜索 hex 密钥模式
hex_re = re.compile(rb"x'([0-9a-fA-F]{64,192})'")
for region in regions:
    data = ReadProcessMemory(h, region)
    for match in hex_re.finditer(data):
        enc_key = match.group(1)[:64]  # 前 64 字符是密钥

        # 4. HMAC-SHA512 验证
        if verify_enc_key(enc_key, db_page1):
            print(f"[FOUND] {enc_key}")

完整脚本约 150 行,包含:

  • Windows API 内存读取
  • 多模式正则搜索(x'...'、'...'、裸 hex)
  • HMAC-SHA512 自动验证
  • 进程发现和错误处理

Step 3: 运行脚本

# 确保 Trae CN 正在运行(需要 ai_agent.dll 加载)
# 然后运行扫描
python scan_memory.py

如果一切顺利,你会看到:

[+] Database: C:/Users/xxx/AppData/Roaming/Trae CN/ModularData/ai-agent/database.db
[+] Salt: 669579095da5204507fcebbb736a2940
[+] Size: 420MB
[+] Found ai_agent.dll in PID 15972 (489MB)
[+] Process PID=15972: 851MB in 3426 regions

  [FOUND] Key verified!
    enc_key=3605f6691095a993f03d5009c918352ef5be31ae31e8f000212b81ff058da773
    Address: 0x000001BF0A625230

============================================================
Scan complete: 0.2s
Found: 1 verified keys
[+] Key saved to decrypted_key.json

Step 4: 读取数据库

# 使用 SQLCipher CLI
sqlcipher database.db
PRAGMA key = "x'你的密钥'";
SELECT count(*) FROM sqlite_master;
-- 输出: 139 (表示有 139 张表)

或使用 Python:

import sqlite3
db = sqlite3.connect('database.db')
db.execute("PRAGMA key = 'x你的密钥'")
tables = db.execute("SELECT name FROM sqlite_master WHERE type='table'").fetchall()
print(f"Found {len(tables)} tables")

关键数据表

表名 内容
history_v2 完整聊天内容(JSON 格式,15914 条)
chat_session 会话列表(238 个)
agent Agent 定义(含 system prompt)
agent_run Agent 运行记录
task 任务记录

导出对话记录

import sqlite3, json
db = sqlite3.connect('database.db')
db.execute("PRAGMA key = 'x你的密钥'")

history = db.execute("SELECT messages FROM history_v2").fetchall()
for h in history:
    msgs = json.loads(h[0])['raw_messages']
    for m in msgs:
        text = ' '.join([p['text'] for p in m['content'] if p['type']=='text'])
        print(f"[{m['role']}] {text[:100]}")

Part 2: 我们的来时路

起点

用户需要解析 Trae CN 的 database.db 文件。这是一个 SQLCipher 4 加密的数据库,存储了所有 AI Agent 的对话记录、任务执行、项目配置等数据。

听起来很简单:找到密钥,解密数据库。

我开始尝试。


第一条路:Frida Hook

我的第一个想法是用 Frida hook sqlite3_key 函数。这个函数在数据库打开时被调用,会传递密钥。如果我能 hook 它,就能捕获密钥。

这是一个很自然的思路。SQLCipher 是开源的,sqlite3_key 的函数签名是公开的。用 Frida hook 一个函数,捕获它的参数,这是动态分析的标准操作。

我安装了 Frida,写了 hook 脚本,启动了 Trae CN,等待 ai_agent.dll 加载…

然后我发现了一个问题:sqlite3_key 没有被导出。它是 SQLCipher 的内部函数,静态链接在 ai_agent.dll 中。

没有导出符号,就无法直接 hook。

我愣了一下。

这个 DLL 是 160MB 的 Rust 二进制文件,包含了完整的 SQLCipher 实现。它不是通过标准 DLL 导出表暴露函数的——它是 Rust 编译器把所有代码都打包进了一个巨大的二进制文件。

这意味着:标准的动态分析方法,在这里全部失效。

第二条路:字符串交叉引用

既然函数没有导出,那我可以找它的字符串引用。

SQLCipher 在打印调试信息时会使用 “sqlite3_key: db=%p” 这样的格式字符串。如果我能找到这个字符串在内存中的位置,就能反向追踪到函数地址。

这是一个经典的逆向工程思路。我用 Frida 扫描了整个 DLL,找到了这个字符串。

然后我尝试用 LEA 指令的模式匹配来找引用这个字符串的代码…

找不到。

我扫了 80MB 的 .text 段,匹配了数千条 LEA 指令,没有一条指向这个字符串。

这一刻,我开始怀疑:Rust 编译器到底用了什么方式来引用字符串?是内联了?还是用了某种我没想到的寻址方式?

在 160MB 的二进制文件中搜索特定的指令模式,就像在大海里捞一根针。而我甚至不知道这根针长什么样。

第三条路:内存扫描

我放弃了找函数地址,转向直接扫描进程内存。如果密钥在内存中,我应该能直接找到它。

这是一个更直接的思路。我用 Frida 扫描了 387,456 个 32 字节块,寻找高熵数据。

结果:0 个候选。

我愣住了。

387,456 个块,没有一个符合 AES-256 密钥的特征。这怎么可能?密钥明明在内存中——数据库正在被使用,密钥一定在某个地方。

我开始意识到:密钥可能不是以连续的 32 字节块形式存储的。它可能被拆分、加密、或者以某种格式编码后存储。

这个发现让我感到沮丧。因为这意味着:即使我知道密钥就在内存中,我也无法用简单的模式匹配找到它。

第四条路:尝试所有密钥候选

我开始怀疑:密钥可能是从某个已知值派生的。如果是这样,我应该能通过分析派生算法来找到它。

我从各种可能的来源生成了 67 种密钥候选:

  • telemetry_id(64 字符 hex 字符串)
  • machine_id(UUID 格式)
  • 各种 hash 组合(SHA256、SHA512、HMAC)
  • 环境变量组合

全部 HMAC 校验失败。

67 个候选,没有一个是对的。

我停住了。

这意味着:密钥不是简单地从某个已知值派生的。它是一个随机生成的值,只存在于进程内存中。

这个发现让我意识到:我需要换一种思路。与其试图"推导"密钥,不如直接"找到"密钥。

第五条路:反汇编分析

我开始考虑一个更激进的方法:直接分析 DLL 的二进制代码,找到密钥生成逻辑。

我用 Capstone 反汇编器分析了 DLL 的代码,寻找密钥生成函数。

在 160MB 的二进制文件中,这就像试图阅读一本没有目录的百科全书。我找到了数千个函数,但不知道哪个是密钥生成函数,也不知道它的输入和输出在哪里。

更糟糕的是,Rust 编译器会大量使用内联和优化,让函数边界变得模糊。我花了几个小时分析代码,最终放弃了。

这个尝试让我意识到:对于这种规模的二进制文件,静态分析几乎是不可能的。除非你有符号信息,否则你就是在黑暗中摸索。

第六条路:Frida Stalker

我尝试了最后一种"专业"的方法:用 Frida 的 Stalker 功能跟踪执行流,希望在数据库操作时捕获密钥。

这是一个理论上完美的方案。Stalker 可以记录每一条指令的执行,让我看到程序到底在做什么。

但 Stalker 对于这种大型进程来说太慢了。160MB 的 DLL,63 个线程,每秒数百万条指令。Stalker 试图记录这一切,结果是:CPU 占用飙升,进程几乎卡死,而数据库操作可能在我们监控之外发生。

我盯着监控窗口,等待着那个永远不会到来的调用。

这一刻,我意识到:被动监控无法捕获只发生一次的关键操作。密钥只在数据库打开时被使用一次,之后就可能被清除或移动。

转折点

在我几乎要放弃的时候,用户发来了一条消息:

“看看这个项目对你有没有灵感帮助,别灰心,我们重头再来”

附带了一个 GitHub 链接:wechat-decrypt。

我点开了这个项目。

这个项目做的事情和我完全一样:从微信进程内存中提取 SQLCipher 密钥。但他们的方法完全不同。

我打开了他们的代码。

然后我看到了一行简单的正则表达式:

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

我愣住了。

这不是什么高深的技术。这只是在内存中搜索一个 64-192 字符的 hex 字符串。但这就是关键。

微信(和 Trae CN 一样)使用 raw key 模式。密钥被存储为 hex 字符串,格式是 x'<64hex_enc_key><32hex_salt>'。这个字符串在内存中是连续的、可搜索的。

我之前扫描内存时,寻找的是 32 字节的二进制块。但密钥实际上是 64 字符的 hex 字符串——这完全不同。

用户的一句话,改变了整个方向。

新的方法

wechat-decrypt 的核心洞察是:

不要试图 hook 函数。直接扫描内存。

他们用 Windows API(不是 Frida)直接读取进程内存,用正则表达式搜索 x'([0-9a-fA-F]{64,192})' 这样的模式。

为什么这能工作?

因为微信(和 Trae CN 一样)使用 raw key 模式。密钥被存储为 hex 字符串,格式是 x'<64hex_enc_key><32hex_salt>'。这个字符串在内存中是连续的、可搜索的。

更关键的是,他们用 HMAC-SHA512 验证来确认找到的密钥是正确的。这样就不需要知道密钥是怎么生成的,只需要验证它是否能解密数据库。

这个方法简单得几乎荒谬。没有 Frida,没有 hook,没有反汇编。只是:读内存 → 搜索字符串 → 验证。

但就是这个简单的方法,解决了我 12 小时都解决不了的问题。

0.2 秒

我用 wechat-decrypt 的方法重写了扫描脚本。

运行。

0.2 秒后,密钥被找到了。

[FOUND] Key verified!
  enc_key=3605f6691095a993f03d5009c918352ef5be31ae31e8f000212b81ff058da773

我盯着屏幕,一动不动。

0.2 秒。

12 小时的努力,换来了 0.2 秒的结果。

这不是技术问题。这是认知问题。我一直在用"复杂"的方法解决一个"简单"的问题。

我花了大量时间试图 hook 函数、分析二进制、生成密钥候选… 这些都是"正确"的方法,但它们都是错的。

正确的方法就在那里:读内存,搜索字符串,验证。三步。20 行代码。

我突然想起一句话:「简单是终极的复杂。」

为什么这个方法有效

  1. 直接读内存:用 Windows API 读取进程内存,不需要 Frida 这种侵入式工具
  2. 正则搜索:搜索 hex 字符串比扫描二进制块更高效
  3. HMAC 验证:不需要知道密钥是怎么生成的,只需要验证它是否正确
  4. 简单可靠:20 行核心代码,0.2 秒完成

12 小时 vs 20 行代码

我花了 12 小时尝试各种"复杂"的方法:

  • Frida hook(函数未导出)
  • 字符串交叉引用(找不到引用)
  • 内存块扫描(密钥不是连续块)
  • 密钥候选生成(67 种全部失败)
  • 反汇编分析(160MB 二进制文件)
  • Stalker 跟踪(太慢)

最后,20 行代码,0.2 秒。

尾声

我保存了所有导出的数据:238 个会话、15,914 条历史记录、1,689 次 Agent 运行。

但我心里想的不是这些数据。

我想的是:如果我一开始就搜索 “SQLCipher key extraction”,而不是直接上 Frida,我能节省多少时间?

这个问题没有答案。但我记住了这个教训:

在尝试复杂的方法之前,先看看有没有简单的解决方案。

有时候,答案不是在你的工具箱里,而是在别人的代码库里。


总结

解密步骤

  1. 运行 scan_memory.py 提取密钥(0.2 秒)
  2. 用 SQLCipher CLI 或 Python 读取数据库
  3. 从 history_v2 表导出对话记录

数据库表结构

解密后发现数据库包含 39 张表,核心数据:

核心对话表

表名 行数 内容说明
history_v2 15,914 完整聊天内容(JSON格式,包含用户消息和AI回复)
chat_session 238 会话列表(含标题、类型、创建时间)
chat_message 3,048 消息元数据(关联会话和消息)
chat_turn 1,527 对话轮次(含agent类型、状态、引用)

Agent 相关表

表名 行数 内容说明
agent 17 Agent定义(含system prompt、描述、工具列表)
agent_run 1,689 Agent运行记录(含session、状态、token使用)
agent_member_relation 28 Agent成员关系

任务/项目表

表名 行数 内容说明
task 1,424 任务记录(含session、状态、时间)
project 23 项目信息(含路径、创建时间)
todo_list 1,739 待办事项(JSON格式,含任务列表)
history_todo_list 1,739 历史待办记录

配置/缓存表

表名 行数 内容说明
server_history_info 32,692 服务器历史信息(含完整消息、工作区)
session_project 238 会话-项目关联
model_config_cache 42 模型配置缓存
mcp_server_agent_relation 58 MCP服务器-Agent关联
rules_attachment 744 规则附件
user_configuration 2 用户配置
multi_root_path 15 多根路径配置

重要发现

  1. history_v2 是最重要的表,包含完整对话内容(JSON格式)
  2. agent 表存储了所有自定义Agent的system prompt
  3. todo_list 包含JSON格式的待办事项列表
  4. server_history_info 有32,692行,包含服务器端的完整消息记录

关键文件

  • scan_memory.py - 密钥扫描脚本
  • decrypted_key.json - 提取的密钥
  • database.db - 加密的数据库

参考项目


如果你也遇到了类似的问题,希望这篇文章能帮你节省 12 小时。

如果这篇文章让你想起了某个类似的「简单却困难」的问题,那它就没有白写。

4 个赞

2 个赞

666,永远不要低估直球的魅力
我之前做的一个项目,要生产环境的数据库密码,走了一周申请,进机房还得交手机,结果出来发现他们数据库密码居然跟测试环境一模一样的。。。

3 个赞

这个还挺有趣的,最后一个密钥结束

2 个赞

数据拿出来是要跑路了吗?

2 个赞

已经跑了, 不过是要导入进claude-mem里面的, 导入到claude插件里面统一管理

2 个赞

很强,赞一个

3 个赞

Hello 你好哇,关于你提到的这个逆向解密方式,首先夸赞一波你的技术钻研精神。其次我们也为大家解释一下:

  1. 这种解密方式不会对其他用户有影响,如果大家使用这个方式也只是看到自己的本地数据

  2. TRAE 对数据加密,本意是为了避免大家无意间打开修改破坏了结构,导致程序运行失败,是一种防呆机制。

其他同学看到也不用担心哈~ :smiling_face_with_three_hearts:

2 个赞