QQBackup / qq-win-db-key

全平台 QQ 聊天数据库解密
Other
539 stars 60 forks source link

Feature request: 读取新版QQNT数据库 #38

Open ygjsz opened 1 year ago

ygjsz commented 1 year ago

现在腾讯在推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/ntqq{一串ID}/nt_db 目测数据库格式和其他平台是一样的 image

Young-Lord commented 1 year ago

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

ygjsz commented 1 year ago

是的 大佬加油!

Young-Lord commented 1 year ago

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

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

Young-Lord commented 1 year ago

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

Young-Lord commented 1 year ago

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

Young-Lord commented 1 year ago

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

ygjsz commented 1 year ago

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

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

Young-Lord commented 1 year ago

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

Young-Lord commented 1 year ago

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

alphagocc commented 1 year ago

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

Young-Lord commented 1 year ago

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

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

alphagocc commented 1 year ago
image

🥳🥳🥳 方法

hook nt_sqlite3_key_v2 拿 key

删掉 nt_msg.db 前 1024 个字符

打开的时候 pbkdf iter=4000

alphagocc commented 1 year ago

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

alphagocc commented 1 year ago

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

Young-Lord commented 1 year ago
image

🥳🥳🥳 方法

hook nt_sqlite3_key_v2 拿 key

删掉 nt_msg.db 前 1024 个字符

打开的时候 pbkdf iter=4000

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

alphagocc commented 1 year ago

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

是的,开源的那个就可以

Blob我明天再看看

PR 的话

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

Young-Lord commented 1 year ago

PR 的话

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

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

alphagocc commented 1 year ago

那我试着写个教程

ygjsz commented 1 year ago

更新 已经寄了 8.9.78+上vmp了

yllhwa commented 1 year ago

更新 已经寄了 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);
Young-Lord commented 1 year ago

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

yllhwa commented 1 year ago

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

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

Young-Lord commented 1 year ago

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

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

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

yllhwa commented 1 year ago

呼,搞了一晚上终于搞好了。 之前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);
Young-Lord commented 1 year ago

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

主要就是几个点:

Young-Lord commented 1 year ago

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

Young-Lord commented 1 year ago

以及,丢个安装包直链: https://downv6.qq.com/qqweb/QQ_1/android_apk/Android_8.9.78_64.apk 来源

Young-Lord commented 1 year ago

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

yllhwa commented 1 year ago

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

主要就是几个点:

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

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

另外剩余的工作可能就是

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

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

yllhwa commented 1 year ago

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

没问题,麻烦整理下(

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

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

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

Young-Lord commented 1 year ago

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

Young-Lord commented 1 year ago

更新 已经寄了 8.9.78+上vmp了

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

Young-Lord commented 1 year ago

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

happyme531 commented 1 year ago

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

Ljzd-PRO commented 11 months ago

iOS Frida hook 得到了密钥,不过似乎还是无法解密,长度32位,只有字母和数字。

Young-Lord commented 11 months ago

iOS Frida hook 得到了密钥,不过似乎还是无法解密,长度32位,只有字母和数字。

看看这个? https://github.com/QQBackup/qq-win-db-key/blob/master/android_dump.js 以及怎么跨年还在折腾这个

Ljzd-PRO commented 11 months ago

iOS Frida hook 得到了密钥,不过似乎还是无法解密,长度32位,只有字母和数字。

看看这个? https://github.com/QQBackup/qq-win-db-key/blob/master/android_dump.js ~以及怎么跨年还在折腾这个~

😂 不过已经解决了,在iOS上 HMAC 算法需要设置为 HMAC_SHA1

Young-Lord commented 11 months ago

😂 不过已经解决了,在iOS上 HMAC 算法需要设置为 HMAC_SHA1

和这一部分配置一样吗?

https://github.com/QQBackup/qq-win-db-key/blob/master/nt%20qq%20android%20db%20%E6%95%99%E7%A8%8B.md#%E4%BD%BF%E7%94%A8-sqlitestudio-%E6%89%93%E5%BC%80%E6%95%B0%E6%8D%AE%E5%BA%93%E6%9C%AA%E6%B5%8B%E8%AF%95%E5%8F%AF%E8%83%BD%E8%BF%87%E6%97%B6

Ljzd-PRO commented 11 months ago

😂 不过已经解决了,在iOS上 HMAC 算法需要设置为 HMAC_SHA1

和这一部分配置一样吗?

https://github.com/QQBackup/qq-win-db-key/blob/master/nt%20qq%20android%20db%20%E6%95%99%E7%A8%8B.md#%E4%BD%BF%E7%94%A8-sqlitestudio-%E6%89%93%E5%BC%80%E6%95%B0%E6%8D%AE%E5%BA%93%E6%9C%AA%E6%B5%8B%E8%AF%95%E5%8F%AF%E8%83%BD%E8%BF%87%E6%97%B6

确实是一样的

yllhwa commented 9 months ago

快过年了没事儿干又来折腾了😂

import sqlite3
import blackboxprotobuf

conn = sqlite3.connect('plaintext.db')
c = conn.cursor()
print ("数据库打开成功")

def get_message_from_single(message):
    # print(message)
    if isinstance(message, list):
        return [get_message_from_single(m) for m in message]
    try:
        message_id = message.get("45001")
        message_type = message.get("45002")
        if message_type == 1:
            # 普通文本消息
            message_content = message.get("45101").decode("utf-8")
        elif message_type == 2:
            # 图片消息
            local_name = message.get("45402")  # ?
            if message.get("45804"):
                picture_url = "https://c2cpicdw.qpic.cn"+ message.get("45804").decode("utf-8") # 45802, 45803, 45804 区别?(可能是清晰度)
            else:
                picture_url = ""
            message_content = f"[图片消息 {picture_url}]"
        elif message_type == 3:
            # 文件消息
            file_name = message.get("45402")
            message_content = f"[文件消息 {file_name}]"
        elif message_type == 6:
            # 表情消息
            message_content = "[表情消息]" # TODO
        elif message_type == 10:
            # 应用消息
            # message_content = message.get("47901")
            message_content = "[应用消息]"
        else:
            message_content = "[未知消息类型]"
        if message_content == "[未知消息类型]":
            # print(message)
            pass
        if message_content == None:
            message_content = ""
        return message_content
    except Exception as e:
        print(e)
        return ""

def get_message_from_raw(raw_message):
    (messages, typedef) = blackboxprotobuf.decode_message(raw_message)
    if not isinstance(messages, list):
        messages = [messages]
    results = []
    for message in messages:
        message = message.get("40800")
        results.append(get_message_from_single(message))
    return results

cursor = c.execute("SELECT * from c2c_msg_table")
for row in cursor:
    data = row[17]
    print(get_message_from_raw(data))

conn.close()

现在的效果: image

我感觉数据库里面那些奇怪的字段名和protobuf里面的id有关。 估计只能手搓protobuf定义了。

yllhwa commented 9 months ago

先手搓了一份

syntax = "proto3";

message Message { repeated SingleMessage messages = 40800; }

message SingleMessage {
  uint64 messageId = 45001;
  uint32 messageType = 45002;
  // 1:文字,2:图片,3:文件,6:表情,7:回复,
  // 8:提示消息(中间灰色),10:应用消息
  // 21:电话
  // 26:动态消息

  // 回复消息
  string senderId = 40020;
  string receiverId = 40021;

  // 文字消息
  string messageText = 45101;

  // 文件消息
  string fileName = 45402;
  uint64 fileSize = 45405;

  uint64 sendTimestampFile = 45505; // ?

  // 图片消息
  string imageUrlLow = 45802;
  string imageUrlHigh = 45803;
  string imageUrlOrigin = 45804;
  string imageText = 45815;

  uint32 senderUid = 47403;
  uint32 sendTimestamp = 47404;
  uint32 receiverUid = 47411;
  SingleMessage replyMessage = 47423;

  // 表情消息
  // 1: QQ 系统表情,2: emoji 表情
  // https://bot.q.qq.com/wiki/develop/api/openapi/emoji/model.html
  uint32 emojiId = 47601;
  string emojiText = 47602;

  // 应用消息
  string applicationMessage = 47901;

  // 语音消息
  string callStatusText = 48153;
  string callText = 48157;

  // 动态消息
  FeedMessage feedTitle = 48175;
  FeedMessage feedContent = 48176;

  string feedUrl = 48180;
  string feedLogoUrl = 48181;
  uint32 feedPublisherUid = 48182;

  string feedJumpInfo = 48183;
  string feedPublisherId = 48188;

  // 提示消息
  string noticeInfo = 48214;
  string noticeInfo2 = 48271; // ?
}

message FeedMessage { string text = 48178; }
Horbin-Magician commented 9 months ago

大佬们,我想问一下到目前有可行的方案吗?我的qq版本目前是9.0.17.15190

yllhwa commented 9 months ago

大佬们,我想问一下到目前有可行的方案吗?我的qq版本目前是9.0.17.15190

目前的方案是安卓端有root就可以,见教程

Horbin-Magician commented 9 months ago

大佬们,我想问一下到目前有可行的方案吗?我的qq版本目前是9.0.17.15190

目前的方案是安卓端有root就可以,见教程

好难T.T

lavmaharjan commented 7 months ago

I'm currently trying to open the encrypted nt_msg.db of QQNT (Windows) in DB Browser with SQLCipher with the passphrase obtained as mentioned in the [QQ NT Windows database decryption + image/file cleanup] (https://github.com/Mythologyli/qq-nt-db) and QQBackup/qq-win-db-key but i m not able to do so. I'm not sure where it is going wrong.

Ljzd-PRO commented 7 months ago

I'm currently trying to open the encrypted nt_msg.db of QQNT (Windows) in DB Browser with SQLCipher with the passphrase obtained as mentioned in the [QQ NT Windows database decryption + image/file cleanup] (https://github.com/Mythologyli/qq-nt-db) and QQBackup/qq-win-db-key but i m not able to do so. I'm not sure where it is going wrong.

Make sure KDF iterations set to 4000, page size set to 4096, HMAC algorithm set to HMAC_SHA1 and KDF algorithm set to PBKDF2_HMAC_SHA512

lavmaharjan commented 7 months ago

I'm currently trying to open the encrypted nt_msg.db of QQNT (Windows) in DB Browser with SQLCipher with the passphrase obtained as mentioned in the [QQ NT Windows database decryption + image/file cleanup] (https://github.com/Mythologyli/qq-nt-db) and QQBackup/qq-win-db-key but i m not able to do so. I'm not sure where it is going wrong.

Make sure KDF iterations set to 4000, page size set to 4096, HMAC algorithm set to HMAC_SHA1 and KDF algorithm set to PBKDF2_HMAC_SHA512

Even though I have kept similar settings as you have mentioned, I m not able to open the database (nt_msg.db). image

bczhc commented 7 months ago

Please show the error message. Also make sure the original file has 1024 bytes skipped.

For CLI decryption, if the passphrase is correct, this should work:

cp ~/.config/QQ/nt_qq_6bb87db59dd2e7d303966b6fc81dc8dd/nt_db/nt_msg.db .
cat nt_msg.db | tail -c +1025 > db
sqlcipher db "pragma key = 'XXXXXXXXXXXXXXXX'; pragma kdf_iter = 4000" .d | tail +2 | sqlite3 decrypted-db
lavmaharjan commented 7 months ago

Please show the error message. Also make sure the original file has 1024 bytes skipped.

For CLI decryption, if the passphrase is correct, this should work:

cp ~/.config/QQ/nt_qq_6bb87db59dd2e7d303966b6fc81dc8dd/nt_db/nt_msg.db .
cat nt_msg.db | tail -c +1025 > db
sqlcipher db "pragma key = 'XXXXXXXXXXXXXXXX'; pragma kdf_iter = 4000" .d | tail +2 | sqlite3 decrypted-db

The error message : sqlite> .open nt_msg.clean.db sqlite> pragma key = "################"; pragma kdf_iter = "4000"; ok sqlite> attach database "nt_msg.db" as plaintext key ""; Select sqlcipher_export("plaintext"); Detach database plaintext; Parse error: file is not a database (26) sqlite> I m not sure the key extracted from the method is correct.