在 QQ for Android 中,聊天记录被加密存储到 /data 分区应用数据目录下的 databases/<QQ>.db,其中 <QQ> 为用户的 QQ 号。

若已获得手机 root 权限,或是刷入了 TWRP,则可以使用 adb pull 命令或 TWRP 文件管理器直接提取对应的文件,此处不加赘述。

在手机没有 root 的情况下,新版 QQ 已经设置了禁止通过 adb backup 备份软件数据,因此无法直接得到数据库文件。在 Android 的安全机制下,我们无法直接安装旧版 QQ 安装包,但可以先保留数据卸载 QQ,再将指定的安装包 adb install 到手机。

adb shell pm uninstall -k com.tencent.mobileqq
adb install <QQ-old-version>.apk

此时,便可以进行 QQ 的数据备份。

adb backup com.tencent.mobileqq -f backup.ab

得到的 backup.ab 实质上是一个更改了前 24 字节的 tar 压缩包,因此,可使用 SourceForge 提供的 abe.jar 恢复 tar 标准文件头。此前,博主也曾提到过,可以使用 Python 中的 zlib 库完成同样的操作,有兴趣可以借鉴博主之前的文章。

数据库文件 <QQ>.db 可以使用 SQLite Viewer 查看。尽管消息内容经过了加密,但是不难看出,使用了简单的异或密码。这种加密的特征是,在获得了加密内容与原始内容的情况下,可以直接推导出密钥。

因此,先使用 Python 提取所有与聊天记录有关的信息。

import sqlite3
conn = sqlite3.connect("<QQ>.db")
friends = conn.execute("SELECT uin, remark, name FROM Friends")
messages = conn.execute("SELECT msgData, senderuin, time FROM mr_friend_" + <Sender> + "_New")

通过简单的分析即可得知,<Sender> 是发送者 QQ 的 MD5 值。我们选择行数较多的表,并将 <QQ> 与 messages 中出现的 uin 进行交叉对比,得到密钥为手机的电信 MEID(14位)或 IMEI(15位)。

聊天记录文本所在的 msgData 为 bytes 结构,解密之后,如果符合 UTF-8 格式,说明为文本或 JSON 链接;若无法操作,说明为多媒体格式。

string = ""
array = bytearray()  
for i in range(0, len(data)):
    array.append(data[i] ^ ord(key[i % len(key)]))
try:
    string = array.decode("utf-8")
except:
    string = "[Unsupported]"

而其他内容,如发信人 senderuin 则为 str 结构,处理相对简单。

string = ""
for i in range(0, len(data)):
    string += chr(ord(data[i]) ^ ord(key[i % len(key)]))

回到之前的 SQL 语句,博主当时指定了好友 QQ 号的 MD5,通过拼接字符串完成了选择表的操作。此时,我们可以解密 friends 中的 uin,得到所有好友的 QQ 号,并加密为全大写 MD5,如此便可不用指定 QQ 号,而实现对聊天记录的遍历。

接下来,我们可以通过 Python 的 datetime 库,将 messages 中的时间戳 time 转为可读的时间格式,并遍历 friends 中的 remark 和 name 构造关于 QQ 号和昵称的字典,在命令行中输出一份可视化的 QQ 聊天记录。