QQBackup / QQ-History-Backup

【停更】QQ/TIM 聊天记录导出为 HTML,支持图片、语音,可 GUI 与 非 GUI 操作 (Python)

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Feature request: 读取新版QQNT数据库

ygjsz opened this issue · comments

commented

现在腾讯在推NT架构的QQ,要搞全平台统一,然后手机版的NT架构的QQ也开始内测了。
最新的手机版内测QQNT上的数据库架构已经大改了,变得和Windows/Mac/Linux版QQNT一样了。
版本:8.9.58.11050
如图 老版本的数据库仍然存在,但是很明显聊天记录已经不存放在老库里面了
image
S30425-20053693

现在新QQNT聊天记录数据库的位置是/databases/nt_db
S30425-20055692
S30425-20054788

从文件名来看这个数据库架构和电脑版QQNT是一样的
image
(Windows版QQNT数据库)
目前还没研究出新数据库密钥存放的位置以及新数据库的格式
image
(从文件头来看是SQLite3?)

不知道有没有希望搞定新版的数据库解密

另:手机版QQNT内测包下载链接:https://downv6.qq.com/qqweb/QQ_1/android_apk/qq_8.9.58.11050_64.apk
(就算没有内测资格也能用,在弹出内测活动已结束的窗口的时候按两下返回就可以把那个窗口关掉)
(不建议用大号测试,这个内测QQNT一旦登录之后就会把所有的老库里的聊天记录全都迁移进新库,无法撤销)

另2:MacQQNT的数据库位置:/Users/{用户名}/Library/Containers/com.tencent.qq/Data/Library/Application Support/QQ/nt_qq_{一串ID}/nt_db
目测数据库格式和其他平台是一样的
image

Electron那个?有计划,有时间可能去试一试
运气好的话跟msg3.0.db一样的加密方式就会很轻松

commented

是的 大佬加油!

目前是定位到了相关代码在libkernel.so里,也是用的和Msg3.0.db一个思路的 sqlite3 加密。就是具体的算法可能有改动
周末试着动态调试一下吧
IDA中libkernel.so的字符串列表

(具体怎么找的:find . | xargs grep -Hn nt_msg.db {} 2>/dev/null

然后Windows版貌似是在wrapper.node里,是编译为 PE 可执行文件格式
由进程标题为QQ的进程以dll方式加载,用了sqlcipher,理论上 hook nt_sqlite3CodecAttach即可

那应该是比较简单的,用Frida hook一下就可以了
(如果没改sqlcipher算法的话)

commented

试试? https://github.com/Young-Lord/qq-win-db-key/blob/master/android.md

Screenshot_20230514-163557
我这不太行 frida貌似跑不起来的样子
(SELinux已经是permissive了 有空我换个设备再试一下

我这边貌似也不太行……只要注入 Frida 就会段错误/拒绝注入,想必是有反调一类的

成了。
以及,如果你跑不起来也可以再试试,看起来这个只是在QQ未运行时不工作而已
(不过也挺奇怪的,之前好像是看到有几处地方相关代码被inline了,但也看不出逻辑)
成功时的终端输出

似乎 Windows 上 key 长度为 16(?
为啥感觉不是很对的样子

似乎 Windows 上 key 长度为 16(? 为啥感觉不是很对的样子

我在这边没有什么进展,要是有什么进度可以给我发发

image

🥳🥳🥳
方法

hook nt_sqlite3_key_v2 拿 key

删掉 nt_msg.db 前 1024 个字符

打开的时候 pbkdf iter=4000

顺带一提,windows 上 key 长就是 16

但是我不知道 BLOB 中的内容如何解码

image 🥳🥳🥳 方法

hook nt_sqlite3_key_v2 拿 key

删掉 nt_msg.db 前 1024 个字符

打开的时候 pbkdf iter=4000

“打开”是用社区版sqlcipher就可以了吗?看起来我后面这两步都没有做XD
方便的话可以提个pr,感谢
以及,blob的话可以考虑下Protobuf,盲猜也是写在native层

“打开”是用社区版sqlcipher就可以了吗?

是的,开源的那个就可以

Blob我明天再看看

PR 的话

好像这个函数在不同版本的 wrapper.node 里二进制不太一样,比较难写成脚本。。。。

PR 的话

好像这个函数在不同版本的 wrapper.node 里二进制不太一样,比较难写成脚本

有什么特征汇编指令序列一类的吗?再不济用户手动输入函数地址都没问题,毕竟这个repo本身就不是为了提供全套解决方案的
就算是丢个 教程.md 也很有用!

那我试着写个教程

更新 已经寄了
8.9.78+上vmp了

commented

更新 已经寄了 8.9.78+上vmp了

8.9.78+包括8.9.78.12275吗?我这儿显示是最新版本了,但是能hook到呢,只是地址变了。
就是hook出来密钥还是没法解密,这方面还需要研究。

setTimeout(function () {
  let base_addr = Module.findBaseAddress("libkernel.so");
  console.log("libkernel.so base address: " + base_addr);
  // nt_sqlite3_key_v2, sub_1CCE4DC
  let nt_sqlite3_key_v2_addr = base_addr.add(0x1cce4dc);
  console.log("nt_sqlite3_key_v2_addr: " + nt_sqlite3_key_v2_addr);
  Interceptor.attach(nt_sqlite3_key_v2_addr, {
    onEnter: function (args) {
      console.log("pKey: " + Memory.readCString(args[2]));
      console.log("nKey: " + args[3].toInt32());
    },
  });
}, 1200);

解密的话你看下那个仓库里的android.md,我没试过。
看看有没有什么特征字节?(也就是single_function的参数)
或者我再拉出来我之前写的一些小工具找offset吧…
@yllhwa

commented

解密的话你看下那个仓库里的android.md,我没试过。 @yllhwa

我测试是不行的,我怀疑那是Windows的解密方式
看起来qq用的是4.5.1版本的sqlcipher,我打算bindiff下看看是不是哪儿有魔改的地方

解密的话你看下那个仓库里的android.md,我没试过。 @yllhwa

我测试是不行的,我怀疑那是Windows的解密方式

那我最近也试一试,不行的话去加个“可能不可靠”的标志

commented

呼,搞了一晚上终于搞好了。
之前attach会出现问题应该是奇怪的权限问题引起的,将导出地址设置为公共目录即可。
以下代码对应的安卓qqnt版本为8.9.78.12275。
不保证不会对聊天记录产生影响(

// frida -U -f com.tencent.mobileqq -l final.js
const DATABASE_URI =  "/data/user/0/com.tencent.mobileqq/databases/nt_db/nt_qq_{CHNAGE_THIS_TO_YOURS}/nt_msg.db";

// FOR LOG
let SQLITE3_EXEC_CALLBACK_LOG = true;
let index1 = 0;
let xCallback = new NativeCallback(
  (para, nColumn, colValue, colName) => {
    if (!SQLITE3_EXEC_CALLBACK_LOG) {
      return 0;
    }
    console.log();
    console.log(
      "------------------------" + index1++ + "------------------------"
    );
    for (let index = 0; index < nColumn; index++) {
      let c_name = colName
        .add(index * 8)
        .readPointer()
        .readUtf8String();
      let c_value = "";
      try {
        c_value =
          colValue
            .add(index * 8)
            .readPointer()
            .readUtf8String() ?? "";
      } catch {}
      console.log(c_name, "\t", c_value);
    }
    return 0;
  },
  "int",
  ["pointer", "int", "pointer", "pointer"]
);

// CODE BELOW
let get_filename_from_sqlite3_handle = function (sqlite3_db) {
  // full of magic number
  let zFilename = "";
  try {
    let db_pointer = sqlite3_db.add(0x8 * 5).readPointer();
    let pBt = db_pointer.add(0x8).readPointer();
    let pBt2 = pBt.add(0x8).readPointer();
    let pPager = pBt2.add(0x0).readPointer();
    zFilename = pPager.add(208).readPointer().readCString();
  } catch (e) {}
  return zFilename;
};

setTimeout(function () {
  let base_addr = Module.findBaseAddress("libkernel.so");
  console.log("libkernel.so base address: " + base_addr);

  // sqlite3_exec -> sub_1CFB9C0
  let sqlite3_exec_addr = base_addr.add(0x1cfb9c0);
  console.log("sqlite3_exec_addr: " + sqlite3_exec_addr);

  let sqlite3_exec = new NativeFunction(sqlite3_exec_addr, "int", [
    "pointer",
    "pointer",
    "pointer",
    "int",
    "int",
  ]);

  let target_db_handle = null;
  let js_sqlite3_exec = function (sql) {
    if (target_db_handle == null) {
      return -1;
    }
    let sql_pointer = Memory.allocUtf8String(sql);
    return sqlite3_exec(target_db_handle, sql_pointer, xCallback, 0, 0);
  };

  // ATTACH BELOW
  Interceptor.attach(sqlite3_exec_addr, {
    onEnter: function (args) {
      // sqlite3*,const char*,sqlite3_callback,void*,char**
      let sqlite3_db = ptr(args[0]);
      let sql = Memory.readCString(args[1]);
      let callback_addr = ptr(args[2]);
      let callback_arg = ptr(args[3]);
      let errmsg = ptr(args[4]);
      let databasae_name = get_filename_from_sqlite3_handle(sqlite3_db);
      if (databasae_name == DATABASE_URI) {
        console.log("sqlite3_db: " + sqlite3_db);
        console.log("sql: " + sql);
        target_db_handle = sqlite3_db;
      }
    },
  });
  setTimeout(function () {
    let ret = js_sqlite3_exec(
      `ATTACH DATABASE '/storage/emulated/0/Download/plaintext.db' AS plaintext KEY '';SELECT sqlcipher_export('plaintext');DETACH DATABASE plaintext;`
    );
    console.log("js_sqlite3_exec ret: " + ret);
  }, 4000);
}, 1200);

完美。
运行时的日志输出,包含了 SQLCipher 参数
发个pr?我稍微修改一下后 merge 了

主要就是几个点:

  • ===/!== 而非 ==/!=
  • databasae_name 那里直接indexOf一下基本就行了,个人认为没必要用完整路径
  • hook libkernel 的时候可以去 hook dlopen,而非等待常数时间(ref,我也不太记得这个能不能用)

下一步的话可以看看insert是怎么实现的/能不能hook到 大概

以及,各位有兴趣进一下这个组织吗
https://github.com/QQBackup

commented

发个pr?我稍微修改一下后 merge 了

主要就是几个点:

  • ===/!== 而非 ==/!=
  • databasae_name 那里直接indexOf一下基本就行了,个人认为没必要用完整路径
  • hook libkernel 的时候可以去 hook dlopen,而非等待常数时间(ref,我也不太记得这个能不能用)

issue发在另一个仓库了QQBackup/qq-win-db-key#12
熬夜赶工,有些地方可能比较hacky(

另外剩余的工作可能就是

  1. 跨版本适配(有必要吗?)
  2. 对解密后的数据库进行解析,因为字段名和编码方式都不太明确,不清楚Windows和Mac上解密出来是否类似。不过好在没有旧版QQ奇怪的混淆了。

issue发在另一个仓库了QQBackup/qq-win-db-key#12 熬夜赶工,有些地方可能比较hacky(

主要是标记成contributor,虽然你不在意的话也没问题

  1. 跨版本适配(有必要吗?)

只要他不把那个log的字符串删掉,直接搜adrp和ldr/add命令的机器码应该就行,具体我有时间再看一下
(以及,我感觉定位offset这一部分可以从脚本里抽离出来,拿Python之类的写)

  1. 对解密后的数据库进行解析,因为字段名和编码方式都不太明确,不清楚Windows和Mac上解密出来是否类似。不过好在没有旧版QQ奇怪的混淆了。

确实,所以能多hook几个sqlite3_prepare之类的可能会有点用

关于prepare等: https://zhuanlan.zhihu.com/p/583446952

commented

主要是标记成contributor,虽然你不在意的话也没问题

没问题,麻烦整理下(

确实,所以能多hook几个sqlite3_prepare之类的可能会有点用

原来sqlite3_prepare走的和sqlite3_exec不是一条路啊,我惯性思维觉得prepare底层调exec了,难怪觉得少了很多调用。

不过prepare里面的字段名称还是[40055],[40010],[40027]这样的无意义数字,感觉是上层进行了某种映射

关于跨版本适配,貌似FF4302D1FD7B03A9FC6F04A9FA6705A9F85F06A9F65707A9F44F08A9FDC3009154D03BD5881640F9F80304AAF50303AAF60302AA这个sig是没变的,直接用大概就行?

更新 已经寄了 8.9.78+上vmp了

以及,想问下你是从何看出有vmp的?在你那边有造成什么具体影响吗?

猜对了。hook prepare + bind可以得到insert一类的具体sql语句
sqlite3_prepare_v2也很好找,sqlite3_exec函数里直接就有调用(前文中这个版本 8.9.78.12275 是0x1d2da74)
(顺带一提,是 exec 调 prepare & step )
hook prpare 得到的输出,比 exec 多了 insert into

NT QQ的(目前无法解析的)聊天记录能否使用QQ自带的聊天记录迁移功能迁移到到非NT QQ?