Skip to content

Latest commit

 

History

History
918 lines (600 loc) · 94.4 KB

File metadata and controls

918 lines (600 loc) · 94.4 KB

Msg2.0.db 解析 — IDA Pro (IM.dll) 与样例数据对照

本文档随分析逐步更新。分析目标:为 QQ 2010 时代 Msg2.0.db 的归档/转可读格式,理清客户端如何解析该库及侧录目录结构。


1. 分析环境与二进制

IDB .../msg2.0/QQ2010/Bin/IM.dll.i64
模块 IM.dll(主聊天/消息相关逻辑集中于此)
ImageBase 0x31000000
输入路径(构建机) C:\Users\...\Desktop\QQ2010\Bin\IM.dll

MCP server_health:分析已完成(auto_analysis_ready: true),Hex-Rays 可用。


2. 文件层:Msg2.0.db 不是“裸 SQLite 单文件”

2.1 物理格式

  • 对样例 msg2.0/Msg2.0.db 执行 filexxd:头部为 D0 CF 11 E0 A1 B1 1A E1,即 复合文档 / OLE 结构化存储(Compound File Binary, CFB) 魔数。
  • 与“纯自定义二进制”不同,Windows 上通过 StgOpenStorageIStorage / IStream COM 接口访问。

2.2 与标准 OLE 读库的对照(重要)

  • 使用 Python olefile 打开同一路径的 Msg2.0.db 时抛出 OSError: incorrect DIFAT
  • 可能原因(需后续用 Windows/原始解密链验证,不限于一种):
    • 文件经 TX 加密层非标准扩展 处理,导致 FAT/DIFAT 与规范不一致;
    • 文件截断/损坏;
    • 或需先经 Matrix.dat + TXEncryptMgr 得到可读存储(见第 5 节)。
  • 结论:归档管线应同时保留 Msg2.0.dbMatrix.datseqbase.dat、侧录目录,不能只假设“任意 OLE 库可直接枚举流”。

3. 虚拟路径:UserDataMsgStorage: → Msg2.0.db

3.1 挂载点(sub_3103E5C0,日志上下文 MsgStorage

反编译逻辑概要:

  1. Util::Sys::GetGlobalDataUsersDir → 拼出用户目录,再拼 Msg2.0.db(源码字符串为 Msg2.0.db,与磁盘大小写可能不一致)。
  2. FS::AddFileSystem(2, "UserDataMsgStorage:" 对应宽字符路径)
    将类型 2 的文件系统命名空间挂到真实文件上。
  3. 同时用 OSRoot: 前缀与 FS::IsFileExist 检查文件是否存在;失败时走备份/重命名(MsgBackFile + GUID + .db)、ImportWizard 配置等分支。

含义:客户端内访问消息库时,并不是直接写死盘符路径,而是通过 UserDataMsgStorage:\... 与内部 FS 层,最终落到 Msg2.0.db 的 OLE 存储

QQ2009 注记UserDataMsgStorage: 与盘上 Msg2.0.dbAddFileSystem(2, …) 注册 在静态 xref 中出现在 KernelUtil.dllsub_60A3F970§19),不是 IM.dll?AddFileSystem@FS@@ 的调用点。§3.1 仍以 QQ2010 IM.dllMsgStorage 逻辑为据;2009IM.dll 主要在 前缀已挂好后CombineQNC / 建目录 / 开流

3.2 按“通道名”建子目录(sub_3102D560

  • 对传入的名称(如 buddy / group / …)执行 FS::CombineQNC(L"UserDataMsgStorage:", name),然后 FS::CreateDirectoryW,要求 FS::IsDirectoryExist 成功。
  • 与用户磁盘目录一致:样例根目录下存在 buddy/discuss/group/mobile/system/,与代码中的通道名一一对应。

4. 消息路由:五类存储名 → 不同解析入口(sub_3102F7F0

该函数根据对象内 CTXBSTR(偏移约 this+12 与 UTF-16 字面量比较,分发到不同处理函数,最后统一调用:

Util::Msg::TranslateOldMsgToMsgPack(...)
(旧缓冲区 → ITXMsgPack,便于上层展示或导出)

存储名(宽字符串) 调用链
buddy sub_3102E8F0 / sub_3102F150(是否临时会话由参数 a2 分支)
group sub_3102EE60,并置标志供后续使用
discuss sub_3102EB40,并置标志
system sub_3102F3A0
mobile sub_3102F5A0
其他 清理并返回失败

含义Msg2.0.db 内部或镜像目录上,同一 OLE 树下用 服务类目录名 区分会话类型;解析二进制布局前先按 buddy/group/discuss/system/mobile 分流。

4.1 Buddy 旧消息体示例(sub_3102E8F0

  • ITXDatabSelfMsg 等字段,从缓冲中解析 GBK 或 BIG5 文本(Util::Convert::GBKToUnicode / BIG5ToUnicode),写入 strSenderstrSenderShowName 等到 CTXStringW/MsgPack
  • 后续还有 sub_31002B90 处理附加块,说明 一条记录 = 头 + 可选尾块,属 QQ 私有 TLV/分段格式(需结合更多函数拆解字节)。

5. 加密与 Matrix.dat(sub_31042D80

当路径经 CTXStringW::MakeLower 后包含 msg2.0.db(且不为 msgss.db)时:

  1. FS::CombineQNC(..., L"Matrix.dat") 得到与消息库同根的密钥/封印文件路径;
  2. TXEncryptMgr::CreateDataStorage 创建数据存储接口;
  3. 通过 bufSvrSealEncBSTR 键 从存储对象读写配置;

归档启示:若导出工具绕过 TXEncryptMgr,可能无法打开 IStorage 或内部流;需复现 Matrix.dat + seal 语义(逆向工作量独立于 OLE)。


6. OLE 打开封装(sub_31287E10

// 语义摘要(Hex-Rays)
BOOL sub_31287E10(int ctxStringPath, IStorage **ppstgOpen) {
  const WCHAR *p = CTXStringW::operator wchar_t const *(ctxStringPath);
  return StgOpenStorage(p, ..., 0x12, 0, 0, ppstgOpen) >= 0 && *ppstgOpen;
}
  • grfMode = 0x12:对应 STGM_READ | STGM_SHARE_DENY_WRITE(典型只读独占写)。
  • 调用方包括(xref):sub_31281C90(转换准备)、sub_31283AD0sub_31285E80ConvertSS2CD / 迁移 工具链。

  1. sub_31010F20 校验/sub_310EBAA0 清理。

7. info.db 内嵌库:魔数分支(sub_31285C50

sub_31285E80CTXConvertSS2CDMgr::GetMsgRecordUinList

  • this+19 + info.db 拼路径,FS::SplitQNCsub_31287E10 打开 IStorage
  • sub_31285C50(ppstgOpen, L"User\\Account.db", ...)Group\\Basic.db,从存储树读取 好友 QQ 号列表 / 群列表(日志:Get Buddy List / Get Group List)。

sub_31285C50 行为摘要

  • sub_31288170:在父存储上 OpenStream(失败打日志 FileHelp::ReadStream);
  • 取流首 4 字节,与两常量比较(注意 小端序 int):
    • 168602440x01014454,内存字节序 54 44 01 01 → 与样例 Matrix.dat 文件头 54 44 01 01(可视作 "TD" + 版本类字段)一致 → 走 sub_31285AD0
    • 168607500x0101464E,内存字节序 4E 46 01 01 → 走 sub_31285820

结论info.db 内的嵌套 DB 并非随意扩展名,而是带 固定 4 字节类型标签TX 私有容器;与 Matrix.dat 共享同类签名前缀。


8. 另一路 OLE:自定义表情 / 导入(sub_311FF6F0

该函数同样 StgOpenStorage,但典型子路径为:

  • 存储 config → 流 Face.Xml(读入后字符串替换 > / <FILE ORG> 等);
  • 存储 Files
  • CUSTOMFACE / CUSTOMFACEGROUP 下迭代 FACE 子项与 Util::Data/ITXData

对应 表情/导入向导,与 日常 buddy 文本消息 路径不同,但证明 IM.dll 大量依赖 OLE 层次遍历


9. 与用户样例目录的对照验证

路径:.../stale-reader-encrypted/msg2.0/

观察 与二进制一致性
根下 buddy/discuss/group/mobile/system/ sub_3102F7F0 / sub_3102D560buddydiscussgroupmobilesystem 字面量一致
buddy/<UIN>/content.datindex.datinfo.dat 与 OLE IStream 落盘或导出文件 的典型三重划分一致(内容 / 索引 / 元信息);具体字段待继续跟 FS::OpenStream 命名
Matrix.dat54 44 01 01 sub_31285C5016860244 分支魔数一致
lastmsginfo.dat54 41 01 01 另一类 TX 签名(TA),需单独 xref

9.1 路径拼接的确凿代码(sub_3102FDB0

该函数用 FS::CombineQNC 构造:

  1. UserDataMsgStorage: + \\ + 第一段目录名 a2(长度 < 0x20 宽字符) + \\ + 第二段目录名 a3(长度 < 0x20);
  2. 在其上再拼 \\index.dat\\content.dat,并 FS::CreateFileW 打开(失败时换标志 4098 重试)。

因此磁盘上的:

buddy\<QQ号>\index.datbuddy\<QQ号>\content.dat

与虚拟路径:

UserDataMsgStorage:\buddy\<QQ号>\index.dat

完全同源discuss / group 等同理)。info.dat 未在此函数出现,应由其它入口创建(待 xref)。

迁移工具 CTXConvertSS2CDMgr::RepairMsgByType 使用 %s\%s\content.dat / index.dat,与上述双层目录模型一致。


10. 归档为“人类可读”的可行工程路径(基于当前逆向)

  1. 容器层:实现或使用 StgOpenStorage,若失败则实现 TXEncryptMgr + Matrix.dat 后再 OLE(与 sub_31042D80sub_31287E10 对齐)。
  2. 索引层:按 buddy/group/discuss/system/mobile/ 分桶,再按 UIN / 群号 子目录解析 index.dat / content.dat
  3. 语义层:对每条记录跑 Util::Msg::TranslateBuddyMsgToMsgPack / TranslateOldBuddyMsgToMsgPack 同类逻辑,或静态复现其 CTXBuffer 布局(需继续向下拆解 sub_31037800 等)。
  4. 文本编码:注意 GBK/BIG5 分支(sub_3102E8F0),导出 UTF-8 时需标明原始代码页。

11. 下一步(待写入后续段落)

  • content.dat / index.dat:已在 sub_3102FDB0RepairMsgByType 确认 UserDataMsgStorage:\<通道>\<UIN>\ 模板。
  • info.dat:见 §12TXEncryptMgr::CreateDataStorage + 按通道/会话路径拼接;与 SNSFileSystem:\info.dat 区分)。
  • seqbase.datlastmsginfo.dat:见 §12MsgStorage 初始化读入 + 定时器回写 lastmsginfoseqbase 为序列基线/水位相关二进制)。
  • Linux 下 OLE:已用 Wine + MinGW 生成的 msg2ole_extract.exe(逻辑同源 QQMgrMsg_src/ComFileExtr.cpp)成功 StgOpenStorage 打开样例 Msg2.0.db 并递归解压出 buddy/ 等树;同一文件 Python olefile 仍报 incorrect DIFAT(走的不是 Win32 OLE)。

12. info.dat / seqbase.dat / lastmsginfo.dat 如何读写(IM.dll)

以下地址均以 ImageBase 0x31000000 为模块基址;函数名为 IDA 默认名。

12.1 三条虚拟路径(消息库根)

sub_31041020(某 MsgStorage 辅助类构造函数)中,成员 this+5this+6 被设为:

成员 构造方式 语义
this+5 FS::CombineQNC(L"UserDataMsgStorage:", L"seqbase.dat") 全局序列基线文件 seqbase.dat(OLE 根下的流/顶层文件)
this+6 FS::CombineQNC(L"UserDataMsgStorage:", L"lastmsginfo.dat") 最近一条消息摘要/索引 lastmsginfo.dat

二者经过 TXEncryptMgr 包裹,而是直接 FS 路径(与 Matrix.dat 密钥流分离);与 buddy/... 子目录下的 info.dat 打开方式不同。

12.2 seqbase.dat / lastmsginfo.dat:初始化读 + 定时写

sub_3103F5D0(日志上下文 MsgStorage)在 sub_3103E5C0 成功挂载 Msg2.0.db 之后:

  1. FS::CreateFileW 打开 this+5seqbase.dat),通过 ITXFile 虚表读长度;若有效载荷长度为 0,打日志 seqbase size = 0(字符串 aSeqbaseSize0),并走一套 GUID 3EDD1723-0FF2-4DD8-A6F9-8576D8FF4561 相关的默认/修复分支(与 sub_3103F450 枚举的对象列表配合——疑似空库初始化或解密占位)。
  2. CreateFileW 打开 this+6lastmsginfo.dat),把内容解析进 this+15 上的 ITXArrayUtil::Data::CreateTXArray),供内存侧维护「最近消息」集合。
  3. 调用 TXTimer::SetInterval(0xEA60, this+11, ...),即约 60 秒周期触发 this+11 上的回调。

定时回调 sub_31040470(由 sub_31040A70 指向):

  • *(this+2) 置位且 sub_310411B0(读取 this+8 标志)通过时,遍历 this+12 / this+13 处的链表结构,按 CTXBSTR 键与 ITXFile/ITXBuffer** 协作做待同步项处理(日志 MsgStorage);分支内含 sub_31040070 迭代、sub_3103D840 删除节点等典型「队列 drain」形态。
  • this+15lastmsginfo 对应的 ITXArray)非空:再次 FS::CreateFileW 打开 this+6,把 ITXArray 序列化进 ITXBuffer,经 ITXFile 虚表 写入/提交(多处 vtable+52 / +60 / +80 组合符合「取缓冲 → 写文件 → 收尾」)。

结论lastmsginfo.dat 在运行期被 周期性写回seqbase.dat 在初始化阶段重点读取(长度校验 + 空文件分支);二者共同支撑 消息序号基线与「最后一条消息」展示/同步,与 sub_3102FDB0index.dat/content.dat 会话正文索引形成分工。

12.3 info.dat(会话级):TXEncryptMgr 封装的两套路径

按会话目录(与磁盘 buddy\<UIN>\info.dat 一致):

  • sub_3102E700 / sub_3102E7D0:格式化 L"%s%s\\%s\\info.dat",参数为 UserDataMsgStorage: + this+12 + this+16 两段宽字符串(通道名 + UIN/子 ID),再 TXEncryptMgr::CreateDataStorage。二者唯一差别是对 ITXDataStorage 虚表调用偏移 +12+16(可理解为 读/查询写/更新两种 COM 方法 成对出现)。

按通道根目录(仅一层子名):

  • sub_3102D660 / sub_3102D720UserDataMsgStorage:\ + this+8CTXBSTR + \info.dat,同样 CreateDataStorage,虚表 +16 / +12 成对。

以上说明 info.dat 并非裸 CreateFileW,而是 TXEncryptMgrITXDataStorage,与 Matrix.dat 封印链一致(参见上文 §5)。

12.4 易混淆同名:SNSFileSystem:\info.dat

sub_31274220Infocenter.db 目录上挂载 SNSFileSystem:,再访问 SNSFileSystem:\info.dat(小写 info.dat 字面量在 aInfoDat_1)。若不存在则 CreateFileW 并写入 dwFileVersionITXData。这是 资讯中心/SNS,与 Msg2.0.db 树内 buddy\...\info.dat 不是同一条业务线;归档时勿混用解析器。

12.5 迁移工具中的三文件

sub_31285240CSS2CD / RepairMsgDb):依次对 PrefixString + Matrix.dat+ seqbase.dat+ lastmsginfo.dat 调用 sub_31282C70 做修复,再 sub_31285030 处理 buddy/group,并调用 CTXConvertSS2CDMgr::RepairMsgByType。说明 seqbase/lastmsginfo/Matrix 在版本迁移时被视作 同一批关键侧车文件


13. 命令行解压工具 msg2ole_extract(基于 QQMgrMsg_src)

路径:ai-work/msg2ole_extract/

  • 源码msg2ole_extract.cpp — 从 ComFileExtr::ExtrDB / OnStart 抽出 IStorage::EnumElements → 子存储建目录、流写文件,去掉 MFC/CDialog,改为 wmain 参数:<Msg2.0.db> <输出目录>
  • 构建make(依赖 x86_64-w64-mingw32-g++),链接 -static -lole32 -luuid -lshell32,避免 Wine 下缺 libwinpthread-1.dll
  • 运行(Linux)wine msg2ole_extract.exe <db路径> <输出目录>(Wine 可接受 Unix 路径)。
  • 样本结果:对 stale-reader-encrypted/msg2.0/Msg2.0.db 解压得到 buddy\<UIN>\index.dat|content.dat|info.dat 等,与已有侧录目录结构一致。

14. index.dat / content.dat 容器与 buddy 明文体内层(直至可解析文本)

14.1 索引:index.dat

每条记录 8 字节uint32 LE 标记字 + uint32 LE 数值

标记字 不是 UTF-16 双宽字符串,而是 小端 32 位整数,其 低 16 位为两段 ASCII<标点><0x3a ':'>。实测常量:

uint32(十六进制) 可视
0x00003A2D -:(字节序列 2D 3A 00 00
0x00003A2E .:
0x00003A2F /:

典型 三条(好友会话样本 buddy/1002598880):

  • -:0
  • .:第一块载荷字节数(与 content.dat 首部 uint32 相等,如 0x166 = 358
  • /:第二块相关长度;与盘上 content.dat 对照:/: == len(文件) - 4 - .: 数值 + 4**(第二块前另有 **uint32 长度**类开销时与 **/:`4 字节对齐)

极小会话(如 buddy/498795657)只有 8 字节索引,第一条 uint32 非上述 magic(如 0x1EC8),表示 缩略/占位索引,与 content.dat 首块内的 uint32 对应同一元数据。

14.2 正文容器:content.dat

两种外层模式(由首部 uint32len(文件) 关系判定):

  1. 单块文件:首部 uint32 == len(文件),_payload = raw[4:](总长含头部 4 字节)。
  2. 双块文件:首部 uint32 == L1 < len(文件),第一块体 raw[4:4+L1],余 raw[4+L1:] 为第二块。

每块内部常见:前 4 字节常为 0,随后在 偏移 4 出现 packed 标记 2D 3A 00 00-:)或 2E 3A 00 00.:),其后为 密文(需 Matrix.dat + TXEncryptMgr 解密后才能得到 buddy 体内层)。

14.3 Buddy 消息明文体内层(sub_3102E8F0,解密/反序列化之后)

指向 ITXData 子对象 v6 时:

偏移(相对 v6 含义
[0:4] 保留 / 未在本函数使用的字段
[4] uint8,与 bSelfMsg 等一起参与 UI 逻辑
[5:9] int32 LE L文本字节长度(x86 非对齐加载)
[9:9+L] GBK / BIG5 正文(由调用方 a1 选择 GBKToUnicode / BIG5ToUnicode
之后 可选 int32 长度 + 字节块 链,经 sub_31002B90 写入扩展 ITXBuffer

14.4 工具与验证状态

  • 脚本ai-work/msg2_parser/msg2_session_parse.py
    • --self-test:对 合成 buddy 内层字节流解析出 你好GB18030),验证字段布局。
    • <index.dat> <content.dat>:打印 索引三元组单块/双块划分、-: / .: 偏移;对多样本校验 .: == 首块长/:` == 尾块相关长度 + 4
  • 加密:当前样本 content.dat 在标记 -: / .: 之后为 高熵密文;在实现 TXEncryptMgr + Matrix.dat(TD 54 44 01 01 解密链之前,无法从磁盘单独还原 UTF-8 聊天正文。归档管线需在解密后接上 parse_buddy_plain_inner 同类逻辑。

修订记录

日期 内容
2026-05-02 初稿:IM.dll 中 Msg2.0.db / UserDataMsgStorage / 五类通道 / StgOpenStorage / Matrix.dat & 魔数 / 样例目录对照 / olefile 失败记录
2026-05-02 增补 sub_3102FDB0 路径模板、RepairMsgByTypesub_31285C50 魔数与 Matrix.dat 头对应关系
2026-05-02 msg2ole_extract:MinGW CLI + Wine 实测解压 Msg2.0.db
2026-05-02 §12info.dat / seqbase.dat / lastmsginfo.dat 读写链(sub_31041020sub_3103F5D0sub_31040470sub_3102E700/E7D0、D660/D720、sub_31285240
2026-05-02 §14index.dat/content.dat packed 标记 0x3A2D/2E/2F、双块长度关系;buddy 内层 sub_3102E8F0msg2_session_parse.py 实测交叉验证;密文依赖 Matrix/TX
2026-05-02 §15TXEncryptMgrCommon.dllMatrix.dat 双前缀;密钥字段 bufSvrSealEnc/bufPwdHashOne/buf16byteSessionKeyCLSID {49DB3AB8-7E8B-DAF8-D45F-C0DF-00000000}ITXEncrypt+12content.datITXEncrypt 而非 FS 透明解密
2026-05-02 §16Common.dllInit = MD5(flags∥16B)sub_30001CF0 = XXTEAsub_30002340 = XXTEA 信封;Seal 字段 bufSvrSealEnc 等;MachineGuid 链路与 XOR 表 byte_302078F0
2026-05-02 §16.7静态钉扎QueryEncryptsub_300990F0→工厂 QIoff_301B9FB0/unk_301B9FC8);ITXEncrypt +12 = sub_30095FA0sub_30001FA0(加密);+16 = sub_30095FE0sub_30095E10sub_30002340(信封解密);修正 §15.5–15.6+12 的笼统表述
2026-05-03 §17:QQ2009 MsgMgr.dllMsg2.0.db 字面量仅 sub_61E4B590 / 向导 sub_61E4D460sub_61E60420 串联初始化;sub_61E5A690FS::SetExitDelConfig(UserDataMsgStorage:, …)bClrearMsgExit;QQ2009 IM.dll RemoveFileSystem 枚举无 UserDataMsgStorage:
2026-05-03 §18:QQ2009 Common.dllSetExitDelConfig/GetExitDelConfigsub_604AEA20/sub_604AE9E0 → vtbl +0x6C/+0x68sub_604B34E0/sub_604B34B0this+0x30);析构 sub_604A7480 在标志非 0 时对 CTXStringW(this+8) 调用 DeleteFileW(常为 Msg2.0.db 路径);off_6057FEA0 vs off_6057F940;MsgMgr 五处 AddFileSystemUserDataMsgStorage:Matrix.datIM.dll + TXEncryptMgrExitDel 不按名删 Matrix.dat,并列文件时 Matrix.dat 多数仍保留
2026-05-03 §19:QQ2009 KernelUtil.dllFS::AddFileSystem(2, <Msg2.0.db 全路径>, L"UserDataMsgStorage:", …)Info.dbUserDataInfoStorage: 均在 sub_60A3F970RemoveFileSystem(UserDataMsgStorage:) 等见 sub_60A3F970 开头与 sub_60A37CB0;与 §15.3 两套 Matrix.dat§18.5 IM.dll 仅消费挂载 对齐
2026-05-04 §15.3.1Common.dll sub_604A6C60GetFileAttributesW + TXOpenStorage + TXCreateCompoundDocumentMatrix.dat 落盘IM.dll sub_606475A0 / sub_606476E0 分用 Matrix.datmsg2.0.dbMatrix\Matrix.dbmsgex/msgssCommon.dllMatrix.db 字面量
2026-05-05 §15.3.1 增补:「侧车文件一旦创建则走真实盘路径」≠「凡有 Msg2.0.db 则旁必有 Matrix.dat — 条件创建、仅拷贝库文件、版本/路径不一致等
2026-05-06 §15.3.2QQ2009 Common.dll sub_604A6C60TXCreateCompoundDocument布尔前提(与 sub_604AA5A0 / this+40 / 第二形参 a2);IM.dll sub_606475A0 / sub_60647120 门槛
2026-05-07 §15.3.3sub_60643D10 /「没到 sub_604A6C60汇编级判定[arg+0x18]sub_60647120 xrefsub_604A7100 / sub_604A8460 直接调 sub_604A6C60
2026-05-08 §15.3.4:QQ2009 IM.dll CreateDataStorage 八调用点 + CheckMsg:/ImportMsg:/info.dat 上游;Common.dll sub_604A6C60 十四调用者端到端链条钉扎
2026-05-09 §15.3.4(3.1)QQ2009 IM.dllsub_606475A0/60648620/60649500函数入口code xref 穷尽印证sub_60647120 仅虚表数据 xref勿将口语场景等同于穷尽
2026-05-10 §15.3.5Matrix.dat 生成/打开的上层条件 — 钉 sub_60636130 首参 = pUnkOuter(COM 聚合)ATL CreateInstancesub_60609D10)→ this+0x24 槽位调用 sub_60636130;与 CheckMsg: / ImportMsg: 分支表、CLSID {A875AE08-…} 对象表项同列
2026-05-11 §15.3.6消息库 COM 对象三路虚表 + ATL 接口映射off_6084D678/off_6084D5A8 全槽钉函数;更正 §15.3.5聚合 vs 非聚合 互斥 sub_60647120 槽位
2026-05-12 §15.3.7off_6084D618 _ATL_INTMAP_ENTRY 逐项解析IID×dw 钉死6084d654/6084d664 内联 IID + 第三张虚表off_6084D664 前缀五槽
2026-05-13 §15.3.8顶层调用链 + Matrix.dat 首次落盘sub_60648D70 亦仅虚表 xrefsub_6064F650 委托槽→sub_6064F430;修正 §15.3.4(3.1)sub_60647120/聚合 的旧误

15. TXEncryptMgr:模块边界、API、密钥来源与解密链(IM.dll → Common.dll)

本节目标:弄清 谁在何处实现加解密密钥从哪些结构化字段来Matrix.dat / 会话文件如何衔接。结论:对称算法与 TXEncryptMgr 本体不在 IM.dll,而在导入模块 Common.dll;离线还原正文必须在 Common.dll + Matrix.dat + 登录阶段密钥材料 三条线索上继续。

15.1 实现位置(确凿)

IM.dll 的导入表枚举:TXEncryptMgr::QueryEncrypt / CreateDataStorage / AddEncryptInfo / Init 均从 Common 模块导入(IDA:module idx 0Common.dll)。因此:

  • 逆向「算法本体」:应打开 QQ2010/Bin/Common.dll(与其它 Tencent 客户端同名 DLL 无关——必须以本安装目录为准)。
  • IM.dll 仅持有 调用序列GUIDITXData / ITXEncrypt 上的缓冲区搬运

15.2 面向归档的核心 API(导入符号)

修饰名(Mangling) 语义(根据调用点归纳)
?CreateDataStorage@TXEncryptMgr@@YAJPB_WPAPAUITXDataStorage@@@Z HRESULT CreateDataStorage(wchar_t const *virtualOrOsPath, ITXDataStorage **pp) — 打开 Matrix.dat 等路径对应的 ITXDataStorage。实现侧经 TXOpenStorage / TXCreateCompoundDocument 落盘为 TD 类复合文档(见 §15.3.1),不是纯内存假路径。
?AddEncryptInfo@TXEncryptMgr@@YAJU_GUID@@PAUITXDataStorage@@PAUITXSvrSealCrypto@@PAUITXEncUIGetPass@@PAUITXCallback@@H@Z GUID(加密实现 CLSID)Matrix 打开的 ITXDataStorageUtil::SvrSeal::CreateSvrSeal 得到的 ITXSvrSealCrypto、可选密码 UI、回调等 绑定到全局加密管理器
?Init@TXEncryptMgr@@YAXKPAUITXBuffer@@@Z void Init(unsigned long flags, ITXBuffer *pwdOrMaterial) — 见 sub_31022140:材料来自 bufPwdHashOne(见下)。
?QueryEncrypt@TXEncryptMgr@@YAJU_GUID@@PAPAUITXEncrypt@@@Z HRESULT QueryEncrypt(GUID const &, ITXEncrypt **pp) — 按 固定 CLSID 取出 ITXEncrypt 实例,供消息包装 加密/解密(见 §15.5)。

15.3 Matrix.dat 的两套挂载(勿混淆)

路径前缀 典型拼接 函数线索 用途
UserDataMsgStorage:\Matrix.dat FS::CombineQNC(..., L"Matrix.dat") sub_31041510PerfStand.InitUserFileSystem)、sub_31042D80QQ2009 钉扎 sub_60648620 / sub_60649500 / sub_606475A0§15.3.4 消息库侧:与 Msg2.0.db 同逻辑根的 Matrix.datsub_31042D80(或 QQ2009 sub_60648620)在读 bufSvrSealEnc(见 §15.4)前 CreateDataStorage
UserDataInfoStorage:\Matrix.dat 同上,前缀不同 sub_31021BE0(PreLogin) 账号信息侧:与 Info.db / bufPwdHashOne 初始化 TXEncryptMgr::Init 相关;Util::SvrSeal::CreateSvrSeal(2, …)(类型 2)。

与物理挂载(QQ2009)KernelUtil.dllsub_60A3F970 依次 AddFileSystemUserDataRoot:(类型 1)→ Msg2.0.db 全路径 + UserDataMsgStorage:(类型 2)→ Info.db 全路径 + UserDataInfoStorage:(类型 2)(字面量 Msg2.0.db / Info.dbaUserdatamsgsto_0 / aUserdatainfost_0 同函数内成对出现)。因此 §15.3 两前缀下的 Matrix.dat 各落在 两套用户数据根旁的两个独立文件(通常与 Msg2.0.db / Info.db 并列目录),不是 Msg2.0.db 复合文档内部的两个同名流

15.3.1 **Matrix.dat 是否落盘;Matrix.db 与谁配对(钉扎)

曾不严谨处:仅说 CreateDataStorage 非裸 CreateFileW 容易让人以为 数据未必在真实文件上 —— 应对齐 Common.dll打开/创建存储 的实现。

  • 钉扎(QQ2009 Common.dll,ImageBase 0x60400000:包装类在首次需要 IStoragesub_604A6C60 同类逻辑(与 this+8CTXStringW 已解析的完整 …Matrix.dat 宽路径 绑定):GetFileAttributesWTXOpenStorage;若仍打不开且条件满足则 TXCreateCompoundDocument —— 在盘上创建/维持 TD 类复合文档。因此 Matrix.dat真实路径上的侧车文件(与 Msg2.0.db 并列、独立,内容格式为 TX 文档而非裸 SQLite);若从未触发创建或路径不可写,盘上可以 暂时不存在
  • Matrix.db(勿与 Matrix.dat 混名)IM.dll sub_606476E0 仅在路径中含 msgex.dbmsgss.db 时拼接 <基路径>\Matrix\Matrix.db(字面量 L"Matrix.db",如 QQ2009 0x6084e68c),用 FS::CreateFileW另一套消息库sub_606475A0 在路径含 msg2.0.db 时则使用 L"Matrix.dat" + TXEncryptMgr::CreateDataStorage。对 Msg2.0.db 主归档线客户端字符串与调用链均以 Matrix.dat 为准Matrix.dbMsgEx/MsgSS 场景下的 不同文件名 + 子目录布局
  • 静态覆盖:在 QQ2009 Common.dll 内对 UTF-16 Matrix.db 的按字节搜索 无命中Matrix.db 字面量仅见于 IM.dll 上述分支。
  • 与实测「只有 Msg2.0.db、目录里找不到 Matrix.dat」不矛盾(分析并无内在冲突):
    • 静态结论钉的是能力一旦某次运行沿 TXEncryptMgr::CreateDataStoragesub_604A6C60打开/创建 该路径,则性质是 真实文件上的 TD 复合文档,不是「纯虚拟假名」。这 不蕴含:任意一台机器上 只要存在 Msg2.0.db,同目录就 必然 带着 Matrix.dat
    • Matrix.dat 依赖调用链是否跑到「要读/写 Seal 侧车」(例如 sub_31042D80 / PerfStand 一类路径,见 §15.3 表)。若从未触发、失败提前返回、或某版本/配置下加密侧未初始化,库文件仍可 grow,侧车文件可 始终未创建
    • 离线样本常见:只拷贝了 Msg2.0.db(或整机镜像里漏掉并列文件)、原装机已卸载/清理、杀软或手动删过 *.datQQ 安装路径与数据目录 与当前查看的文件夹 不是同一棵目录树(需在 Users\...\Tencent Files\<QQ号>\ 一类完整配置根下找 Msg2.0.db 同父目录Matrix.dat,而不是只看你手上的这份 msg2.0.db 拷贝所在目录)。
    • 仓库 README.md 已记录 本地曾找不到 Matrix.dat 的情形 —— 与 §14–§15 所述「密文还原依赖密钥材料」并行成立:没有侧车文件时,更难离线解密,但不反证「客户端从不实现盘路径」

15.3.2 何时真正创建磁盘上的 Matrix.dat(QQ2009 钉扎条件)

导出 ?CreateDataStorage@TXEncryptMgr@@…0x604A4CC0)只做:operator new 包装对象、把 wchar_t const * 路径写入对象内 CTXStringW*((_BYTE*)obj+16)=0并不在一开始就调用 TXOpenStorage。首次落到盘上发生在 sub_604A4D60 惰性取 IStorage 时:sub_604A6C60(this, 0)第二实参恒为 0 的这条主路径)。

sub_604A6C60(this, a2)Common.dll0x604A6C60)里,TXCreateCompoundDocument 仅当同时满足:

  1. this+0xC 上缓存的 ITXStorage* 仍为空(已成功打开过则直接返回 成功,不会再创建)。
  2. sub_604AA5A0(路径) 返回 非 0:路径非空;并对 父目录链GetFileAttributesW / 递归 + CreateDirectoryW —— 若无法在盘上构造出可写的目录前缀,函数 提前返回 0根本不会调用 TXOpenStorage / TXCreateCompoundDocument
  3. TXOpenStorage 失败:HRESULT < 0(文件不存在时通常会失败,才会考虑创建)。
  4. a2 == 0(惰性打开路径):若 a2 != 0,会先要求 GetFileAttributesW 成功且路径不是目录FILE_ATTRIBUTE_DIRECTORY 未置位),且 不会 走下面的创建分支;a2 == 0 做这一组「必须已存在普通文件」的检查。
  5. this+0x28this+40)为 0:若 非 0,在 TXOpenStorage 已失败直接返回 E_FAIL-2147467259同样不会调用 TXCreateCompoundDocument(静态语义:禁止在该对象上落盘新建,具体何种运行时状态会把 +40 置位尚未在此处钉死)。
  6. 以上成立后调用 TXCreateCompoundDocument(路径, 3, …)

补充:TXOpenStorage 若已成功(盘上已有可读 TD 文件),则 TXCreateCompoundDocument 不会执行 —— 你看到 Msg2.0.db 很大但没有 Matrix.dat,在静态语义下仍可能是:**惰性打开从未成功跑到 **sub_604A6C60****(根本没触发 Matrix.dat 路径),或 sub_604AA5A0 / this+40 挡住了创建 —— 不等于「库不完整」

IM.dll 侧何时会去拼 Matrix.dat 并调用 CreateDataStorage(仍不代表当次必落盘):

  • sub_606475A0CTXStringW::MakeLowerFind(…, L"msg2.0.db", 0) != -1(路径宽字符串里 必须出现 msg2.0.db 子串);然后 FS::CombineQNCMatrix.dat,再 TXEncryptMgr::CreateDataStorage;接着对返回的 ITXDataStorage* 做一次 vtable+0x10 调用并得到 vtable+0x8 上的对象 —— 任一步失败整条作废。也就是说:仅适用于「某条逻辑路径携带含 msg2.0.db 的长路径」,不是你的离线文件名随口叫 msg2.0.db 就一定会命中这条 helper。
  • sub_60647120CombineQNC(L"UserDataMsgStorage:", L"Matrix.dat")CreateDataStorageAddEncryptInfo):先判定 sub_60643D10(this) —— 内部依赖 seqbase.dat / lastmsginfo.dat 等经 FS::CreateFileW 打开与读取(失败则打 SeqHelper Init 类日志并 不调用 CreateDataStorage)。即使你自认目录里其它文件都在,只要 sub_60643D10 走失败分支,Matrix.dat 这一条根本不会发起。

15.3.3 「人话」:sub_60643D10 何时算失败;何时叫「没到 sub_604A6C60

一、sub_60643D10(QQ2009 IM.dll0x60643D10)——只在一条链上用

  • IDA xref sub_606471200x60647151)调用它。别的 Matrix / Seal 路径根本不跑这个函数;你若从没走过 sub_60647120,讨论 sub_60643D10 对你的 **Matrix.dat 有没有出现 无关

二、调用约定钉死(sub_60647120 开头,0x606471470x60647151

  • sub_60647120 的第一个参数 arg0(上层传来的 ITXDataStorage* 一类指针):
    • this 传给 sub_60643D10 的是 arg0 - 4lea ecx,[eax-4] + push ecx,栈参)。
    • ECXcall被设为 [arg0 + 0x18],作为 **sub_60643D10 的第二个参数 a2(宽字符上下文指针)。
  • 因此静态可复查的第一条失败条件[arg0+0x18] == NULLsub_60643D10 立刻 return 0,并打 SeqHelper Init fail,参数不正确(字面量 aSeqhelperInitF)。人话:上层给的 **SeqHelper 宿主对象里,偏移 0x18 那份指针没填好,整条专线一票否决。

三、sub_60643D10 里还怎样会变成 0(进不了后面的 CreateDataStorage

伪代码里 return HIBYTE(a2) 实为 复用指针高位当成功标志(编译器技巧);置 1 的路径都在 成功打开并处理了 seqbase / 写出缓冲 一类分支。大致可操作的判别

现象(静态) 含义(人话)
a2 为空 [arg0+0x18] 没指到合法宽字符串上下文失败
FS::CreateFileW(this+5 路径…) 两处尝试都拿不到可读 ITXFile seqbase.dat(或 FS 映射到的同名逻辑文件)在虚拟 FS 下打不开 → 走 unk_6084E424 日志串(与 seqbase 打开失败 语义一致),HIBYTE 保持 0sub_60643D10 返回 0
seqbase 可读但长度 < 4 seqbase size = 0 分支(约 0x60643eed),打调试日志;后续仍可能 HIBYTE=1 —— 不完全等价于「失败」,随后 LABEL_20 还会继续 CreateFileW(this+6 …) 处理 lastmsginfo

人话总结sub_60643D10 失败」在实务里优先核对:[宿主+0x18] 是否非空seqbase.dat 是否在 QQ 当时挂载的 FS 下能被 FS::CreateFileW 打开(路径是否真是你以为的那份、是否只拷了 Msg2.0.db 没拷 seqbase.dat、权限/占用)。

四、「惰性打开从没走到 sub_604A6C60」是什么意思

  • TXEncryptMgr::CreateDataStorageCommon.dll 0x604A4CC0只构造包装对象并写入路径,**不等于已经执行 **sub_604A6C60****。
  • sub_604A6C60 会被 多条 Common.dll 封装函数 直接 调用,例如:
    • sub_604A71000x604a714e):若 a2&&a3 非空、且 a1[10]==0,则 sub_604A6C60(a1, 0),再继续 vtable+0x38 / +0x30 等;
    • sub_604A84600x604a8460):若 *(a1+40)==0 且两个宽串非空,则 sub_604A6C60(a1, 0),再拼流路径。
  • 人话只要你拿到的 ITXDataStorage* 从来没被客户端拿去执行「需要底层 IStorage 已经就绪」的操作(读某个属性流、打开子存储等——对应上述封装入口),sub_604A6C60 就不会执行,Matrix.dat 也不会被创建或打开。这和 Msg2.0.db 是否在膨胀无关:后者走 OLE 挂载,前者走 TXEncryptMgr 侧车路径

15.3.4 QQ2009 端到端:UserDataMsgStorage:Matrix.dat / bufSvrSealEnc 全链条(钉扎)

约定IM.dll ImageBase 0x60600000Common.dll ImageBase 0x60400000(与 §15.3.2–15.3.3 一致)。TXEncryptMgr::CreateDataStorage 的导入桩 __imp_…IM.dll0x60842238

(1)底层:KernelUtil 先把物理消息库挂到虚拟前缀

§19FS::AddFileSystem(2, <Msg2.0.db 绝对路径>, L"UserDataMsgStorage:", …)。没有这一步,后面所有 UserDataMsgStorage:\… 宽路径都无法解析。

(2)IM.dllCreateDataStorage 的全部八条调用点(对 0x60842238code xref 穷尽)

前四行打开的是 …\info.dat(经 UserDataMsgStorage:),**不是 Matrix.dat;后四行才会拼 L"Matrix.dat"

地址 函数 拼的路径 / 行为(摘要)
0x60638B60 sub_60638B60 UserDataMsgStorage: + (对象 a1+8 的通道名 BSTR) + \info.dat;惰性 CreateDataStorage 写入 a1+28 上的 ITXDataStorage*;再 vtable+0x10 调用。
0x60638C20 sub_60638C20 与上一行 同路径拼接;差别仅在 vtable+0x0C(与 §12 所述 +12/+16 成对读写 对齐)。
0x60639D70 sub_60639D70 Format(L"%s%s\\%s\\info.dat", L"UserDataMsgStorage:", …)(两截 BSTR 来自对象 +12/+16);惰性 CreateDataStorage 写入 a1+48vtable+0x10
0x60639E50 sub_60639E50 同上 Formatvtable+0x0C
0x60647120 sub_60647120 CombineQNC(L"UserDataMsgStorage:", L"Matrix.dat")CreateDataStorageCreateSvrSeal(1)AddEncryptInfo此前必须 sub_60643D10 成功(见 §15.3.3)。仅经虚表指针引用(0x6084D5B4/0x6084D684** 数据 xref),无指向函数入口的 code xref(见下 (3.1))。
0x606475A0 sub_606475A0 路径 Find(L"msg2.0.db")CombineQNC(基路径, L"Matrix.dat")CreateDataStoragevtable+0x10vtable+0x8(轻量打开/查询链)。
0x60648620 sub_60648620 Find(L"msg2.0.db")msgss.db → **Matrix.datCreateDataStoragevtable+0x10 → 按名 bufSvrSealEncvtable+0x44(十进制偏移 +68,属性读写) —— 与旧版笔记里 sub_31042D80(QQ2010)同一职责:**从 Matrix 拉 bufSvrSealEnc
0x60649500 sub_60649500 **CombineQNC(调用方传入的基前缀, L"Matrix.dat")CreateDataStorage → 读 bufSvrSealEncITXBuffer 校验 → CreateSvrSeal(1) / Seal 加密对象衔接(失败码 0xE0632710 等特判)。
(3)谁把这八处串进「用户能感知」的业务
  • sub_60648620唯一 code xref 来自 sub_60648D700x60648ed4)。sub_60648D70校验盘上 Msg2.0.db 存在FS::AddFileSystem(2, …, L"CheckMsg:", …) → 构造 ITXData → 若 sub_60648830 某条件不成立则调用 sub_60648620(物理路径, L"CheckMsg:")bufSvrSealEnc → 最后 RemoveFileSystem(L"CheckMsg:")。即 「检查/修复消息库」向导链路上的 Matrix 读取
  • sub_606475A0sub_60649500对二者函数入口的 code xref 均只有 sub_6064F430ImportMsg:),调用点分别为 0x6064f56b0x6064f589sub_6064F430** 内部按 this+11 路径与分支msgex/msgsssub_606478C0buddy 解析 等)择路。sub_6064F650sub_6064F430 放进虚表;sub_60635C90 工厂 operator new(0x58) 并挂 off_6084D5F0 / off_6084D5A8 / off_6084D594间接调用 该虚方法(数据 xref:0x60635d00)。
  • info.dat 四条sub_60638B60 / 60638C20 / 60639D70 / 60639E50 无直接代码 xref(典型 COM/接口表或间接调用),语义上对应 §12 的 **按会话 info.dat 惰性 **CreateDataStorage****。
(3.1)Matrix.dat 三条「命名函数」+ sub_60647120:IDA code xref 印证与局限(QQ2009 IM.dll

前提:下列为对 函数首地址xref type == code(IDA);不统计寄存器间接跳转、不覆盖插件/热补丁;**仅代表本 IM.dll 静态库。

目标 指向入口的 code xref 条数 钉死的直接上层(若有) 备注
sub_606475A0 1 sub_6064F4300x6064f56b 在本 DLL 内 没有其它 call sub_606475A0
sub_60648620 1 sub_60648D700x60648ed4 「检查消息库」CheckMsg:)链路。
sub_60649500 1 sub_6064F4300x6064f589 「导入消息」ImportMsg:)链路之一支。
sub_60647120 0(入口) 不经过 call经 COM 虚槽 / QI 可达 数据 xref:off_6084D5B0/D678/D664/D5A8/D594 等(§15.3.6)。sub_60636130 首参 = pUnkOuter聚合与非聚合两套布局在次级虚表均暴露同一 sub_60647120 指针§15.3.5(3))—— 再说「pUnkOuter==0 就不走 60647120」。
sub_60648D70 0(入口) 不经过 call经 COM 虚槽 数据 xref:0x6084D5D80x6084D6A8off_6084D5A8 / off_6084D678CheckMsg:12)。与 sub_60647120 同类:IM.dll 内不存在「谁 call 了检查消息库入口」 的静态边;顶层为宿主对已创建对象的 虚调用

口语归纳的边界:说「检查消息库导入消息」会碰 Matrix.dat —— 对 60648620 / 606475A0 / 60649500 这三条 有直接代码印证call60648D70 / 6064F430);60647120** 与 60648D70仅能从 COM 业务接口进入不是「未解析的神秘分支」。另:CreateDataStorage 另外四条走的是 info.dat勿与 Matrix.dat 混谈

(4)Common.dll:谁在 CreateDataStorage 之后 触发 sub_604A6C60

sub_604A6C60直接 call14 处(IDA xref type code 穷尽):sub_604A4D60sub_604A7100sub_604A8150sub_604A8260sub_604A8460sub_604A8790sub_604A8960sub_604A8B40sub_604A8D30sub_604A9090sub_604A9360sub_604A96D0sub_604A99F0sub_604A9E80。其中 sub_604A4D60 的指针落在 off_6057F940 系虚表(数据 xref 0x6057FAE4 / 0x6057FB2C),是 CreateDataStorage 包装对象IStorage 的主惰性入口;其余封装均在 「两参数宽路径 / 三参数流拷贝 / 打开子存储」 等操作前 sub_604A6C60(this,0)人话IM 调了 CreateDataStorage 之后,只要后续走到「在 TD 里按名字碰流/子存储」的 Common 侧实现,就一定会先落到 sub_604A6C60;若 IM 只拿到了指针却不再调用这些封装,则磁盘侧车永远不会被打开。

(5)一图式串联(只列主线)

挂载KernelUtil::…AddFileSystem(UserDataMsgStorage:)§19)→ FS::CombineQNC 任意 UserDataMsgStorage:\… 有效八条之一 CreateDataStorageMatrix.dat / info.dat / …)→ Common.dll 某封装 sub_604A…sub_604A6C60TXOpenStorage / TXCreateCompoundDocument

bufSvrSealEnc(与 §15.4 表对照):静态已钉 → sub_60648D70sub_60648620CheckMsg:);sub_6064F430sub_60649500ImportMsg:,分支条件见 (3)/§15.3.5)。另:sub_60647120CreateDataStorage(Matrix.dat)上层同一 coclass 之 COM 虚槽§15.3.5–15.3.8),不是未解析的「神秘界面」。

消息初始化路径 sub_31041510 调用 Util::SvrSeal::CreateSvrSeal(1, …)(类型 1),再 AddEncryptInfo —— 与登录侧 类型 2 区分,表示 两套 Seal 上下文(同一 API,不同实例/用途)。

15.3.5 Matrix.dat 生成/打开:上层条件总表(QQ2009 IM.dll + Common.dll

约定「生成」 = 盘上首次 TXCreateCompoundDocumentCreateDataStorage 后惰性打开并创建§15.3.2);「仅打开」 = 已存在 TD 文件则 TXOpenStorage 成功TXCreateCompoundDocument

(0)跨线公共前提(几乎恒成立)
  • 虚拟前缀解析KernelUtil.dllAddFileSystem(…, L"UserDataMsgStorage:", …)§19),否则 UserDataMsgStorage:\… 宽路径在 FS::CombineQNC 侧无意义
  • Common 惰性落盘门闩§15.3.2):sub_604AA5A0 成功TXOpenStorage 失败a2==0this+0x40==0同时满足,才会 TXCreateCompoundDocument;否则可能 仅持有 ITXDataStorage*从未 在当次运行中 创/开 盘文件。
(1)CheckMsg: 线(sub_60648D70sub_60648620)— 读 bufSvrSealEncCreateDataStorage
条件 静态含义
lpFileName 合法且盘上可读 GetFileAttributesW(lpFileName) != -1;否则 E_POINTER-2147024809)提前返回。
CheckMsg: 挂载成功 FS::AddFileSystem(2, …, L"CheckMsg:", …)(忽略返回值语义时需结合上下文)。
CreateTXData 成功 分配 ITXData* 失败则 E_FAIL
sub_60648830(L"CheckMsg:", a3, …) 返回假 才会 call sub_60648620(物理库路径, L"CheckMsg:");若该函数先 成功 建立/读到加密元数据,则走 另一支eEncryptType / strEncryptQuestionsub_60648620
sub_60648620 内部 路径 MakeLower:若含 msgss.db直接返回成功标记且不 CreateDataStorage短路);否则必须 Find(msg2.0.db) 命中CombineQNC(…, Matrix.dat) + CreateDataStorage
(2)ImportMsg: 线(sub_6064F430sub_606475A0 / sub_60649500

sub_6064F430 反编译(this+10/+11/+12/+13/+16 为对象字段)为准:

条件 静态含义
*(this+10) != 0 整条导入块才会执行;否则函数 尽早返回ImportMsg:Matrix.dat 两条 helper
挂载与 sub_606476E0 分支 CTXBSTR(this+11) + FS::AddFileSystem(…, L"ImportMsg:", …),再 sub_606476E0(路径, L"ImportMsg:", &v21)若为真:进入 msgss/buddy/CreateTXBuffer子分支sub_60649110 / sub_6064B160 等),**此树内未必调用 sub_606475A0;若为 :落入 else if (sub_606478C0) / else
sub_606478C0(L"ImportMsg:") 为真 sub_6064BEE0走本节 Matrix.dat 两 helper
上述均为假时的 else sub_606475A0(路径, L"ImportMsg:")0x6064f56b)。仅当其返回 非 0sub_606475A0 内对 msg2.0.db 打开链成功)时:若 *(this+12) != 0sub_60647A30 + sub_6064E6E0*(this+12)==0sub_60649500(this, 路径, L"ImportMsg:")0x6064f589**)。**若 sub_606475A0 返回 0(路径里 没有 msg2.0.db 子串或存储打开失败),落 sub_6064CB90 支,调用 sub_60649500
sub_606475A0 自身 Find(L"msg2.0.db") 必须命中;然后 CombineQNC(基前缀, L"Matrix.dat") + CreateDataStorage,并对 ITXDataStoragevtable+0x10vtable+0x8 轻量探测;任一步失败则 返回 0
sub_60649500 自身 CreateDataStorage 失败或 bufSvrSealEnc / ITXBuffer 校验失败 返回 0xE0632710-530372848)等;成功路径可能 CreateSvrSeal(1)
(3)sub_60647120 线(SeqHelper + AddEncryptInfo)— COM 聚合 vs 普通创建
事实 说明
sub_60636130(a1,a2,a3) 的分支 a1 != 0sub_60635EC0a1 == 0sub_60635C90(0,…)
a1 的语义(更正) sub_60609D10(ATL 风格 CreateInstance 外壳)通过 (*(…))(this+0x24) 调用 sub_60636130 时,第一形参来自 CreateInstancepUnkOuter(反编译中 sub_60609D10(a1,a2,a3,a4) 对槽函数调用 (a2,a3,a4),其中 **a2 即 outer)。因此 a1 非「神秘 flag」,而是 标准 COM:外层控制对象非空 ⇒ 聚合(aggregation)内对象创建
聚合路径 pUnkOuter != 0sub_60635EC0operator new(0x60) + sub_60635DA0:主虚表 off_6084D6E4*(this+8) 起依次为 off_6084D6C0off_6084D678off_6084D664*(this+20)=outer。次级调度虚表 off_6084D678[3]sub_60647120(见 §15.3.6)。
非聚合路径 pUnkOuter == 0sub_60635C90operator new(0x58):主虚表 off_6084D5F0obj[1]=off_6084D5A8obj[2]=off_6084D594更正非聚合对象同样在次级虚表暴露 sub_60647120off_6084D5A8 槽 [3](及 off_6084D594 槽 [8] 再起一段相同尾部)与 off_6084D678[3] 同源指针。因此「Import 只有 sub_6064F430、绝无 60647120」不成立:直接 callImportMsg:/Matrix.dat helpers 仍只见 606475A0/6064950060647120** 在该分支下主要为 COM 虚槽 / QI 可达,与 sub_6064F430 并行(不同调用约定)
对象表钉扎(本映像) .data0x608C82F8 指向 **CLSID 本体 0x60843E9C{A875AE08-E87E-48F4-8806-CE4E1BE59758};紧随 0x608C83000x608C8304sub_6060AC30 / sub_60636130,与本组件 ATL 注册块 同一邻域 —— 哪个外层 coclass 在运行时以聚合方式创建该接口 → 需结合宿主进程/UI DLL,但 静态上已把「触发源」收窄到「聚合 CoCreateInstance」这一类
函数体内门槛 sub_60643D10 失败§15.3.3)则 **打 SeqHelper Init 失败 日志并 E_FAIL执行 CreateDataStorage
(4)小结:四条 Matrix.dat 触顶条件(不含 Common.dll 惰性细节)
线 顶层充分条件(代码层)
sub_60648620 sub_60648D70 走到 call sub_60648620,且 msgss.db 未短路msg2.0.db 命中
sub_606475A0 sub_6064F430 活跃*(this+10)!=0)且分支落到 sub_606476E0sub_606478C0、并最终 call sub_606475A0;且 sub_606475A0Find(msg2.0.db) 命中
sub_60649500 同上 ImportMsg: 树,且 sub_606475A0 已成功返回*(this+12)==0,从而 call sub_60649500
sub_60647120 虚槽调用 + sub_60643D10 成功构造路径pUnkOuter != 0sub_60635DA0 布局pUnkOuter == 0sub_60635C90 布局两套均在次级虚表 [3](或 D594 [8] 嵌套)挂 sub_60647120§15.3.6**)。**仅当宿主通过对应 IID 调该槽且 SeqHelper 就绪,才实际 CreateDataStorage(Matrix.dat)

15.3.6 IM.dll 消息库 COM 对象:虚表 / ATL 映射静态全集(QQ2009,ImageBase 0x60600000

本节把 sub_60636130 分叉两侧的 .data 虚表 钉到 函数名,并说明 AtlInternalQueryInterface 用的 接口映射块槽索引均为「函数指针下标」(×4 字节)

(1)构造体与三套接口指针
构造 大小 主虚表(对象首字 this+8 / +12 / +16 三接口指针
sub_60635DA0(聚合链 sub_60635EC0 0x60 off_6084D6E4 off_6084D6C0off_6084D678off_6084D664*(this+20)=outer
sub_60635C90sub_60636130pUnkOuter==0 0x58 off_6084D5F0 obj[1]=off_6084D5A8obj[2]=off_6084D594
(2)IUnknown / CreateInstance 钉扎
  • sub_60609D10(*(…))(factory_this+0x24)(pUnkOuter, riid, ppv)sub_60636130(即 CreateInstance Creators 槽,不是 sub_60647120 本体)。
  • sub_6060E6E0return sub_607D44B0(a1-4, a2, a3)this 调整 -4 的标准 IUnknown 转发(指向聚合对象 inner 的首部)。
  • 主接口 QueryInterface
    • off_6084D6E4[0]sub_60635E30AtlInternalQueryInterface(a1 + 8, &off_6084D618, …) — QI 相对真实对象 +8(首个接口指针槽)。
    • off_6084D5F0[0]sub_60635D80AtlInternalQueryInterface(a1, &off_6084D618, …) — QI 相对 对象首址(无 +8)。
  • sub_60635E30 快路径:若 riid 通过 IUnknown 判定(反编译:*a2==0 && a2[1]==0 && a2[2]==192 && a2[3]==1174405120),则 直接返回 thisAddRef;否则走 off_6084D618 映射表。
(3)AtlInternalQueryInterface 映射 off_6084D618:首部 IID
  • unk_60848E80 起首 16 字节(LE) 解析为 IID{EB5398EC-11ED-49EE-A1FA-B478FAA157C6}
  • 紧随其后的 wchar_t[]MSGFILE/SHARE壳层注册串 混排 —— 整块作为 _ATL_INTMAP / _ATL_OBJMAP_ENTRY 邻域数据 被链接进 off_6084D618后续 dword 中夹杂 0x00000004/0x00000001ATL 元数据不是有效 call 目标off_6084D5F0 主虚表 [10]– 亦可见同类 「代码/数据交错」)。
(4)**次级调度虚表 off_6084D678(聚合对象 *(this+12))— 全槽函数

off_6084D5A8(非聚合 obj[1])从 「dtor + Matrix 槽」函数指针序列对齐D5A8 [2]=sub_6060F1E0 对应 D678 前接 IUnknown 三槽)。

地址 语义(反编译摘要)
0 sub_6060E6E0 sub_607D44B0(this-4, …)IUnknown
1–2 sub_6062B410 / sub_6062B400 引用计数 / IUnknown 余槽
3 sub_60647120 UserDataMsgStorage: + Matrix.dat + CreateSvrSeal(1) + AddEncryptInfo§15.3.3
4–7 sub_6064AFB0sub_6064A910 Seal / Matrix 辅助(.delegate this+32 等)
8 sub_60647500 *(this+32) 空则 sub_60638630;再 vtable+12 转发
9 sub_606470A0 vtable+16 单次调用
10 sub_60647410 **链式 vtable+24 取子对象再聚合调用
11 sub_60649840 ExportMsg:DeleteFileWAddFileSystem(ExportMsg)QueryEncryptinfo.dat/content.dat/msg.dat 拷贝等 整段导出
12 sub_60648D70 CheckMsg: — 与 §15.3.4 所述 call sub_60648620 同一函数
13 sub_60648FB0 CheckMsg: 另一入口 — sub_606476E0 / sub_60647F80 / sub_60647A30 分支
14 sub_6064A5C0 **写 CTXBSTR 路径、sub_60646F10TXTimer::SetAsyncCallback异步导出/回调登记
15–16 sub_607C5060 ×2 占位 thunk
17 sub_60646E40 下游委托
18–26 sub_607D44B0sub_606497D0 ATL/Tencent 通用 thunk 与 sub_606497D0(与 D5F0[8]/D5B0[24] 同源)
27 sub_60635E30 **再次出现 QueryInterface聚合对象 secondary 侧 QI)
28+ sub_606E6300 尾部 IUnknown/tear-off 与 GUID 常量区交叠 — **静态上视为 元数据/衔接块勿按代码指针调用
(5)off_6084D5B0 / off_6084D664 的关系
  • off_6084D5B0「缩短版」调度表[0]=sub_6060F1E0(dtor)[1]=sub_60647120[2..14]off_6084D678 同一业务序列偏移对齐[16]=sub_60635D80直接 AtlInternalQueryInterface(..., off_6084D618));[25]=sub_60635C30[26]+** 为 unk_60848E80 + 小整数,同 (3)ATL 掺数据
  • off_6084D664第三接口指针sub_60635DA0 写入 *(this+16)):不是 off_6084D678 的简单尾拷贝 —— §15.3.7 钉死:虚表首址 0x6084D664;槽 [0]–[4] 为 sub_60737350sub_607F6850sub_606DB400sub_60647070sub_60649800(Tencent 前缀 thunk);自 [5] 起 sub_6060E6E0IUnknown)…,sub_60647120 落在槽 [8]**(对照 off_6084D678 [3]:**整块 IUnknown+业务 序列相对前移 5 槽)。内联 IIDAtlInternalQueryInterface 映射§15.3.7
(6)结论(回应「虚函数挖完没有」)
  • sub_60647120唯一实现体出现在 off_6084D5B0[1]off_6084D678[3]off_6084D664[8]off_6084D5A8[3]off_6084D594[8].data —— 语义均为同一实现经由 不同虚表/前缀槽位 暴露(§15.3.7** 补 off_6084D664)。
  • CheckMsgsub_60648D70(槽 12)与 sub_60648FB0(槽 13)并存仅前者sub_60648830 失败call sub_60648620§15.3.4)。
  • ExportMsgsub_60649840(槽 11)独立大函数,与 ImportMsg:sub_6064F430 不同栈,但 同属该 COM 对象的业务接口

15.3.7 AtlInternalQueryInterface(&off_6084D618):INTMAP 逐项解析 + 内联 IID/off_6084D664(已钉)

.rdataDWORD 级遍历:off_6084D6180x6084D618)起为标准 **_ATL_INTMAP_ENTRY 对 **(const IID *piid, DWORD_PTR dw)****,**直至 (NULL,NULL)

(1)INTMAP 主体(0x6084D6180x6084D648
# piid(_VA) dw resolved IID(若 piid 指向 16B GUID) 静态含义
0 0x60848E80 0x00000004 {EB5398EC-11ED-49EE-A1FA-B478FAA157C6} ATL 映射第一项:**接口指针相对 IUnknown/对象基址 的 **ATL 偏移 **dw=4****(与对象 this+4 字段共存约定一致)。
1 0x00000001 0x6084D654 ATL 扩展项(**第一DWORD为哨兵 1):**第二DWORD为 const IID *0x6084D654,解析见下行。
0x6084D654 (内联存储) {CC6B8374-D121-C849-A8B6-97C973A26192} 物理存放INTMAP 项 #1 所指 IID,亦为 0x6084d650 区段内联 GUID(见 (2))。
2 0x00000008 0x00000001 哨兵/附加字段项(典型 ATL/COM 链式映射宏变体);**具体宏名需对照编译期 atlcom.h,静态 仅钉数值
3 0x60846A48 0x00000000 {5D206F0D-9BFE-4691-B1CE-1C7659BCAC0D} dw=0:**接口指针与 QI 接受的 this 同址零偏移绑定
4 0x00000001 0x60846A58 同类型哨兵项第二DWORD 0x60846A58 → IID {00020400-0000-0000-C000-000000000046}IDispatch**)。
5 0x00000000 0x00000001 列表尾部附加项(0,0) 终止子);ATL 实现细节项
结束 0x00000000 0x00000000 INTMAP 终止子{NULL,NULL})。
(2)INTMAP 之后的 内联 IID + vtable0x6084D650 起**)**
  • 0x6084D650DWORD 0 填充。
  • 0x6084D65416 字节 IID = {CC6B8374-D121-C849-A8B6-97C973A26192}(与 表项 #1 指针一致)。
  • 0x6084D664即符号 off_6084D664 — 该 IID 对应虚表首址。槽 [0]–[4]:sub_60737350sub_607F6850sub_606DB400sub_60647070sub_60649800;槽 [5]–[7]:IUnknownsub_6060E6E0sub_6062B410sub_6062B400);槽 [8]:sub_60647120;槽 [9]–[15]:与 off_6084D678 同序列(6064AFB060647410)。
    结论:第三接口相对 off_6084D678 多 5 个前缀槽,
    60647120 的槽下标由 [3] 变为 [8]** —— §15.3.6(5) 已据此更正。
(3)unk_60848E80 的 UTF-16 尾巴

0x60848E80+0x10 起为宽字符串字面量(MSGFILE… / …IN_SHARE / …CALLBACK 碎片 等),与 IID 头 16 字节同属 ATL 注册/类型库邻域 —— 勿把 UTF-16 字符单元当成 IID* 指针去解引用

15.3.8 顶层调用链再挖:Matrix.dat 何时首次生成(盘上新建 TD)

区分三件事:(A)业务上 谁会走到 CreateDataStorage(UserDataMsgStorage:\Matrix.dat);(B)惰性 I/OCommon.dll sub_604A6C60 是否执行;(C)在该执行路径上 TXOpenStorage 失败且 §15.3.2 全成立 时,才会 TXCreateCompoundDocument —— 才是「生成」Matrix.dat 文件。仅有 Msg2.0.db 或仅 CreateDataStorage 返回指针不足以 保证(C)。

(1)IM.dll 内「谁调用顶层入口」— xref 复查(QQ2009)
  • sub_60648D70CheckMsg: 槽 12,会 call sub_60648620:对函数入口的 code xref 条数为 0 .data 0x6084D5D80x6084D6A8 指向其地址 —— 与 sub_60647120 同构:检查消息库 在 **IM.dll 内也不是普通 call 图可达,而是 消息库 coclass 实例化之后虚表槽 12 的调用。
  • sub_6064F430ImportMsg: 主入口,call sub_606475A0 / sub_60649500 sub_6064F6500x6064f724数据 引用;sub_6064F650 在构造里执行 *(_DWORD *)(*(this+18)+16) = sub_6064F430 —— 把导入逻辑挂到 this+18 所指 内部委托对象vtable 槽不是 主对象 off_6084D5F0[0] 直接指向 sub_6064F430sub_6064F650sub_60635C90 / sub_60635DA0CoCreate 路径上调用(0x60635d00 / 0x60635ddc
  • 结论跨 DLL 的「顶层」 = 某宿主(主程序 / MsgMgr.dll / 脚本宿主等) CoCreateInstance(CLSID …) 拿到 {A875AE08-E87E-48F4-8806-CE4E1BE59758}0x60843E9C)对象后,再 QueryInterface/虚调用CheckMsg: / ImportMsg: / 槽 [3] sub_60647120 等。§17MsgMgr.dll sub_61E4D460(导入向导状态 20)与 sub_61E4B590Msg2.0.db 路径,是 业务侧导入 最相近的静态锚点;在本仓库未附带 MsgMgr 二进制供 IDA 自动化时GUID→具体「哪一个按钮/哪一个 CoCreate 调用点」 仍需在 宿主 IDB 内对 16 字节 CLSID相关 ProgID 做检索 —— IM.dll 单独静态分析无法再接上一层
(2)「生成」Matrix.dat 的充分条件(把 §15.3.2 接到具体 IM 路径)

(A)KernelUtil 已挂 UserDataMsgStorage:§19)前提下,任一条 成立且 后续真正触发 Common 惰性打开 时,才可能 在盘上 新建 Matrix.dat(若已存在则 只打开):

业务线(IM) 进到 CreateDataStorage(Matrix.dat) 的最窄门槛(§15.3.5 已列) 进而触发 sub_604A6C60 的典型动作
sub_60648D70sub_60648620 sub_60648830("CheckMsg:",…)==0;路径 Find(msg2.0.db) 命中且 msgss.db 短路 CreateDataStorage 后对 ITXDataStorage*vtable+0x10(取子对象/打开存储)vtable+0x44 按名读 bufSvrSealEncsub_60648620** 反编译 0x606487050x60648760)— 会拉 IStorage,从而进入 sub_604A6C60
sub_6064F430sub_606475A0 *(this+10)!=0sub_606476E0/sub_606478C0 分支不抢走,且路径 Find(msg2.0.db) 命中 同上,vtable+0x10+0x8 轻量链(§15.3.4
sub_6064F430sub_60649500 在上一行 sub_606475A0 已成功*(this+12)==0(见 §15.3.5(2) **读 bufSvrSealEnc / ITXBuffer 校验 等 — 必碰存储
sub_60647120 sub_60643D10 成功§15.3.3),随后 CreateDataStorage + AddEncryptInfo 注册 Seal 后,若 仍有代码路径 对同一 ITXDataStorage*需底层 IStorage 的操作,才 落盘CreateDataStorage 不读不写仍可能无文件(与 §15.3.3「没到 sub_604A6C60」一致)

人话:**只有在你跑了「检查消息库」或「导入消息」里命中 msg2.0.db 分支、或 有别处虚槽调用了 sub_60647120 且 SeqHelper 成功 —— 并且 Common 允许创建 —— 目录里才会 第一次出现 Matrix.dat。日常 只登录、只收发消息从未 触发上述 UI/组件路径,可以一直没有该文件;这与 Msg2.0.db 存在 不矛盾

15.4 密钥与封印材料(ITXData 字段名 — 来自宽字符串字面量)

下列名称均在 IM.dll 中以 CTXBSTR + ITXDataStorage/ITXDataRead vtable 偏移 +68(按属性名读写缓冲区)等形式出现:

字段名 出现场景 作用(推断级别)
bufSvrSealEnc sub_31042D80(路径含 msg2.0.db 时) UserDataMsgStorage: + Matrix.datITXDataStorage 读取;随后 sub_31010F20 校验 — 服务端封印扩展,与消息加密链路绑定。
bufPwdHashOne sub_31022140 读出后放入 TXEncryptMgr::Init;与 本地口令/二次哈希 相关(具体哈希算法在 Common.dll)。
buf16byteSessionKey sub_3101CF30(登录) Util::Data::GetTXDataBufCTXBuffer;配合 Util::Time::SetServerTime16 字节会话密钥,显然参与后续 ITXEncrypt 或 Seal 密钥派生
bufSigLastLoginInfo 同上 另一块与上次登录相关的 签名/绑定缓冲,用于 TXEncryptMgr / Seal 完整性或密钥封装
cPassSeqID sub_3101CF30 登录 ITXData 上的 uint8;与 Util::SvrSeal::CheckKeyID(..., ITXEncrypt *) 搭配 — 密钥序号/版本,用于校验 ITXEncrypt 是否与当前 Matrix.dat / 封印一致。

离线含义:仅有 buddy/.../content.dat 而不具备 Matrix.dat + buf16byteSessionKey/bufSvrSealEnc/bufPwdHashOne 中已在磁盘落库的材料,无法在纯 Python 内还原明文 —— 必须先复现 Common.dll 中的 派生与解密

15.5 固定 CLSID 与 ITXEncrypt 用法(消息加密)

多处 TXEncryptMgr::QueryEncrypt 之前写入同一 GUID 常量:

  • *(QWORD*)&guid.Data1 = 0xDAF87E8B49DB3AB8
  • *(DWORD*)&guid.Data4[0] = -542260844 → 无符号 0xDFC05FD4
  • Data4 后 4 字节为 0,则规范字符串为:{49DB3AB8-7E8B-DAF8-D45F-C0DF-00000000}(Python uuid.UUID(bytes_le=…) 验证)。

sub_31030080 / sub_31033B40(消息打包写会话存储)在 QueryEncrypt 成功后:

  • ITXEncrypt vtable 第一个接口方法(相对接口指针偏移 +12,紧接在 IUnknown 三个槽之后):在 Common.dll 工厂虚表 off_301B9F70 上对应 sub_30095FA0(见 §16.7)→ sub_30095CD0sub_30001FA0 —— 加密写盘不是 sub_30002340 信封解密链)。
  • 头部拼接 Util::Msg::GetMsgTimeGetMsgRand32,再 sub_31040B60 / sub_3103DE60 写入 index.dat/content.dat 对应路径

读档 / 解密复原应对 ITXEncrypt 上下一槽(工厂虚表里为偏移 +16sub_30095FE0sub_30095E10sub_30002340(见 §16.7)。

15.6 content.datTXEncryptMgr 的关系(重要)

sub_3102FDB0index.dat / content.datFS::CreateFileW不在此处调用 TXEncryptMgr。结合 §14 的高熵密文:

  • 磁盘上的密文 = 上层已用 ITXEncrypt(或等价链路)处理后的字节,再写入 ITXFile
  • 解密入口不在 FS::CreateFileW,而在 读出 ITXBuffer 之后、进入 TranslateOldMsgToMsgPack / sub_3102E8F0 之前 —— 走 ITXEncryptsub_30095FE0 槽位(相对 IUnknown+16)→ sub_30002340(实现位于 Common.dll;与 +12 写加密槽 不对称,见 §16.7)。

15.7 迁移工具中的 TD / NF 容器(非算法,属封装格式)

sub_31285C50:读流首 4 字节,区分 16860244 (54 44 01 01,TD)16860750 (4E 46 01 01,NF);TD 分支 sub_31285AD0(日志 CTXConvertSS2CDMgr::LoadFromTDFile)把 ITXBuffer 解成 ITXData 字段(跳过 pExtraInfo)。这是 数据库迁移时的文档解析不是 TXEncryptMgr 内部对称算法本身;真正加密仍由 Common.dll 提供。

15.8 移植 vs ctypes/Wine 调用现有 DLL(工程建议)

路线 条件 说明
完整移植 Common.dll 中核对 MD5/XXTEA/sub_30002340 信封Matrix 字段派生(见 §16 已确认 非 AES/RC4:会话侧对称核心是 XXTEA(64-bit 块) + 自定义二进制信封ITXEncrypt 虚函数体内仍需对照 Common.dll 内对应类的 xref。
就地调用 Linux 上 wine + 拷贝 Common.dll + 依赖 DLL + 最小桩还原 FS/UserDataMsgStorage 映射 通过 ctypes.cdll.LoadLibrary 取导出符号(注意 thiscall/stdcallHRESULT);仍需 有效 Matrix.dat 与密钥材料,否则 Init/QueryEncrypt 失败。
务实取证 Windows / Wine 里跑 QQ.exe + 钩子ITXEncrypt +12 出入参 可比静态逆向更快拿到 明文缓冲与密钥长度

对称算法骨架见 §16QueryEncryptITXEncryptsub_30002340 的静态钉扎见 §16.7(已完成)。下一步工程验证:在 buf==1 信封 + this+31 密钥buddy/.../content.dat 切片之间 对齐帧边界 实测。


16. Common.dllTXEncryptMgr::Init、MD5、XXTEA 与信封解密(IDA 已加载 Common.dll.i64

模块基址0x30000000。导出 TXEncryptMgrInit 0x300990D0QueryEncrypt 0x300995B0AddEncryptInfo 0x300998A0CreateDataStorage 0x3009A110

16.1 TXEncryptMgr::InitMD5(uint32 flags ∥ 16 字节材料)

sub_30098DE0Init 本体):若 ITXBuffer 提供的载荷长度 Size == 16

  1. sub_30001000:初始化标准 MD5 状态向量 — 0x674523010xEFCDAB890x98BADCFE0x10325476(与 RFC 1321 IV 一致)。
  2. sub_300016C0:依次 MD5_update(flags, 4)MD5_update(payload, 16)
  3. sub_30001A80MD5 结尾填充与摘要,把 16 字节摘要写回 TXEncryptMgr 全局状态(供后续 QueryEncrypt / Seal 使用)。

即:口令/会话种子经过「4 字节标志 + 16 字节二进制」再 MD5,不是裸用 16 字节当对称密钥。

16.2 sub_30001CF0:单块 XXTEA 解密(64-bit 块 + 128-bit 密钥)

反编译特征:

  • 输入 uint32 v4, v5_byteswap_ulong 做大端置换后与 4×32-bit 密钥字 v8[0..3] 交替迭代;
  • 循环变量 v6 初值 0xE3779B90(即 0x9E3779B9 * 16),每轮 v6 += 0x9E3779B9,共 16 轮
  • 更新式含 (x + (y<<4))(y>>5) 形式的 XXTEA 典型混合。

结论:**腾讯在此处使用 Corrected Block TEA(XXTEA)对一个 8 字节块解密;密钥材料为 紧跟调用点的 16 字节(四 dword)

16.3 sub_30002340:基于 XXTEA 的 变长二进制信封(密文解析)

a2 % 8 == 0a2 >= 16 前提下:

  • 先用密钥 a3(128-bit)sub_30001CF0 派生 8 字节工作态 v27
  • 根据首部 v27[0] & 7 计算实际明文长度并写回 a5
  • a1 指向的缓冲区解释为:版本/类型字节 *a1 == 1(与 sub_30095E10 中判断一致)+ 后续 8 字节对齐密文
  • 内层双重循环:XXTEA 反馈式 XOR(块链),把解密结果写入 a4

该函数被 sub_30095E10(封装 ITXBuffer → 明文 CTXBuffer)、sub_300D5F70、以及 sub_3000C5C0(读 txssogbcf.db)等多处调用。

16.4 Seal / Matrix.dat 上下文初始化(sub_300960A0 / sub_30097FF0

sub_300996F0AddEncryptInfo 主体)在注册 GUID 对应的链表节点后,调用 sub_300960A0(最后一参非 0)或 sub_30097FF0(最后一参为 0),从挂载的 ITXDataStorage 读出字段并完成 16 字节会话密钥材料写入对象偏移 this+31

典型 ITXData 键名(宽字符串):bValidInitbSysLocalHashbufRandKeyEncbufSvrSealEnc;另有 buffrLocalPasswdHashbuffLocalAnswerHashbufLocalHash1 等分支(本地口令/密保路径)。

sub_30097FF0 中若缺失服务端封印:用 Util::Sys::RandomCoCreateGuidthis+31..XOR 混淆,并可经 sub_30095CD0 写入 bufRandKeyEnc;若存在 bufSvrSealEnc:打日志 PerfStand.DecryptMsg.Begin,并调用 ITXSvrSealCrypto::vtable+16DecryptMsg 语义)把封印解密进缓冲。

16.5 旁路:MachineGuid → XXTEA → XOR 表(sub_3000BA00 + sub_3000C5C0

sub_3000BA00:读注册表 HKLM\SOFTWARE\Microsoft\Cryptography\MachineGuid,经自定义 base36 风格映射表 "FGDEBC@A-JKHI-…" 折叠成 16 字节,作为 sub_30002340sub_3000BA00(&v30) 密钥输入

sub_3000C5C0:成功解密后,对明文逐字节 ^= byte_302078F0[2 * (i & 0x3F)]128 字节查找表再混淆一层。

16.6 与 content.dat 对接:解密链 vs 写盘加密链

已证实:对称基元为 MD5(密钥派生) + XXTEA(块密码) + sub_30002340(自定义信封)****。
读档解密与 IM.dll 写盘时调用的 +12 槽 不是同一条函数链:+12 → 加密(sub_30001FA0
解密信封 → sub_30095FE0 槽 → sub_30002340。详见 §16.7

16.7 静态钉扎TXEncryptMgr::QueryEncryptITXEncryptsub_30002340 / sub_30001FA0Common.dll.i64ImageBase 0x30000000

下列结论来自 反汇编 + 交叉引用无运行依赖

16.7.1 QueryEncrypt 本体

  • TXEncryptMgr::QueryEncryptsub_300995B0sub_300990F00x300990F0)。
  • sub_300990F0:在 this+0x14 起的 GUID 链表上按 sub_30098F70 查找节点;命中后取 (list_node + 0x1C) 处的 ATL 类厂对象指针,且
    call dword ptr [eax]call dword ptr [edx]0x3009919c0x300991a7):等价于 factory->lpVtbl[0](factory, &unk_301B9FC8, ppITXEncrypt) —— AtlInternalQueryInterfacesub_30098810),对象映射表off_301B9FB00x301B9FB0),内嵌 IID / 类对象 vtable 指针 unk_301B9FC80x301B9FC8)。

16.7.2 类厂虚表(加密对象由此 QI 出来)

  • 类厂 vtableoff_301B9F700x301B9F70)。
  • [0] sub_30098810AtlInternalQueryInterface 包装。
  • [3] sub_30095FA0[4] sub_30095FE0:两条 实例方法桩(与 IUnknown 后第一个、第二个接口槽对齐讨论 §15.5)。

16.7.3 密钥门闸 *(this+0x1C) 与材料 this+0x1F

  • sub_30095FA0 / sub_30095FE0 汇编一致:cmp byte ptr [this+1Ch], 00x30095fa6 / 0x30095fe6);为假则 HRESULT 0x8000FFFF
  • 两函数均在 add eax, 1Fh 后把 this+0x1F31)压栈作 sub_30095CD0 / sub_30095E10第三参 —— 即 16 字节密钥材料指针(与 §16.4 this+31 叙述一致)。

16.7.4 写加密槽 sub_30095FA0sub_30001FA0(不经 sub_30002340

  • sub_30095FA0sub_30095CD0
  • sub_30095CD0:从 ITXBuffer vtable +48 / +52 取指针与长度;核心变换 sub_30001FA00x30001FA0)—— LCG(214013 / 2531011)+ 多轮按字节混淆,循环内调 sub_30001C600x30001C60)。
  • sub_30001C60XXTEA「加密方向」单块混合(与 sub_30001CF0 解密成对),**xref 上不接 **sub_30002340****。

16.7.5 解密信封槽 sub_30095FE0sub_30095E10sub_30002340

  • sub_30095FE0sub_30095E100x30095E10)。
  • sub_30095E10ITXBuffer vtable +48 / +52 取缓冲;若 *pb == 10x30095e8b)则 sub_30002340(v10+1, len-1, key, …)0x30095ebc)。
  • sub_30002340 内部 call sub_30001CF0XXTEA 解密0x30002393 等)—— 与 §16.2–16.3 一致。

16.7.6 结论(回答「ITXEncrypt 是否钉到 sub_30002340」)

槽位(相对 **IUnknown,x86) 代表函数 是否调用 sub_30002340 备注
+12 sub_30095FA0 sub_30095CD0sub_30001FA0PRNG + sub_30001C60
+16 sub_30095FE0 (经 sub_30095E10 sub_30002340,且要求 信封首字节 1

因此:静态分析已确定 sub_30002340 落在 ITXEncrypt 工厂虚表相邻的「下一接口方法」,与 IM.dll 写路径常用的 +12(加密)不是同一实现;从 **content.dat 还原正文应对齐 sub_30095E10/sub_30002340 的缓冲语义(version byte + 8 字节对齐体 + this+31 密钥)。


17. QQ2009 MsgMgr.dll:向导里的 Msg2.0.dbUserDataMsgStorage:FS::SetExitDelConfig(IDA MCP)

IDB.../msg2.0/QQ2009/Bin/MsgMgr.dll.i64。本节与 §3–§5IM.dll 主链路并列:MsgMgr 侧重 UI / 导入向导与 FS 初始化 Matrix.dat 字面量(strings -el / IDA 文本检索均为负)。

17.1 Msg2.0.db 在 MsgMgr 中的位置:消息导入向导,非主打开路径

  • sub_61E4B590(地址以 QQ2009 IDB 为准):按对象内模式字段分支;在 分支 2 中构造 SelfUin 子目录,依次探测字面量 Msg2.0.dbMsgSS.dbMsgEx.db,通过 Util::Misc::CombinePathFS::CombineQNCFS::IsFileExist / FS::IsDirectoryExist 选出存在的路径并写入对象内 CTXStringW 字段(供导入 UI 使用)。
  • 唯一代码引用来自 sub_61E4D460:在向导状态 20(界面串 Import_Msg_Btn_Next)下调用 sub_61E4B590,随后继续表情目录、定时器、导入进度等逻辑。
  • 结论:此处 Msg2.0.db 服务于 「消息导入」对话框,与 IM.dll 中挂载 UserDataMsgStorage: 并配合 TXEncryptMgr 打开在线消息库 不是同一条主链路

17.2 用户根初始化入口 sub_61E60420

  • 参数为 wchar_t * 用户目录;依次调用 sub_61E5A690sub_61E5CB10sub_61E5FD30sub_61E5D1D0sub_61E59F40
  • 对该函数的 xref 主要来自 数据(导出 / COM 注册),符合「上层传入用户路径做一次批量初始化」的形态。

17.3 UserDataMsgStorage: 与退出清理:FS::SetExitDelConfig + bClrearMsgExit

初始化函数 sub_61E5A690 在前期完成 QQOldConfigDb:ComQQ\SysData.dat 等与 FS::AddFileSystem / CreateFileW / RemoveFileSystem 相关的挂载后,从配置 ITXData 读取键 bClrearMsgExit(二进制字面量拼写为 bClrear…,语义为 ClearMsgExit 一类开关)。条件满足时调用:

FS::SetExitDelConfig(L"UserDataMsgStorage:", flag)Common.dll?SetExitDelConfig@FS@@YAHPB_WH@Z)。

含义:是否在 进程退出 时对虚拟前缀 UserDataMsgStorage: 做「按配置的删除 / 清理」由配置与 Common 实现共同决定,并非 MsgMgr 写死;是否等价于删除盘上 Matrix.dat,需在 Common.dll 内对该 API 反编译 才能钉死。

17.4 与 QQ2009 IM.dllRemoveFileSystem 的对照

IM.dllFS::RemoveFileSystem 全部调用点核对字面量,仅出现 CheckMsg:ExportMsg:ImportMsg:、Hummer 临时挂载、UserCustomFace:* 等,未出现UserDataMsgStorage:RemoveFileSystemQQ2009 上对 UserDataMsgStorage: 等前缀的 RemoveFileSystem 出现在 KernelUtil.dllsub_60A3F970 开头、sub_60A37CB0§19)。与 §17.3 的可配置退出清理不矛盾;细节在 Common 层。


18. QQ2009:Common.dllFS::SetExitDelConfig / ExitDel 全链路;与 Matrix.dat 是否被删(IDA MCP,2026-05)

以下地址均以 QQ2009 Common.dll.i64 为准,ImageBase 0x60400000(与 QQ2010 Common.dll 基址 不同,勿混用 §15–§16 的 0x300… 符号)。

18.1 导出 API:薄封装

导出 地址(约) 行为
?SetExitDelConfig@FS@@YAHPB_WH@Z 0x604AA300 sub_604A6450() 取/建 FS 单例(dword_605C00C8;首次 operator new、构造、Util::Misc::AddToDeadQueue(2, sub_604A6310, …));再 call sub_604AEA20,传入 虚拟前缀宽字符串整型 flag
?GetExitDelConfig@FS@@YAHPB_WPAH@Z 0x604AA2E0 同样 sub_604A6450,再 call sub_604AE9E0,把结果写入调用方 int*

18.2 按前缀查找挂载项后写「开关」:sub_604AEA20 / sub_604AE9E0

  • sub_604AC510:在带 EnterCriticalSection 的向量里用 CTXStringW::CompareNoCase 匹配已注册的 虚拟盘前缀(如 UserDataMsgStorage:)。
  • 命中项 entry+0x0C 指向 具体挂载实现对象 impl*impl* 首 dword 为 vtable
  • Setcall [vtable + 0x6C],参数 (impl*, int flag)HRESULT 语义< 0 视为失败(反汇编 mov eax, [ecx+6Ch] / call eax)。
  • Getcall [vtable + 0x68],参数 (impl*, int *out)

本层无 Matrix.datDeleteFileWRemoveFileSystem 字面量——只做「给这一条已注册挂载打一个 ExitDel 标志」。

18.3 OLE 挂载类虚表:off_6057FEA0+0x68 / +0x6C 的钉扎实现

FS::AddFileSystemsub_604AF2B0)在 类型为 2(OLE/结构化存储) 时通过 sub_604ABE40 构造对象,其 vtable 为 off_6057FEA0

对该 vtable 按槽位读取(32 位 dword 槽):

槽(字节偏移) 函数 行为摘要
+0x68 sub_604B34B0 *out = *(_DWORD *)(this + 48),即读 this + 0x30
+0x6C sub_604B34E0 *(_DWORD *)(this + 48) = flag,即写 this + 0x30

另:TXEncryptMgr::CreateDataStorage 用的 sub_604A4930 小包装类虚表 off_6057F940 槽位布局不同(例如 +0x6C 曾落到 sub_604A8150),勿与 OLE 挂载类混淆

18.4 退出时谁 DeleteFileWsub_604A5000sub_604A7480

  • sub_604A6310DeadQueue):只做 FS 单例析构前的收尾(sub_604AF230 清注册表向量等),不是按 ExitDel 删某个文件名。
  • sub_604A5000(scalar deleting dtor):换 vtbl、释放全局引用后 call sub_604A7480
  • sub_604A7480(大块清理逻辑节选):在释放内部链表与 this+0x0C 上 COM 引用之后,判断 this + 0x30(即 §18.3 的 ExitDel dword):
    v7 = (*(_DWORD *)(this + 48) == 0)if (!v7)(即 标志非 0)时取 CTXStringW(this + 8) 的宽路径,DeleteFileW 一次

含义:ExitDel 生效时删除的是 **挂载实现对象里 this+8 记录的那一条物理路径——对消息 OLE 场景通常是 Msg2.0.db 的完整路径(复合文档文件)不是在同一析构函数里再按文件名删除 Matrix.dat

18.5 MsgMgr.dll(QQ2009):bClrearMsgExitSetExitDelConfig(UserDataMsgStorage:, …)AddFileSystem 字面量不含该前缀

  • sub_61E5A690:读配置键 bClrearMsgExit(字面量约 0x61E8A6BC);成功则 push flagpush offset "UserDataMsgStorage:"(约 0x61E5B013),调用 FS::SetExitDelConfig
  • ?AddFileSystem@FS@@ 在 MsgMgr 内的 xref(如 0x61E59FB20x61E5A71A0x61E5CB940x61E5D2420x61E5FDB4)核对:均为 QQOldConfigDb:QQOldUserDataDb:QQOldUserDb:QQOldCQQApplicationConfigDb: 等与 UserDataMsgStorage: 无关的前缀
  • UserDataMsgStorage: 挂到 磁盘上 Msg2.0.db 全路径FS::AddFileSystem(2, …)KernelUtil.dllsub_60A3F970§19),不在 IM.dll / MsgMgr.dll
  • IM.dll 在挂载已生效后负责 子路径(如 sub_60638A60 CombineQNC + CreateDirectoryWsub_6063B290 CreateFileW 打开 index.dat/content.dat 等)。

18.6 IM.dll(QQ2009):**Matrix.datMsg2.0.db 并列使用;与 ExitDel 删除目标不同

  • sub_60647120FS::CombineQNC(L"UserDataMsgStorage:", L"Matrix.dat")TXEncryptMgr::CreateDataStorage(与 sub_606475A0 等在路径含 msg2.0.db 时分支一致)。
  • Matrix.datTXEncryptMgr 小对象虚表(off_6057F940 那条链),与 §18.3–18.4off_6057FEA0 + sub_604A7480 的「删 this+8 单路径不是同一条析构语义

18.7 结论:ExitDel 触发后 Matrix.dat 还在不在?

  • 若盘上 Matrix.datMsg2.0.db 是两个独立文件(常见):sub_604A7480 只对 this+8 指向的那一个路径调用一次 DeleteFileW,通常对应 Msg2.0.db不会在同一函数内再删名为 Matrix.dat 的文件。因此 Matrix.dat 多数仍会留在目录中(除非另有整目录删除、手工删除或其它未在本次静态路径中出现的清理代码)。
  • §17.3 中「是否等价于删 Matrix.dat」——按当前钉死的 Common 实现不等价;删的是 OLE 挂载绑定的单一路径(常为 Msg2.0.db不是并列的 Matrix.dat

19. QQ2009 KernelUtil.dllUserDataMsgStorage: / UserDataInfoStorage: 的真实 AddFileSystem;与 RemoveFileSystem

IDB.../msg2.0/QQ2009/Bin/KernelUtil.dll.i64。本节钉 「虚拟前缀对应哪条物理路径」注册点;与 §3(QQ2010 IM.dll 内的 MsgStorage 叙事)、§17–§18(MsgMgr / Common / IM.dll 消费侧)互补。

19.1 为何不在 IM.dll 里看到 AddFileSystem(..., L"UserDataMsgStorage:", …)

IM.dll(QQ2009)?AddFileSystem@FS@@ xref 核对:CheckMsg:ExportMsg:OldVer*:UserCustomFace:SNSFileSystem:均无 字面量 UserDataMsgStorage:
UserDataMsgStorage:IM.dllCombineQNC / CreateDirectoryW / TXEncryptMgr::CreateDataStorage 等同现——前提是 进程更早 已完成 FS 注册

19.2 sub_60A3F970:先 RemoveFileSystem,再 UserDataRoot:,再 Msg2.0.dbUserDataMsgStorage:,再 Info.dbUserDataInfoStorage:

函数开头对 UserDataRoot:UserDataMsgStorage:UserDataInfoStorage:一批前缀 调用 FS::RemoveFileSystem(与 §17.4IM.dll 不卸 UserDataMsgStorage:不矛盾——卸载在这里做)。

随后 Util::Sys::GetGlobalDataUsersDirCTXStringW::Format(L"%lu", UIN)operator+(含 \\)拼出 用户数据目录;再:

  1. FS::AddFileSystem(1, …, L"UserDataRoot:", 0, 0)FILESYSTEM_TYPE = 1;字面量 aUserdataroot_0)。
  2. operator+ 接上 Msg2.0.db(字面量 aMsg20Db)得到 OLE 文件全路径,再 FS::AddFileSystem(2, <该宽路径>, L"UserDataMsgStorage:", 0, 0)类型 2;字面量 aUserdatamsgsto_0)。
  3. 同理 Info.dbaInfoDb)+ FS::AddFileSystem(2, …, L"UserDataInfoStorage:", …)aUserdatainfost_0),以及 Misc.db 等后续挂载(同函数内连续 push 2 + AddFileSystem 块)。

结论UserDataMsgStorage: 在磁盘上绑定的是 当前用户目录下的 Msg2.0.db(CFB 复合文档)的绝对路径UserDataInfoStorage: 绑定 Info.db。这与 §15.3 两套 Matrix.dat 的前缀划分一致(消息树 vs 账号信息树)。

19.3 sub_60A37CB0:成批 RemoveFileSystem(含 UserDataMsgStorage:

在构造 全局 / 用户目录 相关 CTXStringW 之后,对 UserDataRoot:UserDataMsgStorage:UserDataInfoStorage:连续调用 RemoveFileSystem(字面量 aUserdatarootaUserdatamsgsto 等),形态为 「切换账号 / 重建 FS 前清空注册」 一类逻辑。

19.4 与 Matrix.datExitDel 的衔接(只读归纳)

  • Msg2.0.dbMatrix.dat 仍是两个不同角色:前者是 OLE 挂载目标§18.4DeleteFileW 常指向 this+8Msg2.0.db);后者走 TXEncryptMgr 小对象链(§18.6),ExitDel 路径不会按文件名删除 Matrix.dat
  • UserDataInfoStorage:Matrix.dat 不在 Msg2.0.db 目录内;归档时需 分别复制 两套前缀旁(或与 Info.db/Msg2.0.db 并列)的 Matrix.dat(见 §15.3 增补段)。