Keuin's

破解加密的QQ闪照

有过经验的朋友应该都知道,如果要保存闪照,在以前的安卓QQ里,是可以从它的磁盘缓存中复制出闪照的图片文件的。然而,新版安卓QQ(>=8.8.20)在把闪照存储在磁盘前进行了加密。之所以要把闪照存储在磁盘中,是为了能让用户在不联网的时候也可以查看闪照,这就给了我们保存闪照的机会。可是现在,我们只能拷贝出加密后的闪照文件。那么,有没有办法破解它的加密呢?

QQ闪照的加密方式

通过查看一些公开的博客和工具,我们可以得知QQ闪照文件的加密方式。细节如下:

QQ闪照的加密方式是DES ECB模式,密钥长8字节,为它自身MD5的大写16进制字符串的前8个字符用ASCII编码得到的8个字节。这看起来让我们难以破解:要解密闪照,首先要知道闪照源文件的MD5;而要知道闪照源文件的MD5,就必须先解密闪照。不过既然我们可以离线查看闪照,说明闪照的数据是可以被离线解密的,即密钥就保存在手机的某个位置里。我们把QQ的私有文件拷贝出来,从中找到自己QQ号对应的数据库(databases/QQ号.db),这是一个SQLite3数据库文件,打开之后找到表mr_troop_<MD5>_New(其中<MD5>是群号的ASCII字符串的MD5值),在里面可以找到闪照对应消息的extStr列,该列的类型为BLOB,存储的是一个protobuf序列化的二进制字节串,从中可以找到闪照文件的MD5,从而可以解密闪照文件。

不过,有很多时候我们只能拿到加密的闪照文件,数据库文件很难被拷贝出来。只有闪照文件的时候,能不能解密闪照呢?

QQ闪照的解密——误用密码学工具的后果

爆破4个字节

我们知道,错误使用密码学工具很可能得不到足够的安全保障,闪照就是一个例子。闪照使用了DES这一废弃的加密算法,该算法的密钥长度只有64位,其中每8位还都有1位是校验位、没有任何作用,也就是实际参与加密的密钥只有56位。更糟的是,QQ使用一个十六进制字符串经过ASCII编码后的值作为密钥:1个字节的十六进制表示有两个[0-9A-F]字符,这两个字符再经过ASCII编码,变成了2个字节!因此,密钥对应的实际数据来源只有$8 / 2 = 4$字节,只有$2^{32} = 4294967296$种可能。这个数量足够小,我们可以轻易地使用一台现代的笔记本进行爆破。实际上,我用C语言编写了一个多线程搜索程序。这个程序在使用AMD 5950X处理器的计算机上,只需要10分多钟就可以搜索完整个密钥空间。

能不能更快一些呢?

32个bit太多了,那就搜25.35940002个bit吧

上面那个实现能用,但还是有点慢了,平均爆破一张闪照需要好几分钟,还是在16c 32t的5950X满载的情况下。我想了很久也没想到啥好方法来加速。后来经Merrg1n的提醒,还有一个特性我们没有用到:DES密钥的校验位。实际上,如果1个字节有8个bit,那么DES密钥的8个字节里,每一个字节的LSB(最低位)都会被忽略。我们知道’B’的ASCII码是66,‘C’的ASCII码是67,那么密钥"BBBBBBBB"和密钥"CCCCCCCC"在DES加密里是等价的!由于我们的密钥是一个大写十六进制字符串,我们密钥的字母表就是0123456789ABCDEF,共16个字母。如果我们把他们的LSB都设置为0,那么最后只会剩下9个字符:02468@BDF,可能的密钥个数为$9^8 = 43046721$,是以前$2^{32}$的$1.00226\%$。我们根据这个想法修改我们的搜索工具,可以得到:

Benchmarking...
Finish.
Speed (single thread): 414347.186 key/sec
Time to search the whole key space:
    1 thread:   103.890 sec
    2 thread:    51.945 sec (estimate)
    4 thread:    25.973 sec (estimate)
    8 thread:    12.986 sec (estimate)
   16 thread:     6.493 sec (estimate)
   32 thread:     3.247 sec (estimate)

即使只用一个线程,我们也只需100秒即可搜索完所有可能的密钥,这个速度是十分令人满意的。我在我的笔记本上测试了多张闪照,耗时最久的一次只用了7秒。有了这样的速度,我们的工具才真正拥有了实用价值——尽管它只是一个暴力破解工具。

更新于2022/11/07:

这几天才发现,我半年前随手写的程序被人在9月份fork了,他在fork仓库里加了readme,里面有相对完整的构建流程,还“贴心”地帮我修复了一个“bug”——移除了一些const,使得代码可以使用ubuntu源里的旧版libtomcrypt编译通过。然而并没有给我提pr或者issue。

实际上如果他试着稍微深入一点,就会发现自己make一个latest版本的libtomcrypt即可通过编译。我在最开始写的时候并没有发现这个问题,直到后来尝试在debian系统的机器上编译时才发现了,但是一直故意没有改,也没有加readme。这是为了防止脚本小子轻易地使用了我的程序。众所周知,这类程序一旦扩散开,就很可能被封杀,然后这个方案就会失效。不过既然有人做了这个“好人好事”,我可以再补充一个方案:

重新思考:我们究竟要解决啥问题?

其实闪照是QQ客户端加密并删除的,QQ服务器发送给客户端的闪照数据仍然是原始图片数据。我们只需要使用任意一个能接收图片消息的QQ协议实现,将图片及时地保存下来即可。不过作为补充,本文的方法适合用作事后提取,以及取证。

参考资料