📚 离职知识库

QQ NT 数据库解密方法(技术方法摘编)

本篇只保留逆向取 key 与 SQLCipher 参数探索的技术方法,不含任何明文 key、数据库头部 hex、salt、本地路径或聊天/联系人信息。 目标场景:解密 macOS 版 QQ NT 的本地 nt_msg.db,本地自用。 状态记录:取 key 一关(最难的逆向部分)已完全攻克并可复现;解密卡在这一版客户端(6.9.80)用了非标准 SQLCipher 配置,标准参数与穷举都失败——留作后续攻关方向。

#一、取 key:在 nt_sqlite3_key_v2 断点抓

  • QQ NT 用 WCDB(SQLCipher 变体)。开库时会调用一个入口函数 nt_sqlite3_key_v2(db, "main", key, keylen),签名与标准 sqlite3_key_v2 一致。
  • 在该函数入口下 lldb 断点,抓第 3 个参数(key 指针),keylen=16。连续 40+ 次调用抓到的值完全一致,x1 恒为 "main",说明 key 原样透传给底层 SQLCipher、没做任何变换,所有库(含 nt_msg.db)共用同一把 key。
  • 触发时机:登录/打开主界面不一定触发开库;点开一个具体聊天会话、滚动历史消息能稳定触发 nt_sqlite3_key_v2

#二、正确定位函数入口 VA(关键坑)

上游工具 qq-win-db-key 自带的 find_key_func.py 在这一版上定位错误,返回的是假地址,据此下的断点永远打不中。两个 bug:

  1. 只匹配字符串地址的低 12 位 → 在上百 MB 的二进制里撞上大量假的 ADD 指令;
  2. fat binary(通用二进制)里 x86_64 切片在前,data.find 会先命中 x86 那份字符串,导致 VA 算成负数。

修正做法(自写的 find_key_func2.py):

  • 完整解析 adrp + add 指令配对,精确还原字符串的完整虚拟地址(VA);
  • LC_FUNCTION_STARTS(Mach-O 的函数起始表)反查字符串引用点所属的函数入口;
  • 通过两条诊断字符串(形如 nt_sqlite3_key_v2: db=no key)交叉确认目标函数。

定位到真入口 VA 后,写进 lldb 自动脚本,断点会自动吸附到函数入口,点开会话即可打印 key。

#三、解密卡点与后续攻关方向

拿着确认无误的 key,用所有已知 SQLCipher 参数都解不开这一版的 nt_msg.db。已排除/尝试过:

  • 页结构已确认:文件剥掉前 1024 字节头后,剩余长度正好是 页数 × 4096,所以「头长 1024、page_size=4096」是对的。头部有明文标识 SQLite header 3 + QQ_NT DB + 版本号 + HMAC_SHA1( 字样。
  • sqlcipher CLI 全组合穷举失败kdf_iter ∈ {1,2,4000,4096,64000,256000,…}hmac ∈ {SHA1,SHA256,SHA512}kdf ∈ {PBKDF2_HMAC_SHA1/256/512}page ∈ {4096,1024}cipher_compatibility 1~4cipher_use_hmac=OFFcipher_plaintext_header_size=1024(不剥头)——全部报 file is not a database
  • 纯 Python AES 逐页解穷举失败:绕过 HMAC 直接逐页 AES 解、看明文特征;穷举 iter(1 到 256000 多档)、raw key 直接当 AES-128/256、以及 md5/sha1/sha256(key) 派生——结果零字节数全在随机基线附近,没有任何组合解出结构化明文。
  • 反汇编确认 nt_sqlite3_key_v2 只是把 key 原样透传给更深一层的 wrapper,wrapper 里无变换。

最靠谱的后续破法(别再盲猜 KDF 参数)

  1. 在更深处断点抓「最终派生出的 32 字节 AES key + SQLCipher 实际用的 salt」:跟进 nt_sqlite3_key_v2 → 底层 wrapper → SQLCipher 的 sqlcipher_codec_key_derive / sqlite3Codec 附近,断点直接读派生后的 raw key 和 salt,拿去 AES-CBC 解,跳过一切 KDF 猜测。
  2. 或对 wrapper 里 PBKDF2 / EVP_* / sqlcipher 相关字符串做 xref(复用上面 adrp+add 的定位法),定位 codec 实现。
  3. 从 QQ 内存直接捞明文页:运行时 SQLite page cache 里有已解密的页,dump 出来能直接读(但只有近期访问过的)。
  4. 兜底:等上游 qq-win-db-key / QQBackup 更新,或实测其全自动流程(可能也栽在同样的 codec 差异上)。

#四、解密成功后的正文表结构(字段号 schema)

消息表:c2c_msg_table(私聊)/ group_msg_table(群)。字段号是纯数字:

字段号 含义
40001 msgID
40020 发送者 UID
40030 对方号或群号
40033 发送者号
40050 时间(Unix 秒)
40090 群名片
40013 方向(0 收 / 1,2 发)
40800 正文(protobuf,文本在 tag 45101

判断发送方:40033 == 自己的号

#五、合规红线

key、解密库、导出记录全部只留本地,绝不上传/部署;机器上的真实聊天数据不外发。仅限用户导出自己的记录、本地自用。

来源:沉淀/04-逆向与数据导出/QQ解密-交接文档.md(整理于 2026-08-18)