Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

新版本 QQNT (Android) 中函数位置改变 #29

Open
Young-Lord opened this issue Jul 14, 2024 · 30 comments
Open

新版本 QQNT (Android) 中函数位置改变 #29

Young-Lord opened this issue Jul 14, 2024 · 30 comments

Comments

@Young-Lord
Copy link
Member

Young-Lord commented Jul 14, 2024

丢进libbasic_share里了,同时把函数的符号加了回来
需要适配一下(具体来说要在libkernel中找不到函数时尝试在libbasic_share中找,并且优先使用findExportByName来利用符号信息)

@yllhwa
Copy link
Member

yllhwa commented Jul 15, 2024

正好说说这次改版之后我一些新的分析。

我勾住libbasic_share中导出的函数后打印调用栈,简单跟踪了下密钥产生的方式,我发现在产生密钥的过程中读取了数据库文件的头部,且头部的一部分也是采用protobuf编码。

image

解出来内容为:

{"2": "6t***IGP", "3": "1.0.0.1", "4": "HMAC_SHA1"}

(这里有个奇怪的点是id2开始,1跑哪儿去了?)

我怀疑数据库的密钥很可能是从这一部分派生出来的。

经过我的观察,在相当长的时间内这个数据库的头部没有发生变化,第一个加密块的内容也没有发生变化,是否可以认为数据库的密钥是一个固定值而非之前设想的每次登录下发?(由于我没有记录之前的密钥,没有验证过密钥是否发生变化)

不过由于C++ class乱飞,我到现在也没分析出来怎么从这一串字符产生密钥。可以按照关键词PsKey去查找。

这次分出libbasic_share还有个好处是可以尝试在pc上把这个库load起来尝试对数据库文件进行解密,可以让没有root的用户使用上。

@Young-Lord
Copy link
Member Author

Young-Lord commented Jul 15, 2024

我现在也在试着hook计算密钥那一部分,就是IDA debugger跑不起来比较麻烦,正在上gdb;理论上直接对着那堆指针硬件断点就能干掉(
密钥确实在退出、重登的时候是定值
9.0.75的arm64 lib里,func_01734438的第一个参数应该存着解密所需所有内容

@Young-Lord
Copy link
Member Author

Young-Lord commented Jul 15, 2024

但我现在被反调试卡住了 貌似是过长时间停顿就不行,现在好了

@Young-Lord
Copy link
Member Author

这次分出libbasic_share还有个好处是可以尝试在pc上把这个库load起来尝试对数据库文件进行解密,可以让没有root的用户使用上。

关于这个,只要tx还在用SQLCipher的话,模拟libbasic_share貌似没什么意义;重点是在libkernel里生成密钥的算法吧?

@Young-Lord
Copy link
Member Author

有进展了

function readStdString (str) {
  const isTiny = (str.readU8() & 1) === 0;
  if (isTiny) {
    return str.add(1).readUtf8String();
  }

  return str.add(2 * Process.pointerSize).readPointer().readUtf8String();
}
var result_addr;
const ggg = {
            onEnter: function(args) {
      console.log('CCCryptorCreate called from:\\n' +
        Thread.backtrace(this.context)
        .map(DebugSymbol.fromAddress).join('\\n') + '\\n');
                //console.log(args[0], args[1], args[2]);
                //result_addr = args[1]
                // console.log("¦- *zDb: " + args[0].readCString());
            },
            onLeave: function(ret) {console.log(ret, readStdString(ret))}
        }
        // Interceptor.attach(Module.findBaseAddress('libkernel.so').add(0x1b394e0), ggg);
        
        //Interceptor.attach(Module.findExportByName('libbasic_share.so', '_ZN4xpng8SHA1HashEPKhm'), ggg);
        //Interceptor.attach(Module.findExportByName('libbasic_share.so', 'SHA256_Update'), ggg);
        // Interceptor.attach(Module.findExportByName('libbasic_share.so', '_ZN4xpng9MD5UpdateEPA88_cPKhm'), ggg);
      //  Interceptor.attach(Module.findExportByName('libbasic_share.so', '_ZN4xpng8MD5FinalEPNS_9MD5DigestEPA88_c'), ggg);
        Interceptor.attach(Module.findExportByName('libbasic_share.so', '_ZN4xpng17MD5DigestToBase16ERKNS_9MD5DigestE'), ggg);
        //Interceptor.attach(Module.findExportByName('libxplatform.so', '_ZN2xp3md55CRC32EjPKhi'), ggg);

每次点击登录都会调用这个没被注释的函数,使用md5生成密钥
(至于为什么会想到这个,直觉)

@Young-Lord
Copy link
Member Author

Young-Lord commented Jul 15, 2024

成了!

const upd = {
            onEnter: function(args) {
                console.log('update param ->', args[1].readCString(args[2].toInt32()));
            }
        }
const ggg = {
            onLeave: function(ret) {console.log('MD5:', readStdString(ret))}
        }
        Interceptor.attach(Module.findExportByName('libbasic_share.so', '_ZN4xpng9MD5UpdateEPA88_cPKhm'), upd);
        Interceptor.attach(Module.findExportByName('libbasic_share.so', '_ZN4xpng17MD5DigestToBase16ERKNS_9MD5DigestE'), ggg);

SQLCipher key 在每次启动应用或点击登录时生成
key 生成算法为key = md5(md5(uid) + rand),其中:
uid形如u_ajU-qj81A1nL2DDWKA13zp,在/data/user/0/com.tencent.mobileqq/databases/beacon_db_com.tencent.mobileqq等多处均可找到
rand形如6tPaJ9GP,就是加密数据库文件头中的的那串可读内容,上图中有
md5(uid)形如6436ec5ce0a0b5e56c91c83d3776f8edkey形如2eb042b70c66e73bedb90af5781cd870
解密算法HMAC_SHA1,也在加密数据库文件头中
数据库路径形如/data/user/0/com.tencent.mobileqq/databases/nt_db/nt_qq_0c6a806134796a80e0b5317e1743e5cd/nt_msg.db,其中0c6a806134796a80e0b5317e1743e5cd记为路径hash,则路径hash = md5(md5(uid) + 'nt_kernel')

@yllhwa 你有时间更新一下吗 鉴于我还要享受一下所剩无几的暑假(

能跑起来的完整 hook 脚本: android_qq_hook_md5_20240715.py.zip

@yllhwa
Copy link
Member

yllhwa commented Jul 15, 2024

牛哇,终于把这算法给搞出来了。
说起来我之前也觉得32位肯定是md5或者hmac,没想到直接去找这个库里面的md5函数。
我最近每天下午在学车,晚上回去写一写。

@yllhwa
Copy link
Member

yllhwa commented Jul 15, 2024

考虑到root用户本来就可以通过hook key函数获得密钥,也许我们应该将面向无root用户介绍这种方法?

例如小米用户通过备份可以获得内部文件,甚至于可以使用CVE-2024-0044漏洞进行读取。

@yllhwa
Copy link
Member

yllhwa commented Jul 15, 2024

关于这个,只要tx还在用SQLCipher的话,模拟libbasic_share貌似没什么意义;重点是在libkernel里生成密钥的算法吧?

安卓QQ不是无法使用原版的SQLCipher进行解密吗?还是我错过了什么信息。

无法使用原版解密的话,我们就可以直接在PC上调用libbasic_share来在获取到密钥的基础上进行解密。

@Young-Lord
Copy link
Member Author

安卓QQ不是无法使用原版的SQLCipher进行解密吗?还是我错过了什么信息。

其实是可以的,我本地都直接拿社区版SQLCipher解密的,参数按教程调对就行

无法使用原版解密的话,我们就可以直接在PC上调用libbasic_share来在获取到密钥的基础上进行解密。

这个我也知道,跑个unicorn一类的
只是现在重点貌似还不是这个

(话说什么时候改的密钥生成算法,我记得之前的密钥是有特殊符号的)

@Young-Lord
Copy link
Member Author

考虑到root用户本来就可以通过hook key函数获得密钥,也许我们应该将面向无root用户介绍这种方法?

你凭你感觉写吧,我个人认为都写在同个文件里,稍作区分就行了

@yllhwa
Copy link
Member

yllhwa commented Jul 15, 2024

既然现在已经知悉安卓端这一最大使用群体的密钥生成方式,我认为我们进一步的工作还有:

  1. 获取其他平台的密钥生成方式。
  2. 为各类用户编写获取QQ数据库文件的教程。
  3. 将解密出的数据库进行可视化展示。

其中我可以继续之前的工作,对解密出的数据库字段进行解析,从而进一步实现可视化。

当然,我之前做过其他平台聊天记录的可视化,还有一个需要考虑的问题是,我们有两种实现的方式:

  1. 直接使用用户提供的腾讯原始的聊天记录数据库
  2. 将聊天消息decode后按照我们自定义的格式存入我们的数据库

我是倾向于第二种实现,即统一为自定义的格式,这个方法有几种优点:

  1. 以后还可以接入其他平台的数据,如微信、旧版本QQ等,只需要为不同平台编写不同的导入方法
  2. 可以实现更多功能,如增量更新、更复杂的搜索等

我之前的思路是对导出的数据库字段进行详细的设计,以达到通用的目的,但发现很难照顾到所有平台。

也许最佳的解决方案仍然是为不同的平台定义不同的自定义消息存储模型,再交给前端进行分别解析。

@Young-Lord
Copy link
Member Author

获取其他平台的密钥生成方式。

PCQQ / 其他平台 NTQQ?这个一般来说只要等的够久PR就会自己长出来(雾

为各类用户编写获取QQ数据库文件的教程。

这个可以有,不过找来 各种系统的具体备份操作 还是比较麻烦的吧。

其中我可以继续之前的工作,对解密出的数据库字段进行解析,从而进一步实现可视化。

这个东西其实主要是费时间,而且容易烂尾(
但是“存入数据库”一定有必要吗?我在写QQBackupNew的时候就是想着先读进内存里,每类消息对应一个类,每个类只要实现导出为 JSON/HTML/文件/纯文本 的接口就行了;而且大部分人(至少我)还是会倾向于保留原始文件的吧

其中我可以继续之前的工作,对解密出的数据库字段进行解析,从而进一步实现可视化。

关于解析、可视化的话不确定你有没有看过这个 Shmily

@yllhwa
Copy link
Member

yllhwa commented Jul 15, 2024

不过找来各种系统的具体备份操作 还是比较麻烦的吧

确实,希望也能自己长出来(

关于解析、可视化的话不确定你有没有看过这个 Shmily

嗯,这个库我是了解的,不过不太符合我的一些需求。也许我会重写一个吧。现在QQ的用户日益减少,大家对nt qq似乎并不太热情😂

@Pevernow

This comment was marked as resolved.

@xCipHanD
Copy link

感谢两位大佬,我在V9.0.65.17370成功复现。不过 教程 - NTQQ (Android).md 方法1-获取聊天记录文件处可能有误,路径中的QQ_UID_hash和下文中QQ Hash 的获取使用uid算出来的应该是不同的值,见 #29

@Young-Lord
Copy link
Member Author

@Pevernow

PRAGMA kdf_iter = 4000;加了吗?
PRAGMA cipher_hmac_algorithm = HMAC_SHA1;或者PBKDF2_HMAC_SHA512或者PBKDF2_HMAC_SHA256都可以试一下

以及QQ版本是最新版吗?
以及你这码打了个寂寞(

@Pevernow

This comment was marked as resolved.

@Pevernow

This comment was marked as resolved.

@Young-Lord

This comment was marked as resolved.

@Pevernow

This comment was marked as resolved.

@Pevernow

This comment was marked as resolved.

@Young-Lord

This comment was marked as resolved.

@Pevernow

This comment was marked as resolved.

@Pevernow

This comment was marked as resolved.

@Pevernow

This comment was marked as resolved.

@Young-Lord
Copy link
Member Author

Young-Lord commented Jul 16, 2024

诶,等等,会不会真的要自己重新算一遍

要!路径里那个hash是不一样的,对于key计算可以认为没有用

@Pevernow
Copy link

Pevernow commented Jul 16, 2024

进去了,感谢各位的支持

文档需要更新,有误。
1024的文件头没错,(害我花了十分钟写个寻找文件头的排除脚本)
hash必须重新算。(这个最好写一下)
所有md5的原字符串不用改大小写,但是md5的结果必须是全小写的。

以及本人尝试成功参数是
PRAGMA cipher_page_size = 4096;
PRAGMA kdf_iter = 4000; -- 非默认值 256000
PRAGMA cipher_hmac_algorithm = HMAC_SHA512; -- 非默认值(见上文)
PRAGMA cipher_default_kdf_algorithm = PBKDF2_HMAC_SHA512;
PRAGMA cipher = 'aes-256-cbc';

版本最新,但是从qqnt8.5测试版更新上去的。

留给后人当参考了

@Young-Lord

This comment was marked as resolved.

@Pevernow

This comment was marked as resolved.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

4 participants