Skip to content

QQNT Frida 采集方案(module 101 · PDH 加密 IM 采集)

把"手动 frida 解密 QQNT 数据库"自动化进 App,让个人助手一句话即可采集 QQ 聊天数据。 状态:设计(待实现)。归属 module 101 个人数据 IDE 桥接的 L4 root 采集层。

0. 背景与定位(先厘清,避免重复造轮子)

QQ 在设备上有 两套 数据库,是两个完全不同的采集问题:

数据库格式路径现状
旧版 <uin>.db(如 896075341.db明文 SQLite,msgData BLOB 用 IMEI XOR/data/data/com.tencent.mobileqq/databases/<uin>.db已有采集器 QQLocalCollector/QQDbExtractor/QQXorDecryptor,但未接入 PDH bridge
新版 QQNT nt_msg.dbSQLCipher 加密,消息体是 protobuf…/databases/nt_db/nt_qq_<md5>/nt_msg.db❌ 无采集器;用户当前主力数据在这里;手动 frida 已能解密

"之前采集成功过" = 你用 scripts/android/pdh-frida-decrypt.sh(Method C)手动把 nt_msg.db 在线导出成 QQ_android_nt_msg_decrypted.db。本方案就是把这一步搬进 App

关键结论:QQNT 走 frida(SQLCipher),旧版不需要 frida(明文 + IMEI XOR)。 本方案聚焦 QQNT;旧版作为零成本的兜底一并接入 bridge。

1. 目标与非目标

目标

  • 个人助手一句话「采集我的 QQ 聊天」→ 解密 nt_msg.db → 解析 c2c/群消息 → 入 vault → 可查询/分析。
  • 可复现(用户首要诉求):frida 二进制 + 导出脚本随 APK 下发;流程确定性、可重跑;密钥永不落盘。
  • 复用现成基建(WeChat frida 注入器 / Method C 在线导出 / collector→vault 管线),不重写。

非目标

  • 不做 QQ 发消息/写操作(只读取证)。
  • 不绕过 QQ 反调试做对抗升级(首版遇 anti-frida 直接如实降级报告,不硬刚)。
  • 不碰云端 QQ 空间(服务端数据,非本地可取)。

2. 端到端数据流

个人助手 chat
  └─(mcp__pdh__collect_qqnt 或 collect_app_data_root{app:"qq"})
      └─ QQNTFridaCollector.snapshot()              [新增, Kotlin]
          1. 前置闸:root? + pidof com.tencent.mobileqq 在前台?
          2. FridaSqlcipherExporter.export()         [新增, 复用 WeChatFridaInjector 骨架]
               - su cp frida-inject-arm64 + qqnt-sqlcipher-export.js → /data/local/tmp
               - su frida-inject -p <pid> -s qqnt-sqlcipher-export.js --runtime=v8
               - hook: 借 QQ 自身已 keyed 连接 → ATTACH '' KEY '' + sqlcipher_export
               - 产出明文副本 /data/local/tmp/dec/nt_msg.plain.db
          3. su cp 明文副本 → app filesDir,chown app uid,chmod 644,删 /data/local/tmp/dec
          4. 解析 nt_msg.plain.db(QQNT schema + protobuf 消息体)→ staging JSON
          5. LocalCcRunner.syncAdapter("messaging-qq", stagingJson)  → 入 vault
          6. finally:删明文副本 + 删 staging(密钥/明文不留存)

复用矩阵:

步骤复用现成文件
frida 注入骨架WeChatFridaInjector(su stage→frida-inject spawn→stdout 解析→cleanup + 测试 seam suExec/spawnProcess/pidofImplpdh/social/wechat/WeChatFridaInjector.kt
在线导出 hookMethod C agent(ATTACH '' KEY '' + sqlcipher_export,库无关,已对 WCDB/WCDB2 验证)scripts/android/pdh-frida-sqlcipher-export.js
frida 二进制APK 内 assets/frida/frida-inject-arm64(+arm)已随 APK
collector→vaultLocalCcRunner.syncAdapter(adapter, inputPath)pdh/LocalCcRunner.kt
适配器messaging-qq(扩展支持 QQNT schema 输入)packages/personal-data-hub/lib/adapters/messaging-qq/
root cohort copy / 凭据QQCredentialsStore(存 uin;QQNT 不需 IMEI)pdh/messaging/qq/QQCredentialsStore.kt

2.5 ⚠️ 真机实测结论(2026-06-21,chopin)— Method C 在 QQNT 上不通

实测 frida-inject 注入 QQ(无反 frida 拦截,attach 成功),但 Method C 的导出 始终为空。逐层定位:

  1. QQNT 的 nt_msg.dblibwcdb.so(Tencent WCDB,自带一份 SQLCipher)。
  2. Module.findExportByName(null, 'sqlite3_*') 命中的是系统 libsqlite.so,不是 WCDB 的。改为从 libwcdb.so 解析后,sqlite3_key/sqlite3_prepare* 都 hook 上了 (mods:["libwcdb.so",null])。
  3. 但 hook 一次都没 fire(诊断 dbfile 0 条):即便重启 QQ + 新鲜打开「消息」, 也没有任何 nt_msg 文件流经被 hook 的导出符号。结论 = WCDB 内部用静态链接的 sqlite 内部函数(sqlite3Prepare 等),不经过导出的 sqlite3_prepare_v2/sqlite3_key, 所以 Method C(借导出符号的 keyed 连接)对 QQNT 行不通。
  4. 多进程坑:nt_msg.db 由主进程 com.tencent.mobileqq(非 :MSF)持有;QQ 启动时 re-fork 主进程,pid 会变 → attach 要盯准持库的那个 pid。

正解(用户确认的方法)= PC 工具:root 把加密的 nt_msg.db 拉到 PC,用 PC 端 QQNT 解密工具(已知 WCDB cipher 参数 / 从内存取 key)离线解密 → 得明文 nt_msg.db。 本仓已验证:明文库 → 解析 → messaging-qq 入 vault 全通(见 §8 实测)。

若坚持端侧自动化,两条路(均比 Method C 重,列为 Phase 2+):

  • spawn-gate:用 frida-server + frida -f com.tencent.mobileqq(spawn 挂起→装 hook→ resume),在 WCDB 设 key 的第一时刻截获(本机缺 PC 端 frida-python,未跑通)。
  • WCDB C++ key hook:hook WCDB::...setCipherKey / 派生函数拿 raw key,再用 WCDB 的 cipher 参数离线解(离线 better-sqlite3 标准 SQLCipher 参数实测不通,须复刻 WCDB 参数)。

2.6 ✅ 真机突破 + ⚠️ MIUI 限制(2026-06-21,chopin)— 派生 key 路线打通

正解 = 派生 key(非 frida),仓库早有 pdh-qq-android-decrypt.mjskey = MD5(MD5(uid)+rand)(rand 读库头、uid 从 QQ 数据 strings|grep u_ 暴破), 标准 SQLCipher(PBKDF2-HMAC-SHA512/AES-256-CBC)。纯 Node,无 frida、无手填 key。

已抽进 cc bundlepackages/personal-data-hub/lib/forensics/qq-nt-collect.js

  • cc hub collect-qq(解密+protobuf 解析+入库)。真机实测(chopin)cc hub collect-qq 在设备上跑通 → 442 条 QQ 消息入设备金库、cc hub search 可查

on-device bridge 工具 collect_qq_nativeQQNTNativeCollector su 取库+uid → cc hub collect-qq)已建并接入 bridge。

⚠️ MIUI/HyperOS 限制(关键)collect_qq_native 在本机采不到,根因 = App 进程的 su(即便 u:r:magisk:s0 root)读不了别的 app 数据目录。逐一验证全失败: 直接 ls /data/data/com.tencent.mobileqq = Permission denied;magiskpolicy --live 放开 SELinux 后 denied(→ 不是 SELinux,是 MIUI 内核级跨应用隔离); su --context / su -mm(mount master) / nsenter -t 1 -m(读不到 /proc/1/ns) / /proc/<qqpid>/fd(pidof 都返空) 全部被拦而 adb shell→su 能读(不同来源 context 不受此 LSM 约束)——这正是 cc hub collect-qq 验证时 staging 的路径。

结论

  • 解密+入库 = 全程手机端、已验证(cc bundle 里跑,442 条)。
  • 取加密库这一步:MIUI 拦 App-进程 root 跨应用读 → collect_qq_native 全自动在 MIUI 上不通非 MIUI 的 stock-Android root 机大概率可(限制是 MIUI 特有,未在 stock 机验证)。
  • MIUI 可用路径 = 混合:adb/PC(或配对桌面经 adb)把加密 nt_msg.db staging 到 app 可读处 → 设备上 cc hub collect-qq 解密入库(已验证)。
  • 代码无 bug;这是 MIUI 设备级限制。

2.7 ✅ MIUI 全自动正解 = Magisk 守护进程(2026-06-22,chopin 验证,460 条)

MIUI 拦的是 App 进程的 su非 App 的 root(adb-shell→su / init 上下文的守护进程) 不受此拦——这就是破法。落地 = Magisk 模块跑一个 root 守护进程替 App 做跨应用读。

已落地 + 验证android-app/magisk-module-pdh-qqd/,打包 pdh-qqd-magisk-v1.0.zip):

个人助手 →(bridge) collect_qq_native (QQNTNativeCollector)
   1. App 写 <cache>/qqd/request                         (App 无需 su)
   2. pdh-qqd.sh(root 守护进程,init 上下文,非 App)轮询到 request:
        - find QQ nt_msg.db → cp 到 <cache>/qqd/nt_msg.enc.db(chown App, 600)
        - **targeted uid 扫描**(files/mmkv + databases,~48s;self uid 在
          files/mmkv/qq_uin_uid_map,**别扫 GB 级 media cache**)→ uids.txt(chown App)
        - touch done / error
   3. App 等到 done → cc hub collect-qq --db …enc.db --uids uids.txt --self <qq>
        → 派生 key 解密 + protobuf 解析 + 入设备金库(纯 Node,bundle 内)
   4. App 抹掉暂存的密文 + uid

实测:守护进程暂存 + 6438 uid → cc hub collect-qq460 条 QQ 消息入设备金库, 全程手机端、无 PC/USBDaemonUnavailable(没装模块) → assist_required,非 root 机不崩 (模块装不上、App 优雅降级)。

复现 runbook(本机/同款 MIUI root 机)

A. 装一次(持久):① Magisk 刷 pdh-qqd-magisk-v1.0.zip → 重启,守护进程自启; ② cc bundle 要带 cc hub collect-qq(走发版链发 pdh+cli,或 dev-stage 见下);③ 装 App APK。 之后个人助手一句话「采集我的 QQ」即可全自动

B. dev-stage 临时验证(不发版,调试用):

bash
B=/data/data/<pkg>/files/usr/lib/node_modules/chainlesschain
PDH=$B/node_modules/@chainlesschain/personal-data-hub
adb push packages/cli/src/commands/hub.js /data/local/tmp/_h && su cp $B/src/commands/hub.js(chown app,644)
adb push packages/personal-data-hub/lib/forensics/qq-nt-collect.js $PDH/lib/forensics/(chown app,644)
# pdh package.json 加 exports "./forensics/qq-nt-collect"
# 起守护进程:su setsid sh /data/local/tmp/pdh-qqd.sh &

坑(复现必看)

  • MSYS 路径adb push/shell/data/.. 会被 Git-Bash 改写 → 全程 MSYS_NO_PATHCONV=1,local 用 C:/..
  • cc bundle 反复 wedge:node/mksh symlink 指向 APK native-lib 路径,重装/系统重优化后 APK hash 变 → symlink 失效 → cc: No such file。修=删 usr/.bootstrap_version + 重启 App 进采集屏触发重解压(会冲掉 dev-stage,要重 dev-stage)。
  • better-sqlite3:bundle 里 native binding 不在标准 bindings 路径 → collect-qq 用 vault 已加载的 Database 构造器hub.vault.db.constructor),别 require('better-sqlite3')
  • os.tmpdir() = /data/local/tmp(App 写不了,EACCES)→ 明文临时库写在输入库同目录(app cache)。
  • App-su Magisk 授权collect_qq_native 走守护进程后 App 不再需要 su;但若要 App 自身 su,需 magisk --sqlite "REPLACE INTO policies (uid,policy,until,logging,notification) VALUES(<uid>,2,0,0,0)"(policy 2=allow)——且在 MIUI 上即便授权也读不了跨应用数据(故才要守护进程)。
  • uid 扫描别全盘:self uid 在 files/mmkv/qq_uin_uid_map;全盘 strings 慢到 timeout 出 0 条。只扫 files/mmkv databases

3. Frida 在线解密(Method C 为主,库无关)— ⚠️ 见 §2.5:对 QQNT WCDB 不通,保留作通用 SQLCipher app 模板

为什么用 Method C(sqlcipher_export)而不是抓 key 离线解密:

  • QQNT 用自研/WCDB 派生 cipher;离线 better-sqlite3 用抓到的 key 不一定能开(cipher 参数不标准)。memory 实测「frida 截 key 后离线 72 组合全败」。
  • Method C 借 App 自己已经 keyed 的连接,在进程内 ATTACH '' KEY ''(空 key=明文目标)+ SELECT sqlcipher_export('plain'),把整库导成明文 —— 绕开 cipher 参数问题。这正是产出你 Desktop 上 *_decrypted.db 的方法。

agent 脚本assets/frida/qqnt-sqlcipher-export.js,从 pdh-frida-sqlcipher-export.js 改):

  • hook sqlite3_key/sqlite3_key_v2/sqlite3_prepare_v2/v3
  • DB_MATCH 正则改为匹配 nt_msg\.db(兼带 group_info.db/profile_info.db 一并导出,你 Desktop 已有这三件套)。
  • 命中 keyed 连接后对每个目标库执行一次 export(DONE map 去重,INEXEC 防递归)。
  • 输出 /data/local/tmp/dec/<name>.plain.db;脚本自身退出前不删(Kotlin 侧搬运后删)。
  • 触发条件:QQ 必须前台进过「消息」列表/某会话,IM 插件 .so 已加载且查询过库(否则 keyed 连接不出现)→ collector 前置提示用户「请打开 QQ 消息页后重试」。

反调试风险:QQ 近年带 libmsaoaidsec(与抖音同源风控)。首版策略:

  1. 先直接 attach(多数机型/版本可成)。
  2. 失败(InjectFailed/秒退)→ 如实返回 AntiFridaSuspected,提示「QQ 反调试拦截,建议旧版 <uin>.db 路径或手动 Method C」。
  3. 不在首版做 spawn-gating/early-instrument 对抗(留 Phase 2 评估)。

4. QQNT schema 解析(nt_msg.db)

以你 Desktop 的 QQ_android_nt_msg_decrypted.db / group_info / profile_infofixtures(+ QQ_关系分析.md 你已做的关系分析)。要点:

  • 主表:c2c_msg_table(单聊)、group_msg_table(群聊);典型列 40050(time)/40011(type)/40020(peer uid)/40030(sender)/消息体 BLOB。
  • 消息体是 protobuf(QQNT 富消息):纯文本在 elem 链里。首版启发式抽取可读文本(UTF-8 段 + 去控制字节),Phase 2 上最小 protobuf 解析(按 elem type 取 text/pic/at)。
  • 联系人/群名:profile_info.db / group_info.db(昵称、群名)→ 用于把数字 uid/peer 映射成可读名(对齐 wechat-pc 群名解析的经验)。
  • 时间:QQNT 用秒级 epoch;统一转毫秒。

落 staging JSON 用 messaging-qq 既有 schema v1(contacts/groups/messages),让 messaging-qq 适配器零改或最小改即可 ingest;新增 source.variant:"qqnt" 区分旧版。

5. 落地分期

Phase 0 — 接通旧版(半天,零 frida 风险,先有数据)

  • 把现成 QQLocalCollector 接入 PDH bridge:新增 CollectQqTool(或并入 collect_app_data_root{app:"qq"} 的 QQ 分支),走 <uin>.db + IMEI XOR。
  • 价值:立刻让 chat 能采到旧版 QQ;验证 collector→vault 管线对 QQ 通。

Phase 1 — QQNT frida 导出 MVP(核心)

  • FridaSqlcipherExporter.kt(抽 WeChatFridaInjector 通用骨架;agent=qqnt-sqlcipher-export.js)。
  • QQNTFridaCollector.kt:前置闸 → export → cohort copy → 解析(启发式文本)→ syncAdapter。
  • bridge 工具 collect_qqnt(L4, requiresRoot);wire 进 PdhBridgeModule
  • 单测:用 Desktop fixtures 跑解析器(纯 JVM,零设备);注入器用 WeChatFridaInjector 同款 seam 做无设备单测。

Phase 2 — 富化 + 健壮

  • protobuf 最小解析(text/at/pic 占位);uid→名映射(profile/group_info)。
  • anti-frida 探测与降级文案;WAL checkpoint 一致性(cp 三件套 .db/-wal/-shm)。
  • 增量(按 lastSyncAt 水位只取新消息)。

Phase 3 — 体验 + 复现

  • chat 信任卡:预览「将采集 QQ 聊天 N 条入金库」→ 用户确认(§3.5.9 预览卡)。
  • 引导卡:未前台打开 QQ 消息页时提示一步(§3.6)。
  • 文档化复现步骤 + fixtures 入 docs/internal/reference(schema 字典)。

6. 安全 / 隐私(北极星:数据主权回个人)

  • 明文副本与 staging 仅落 app 私有目录,ingest 完 finally 即删;绝不入 git、绝不出设备(对齐 memory:明文只存仓库外 ~/pdh-data)。
  • frida agent JS 含个人数据访问逻辑,用完即删 /data/local/tmp 下的脚本与 dec/
  • 密钥永不写盘(Method C 不导出 key,只导明文表;即便 Phase 2 抓 key 也只在内存用)。
  • root + 前台 QQ 是硬前置;非 root / QQ 未登录 → 如实 NoRoot/AppNotRunning,不假装成功。
  • 入 vault 的 QQ 消息按 §3.5.11 标「来源=QQ·设备本地·非 AI 判断」,可见可纠可删(cc hub destroy)。

7. 风险与未知(诚实清单)

风险影响缓解
QQ libmsaoaidsec 反调试attach 秒退首版如实降级;Phase 2 评估 spawn-gating
QQNT cipher 跨版本漂移export hook 命中点变Method C 库无关 + 多符号 hook;版本探测日志
protobuf 消息体复杂文本抽取不全Phase 1 启发式先用;Phase 2 最小 proto
keyed 连接未出现导出空前置引导「打开 QQ 消息页」;轮询/超时如实报
真机机型差异仅本机验证以 chopin(已验) 为基线;端点 best-effort 标注

8. 验收

  • 设备:chopin(rooted)QQ 前台登录态。
  • collect_qqnt → 解密 nt_msg.db → vault 新增 QQ 消息事件 > 0 → analysis.overviewmessaging-qq(qqnt) → chat 可查「我和某人的 QQ 聊天」。
  • 解析器单测对 Desktop fixtures 全绿(无设备 CI 可跑)。
  • 明文/staging/agent 脚本跑后全部清理(ls 验空)。

一句话:QQNT = SQLCipher,复用 WeChat frida 注入器 + Method C 在线 sqlcipher_export(产出你 Desktop 那批 *_decrypted.db 的同款方法)把 nt_msg.db 导明文,解析 protobuf 消息入 vault;旧版 <uin>.db 顺手接入 bridge 作零风险兜底。

基于 MIT 许可发布