先看结果:
方法源自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目录:
打开到目录:data/data/com.tencent.mobileqq/files/uid
可以看到:
形如 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 目录下,如
对QQ_UID_hash 进行如下运算即可得到QQ_path_hash :QQ_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 就是我们要解密的数据库文件,建议备份一份,防止意外。
获取密钥
使用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下载 往下滑动就能看到了
下载后,打开HxD,点击右上角 文件→打开 ,选择nt_msg.db 文件,即可看到如下内容:
其中,8MuONS9V 就是rand ,HMAC_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
打开数据库
下载DB Browser for SQLite 和SQLiteStudio, 其实下载其中一个就够了,我下载两个是因为SQLiteStudio 能看到聊天记录的内容,DB Browser for SQLite 只能看到字节
下面使用SQLiteStudio打开数据库
PRAGMA key = 'pass'; -- pass 替换为之前得到的密码key(32字节字符串)
PRAGMA cipher_page_size = 4096;
PRAGMA kdf_iter = 4000; -- 非默认值 256000
PRAGMA 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 是因为我重命名了
然后就打开数据库成功了,其中c2c_msg_table 就是聊天记录内容表,点击数据即可查看
其中,40020 字段就是发送人的uid,40021 字段就是跟我们聊天的人,40080 字段就是聊天的内容了
聊天内容导出
目前已知消息格式为protobuf ,相关解密代码可以参考提取QQ NT数据库 group_msg_table 中的纯文本、这份 Python 代码与这份 protobuf 定义,完整实现暂无,欢迎贡献。
附上大佬的python代码:
python
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()
protobuf定义
go
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; }