> 技术文档 > Android 手机QQ聊天记录导出(NTQQ),解密聊天数据库_qqnt聊天记录

Android 手机QQ聊天记录导出(NTQQ),解密聊天数据库_qqnt聊天记录

先看结果:
Android 手机QQ聊天记录导出(NTQQ),解密聊天数据库_qqnt聊天记录
方法源自Github大佬项目:地址在这
本教程导出的聊天记录QQ版本的NT版本的,即QQ8.9之后的版本,我的QQ版本是9.1.50.23520,QQ8.9之后的版本为NT架构,导出比较困难,如果是之前的版本,建议查看另一个大佬的项目:地址
以下教程仅是记录了我自己的实际过程,结合了自身的实际情况,过程与大佬的项目不完全一样

获取UID

项目的方法:

1、将data/user/0/com.tencent.mobileqq/databases/beacon_db_com.tencent.mobileqq文件作为纯文本文件打开,查找你的 QQ 号对应的uid,形式如 “home_uin”: “390251789”,“uid”:“u_mIicAReWrdCB-kST6TXH7A”,其中u_mIicAReWrdCB-kST6TXH7A即为uid。
2、在/data/user/0/com.tencent.mobileqq/files/uid/目录下,可见到文件名形如390251789###u_mIicAReWrdCB-kST6TXH7A的若干个文件,其中u_mIicAReWrdCB-kST6TXH7A即为uid。
3、若使用了QAuxiliary模块,可以通过打开[辅助功能]聊天和消息-[消息]转发消息点头像查看详细信息功能,合并转发由自己发送的消息,查看消息的senderUid属性获取,详见#32

我的方法

其实我就是用了上面的方法2,只是我在手机文件管理器查看的时候,并未发现uid目录,然后我想到可能要root才能看见这个目录,于是在电脑上用模拟器(自己电脑上本来就有模拟器-雷电模拟器)开启root模式,然后就看到了uid目录:
Android 手机QQ聊天记录导出(NTQQ),解密聊天数据库_qqnt聊天记录Android 手机QQ聊天记录导出(NTQQ),解密聊天数据库_qqnt聊天记录
打开到目录:data/data/com.tencent.mobileqq/files/uid
可以看到:
Android 手机QQ聊天记录导出(NTQQ),解密聊天数据库_qqnt聊天记录

形如 u_mIicAReWrdCB-kST6TXH7A 的就是uid
对uid进行MD5加密即可得到QQ_UID_hash,在线MD5加密,取小写的值
如:u_mIicAReWrdCB-kST6TXH7A进行 MD5加密得到 255c42fc0f4d295678e6ff0135fcf5dd

获取聊天记录文件

模拟器这里也是可以获取的,位置在一下路径:

/data/user/0/com.tencent.mobileqq/databases/nt_db/nt_qq_{QQ_path_hash}/nt_msg.db

但是这个路径的记录肯定是不全的,当然如过在模拟器QQ这里一直拉取聊天记录,还是会有相关记录的。
我的手机是Redmi K70 Ultra ,在手机备份与恢复中,把QQ进行了备份,得到了QQ的备份文件,我的是在MIUI文件夹下,backup→AllBackup下面的文件夹就是,里面有个QQ(com.tencent.mobileqq).bak的文件,这个就是了,把ta搞到电脑上,然后解压,得到apps文件夹,进入到apps\\com.tencent.mobileqq\\db\\nt_db目录下,如
Android 手机QQ聊天记录导出(NTQQ),解密聊天数据库_qqnt聊天记录
QQ_UID_hash进行如下运算即可得到QQ_path_hashQQ_path_hash = md5(md5(uid) + “nt_kernel”) = md5(“255c42fc0f4d295678e6ff0135fcf5ddnt_kernel”) = “b69bfb8e74137f4e4253d1af3e99493a”

则聊天记录路径

/data/user/0/com.tencent.mobileqq/databases/nt_db/nt_qq_b69bfb8e74137f4e4253d1af3e99493a/nt_msg.db

nt_msg.db就是我们要解密的数据库文件,建议备份一份,防止意外。
Android 手机QQ聊天记录导出(NTQQ),解密聊天数据库_qqnt聊天记录

获取密钥

使用HxD或者其他二进制查看工具打开nt_msg.db文件,将文件头部跟随在QQ_NT DB后的可读字符串复制,形如6tPaJ9GP,记为rand

此时可以计算出数据库密钥key:key = md5(QQ_UID_hash + rand) = md5(“255c42fc0f4d295678e6ff0135fcf5dd6tPaJ9GP”) =+“71c0dfcef3b5ceae7c4a1c68ca662f4a”。

则数据库密钥为71c0dfcef3b5ceae7c4a1c68ca662f4a。

另外,文件头部还可能含有cipher_hmac_algorithm的值(如HMAC_SHA1)等与解密相关的信息,可被解析为Protobuf数据,详见#29 (comment)。

HxD下载 往下滑动就能看到了
Android 手机QQ聊天记录导出(NTQQ),解密聊天数据库_qqnt聊天记录
下载后,打开HxD,点击右上角 文件→打开,选择nt_msg.db文件,即可看到如下内容:
Android 手机QQ聊天记录导出(NTQQ),解密聊天数据库_qqnt聊天记录
其中,8MuONS9V就是randHMAC_SHA1就是下面等会就需要用到的cipher_hmac_algorithm的值。
!!!再次提醒,记得备份nt_msg.db文件!!!

移除无关文件头

首先,将nt_msg.db文件删除前1024字节,这可以通过以下方式完成:

使用二进制编辑器:Android 下的 MT 管理器(需要付费)、Windows 下的 HxD 等软件均可使用,细节从略。

使用tail命令(仅 Linux):tail -c +1025 nt_msg.db > nt_msg.clean.db

使用 Python:python -c “open(‘nt_msg.clean.db’,‘wb’).write(open(‘nt_msg.db’,‘rb’).read()[1024:])”

完成后,得到nt_msg.clean.db文件。
使用Hxd删除也行的,删除到有数据这里就行,然后另存为nt_msg.clean.db
Android 手机QQ聊天记录导出(NTQQ),解密聊天数据库_qqnt聊天记录

打开数据库

下载DB Browser for SQLite 和SQLiteStudio, 其实下载其中一个就够了,我下载两个是因为SQLiteStudio能看到聊天记录的内容,DB Browser for SQLite只能看到字节
Android 手机QQ聊天记录导出(NTQQ),解密聊天数据库_qqnt聊天记录
Android 手机QQ聊天记录导出(NTQQ),解密聊天数据库_qqnt聊天记录
下面使用SQLiteStudio打开数据库

PRAGMA key = \'pass\'; -- pass 替换为之前得到的密码key(32字节字符串)PRAGMA cipher_page_size = 4096;PRAGMA kdf_iter = 4000; -- 非默认值 256000PRAGMA cipher_hmac_algorithm = HMAC_SHA1; -- 非默认值(见上文)PRAGMA cipher_default_kdf_algorithm = PBKDF2_HMAC_SHA512;PRAGMA cipher = \'aes-256-cbc\';

将上面的内容填入下面的加密算法配置里,pass记得改成上面获取到的看key,打开刚才另存的nt_msg.clean.db文件,我图片里的是nt_msg.db是因为我重命名了
Android 手机QQ聊天记录导出(NTQQ),解密聊天数据库_qqnt聊天记录
然后就打开数据库成功了,其中c2c_msg_table就是聊天记录内容表,点击数据即可查看
Android 手机QQ聊天记录导出(NTQQ),解密聊天数据库_qqnt聊天记录
其中,40020字段就是发送人的uid,40021字段就是跟我们聊天的人,40080字段就是聊天的内容了
Android 手机QQ聊天记录导出(NTQQ),解密聊天数据库_qqnt聊天记录

聊天内容导出

目前已知消息格式为protobuf,相关解密代码可以参考提取QQ NT数据库 group_msg_table 中的纯文本、这份 Python 代码与这份 protobuf 定义,完整实现暂无,欢迎贡献。
附上大佬的python代码:

import sqlite3import blackboxprotobufconn = 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()

protobuf定义

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; }