最近几天折腾了很久关于 QQ 和微信聊天记录数据分析的东西,小有成果。这是连鸽了四天训练赛题解的理由吗[?]
微信的聊天记录已经有相对比较成熟的办法了,但是原理不太好找。QQ 这边是有比较成熟的文档,但是现成的项目不太好找。相比之下 QQ 是折腾最久的一个,折腾完之后挺有成就感的,写一篇博客记录一下。
数据库解密
最新的 NTQQ 支持从手机导入聊天记录了,但是可惜不能像老的 QQ 一样直接批量导出聊天记录为文本了,而且我尝试过新 QQ 的聊天记录没办法导入到老 QQ,于是想拿到聊天记录就只能尝试破解本地的数据库密码了……
文档里面对解密的流程讲的很细致(我知识有限,只能认识到是给 QQ 加了一个断点,在一个函数下面查询数据库密码的变量),跟着做就可以拿到数据库密码。
下面的部分是我对自己折腾的记录的存档,系统是 macOS 15.5 24F74 arm64,建议跟着原文档做,做完之后跳到数据导出
分析
复制 wrapper.node
❯ cd ~/Projects❯ mkdir QQDecrypt❯ cd QQDecrypt❯ cp /Applications/QQ.app/Contents/Resources/app/wrapper.node .用 hopper 打开,找到调用密码的函数的位置



记下来 0000000002e25758
调试
运行 QQ,找进程的 pid
❯ ps aux | grep QQ...username 40225 2.1 1.4 1623006720 240480 ?? S 11:45PM 0:00.83 /Applications/QQ.app/Contents/MacOS/QQ...记下来 40225
❯ lldb -p 40225(lldb) process attach --pid 40225Process 40225 stopped* thread #1, name = 'CrBrowserMain', queue = 'com.apple.main-thread', stop reason = signal SIGSTOP frame #0: 0x00000001832e0c34 libsystem_kernel.dylib`mach_msg2_trap + 8libsystem_kernel.dylib`mach_msg2_trap:-> 0x1832e0c34 <+8>: ret
libsystem_kernel.dylib`macx_swapon: 0x1832e0c38 <+0>: mov x16, #-0x30 ; =-48 0x1832e0c3c <+4>: svc #0x80 0x1832e0c40 <+8>: retTarget 0: (QQ) stopped.Executable binary set to "/Applications/QQ.app/Contents/MacOS/QQ".Architecture set to: arm64-apple-macosx-.找到 wrapper.node 加载的位置
(lldb) image list -o -f | grep /Applications/QQ.app/Contents/Resources/app/wrapper.node[ 0] 0x0000000128800000 /Applications/QQ.app/Contents/Resources/app/wrapper.node算出来函数的位置
(lldb) expr 0x0000000128800000 + 0x0000000002e25758(long) $1 = 5022832472打上断点
(lldb) br s -a 5022832472Breakpoint 1: where = wrapper.node`___lldb_unnamed_symbol507012, address = 0x000000012b625758(lldb) cProcess 40225 resuming运行 QQ,触发断点,根据地址查 x2 变量,解析 16 个字符
Process 40225 stopped* thread #38, name = 'thread_general_fixed_2', stop reason = breakpoint 1.1 frame #0: 0x000000012b625758 wrapper.node`___lldb_unnamed_symbol507012wrapper.node`___lldb_unnamed_symbol507012:-> 0x12b625758 <+0>: sub sp, sp, #0x40 0x12b62575c <+4>: stp x22, x21, [sp, #0x10] 0x12b625760 <+8>: stp x20, x19, [sp, #0x20] 0x12b625764 <+12>: stp x29, x30, [sp, #0x30]Target 0: (QQ) stopped.(lldb) register read x2 x2 = 0x0000013c03435140(lldb) memory read --format c --count 16 --size 1 x2 = 0x0000013c03435140error: memory read takes a start address expression with an optional end address expression.warning: Expressions should be quoted if they contain spaces or other special characters.(lldb) memory read --format c --count 16 --size 1 0x0000013c034351400x13c03435140: Iq<B3WXjc:tgS=#3(lldb) exitQuitting LLDB will detach from one or more processes. Do you really want to proceed: [Y/n] Y我的密码是 Iq<B3WXjc:tgS=#3,记下来
解密
把数据库复制一份过来
cp -r ~/Library/Containers/com.tencent.qq/Data/Library/Application\ Support/QQ/nt_qq_168d93e33909444553576552a300ebec/nt_db .移除文件头,复制一份
cat nt_db/nt_msg.db | tail -c +1025 > test.clean.db用 DB Browser for SQLite 按照这个配置就可以打开了。

数据导出
这里以私聊的数据为例,参考这个文档,把 c2c_msg_table 导出为 csv.

数据解析
表格转载自 https://qq.sbcnm.top/view/db_file_analysis/nt_msg.db.html#c2c-msg-table
| 列名 | 类型 | 含义 | 说明 |
|---|---|---|---|
| 40030 | int | 私聊对象 QQ 号 | 对方 QQ 号(无论是对方还是自己发送的消息) |
| 40033 | int | 发送者 QQ 号 | 发送者的 QQ 号 |
| 40050 | int | 时间 | 时间戳(单位为秒) |
| 40058 | int | 日期 | 当日 0 时整的时间戳格式 |
| 40093 | str | 消息发送者 | QQ 昵称或是备注名 |
| 40800 | bytes | 消息内容 | protobuf 格式 |
建议用 vscode 创建一个 ipynb 操作,个人感觉体验很好。
先用 pandas 读取 csv
import pandas as pddf = pd.read_csv('c2c_msg_table.csv')筛选私聊对象
这个操作很好实现,只需要筛选 40030 列
QQ = <VICTIM_QQ_NUMBER> # 替换为目标QQ号data = df.loc[df['40030'] == QQ].copy()data['user'] = (data['40033'] != QQ)这样就把你和 ta 的聊天记录单独复制成了一个新表,并且标记了消息的发送者
时间
时间在 40050 列,是 int64 格式的时间戳,可以用 python 的 datetime 转成 datetime 对象。
from datetime import datetimedata['time'] = data['40050'].apply(datetime.fromtimestamp)当然如果有需要,也可以用 strftime 转成字符串。
# 2024年06月11日 10:45:38data['stime'] = data['40050'].apply( lambda x: datetime.fromtimestamp(x).strftime('%Y年%m月%d日 %H:%M:%S'))消息内容
消息内容在 40080 列,是一个 protobuf 格式的二进制串
如果不想看详细过程,可以直接使用下方的批处理函数#批处理
原理
先取第一个试一下
data['40800'].head()我这里第一条是 gvYTMMj8Ff3lkLWWoO+zZtD8FQHqghYY77yI55yL5p2l5piv5Y+I552h5LqG77yJ8IIWAA==,导出之后这是一个 base64 的串,可以用 python decode 一下。
写一个 export.py.
import base64raw_bytes = base64.b64decode('gvYTMMj8Ff3lkLWWoO+zZtD8FQHqghYY77yI55yL5p2l5piv5Y+I552h5LqG77yJ8IIWAA==')with open("data.bin", "wb") as f: f.write(raw_bytes)开命令行 (protoc 是 anaconda 自带的)
❯ python export.py❯ protoc --decode_raw < data.bin40800 { 45001: 7379074328184500989 45002: 1 45101: "\357\274\210\347\234\213\346\235\245\346\230\257\345\217\210\347\235\241\344\272\206\357\274\211" 45102: 0}根据文档下面的表格 45101 里面的应该是消息内容,验证一下
❯ pythonPython 3.12.7 | packaged by Anaconda, Inc. | (main, Oct 4 2024, 08:22:19) [Clang 14.0.6 ] on darwinType "help", "copyright", "credits" or "license" for more information.>>> b'\357\274\210\347\234\213\346\235\245\346\230\257\345\217\210\347\235\241\344\272\206\357\274\211'.decode('utf-8')'(看来是又睡了)'>>> exit()事实证明猜测正确,这条消息的内容是 “(看来是又睡了)”,于是可以让 GPT 写一个批处理脚本
批处理
import base64import subprocessimport tempfileimport reimport pandas as pd
def decode_protobuf_base64(base64_str): if pd.isna(base64_str): return None
try: # Step 1: base64 解码成 Protobuf 二进制 raw_bytes = base64.b64decode(base64_str)
# Step 2: 写入临时文件供 protoc 使用 with tempfile.NamedTemporaryFile(delete=True) as tmp: tmp.write(raw_bytes) tmp.flush()
# 调用 protoc --decode_raw result = subprocess.run( ['protoc', '--decode_raw'], input=raw_bytes, stdout=subprocess.PIPE, stderr=subprocess.PIPE, )
if result.returncode != 0: print(f"protoc 错误:{result.stderr.decode()}") return None
decoded_text = result.stdout.decode('utf-8')
# Step 3: 用正则提取 45101 字段 match = re.search(r'45101: "(.*?)"', decoded_text) if not match: return None
raw_str = match.group(1)
# Step 4: 将 \xxx 格式解码成中文 decoded_bytes = raw_str.encode('utf-8').decode('unicode_escape').encode('latin1') final_str = decoded_bytes.decode('utf-8') return final_str
except Exception as e: print(f"解码失败: {e}") return None调用的时候可以加个进度条
from tqdm import tqdmtqdm.pandas()data['content'] = data['40800'].progress_apply(decode_protobuf_base64)题外话: GPT 这一顿操作很难不让人想起来 Python 大作业调 subprocess 输入输出和报错的日子
到这里数据就基本上处理完了,接下来就可以做数据分析了。
最后把需要的数据切出来存个 csv。
data_clean = data[['time', 'content', 'user']]data_clean = data_clean[data_clean['content'].notna()]data_clean.reset_index(inplace=True, drop=True)data_clean.to_csv('qq.csv')部分信息可能已经过时







