QQ NT 聊天记录批量导出与数据库解密 (Exporting QQ NT Chats by Decrypting the Local Database)
1. 先分清两类数据源
导 QQ 记录前,第一件事是判断手上的东西属于哪一类。走错路会白忙。
| 数据源 | 形态 | 能否离线解密 |
|---|---|---|
| 桌面上导出的备份 | <QQ号>的<N>个聊天记录.bak | 不能。库头部的 hex 是 key_meta(兑换凭证),真密钥要登录时向服务器换 |
| 本机 QQ 数据目录 | nt_msg.db / group_info.db / profile_info.db | 能。密钥就在已登录的 QQ 进程内存里 |
判断依据:解压 .bak 后看 db/* 文件头部偏移 32 的魔数——
MSG0= 备份库(备份时单独生成的密钥,账号密钥也开不了)QQ_NT DB= 本机在线库
两者 key_meta 必然不同。只要 .bak 和本机是同一个账号,本机 nt_msg.db 里通常已有同样的聊天内容,直接解本机库比导入 .bak 快得多。
.bak 结构(本质是不压缩的 ZIP):
db/<32位hex> # 4 个 SQLCipher 库:消息本体 + group/profile 信息
files/<N>/<40位hex> # 加密的媒体 blob,离线无解要拿到 .bak 里的内容,唯一低成本路径是官方导入:QQ 登录对应账号 → 导入聊天记录 → 消息合并进本机 nt_msg.db → 再走下面流程。
2. 本机数据在哪
%USERPROFILE%\Documents\Tencent Files\<QQ号>\nt_qq\nt_db\
nt_msg.db # 消息本体(c2c_msg_table / group_msg_table)
group_info.db # 群列表、群成员、群昵称
profile_info.db # 好友资料
%USERPROFILE%\Documents\Tencent Files\<QQ号>\nt_qq\nt_data\
Pic\<年-月>\Ori\<md5> # 图片原图
Video\<年-月>\Ori\<md5>.mp4 # 视频
Ptt\<年-月>\Ori\<md5>.amr # 语音
File\Ori\<uuid>\<原文件名> # 聊天里的文件3. 抓数据库密钥(Windows)
工具:QQBackup/qq-win-db-key → scripts/windows/ntqq/windows_ntqq_get_key.ps1
# 先静态验证工具能否识别当前 QQ(不动 QQ,安全)
powershell -ExecutionPolicy Bypass -File .\windows_ntqq_get_key.ps1 -NoDebugForKey
# 抓密钥(会重启 QQ)
powershell -ExecutionPolicy Bypass -File .\windows_ntqq_get_key.ps1原理:脚本自动定位 wrapper.node(版本目录 versions\<ver>\resources\app\wrapper.node),静态分析出 nt_sqlite3_key_v2 字符串的引用位置,然后 CreateProcessW + DEBUG_ONLY_THIS_PROCESS 拉起 QQ,在 nt_sqlite3_key_v2 下 int3 断点,登录时读出 16 字符密钥,随即 TerminateProcess。
要点:
- 必须先关掉正在跑的 QQ,否则新实例起不来(QQ 单实例)。
- 密钥是 16 个可打印 ASCII(如
qE[qsJAQK67X]EN4);hook 到 20 长度的 key 属于login.db,是误报,脚本已过滤。 - 只抓第一把,抓到就杀进程。多账号登录时抓到的可能不是目标号——用目标库做一次解密验证即可确认。
- 脚本顶部有 UTF-8 自动重载逻辑,PowerShell 5.1 下会跑两遍:内层抓到密钥后
exit $LASTEXITCODE抛异常被外层 catch,外层继续执行会再拉起一次 QQ。看到密钥后要把后台任务杀掉。
4. 解密(SQLCipher)
坑:每个库文件前面有 1024 字节自定义头,必须剥掉才是真正的 SQLCipher 库。
with open(src,'rb') as f:
f.read(1024) # 丢掉头部
data = f.read()
open(clear,'wb').write(data)import sqlcipher3
con = sqlcipher3.connect(clear)
cur = con.cursor()
cur.execute("PRAGMA cipher_page_size = 4096;") # 必须在 key 之前
cur.execute(f"PRAGMA key = '{KEY}';")
cur.execute("PRAGMA kdf_iter = 4000;") # 以下必须在 key 之后
cur.execute("PRAGMA cipher_hmac_algorithm = HMAC_SHA1;")
cur.execute("PRAGMA cipher_kdf_algorithm = PBKDF2_HMAC_SHA512;")
cur.execute("SELECT count(*) FROM sqlite_master;") # 能过就是密钥对了PRAGMA 顺序是硬约束,顺序错会报 file is not a database。同一把 key 可开同一账号的所有库(每个库盐不同,PBKDF2 派生结果自然不同)。
导出成普通 SQLite:
cur.execute(f"ATTACH DATABASE '{out}' AS p KEY '';")
cur.execute("SELECT sqlcipher_export('p');")
cur.execute("DETACH DATABASE p;")安装:pip install sqlcipher3。sqlcipher3 有 cp311 的 win_amd64 轮子,直接装;sqlcipher3-binary 只有 manylinux,Windows 上装不了,别在这上面浪费时间。
5. 消息表结构(列名是数字,SQL 里必须加引号)
c2c_msg_table(私聊)/ group_msg_table(群聊):
| 列 | 含义 |
|---|---|
40001 | 消息 id |
40003 | 序号(同一时间戳排序用) |
40011 | 消息类型(1=blank 空占位, 2=text, 3=file, 5=image, 6=voice, 7=video, 8=system, 9=reply, 10=card, 11=emoji, 16=xml) |
40020 | 发送者 uid(u_xxx) |
40021 | 会话 uid / 群号字符串 |
40027 | 群号(群消息按这个分组) |
40030 | 对端 QQ 号(私聊按这个分组)/ 群号 |
40033 | 发送者 QQ 号 |
40050 | 时间戳(秒) |
40090 | 发送者群昵称 |
40093 | 发送者备注/昵称 |
40800 | 消息体 protobuf |
消息体 40800 是嵌套 protobuf,元素字段:
| 字段 | 含义 |
|---|---|
45002 | 元素类型(同 40011) |
45101 | 文本 |
45402 | 文件名 |
45405 | 大小 |
45503 | 文件元素的 <md5>_<uuid>,取 _ 后段即 File/Ori/<uuid> 目录名 |
45923 | 语音转文字 |
空占位行:40011=1、40033=0、40050=0、body 为空的行是垃圾行(真实案例里占 9%),导出时要滤掉。
6. 媒体文件映射
数据库里不存本地路径,只存 md5 / uuid,要靠命名规则去 nt_data 里找:
| 类型 | 库里给的 | 磁盘位置 |
|---|---|---|
| 图片 | file_name = <MD5大写>.<ext> | Pic/<年-月>/Ori/<md5小写>(无扩展名) |
| 视频 | <md5>.mp4 | Video/<年-月>/Ori/<md5>.mp4 |
| 语音 | file_path 有时直接给全路径 | Ptt/<年-月>/Ori/<md5>.amr |
| 文件 | 45503 的 uuid 段 | File/Ori/<uuid>/<原文件名> |
拇指图在 Thumb/ 下,命名 <md5>_750.jpg。原图缺失时可退回 Thumb。
7. 验证方法
- 解密后先试
SELECT count(*) FROM sqlite_master;,能返回数字就是密钥对了。 - 导出后用普通
sqlite3打开明文库,SELECT name FROM sqlite_master看表名。 - 导出完做一次引用一致性检查:txt 里引用的附件名集合 == 附件目录里的文件名集合(缺失 0、多余 0)。
8. 常见坑
| 症状 | 原因 |
|---|---|
file is not a database | PRAGMA 顺序错;或密钥不对;或没剥 1024 字节头 |
解压 .bak 后库打不开 | MSG0 头 = 备份专用密钥,离线无解,必须官方导入 |
脚本报 UnicodeEncodeError(如 \u2062) | 中文 Windows 控制台是 GBK。设 PYTHONIOENCODING=utf-8 和 PYTHONUTF8=1 |
Python 写的 /tmp 找不到 | Python 的 /tmp = C:\tmp,和 Git Bash 的 /tmp(%LOCALAPPDATA%\Temp)不是一回事 |
附件名变成 X.jpg.jpg | 生成名时把 file_name 的扩展名和源文件后缀都拼上了 |
| 用 importlib 加载脚本报 dataclass 错 | 要先 sys.modules[name] = module 再 exec_module |
导出全是 [无法解码] | 空占位行(40011=1),按第 5 节滤掉 |
9. 边界与风险
- 脚本要调试器注入 QQ 进程,仓库作者明言可能破坏数据或导致封号。密钥等于账号所有聊天库的钥匙,勿外传。
- 抓密钥会杀掉一次 QQ。抓完记得把 QQ 重新打开。
- 解出来的明文库、媒体文件含全部隐私与好友关系链,本地处理,不要上传任何云或仓库。
- Windows 抓的密钥开不了同账号 macOS / 手机的库,密钥按设备独立。
- 只用于导出自己的聊天记录。
10. 现成的参考实现
| 项目 | 用途 |
|---|---|
QQBackup/qq-win-db-key | Windows 抓密钥 |
QQBackup/QQDecrypt | 分平台教程与数据库解析文档 |
QQBackup/nt_msg_db_util | nt_msg.db 剥头、解密、导出 |
fnyfree/qqnt-chat-decrypt | .bak 格式分析(docs/bak-format.md 写得最清楚) |
zywstudy123/qq-bak-chat-history-export | protobuf 解码 + txt/json/csv 导出,可直接复用其 decode_message_blob |
相关:QQ 群聊风格蒸馏与调侃人格提示词 (Distilling a Group-Chat Persona from NapCat Logs)、NapCat 多实例部署与 AstrBot 入群门槛审核 (NapCat Multi-Instance & AstrBot Join Moderation)
本文归档于个人知识库 · 领域:CS & AI · 模块:04 Project_CS_AI/AI apps