本文档随分析逐步更新。分析目标:为 QQ 2010 时代 Msg2.0.db 的归档/转可读格式,理清客户端如何解析该库及侧录目录结构。
| 项 | 值 |
|---|---|
| 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 可用。
- 对样例
msg2.0/Msg2.0.db执行file与xxd:头部为D0 CF 11 E0 A1 B1 1A E1,即 复合文档 / OLE 结构化存储(Compound File Binary, CFB) 魔数。 - 与“纯自定义二进制”不同,Windows 上通过
StgOpenStorage以IStorage/IStreamCOM 接口访问。
- 使用 Python
olefile打开同一路径的Msg2.0.db时抛出OSError: incorrect DIFAT。 - 可能原因(需后续用 Windows/原始解密链验证,不限于一种):
- 文件经 TX 加密层 或 非标准扩展 处理,导致 FAT/DIFAT 与规范不一致;
- 文件截断/损坏;
- 或需先经
Matrix.dat+TXEncryptMgr得到可读存储(见第 5 节)。
- 结论:归档管线应同时保留
Msg2.0.db、Matrix.dat、seqbase.dat、侧录目录,不能只假设“任意 OLE 库可直接枚举流”。
反编译逻辑概要:
Util::Sys::GetGlobalDataUsersDir→ 拼出用户目录,再拼Msg2.0.db(源码字符串为Msg2.0.db,与磁盘大小写可能不一致)。FS::AddFileSystem(2, "UserDataMsgStorage:" 对应宽字符路径)
将类型2的文件系统命名空间挂到真实文件上。- 同时用
OSRoot:前缀与FS::IsFileExist检查文件是否存在;失败时走备份/重命名(MsgBackFile+ GUID +.db)、ImportWizard配置等分支。
含义:客户端内访问消息库时,并不是直接写死盘符路径,而是通过 UserDataMsgStorage:\... 与内部 FS 层,最终落到 Msg2.0.db 的 OLE 存储。
QQ2009 注记:UserDataMsgStorage: 与盘上 Msg2.0.db 的 AddFileSystem(2, …) 注册 在静态 xref 中出现在 KernelUtil.dll(sub_60A3F970,§19),不是 IM.dll 对 ?AddFileSystem@FS@@ 的调用点。§3.1 仍以 QQ2010 IM.dll 的 MsgStorage 逻辑为据;2009 上 IM.dll 主要在 前缀已挂好后 做 CombineQNC / 建目录 / 开流。
- 对传入的名称(如
buddy/group/ …)执行FS::CombineQNC(L"UserDataMsgStorage:", name),然后FS::CreateDirectoryW,要求FS::IsDirectoryExist成功。 - 与用户磁盘目录一致:样例根目录下存在
buddy/、discuss/、group/、mobile/、system/,与代码中的通道名一一对应。
该函数根据对象内 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 分流。
- 从
ITXData取bSelfMsg等字段,从缓冲中解析 GBK 或 BIG5 文本(Util::Convert::GBKToUnicode/BIG5ToUnicode),写入strSender、strSenderShowName等到CTXStringW/MsgPack。 - 后续还有
sub_31002B90处理附加块,说明 一条记录 = 头 + 可选尾块,属 QQ 私有 TLV/分段格式(需结合更多函数拆解字节)。
当路径经 CTXStringW::MakeLower 后包含 msg2.0.db(且不为 msgss.db)时:
FS::CombineQNC(..., L"Matrix.dat")得到与消息库同根的密钥/封印文件路径;TXEncryptMgr::CreateDataStorage创建数据存储接口;- 通过
bufSvrSealEnc等 BSTR 键 从存储对象读写配置;
归档启示:若导出工具绕过 TXEncryptMgr,可能无法打开 IStorage 或内部流;需复现 Matrix.dat + seal 语义(逆向工作量独立于 OLE)。
// 语义摘要(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_31283AD0、sub_31285E80等 ConvertSS2CD / 迁移 工具链。
sub_31010F20校验/sub_310EBAA0清理。
sub_31285E80(CTXConvertSS2CDMgr::GetMsgRecordUinList):
- 将
this+19+info.db拼路径,FS::SplitQNC后sub_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):
16860244→0x01014454,内存字节序54 44 01 01→ 与样例Matrix.dat文件头54 44 01 01(可视作"TD"+ 版本类字段)一致 → 走sub_31285AD0;16860750→0x0101464E,内存字节序4E 46 01 01→ 走sub_31285820。
结论:info.db 内的嵌套 DB 并非随意扩展名,而是带 固定 4 字节类型标签 的 TX 私有容器;与 Matrix.dat 共享同类签名前缀。
该函数同样 StgOpenStorage,但典型子路径为:
- 存储
config→ 流Face.Xml(读入后字符串替换>/<FILE ORG>等); - 存储
Files; CUSTOMFACE/CUSTOMFACEGROUP下迭代FACE子项与Util::Data/ITXData。
对应 表情/导入向导,与 日常 buddy 文本消息 路径不同,但证明 IM.dll 大量依赖 OLE 层次遍历。
路径:.../stale-reader-encrypted/msg2.0/
| 观察 | 与二进制一致性 |
|---|---|
根下 buddy/、discuss/、group/、mobile/、system/ |
与 sub_3102F7F0 / sub_3102D560 中 buddy、discuss、group、mobile、system 字面量一致 |
buddy/<UIN>/content.dat、index.dat、info.dat |
与 OLE IStream 落盘或导出文件 的典型三重划分一致(内容 / 索引 / 元信息);具体字段待继续跟 FS:: 与 OpenStream 命名 |
Matrix.dat 头 54 44 01 01 |
与 sub_31285C50 中 16860244 分支魔数一致 |
lastmsginfo.dat 头 54 41 01 01 |
另一类 TX 签名(TA),需单独 xref |
该函数用 FS::CombineQNC 构造:
UserDataMsgStorage:+\\+ 第一段目录名a2(长度 < 0x20 宽字符) +\\+ 第二段目录名a3(长度 < 0x20);- 在其上再拼
\\index.dat、\\content.dat,并FS::CreateFileW打开(失败时换标志4098重试)。
因此磁盘上的:
buddy\<QQ号>\index.dat、buddy\<QQ号>\content.dat
与虚拟路径:
UserDataMsgStorage:\buddy\<QQ号>\index.dat
完全同源(discuss / group 等同理)。info.dat 未在此函数出现,应由其它入口创建(待 xref)。
迁移工具 CTXConvertSS2CDMgr::RepairMsgByType 使用 %s\%s\content.dat / index.dat,与上述双层目录模型一致。
- 容器层:实现或使用
StgOpenStorage,若失败则实现TXEncryptMgr+Matrix.dat后再 OLE(与sub_31042D80、sub_31287E10对齐)。 - 索引层:按
buddy/、group/、discuss/、system/、mobile/分桶,再按 UIN / 群号 子目录解析index.dat/content.dat。 - 语义层:对每条记录跑
Util::Msg::TranslateBuddyMsgToMsgPack/TranslateOldBuddyMsgToMsgPack同类逻辑,或静态复现其CTXBuffer布局(需继续向下拆解sub_31037800等)。 - 文本编码:注意
GBK/BIG5分支(sub_3102E8F0),导出 UTF-8 时需标明原始代码页。
-
content.dat/index.dat:已在sub_3102FDB0、RepairMsgByType确认UserDataMsgStorage:\<通道>\<UIN>\模板。 -
info.dat:见 §12(TXEncryptMgr::CreateDataStorage+ 按通道/会话路径拼接;与SNSFileSystem:\info.dat区分)。 -
seqbase.dat、lastmsginfo.dat:见 §12(MsgStorage初始化读入 + 定时器回写lastmsginfo;seqbase为序列基线/水位相关二进制)。 - Linux 下 OLE:已用 Wine + MinGW 生成的
msg2ole_extract.exe(逻辑同源QQMgrMsg_src/ComFileExtr.cpp)成功StgOpenStorage打开样例Msg2.0.db并递归解压出buddy/等树;同一文件 Pythonolefile仍报incorrect DIFAT(走的不是 Win32 OLE)。
以下地址均以 ImageBase 0x31000000 为模块基址;函数名为 IDA 默认名。
在 sub_31041020(某 MsgStorage 辅助类构造函数)中,成员 this+5、this+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 打开方式不同。
sub_3103F5D0(日志上下文 MsgStorage)在 sub_3103E5C0 成功挂载 Msg2.0.db 之后:
FS::CreateFileW打开this+5(seqbase.dat),通过ITXFile虚表读长度;若有效载荷长度为 0,打日志seqbase size = 0(字符串aSeqbaseSize0),并走一套 GUID3EDD1723-0FF2-4DD8-A6F9-8576D8FF4561相关的默认/修复分支(与sub_3103F450枚举的对象列表配合——疑似空库初始化或解密占位)。- 再
CreateFileW打开this+6(lastmsginfo.dat),把内容解析进this+15上的ITXArray(Util::Data::CreateTXArray),供内存侧维护「最近消息」集合。 - 调用
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+15(lastmsginfo对应的ITXArray)非空:再次FS::CreateFileW打开this+6,把ITXArray序列化进ITXBuffer,经ITXFile虚表 写入/提交(多处vtable+52/+60/+80组合符合「取缓冲 → 写文件 → 收尾」)。
结论:lastmsginfo.dat 在运行期被 周期性写回;seqbase.dat 在初始化阶段重点读取(长度校验 + 空文件分支);二者共同支撑 消息序号基线与「最后一条消息」展示/同步,与 sub_3102FDB0 的 index.dat/content.dat 会话正文索引形成分工。
按会话目录(与磁盘 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_3102D720:UserDataMsgStorage:\+this+8的CTXBSTR+\info.dat,同样CreateDataStorage,虚表 +16 / +12 成对。
以上说明 info.dat 并非裸 CreateFileW,而是 经 TXEncryptMgr 的 ITXDataStorage,与 Matrix.dat 封印链一致(参见上文 §5)。
sub_31274220 在 Infocenter.db 目录上挂载 SNSFileSystem:,再访问 SNSFileSystem:\info.dat(小写 info.dat 字面量在 aInfoDat_1)。若不存在则 CreateFileW 并写入 dwFileVersion 等 ITXData。这是 资讯中心/SNS,与 Msg2.0.db 树内 buddy\...\info.dat 不是同一条业务线;归档时勿混用解析器。
sub_31285240(CSS2CD / RepairMsgDb):依次对 PrefixString + Matrix.dat、+ seqbase.dat、+ lastmsginfo.dat 调用 sub_31282C70 做修复,再 sub_31285030 处理 buddy/group,并调用 CTXConvertSS2CDMgr::RepairMsgByType。说明 seqbase/lastmsginfo/Matrix 在版本迁移时被视作 同一批关键侧车文件。
路径: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等,与已有侧录目录结构一致。
每条记录 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 对应同一元数据。
两种外层模式(由首部 uint32 与 len(文件) 关系判定):
- 单块文件:首部
uint32 == len(文件),_payload =raw[4:](总长含头部 4 字节)。 - 双块文件:首部
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 体内层)。
指向 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 |
- 脚本:
ai-work/msg2_parser/msg2_session_parse.py--self-test:对 合成 buddy 内层字节流解析出你好(GB18030),验证字段布局。<index.dat> <content.dat>:打印 索引三元组、单块/双块划分、-:/.:偏移;对多样本校验.:== 首块长、/:` == 尾块相关长度 + 4。
- 加密:当前样本
content.dat在标记-:/.:之后为 高熵密文;在实现TXEncryptMgr+Matrix.dat(TD54 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 路径模板、RepairMsgByType、sub_31285C50 魔数与 Matrix.dat 头对应关系 |
| 2026-05-02 | msg2ole_extract:MinGW CLI + Wine 实测解压 Msg2.0.db |
| 2026-05-02 | §12:info.dat / seqbase.dat / lastmsginfo.dat 读写链(sub_31041020、sub_3103F5D0、sub_31040470、sub_3102E700/E7D0、D660/D720、sub_31285240) |
| 2026-05-02 | §14:index.dat/content.dat packed 标记 0x3A2D/2E/2F、双块长度关系;buddy 内层 sub_3102E8F0;msg2_session_parse.py 实测交叉验证;密文依赖 Matrix/TX |
| 2026-05-02 | §15:TXEncryptMgr 在 Common.dll;Matrix.dat 双前缀;密钥字段 bufSvrSealEnc/bufPwdHashOne/buf16byteSessionKey;CLSID {49DB3AB8-7E8B-DAF8-D45F-C0DF-00000000};ITXEncrypt+12;content.dat 经 ITXEncrypt 而非 FS 透明解密 |
| 2026-05-02 | §16:Common.dll — Init = MD5(flags∥16B);sub_30001CF0 = XXTEA;sub_30002340 = XXTEA 信封;Seal 字段 bufSvrSealEnc 等;MachineGuid 链路与 XOR 表 byte_302078F0 |
| 2026-05-02 | §16.7:静态钉扎 — QueryEncrypt→sub_300990F0→工厂 QI(off_301B9FB0/unk_301B9FC8);ITXEncrypt +12 = sub_30095FA0→sub_30001FA0(加密);+16 = sub_30095FE0→sub_30095E10→sub_30002340(信封解密);修正 §15.5–15.6 对 +12 的笼统表述 |
| 2026-05-03 | §17:QQ2009 MsgMgr.dll — Msg2.0.db 字面量仅 sub_61E4B590 / 向导 sub_61E4D460;sub_61E60420 串联初始化;sub_61E5A690 → FS::SetExitDelConfig(UserDataMsgStorage:, …) 与 bClrearMsgExit;QQ2009 IM.dll RemoveFileSystem 枚举无 UserDataMsgStorage: |
| 2026-05-03 | §18:QQ2009 Common.dll — SetExitDelConfig/GetExitDelConfig → sub_604AEA20/sub_604AE9E0 → vtbl +0x6C/+0x68 → sub_604B34E0/sub_604B34B0(this+0x30);析构 sub_604A7480 在标志非 0 时对 CTXStringW(this+8) 调用 DeleteFileW(常为 Msg2.0.db 路径);off_6057FEA0 vs off_6057F940;MsgMgr 五处 AddFileSystem 无 UserDataMsgStorage:;Matrix.dat 经 IM.dll + TXEncryptMgr,ExitDel 不按名删 Matrix.dat,并列文件时 Matrix.dat 多数仍保留 |
| 2026-05-03 | §19:QQ2009 KernelUtil.dll — FS::AddFileSystem(2, <Msg2.0.db 全路径>, L"UserDataMsgStorage:", …) 与 Info.db → UserDataInfoStorage: 均在 sub_60A3F970;RemoveFileSystem(UserDataMsgStorage:) 等见 sub_60A3F970 开头与 sub_60A37CB0;与 §15.3 两套 Matrix.dat、§18.5 IM.dll 仅消费挂载 对齐 |
| 2026-05-04 | §15.3.1:Common.dll sub_604A6C60 — GetFileAttributesW + TXOpenStorage + TXCreateCompoundDocument 钉 Matrix.dat 落盘;IM.dll sub_606475A0 / sub_606476E0 分用 Matrix.dat(msg2.0.db) 与 Matrix\Matrix.db(msgex/msgss);Common.dll 无 Matrix.db 字面量 |
| 2026-05-05 | §15.3.1 增补:「侧车文件一旦创建则走真实盘路径」≠「凡有 Msg2.0.db 则旁必有 Matrix.dat」 — 条件创建、仅拷贝库文件、版本/路径不一致等 |
| 2026-05-06 | §15.3.2:QQ2009 Common.dll sub_604A6C60 — TXCreateCompoundDocument 的 布尔前提(与 sub_604AA5A0 / this+40 / 第二形参 a2);IM.dll sub_606475A0 / sub_60647120 门槛 |
| 2026-05-07 | §15.3.3:sub_60643D10 /「没到 sub_604A6C60」 — 汇编级判定([arg+0x18]、仅 sub_60647120 xref;sub_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.dll — sub_606475A0/60648620/60649500 对 函数入口 的 code xref 穷尽印证;sub_60647120 仅虚表数据 xref;勿将口语场景等同于穷尽 |
| 2026-05-10 | §15.3.5:Matrix.dat 生成/打开的上层条件 — 钉 sub_60636130 首参 = pUnkOuter(COM 聚合);ATL CreateInstance(sub_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.7:off_6084D618 _ATL_INTMAP_ENTRY 逐项解析 — IID×dw 钉死;6084d654/6084d664 内联 IID + 第三张虚表 与 off_6084D664 前缀五槽 |
| 2026-05-13 | §15.3.8:顶层调用链 + Matrix.dat 首次落盘 — sub_60648D70 亦仅虚表 xref;sub_6064F650 委托槽→sub_6064F430;修正 §15.3.4(3.1) 对 sub_60647120/聚合 的旧误 |
本节目标:弄清 谁在何处实现加解密、密钥从哪些结构化字段来、与 Matrix.dat / 会话文件如何衔接。结论:对称算法与 TXEncryptMgr 本体不在 IM.dll,而在导入模块 Common.dll;离线还原正文必须在 Common.dll + Matrix.dat + 登录阶段密钥材料 三条线索上继续。
对 IM.dll 的导入表枚举:TXEncryptMgr::QueryEncrypt / CreateDataStorage / AddEncryptInfo / Init 均从 Common 模块导入(IDA:module idx 0 → Common.dll)。因此:
- 逆向「算法本体」:应打开
QQ2010/Bin/Common.dll(与其它 Tencent 客户端同名 DLL 无关——必须以本安装目录为准)。 IM.dll仅持有 调用序列、GUID、ITXData/ITXEncrypt上的缓冲区搬运。
| 修饰名(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 打开的 ITXDataStorage、Util::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)。 |
| 路径前缀 | 典型拼接 | 函数线索 | 用途 |
|---|---|---|---|
UserDataMsgStorage:\Matrix.dat |
FS::CombineQNC(..., L"Matrix.dat") |
sub_31041510(PerfStand.InitUserFileSystem)、sub_31042D80;QQ2009 钉扎 sub_60648620 / sub_60649500 / sub_606475A0(§15.3.4) |
消息库侧:与 Msg2.0.db 同逻辑根的 Matrix.dat;sub_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.dll 的 sub_60A3F970 依次 AddFileSystem:UserDataRoot:(类型 1)→ Msg2.0.db 全路径 + UserDataMsgStorage:(类型 2)→ Info.db 全路径 + UserDataInfoStorage:(类型 2)(字面量 Msg2.0.db / Info.db 与 aUserdatamsgsto_0 / aUserdatainfost_0 同函数内成对出现)。因此 §15.3 两前缀下的 Matrix.dat 各落在 两套用户数据根旁的两个独立文件(通常与 Msg2.0.db / Info.db 并列目录),不是 Msg2.0.db 复合文档内部的两个同名流。
曾不严谨处:仅说 「CreateDataStorage 非裸 CreateFileW」 容易让人以为 数据未必在真实文件上 —— 应对齐 Common.dll 里 打开/创建存储 的实现。
- 钉扎(QQ2009
Common.dll,ImageBase0x60400000):包装类在首次需要IStorage时 走sub_604A6C60同类逻辑(与this+8上CTXStringW已解析的完整…Matrix.dat宽路径 绑定):GetFileAttributesW→TXOpenStorage;若仍打不开且条件满足则TXCreateCompoundDocument—— 在盘上创建/维持 TD 类复合文档。因此Matrix.dat指 真实路径上的侧车文件(与Msg2.0.db并列、独立,内容格式为 TX 文档而非裸 SQLite);若从未触发创建或路径不可写,盘上可以 暂时不存在。 Matrix.db(勿与Matrix.dat混名):IM.dllsub_606476E0仅在路径中含msgex.db或msgss.db时拼接<基路径>\Matrix\Matrix.db(字面量L"Matrix.db",如 QQ20090x6084e68c),用FS::CreateFileW走 另一套消息库;sub_606475A0在路径含msg2.0.db时则使用L"Matrix.dat"+TXEncryptMgr::CreateDataStorage。对Msg2.0.db主归档线,客户端字符串与调用链均以Matrix.dat为准;Matrix.db是 MsgEx/MsgSS 场景下的 不同文件名 + 子目录布局。- 静态覆盖:在 QQ2009
Common.dll内对 UTF-16Matrix.db的按字节搜索 无命中;Matrix.db字面量仅见于IM.dll上述分支。 - 与实测「只有
Msg2.0.db、目录里找不到Matrix.dat」不矛盾(分析并无内在冲突):- 静态结论钉的是能力:一旦某次运行沿
TXEncryptMgr::CreateDataStorage→sub_604A6C60去 打开/创建 该路径,则性质是 真实文件上的 TD 复合文档,不是「纯虚拟假名」。这 不蕴含:任意一台机器上 只要存在Msg2.0.db,同目录就 必然 带着Matrix.dat。 Matrix.dat依赖调用链是否跑到「要读/写 Seal 侧车」(例如sub_31042D80/PerfStand一类路径,见 §15.3 表)。若从未触发、失败提前返回、或某版本/配置下加密侧未初始化,库文件仍可 grow,侧车文件可 始终未创建。- 离线样本常见:只拷贝了
Msg2.0.db(或整机镜像里漏掉并列文件)、原装机已卸载/清理、杀软或手动删过*.dat、QQ 安装路径与数据目录 与当前查看的文件夹 不是同一棵目录树(需在Users\...\Tencent Files\<QQ号>\一类完整配置根下找 与Msg2.0.db同父目录 的Matrix.dat,而不是只看你手上的这份msg2.0.db拷贝所在目录)。 - 仓库
README.md已记录 本地曾找不到Matrix.dat的情形 —— 与 §14–§15 所述「密文还原依赖密钥材料」并行成立:没有侧车文件时,更难离线解密,但不反证「客户端从不实现盘路径」。
- 静态结论钉的是能力:一旦某次运行沿
导出 ?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.dll,0x604A6C60)里,TXCreateCompoundDocument 仅当同时满足:
this+0xC上缓存的ITXStorage*仍为空(已成功打开过则直接返回 成功,不会再创建)。sub_604AA5A0(路径)返回 非 0:路径非空;并对 父目录链 做GetFileAttributesW/ 递归 +CreateDirectoryW—— 若无法在盘上构造出可写的目录前缀,函数 提前返回 0,根本不会调用TXOpenStorage/TXCreateCompoundDocument。TXOpenStorage失败:HRESULT < 0(文件不存在时通常会失败,才会考虑创建)。a2 == 0(惰性打开路径):若a2 != 0,会先要求GetFileAttributesW成功且路径不是目录(FILE_ATTRIBUTE_DIRECTORY未置位),且 不会 走下面的创建分支;a2 == 0时 不 做这一组「必须已存在普通文件」的检查。this+0x28(this+40)为 0:若 非 0,在TXOpenStorage已失败 时 直接返回E_FAIL(-2147467259),同样不会调用TXCreateCompoundDocument(静态语义:禁止在该对象上落盘新建,具体何种运行时状态会把+40置位尚未在此处钉死)。- 以上成立后调用
TXCreateCompoundDocument(路径, 3, …)。
补充:TXOpenStorage 若已成功(盘上已有可读 TD 文件),则 TXCreateCompoundDocument 不会执行 —— 你看到 Msg2.0.db 很大但没有 Matrix.dat,在静态语义下仍可能是:**惰性打开从未成功跑到 **sub_604A6C60****(根本没触发 Matrix.dat 路径),或 sub_604AA5A0 / this+40 挡住了创建 —— 不等于「库不完整」。
IM.dll 侧何时会去拼 Matrix.dat 并调用 CreateDataStorage(仍不代表当次必落盘):
sub_606475A0:CTXStringW::MakeLower后Find(…, L"msg2.0.db", 0) != -1(路径宽字符串里 必须出现msg2.0.db子串);然后FS::CombineQNC→Matrix.dat,再TXEncryptMgr::CreateDataStorage;接着对返回的ITXDataStorage*做一次vtable+0x10调用并得到vtable+0x8上的对象 —— 任一步失败整条作废。也就是说:仅适用于「某条逻辑路径携带含msg2.0.db的长路径」,不是你的离线文件名随口叫msg2.0.db就一定会命中这条 helper。sub_60647120(CombineQNC(L"UserDataMsgStorage:", L"Matrix.dat")→CreateDataStorage→AddEncryptInfo):先判定sub_60643D10(this)—— 内部依赖seqbase.dat/lastmsginfo.dat等经FS::CreateFileW打开与读取(失败则打SeqHelper Init类日志并 不调用CreateDataStorage)。即使你自认目录里其它文件都在,只要sub_60643D10走失败分支,Matrix.dat这一条根本不会发起。
一、sub_60643D10(QQ2009 IM.dll,0x60643D10)——只在一条链上用
- IDA xref:仅
sub_60647120(0x60647151)调用它。别的 Matrix / Seal 路径根本不跑这个函数;你若从没走过sub_60647120,讨论sub_60643D10对你的 **Matrix.dat有没有出现 无关。
二、调用约定钉死(sub_60647120 开头,0x60647147–0x60647151)
sub_60647120的第一个参数arg0(上层传来的ITXDataStorage*一类指针):this传给sub_60643D10的是arg0 - 4(lea ecx,[eax-4]+push ecx,栈参)。ECX在call前被设为[arg0 + 0x18],作为 **sub_60643D10的第二个参数a2(宽字符上下文指针)。
- 因此静态可复查的第一条失败条件:
[arg0+0x18] == NULL→sub_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 保持 0 → sub_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::CreateDataStorage(Common.dll0x604A4CC0)只构造包装对象并写入路径,**不等于已经执行 **sub_604A6C60****。sub_604A6C60会被 多条Common.dll封装函数 直接 调用,例如:sub_604A7100(0x604a714e):若a2&&a3非空、且a1[10]==0,则sub_604A6C60(a1, 0),再继续vtable+0x38/+0x30等;sub_604A8460(0x604a8460):若*(a1+40)==0且两个宽串非空,则sub_604A6C60(a1, 0),再拼流路径。
- 人话:只要你拿到的
ITXDataStorage*从来没被客户端拿去执行「需要底层IStorage已经就绪」的操作(读某个属性流、打开子存储等——对应上述封装入口),sub_604A6C60就不会执行,Matrix.dat也不会被创建或打开。这和Msg2.0.db是否在膨胀无关:后者走 OLE 挂载,前者走 TXEncryptMgr 侧车路径。
约定:IM.dll ImageBase 0x60600000;Common.dll ImageBase 0x60400000(与 §15.3.2–15.3.3 一致)。TXEncryptMgr::CreateDataStorage 的导入桩 __imp_… 在 IM.dll 为 0x60842238。
见 §19:FS::AddFileSystem(2, <Msg2.0.db 绝对路径>, L"UserDataMsgStorage:", …)。没有这一步,后面所有 UserDataMsgStorage:\… 宽路径都无法解析。
前四行打开的是 …\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+48;vtable+0x10。 |
0x60639E50 |
sub_60639E50 |
同上 Format;vtable+0x0C。 |
0x60647120 |
sub_60647120 |
CombineQNC(L"UserDataMsgStorage:", L"Matrix.dat") → CreateDataStorage → CreateSvrSeal(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") → CreateDataStorage → vtable+0x10 → vtable+0x8(轻量打开/查询链)。 |
0x60648620 |
sub_60648620 |
Find(L"msg2.0.db") 且 非 msgss.db → **Matrix.dat → CreateDataStorage → vtable+0x10 → 按名 bufSvrSealEnc → vtable+0x44(十进制偏移 +68,属性读写) —— 与旧版笔记里 sub_31042D80(QQ2010)同一职责:**从 Matrix 拉 bufSvrSealEnc。 |
0x60649500 |
sub_60649500 |
**CombineQNC(调用方传入的基前缀, L"Matrix.dat") → CreateDataStorage → 读 bufSvrSealEnc → ITXBuffer 校验 → CreateSvrSeal(1) / Seal 加密对象衔接(失败码 0xE0632710 等特判)。 |
sub_60648620:唯一code xref来自sub_60648D70(0x60648ed4)。sub_60648D70:校验盘上Msg2.0.db存在 →FS::AddFileSystem(2, …, L"CheckMsg:", …)→ 构造ITXData→ 若sub_60648830某条件不成立则调用sub_60648620(物理路径, L"CheckMsg:")拉bufSvrSealEnc→ 最后RemoveFileSystem(L"CheckMsg:")。即 「检查/修复消息库」向导链路上的 Matrix 读取。sub_606475A0与sub_60649500:对二者函数入口的code xref均只有sub_6064F430(ImportMsg:),调用点分别为0x6064f56b、0x6064f589;sub_6064F430** 内部按this+11路径与分支(msgex/msgss、sub_606478C0、buddy 解析 等)择路。sub_6064F650把sub_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****。
前提:下列为对 函数首地址 的 xref type == code(IDA);不统计寄存器间接跳转、不覆盖插件/热补丁;**仅代表本 IM.dll 静态库。
| 目标 | 指向入口的 code xref 条数 |
钉死的直接上层(若有) | 备注 |
|---|---|---|---|
sub_606475A0 |
1 | 仅 sub_6064F430(0x6064f56b) |
在本 DLL 内 没有其它 call sub_606475A0。 |
sub_60648620 |
1 | 仅 sub_60648D70(0x60648ed4) |
「检查消息库」(CheckMsg:)链路。 |
sub_60649500 |
1 | 仅 sub_6064F430(0x6064f589) |
「导入消息」(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:0x6084D5D8、0x6084D6A8(off_6084D5A8 / off_6084D678 之 CheckMsg: 槽 12)。与 sub_60647120 同类:IM.dll 内不存在「谁 call 了检查消息库入口」 的静态边;顶层为宿主对已创建对象的 虚调用。 |
口语归纳的边界:说「检查消息库、导入消息」会碰 Matrix.dat —— 对 60648620 / 606475A0 / 60649500 这三条 有直接代码印证(call 自 60648D70 / 6064F430);60647120** 与 60648D70 则 仅能从 COM 业务接口进入,不是「未解析的神秘分支」。另:CreateDataStorage 另外四条走的是 info.dat,勿与 Matrix.dat 混谈。
对 sub_604A6C60 的 直接 call 共 14 处(IDA xref type code 穷尽):sub_604A4D60、sub_604A7100、sub_604A8150、sub_604A8260、sub_604A8460、sub_604A8790、sub_604A8960、sub_604A8B40、sub_604A8D30、sub_604A9090、sub_604A9360、sub_604A96D0、sub_604A99F0、sub_604A9E80。其中 sub_604A4D60 的指针落在 off_6057F940 系虚表(数据 xref 0x6057FAE4 / 0x6057FB2C),是 CreateDataStorage 包装对象取 IStorage 的主惰性入口;其余封装均在 「两参数宽路径 / 三参数流拷贝 / 打开子存储」 等操作前 先 sub_604A6C60(this,0)。人话:IM 调了 CreateDataStorage 之后,只要后续走到「在 TD 里按名字碰流/子存储」的 Common 侧实现,就一定会先落到 sub_604A6C60;若 IM 只拿到了指针却不再调用这些封装,则磁盘侧车永远不会被打开。
挂载:KernelUtil::…AddFileSystem(UserDataMsgStorage:)(§19)→ FS::CombineQNC 任意 UserDataMsgStorage:\… 有效 → 八条之一 CreateDataStorage(Matrix.dat / info.dat / …)→ Common.dll 某封装 sub_604A… → sub_604A6C60 → TXOpenStorage / TXCreateCompoundDocument。
读 bufSvrSealEnc(与 §15.4 表对照):静态已钉 → sub_60648D70 → sub_60648620(CheckMsg:);sub_6064F430 → sub_60649500(ImportMsg:,分支条件见 (3)/§15.3.5)。另:sub_60647120 亦 CreateDataStorage(Matrix.dat);上层为 同一 coclass 之 COM 虚槽(§15.3.5–15.3.8),不是未解析的「神秘界面」。
消息初始化路径 sub_31041510 调用 Util::SvrSeal::CreateSvrSeal(1, …)(类型 1),再 AddEncryptInfo —— 与登录侧 类型 2 区分,表示 两套 Seal 上下文(同一 API,不同实例/用途)。
约定:「生成」 = 盘上首次 TXCreateCompoundDocument 或 CreateDataStorage 后惰性打开并创建(§15.3.2);「仅打开」 = 已存在 TD 文件则 TXOpenStorage 成功、不再 TXCreateCompoundDocument。
- 虚拟前缀解析:
KernelUtil.dll已AddFileSystem(…, L"UserDataMsgStorage:", …)(§19),否则UserDataMsgStorage:\…宽路径在FS::CombineQNC侧无意义。 - Common 惰性落盘门闩(§15.3.2):
sub_604AA5A0成功、TXOpenStorage失败、a2==0、this+0x40==0等 同时满足,才会TXCreateCompoundDocument;否则可能 仅持有ITXDataStorage*而 从未 在当次运行中 创/开 盘文件。
| 条件 | 静态含义 |
|---|---|
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 / strEncryptQuestion,不调 sub_60648620。 |
sub_60648620 内部 |
路径 MakeLower 后:若含 msgss.db → 直接返回成功标记且不 CreateDataStorage(短路);否则必须 Find(msg2.0.db) 命中 才 CombineQNC(…, Matrix.dat) + CreateDataStorage。 |
以 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)。仅当其返回 非 0(sub_606475A0 内对 msg2.0.db 打开链成功)时:若 *(this+12) != 0 转 sub_60647A30 + sub_6064E6E0;若 *(this+12)==0 则 sub_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,并对 ITXDataStorage 做 vtable+0x10 → vtable+0x8 轻量探测;任一步失败则 返回 0。 |
sub_60649500 自身 |
CreateDataStorage 失败或 bufSvrSealEnc / ITXBuffer 校验失败 返回 0xE0632710(-530372848)等;成功路径可能 CreateSvrSeal(1)。 |
| 事实 | 说明 |
|---|---|
sub_60636130(a1,a2,a3) 的分支 |
a1 != 0 → sub_60635EC0;a1 == 0 → sub_60635C90(0,…)。 |
a1 的语义(更正) |
sub_60609D10(ATL 风格 CreateInstance 外壳)通过 (*(…))(this+0x24) 调用 sub_60636130 时,第一形参来自 CreateInstance 的 pUnkOuter(反编译中 sub_60609D10(a1,a2,a3,a4) 对槽函数调用 (a2,a3,a4),其中 **a2 即 outer)。因此 a1 非「神秘 flag」,而是 标准 COM:外层控制对象非空 ⇒ 聚合(aggregation)内对象创建。 |
| 聚合路径 | pUnkOuter != 0 → sub_60635EC0 → operator new(0x60) + sub_60635DA0:主虚表 off_6084D6E4;*(this+8) 起依次为 off_6084D6C0、off_6084D678、off_6084D664;*(this+20)=outer。次级调度虚表 off_6084D678 槽 [3] → sub_60647120(见 §15.3.6)。 |
| 非聚合路径 | pUnkOuter == 0 → sub_60635C90 → operator new(0x58):主虚表 off_6084D5F0;obj[1]=off_6084D5A8,obj[2]=off_6084D594。更正:非聚合对象同样在次级虚表暴露 sub_60647120 — off_6084D5A8 槽 [3](及 off_6084D594 槽 [8] 再起一段相同尾部)与 off_6084D678[3] 同源指针。因此「Import 只有 sub_6064F430、绝无 60647120」不成立:直接 call 的 ImportMsg:/Matrix.dat helpers 仍只见 606475A0/60649500;60647120** 在该分支下主要为 COM 虚槽 / QI 可达,与 sub_6064F430 并行(不同调用约定)。 |
| 对象表钉扎(本映像) | .data 中 0x608C82F8 指向 **CLSID 本体 0x60843E9C → {A875AE08-E87E-48F4-8806-CE4E1BE59758};紧随 0x608C8300–0x608C8304 为 sub_6060AC30 / sub_60636130,与本组件 ATL 注册块 同一邻域 —— 哪个外层 coclass 在运行时以聚合方式创建该接口 → 需结合宿主进程/UI DLL,但 静态上已把「触发源」收窄到「聚合 CoCreateInstance」这一类。 |
| 函数体内门槛 | sub_60643D10 失败(§15.3.3)则 **打 SeqHelper Init 失败 日志并 E_FAIL,不执行 CreateDataStorage。 |
| 线 | 顶层充分条件(代码层) |
|---|---|
sub_60648620 |
sub_60648D70 走到 call sub_60648620,且 msgss.db 未短路、msg2.0.db 命中。 |
sub_606475A0 |
sub_6064F430 活跃(*(this+10)!=0)且分支落到 sub_606476E0 假、sub_606478C0 假、并最终 call sub_606475A0;且 sub_606475A0 内 Find(msg2.0.db) 命中。 |
sub_60649500 |
同上 ImportMsg: 树,且 sub_606475A0 已成功返回、*(this+12)==0,从而 call sub_60649500。 |
sub_60647120 |
虚槽调用 + sub_60643D10 成功。构造路径:pUnkOuter != 0 → sub_60635DA0 布局;pUnkOuter == 0 → sub_60635C90 布局 — 两套均在次级虚表 [3](或 D594 [8] 嵌套)挂 sub_60647120(§15.3.6**)。**仅当宿主通过对应 IID 调该槽且 SeqHelper 就绪,才实际 CreateDataStorage(Matrix.dat)。 |
本节把 sub_60636130 分叉两侧的 .data 虚表 钉到 函数名,并说明 AtlInternalQueryInterface 用的 接口映射块。槽索引均为「函数指针下标」(×4 字节)。
| 构造 | 大小 | 主虚表(对象首字) | this+8 / +12 / +16 三接口指针 |
|---|---|---|---|
sub_60635DA0(聚合链 sub_60635EC0) |
0x60 |
off_6084D6E4 |
off_6084D6C0、off_6084D678、off_6084D664;*(this+20)=outer |
sub_60635C90(sub_60636130,pUnkOuter==0) |
0x58 |
off_6084D5F0 |
obj[1]=off_6084D5A8,obj[2]=off_6084D594 |
sub_60609D10:(*(…))(factory_this+0x24)(pUnkOuter, riid, ppv)→sub_60636130(即CreateInstanceCreators 槽,不是sub_60647120本体)。sub_6060E6E0:return sub_607D44B0(a1-4, a2, a3)— this 调整-4的标准 IUnknown 转发(指向聚合对象 inner 的首部)。- 主接口
QueryInterface:off_6084D6E4[0]→sub_60635E30:AtlInternalQueryInterface(a1 + 8, &off_6084D618, …)— QI 相对真实对象+8(首个接口指针槽)。off_6084D5F0[0]→sub_60635D80:AtlInternalQueryInterface(a1, &off_6084D618, …)— QI 相对 对象首址(无+8)。
sub_60635E30快路径:若riid通过IUnknown判定(反编译:*a2==0 && a2[1]==0 && a2[2]==192 && a2[3]==1174405120),则 直接返回this并AddRef;否则走off_6084D618映射表。
unk_60848E80起首 16 字节(LE) 解析为IID:{EB5398EC-11ED-49EE-A1FA-B478FAA157C6}。- 紧随其后的
wchar_t[]与MSGFILE/SHARE等 壳层注册串 混排 —— 整块作为_ATL_INTMAP/_ATL_OBJMAP_ENTRY邻域数据 被链接进off_6084D618;后续 dword 中夹杂0x00000004/0x00000001等 ATL 元数据,不是有效call目标(off_6084D5F0主虚表 [10]– 亦可见同类 「代码/数据交错」)。
与 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_6064AFB0 … sub_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: — DeleteFileW、AddFileSystem(ExportMsg)、QueryEncrypt、info.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_60646F10、TXTimer::SetAsyncCallback — 异步导出/回调登记 |
| 15–16 | sub_607C5060 ×2 |
占位 thunk |
| 17 | sub_60646E40 |
下游委托 |
| 18–26 | sub_607D44B0 … sub_606497D0 |
ATL/Tencent 通用 thunk 与 sub_606497D0(与 D5F0[8]/D5B0[24] 同源) |
| 27 | sub_60635E30 |
**再次出现 QueryInterface(聚合对象 secondary 侧 QI) |
| 28+ | sub_606E6300 … |
尾部 IUnknown/tear-off 与 GUID 常量区交叠 — **静态上视为 元数据/衔接块,勿按代码指针调用 |
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_60737350、sub_607F6850、sub_606DB400、sub_60647070、sub_60649800(Tencent 前缀 thunk);自 [5] 起sub_6060E6E0(IUnknown)…,sub_60647120落在槽 [8]**(对照off_6084D678[3]:**整块IUnknown+业务 序列相对前移 5 槽)。内联 IID 与AtlInternalQueryInterface映射 见 §15.3.7。
sub_60647120:唯一实现体;出现在off_6084D5B0[1]、off_6084D678[3]、off_6084D664[8]、off_6084D5A8[3]、off_6084D594[8]等.data—— 语义均为同一实现经由 不同虚表/前缀槽位 暴露(§15.3.7** 补off_6084D664)。CheckMsg:sub_60648D70(槽 12)与sub_60648FB0(槽 13)并存;仅前者在sub_60648830失败时call sub_60648620(§15.3.4)。ExportMsg:sub_60649840(槽 11)独立大函数,与ImportMsg:的sub_6064F430不同栈,但 同属该 COM 对象的业务接口。
对 .rdata 做 DWORD 级遍历:off_6084D618(0x6084D618)起为标准 **_ATL_INTMAP_ENTRY 对 **(const IID *piid, DWORD_PTR dw)****,**直至 (NULL,NULL)。
# |
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})。 |
0x6084D650:DWORD 0填充。0x6084D654:16 字节 IID ={CC6B8374-D121-C849-A8B6-97C973A26192}(与 表项 #1 指针一致)。0x6084D664:即符号off_6084D664— 该 IID 对应虚表首址。槽 [0]–[4]:sub_60737350、sub_607F6850、sub_606DB400、sub_60647070、sub_60649800;槽 [5]–[7]:IUnknown(sub_6060E6E0、sub_6062B410、sub_6062B400);槽 [8]:sub_60647120;槽 [9]–[15]:与off_6084D678同序列(6064AFB0…60647410)。
结论:第三接口相对off_6084D678多 5 个前缀槽,60647120的槽下标由[3]变为[8]** —— §15.3.6(5) 已据此更正。
0x60848E80+0x10 起为宽字符串字面量(MSGFILE… / …IN_SHARE / …CALLBACK 碎片 等),与 IID 头 16 字节同属 ATL 注册/类型库邻域 —— 勿把 UTF-16 字符单元当成 IID* 指针去解引用。
区分三件事:(A)业务上 谁会走到 CreateDataStorage(UserDataMsgStorage:\Matrix.dat);(B)惰性 I/O 里 Common.dll sub_604A6C60 是否执行;(C)在该执行路径上 TXOpenStorage 失败且 §15.3.2 全成立 时,才会 TXCreateCompoundDocument —— 才是「生成」Matrix.dat 文件。仅有 Msg2.0.db 或仅 CreateDataStorage 返回指针 都 不足以 保证(C)。
sub_60648D70(CheckMsg:槽 12,会call sub_60648620):对函数入口的code xref条数为 0;仅.data0x6084D5D8、0x6084D6A8指向其地址 —— 与sub_60647120同构:检查消息库 在 **IM.dll内也不是普通call图可达,而是 消息库 coclass 实例化之后 对 虚表槽 12 的调用。sub_6064F430(ImportMsg:主入口,call sub_606475A0/sub_60649500):仅sub_6064F650(0x6064f724)数据 引用;sub_6064F650在构造里执行*(_DWORD *)(*(this+18)+16) = sub_6064F430—— 把导入逻辑挂到this+18所指 内部委托对象 的 vtable 槽,不是 主对象off_6084D5F0[0]直接指向sub_6064F430。sub_6064F650又 仅 被sub_60635C90/sub_60635DA0在CoCreate路径上调用(0x60635d00/0x60635ddc)。- 结论:跨 DLL 的「顶层」 = 某宿主(主程序 /
MsgMgr.dll/ 脚本宿主等)CoCreateInstance(CLSID …)拿到{A875AE08-E87E-48F4-8806-CE4E1BE59758}(0x60843E9C)对象后,再QueryInterface/虚调用 到CheckMsg:/ImportMsg:/ 槽 [3]sub_60647120等。§17 中MsgMgr.dllsub_61E4D460(导入向导状态 20)与sub_61E4B590选Msg2.0.db路径,是 业务侧 与 导入 最相近的静态锚点;在本仓库未附带 MsgMgr 二进制供 IDA 自动化时,GUID→具体「哪一个按钮/哪一个CoCreate调用点」 仍需在 宿主 IDB 内对 16 字节 CLSID 或 相关 ProgID 做检索 ——IM.dll单独静态分析无法再接上一层。
在 (A)KernelUtil 已挂 UserDataMsgStorage:(§19)前提下,任一条 成立且 后续真正触发 Common 惰性打开 时,才可能 在盘上 新建 Matrix.dat(若已存在则 只打开):
| 业务线(IM) | 进到 CreateDataStorage(Matrix.dat) 的最窄门槛(§15.3.5 已列) |
进而触发 sub_604A6C60 的典型动作 |
|---|---|---|
sub_60648D70 → sub_60648620 |
sub_60648830("CheckMsg:",…)==0;路径 Find(msg2.0.db) 命中且 非 msgss.db 短路 |
CreateDataStorage 后对 ITXDataStorage* 调 vtable+0x10(取子对象/打开存储) 与 vtable+0x44 按名读 bufSvrSealEnc(sub_60648620** 反编译 0x60648705–0x60648760)— 会拉 IStorage,从而进入 sub_604A6C60 |
sub_6064F430 → sub_606475A0 |
*(this+10)!=0,sub_606476E0/sub_606478C0 分支不抢走,且路径 Find(msg2.0.db) 命中 |
同上,vtable+0x10 → +0x8 轻量链(§15.3.4) |
sub_6064F430 → sub_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 存在 不矛盾。
下列名称均在 IM.dll 中以 CTXBSTR + ITXDataStorage/ITXDataRead vtable 偏移 +68(按属性名读写缓冲区)等形式出现:
| 字段名 | 出现场景 | 作用(推断级别) |
|---|---|---|
bufSvrSealEnc |
sub_31042D80(路径含 msg2.0.db 时) |
从 UserDataMsgStorage: + Matrix.dat 的 ITXDataStorage 读取;随后 sub_31010F20 校验 — 服务端封印扩展,与消息加密链路绑定。 |
bufPwdHashOne |
sub_31022140 |
读出后放入 TXEncryptMgr::Init;与 本地口令/二次哈希 相关(具体哈希算法在 Common.dll)。 |
buf16byteSessionKey |
sub_3101CF30(登录) |
Util::Data::GetTXDataBuf → CTXBuffer;配合 Util::Time::SetServerTime — 16 字节会话密钥,显然参与后续 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 中的 派生与解密。
多处 TXEncryptMgr::QueryEncrypt 之前写入同一 GUID 常量:
*(QWORD*)&guid.Data1 = 0xDAF87E8B49DB3AB8*(DWORD*)&guid.Data4[0] = -542260844→ 无符号0xDFC05FD4- 若
Data4后 4 字节为 0,则规范字符串为:{49DB3AB8-7E8B-DAF8-D45F-C0DF-00000000}(Pythonuuid.UUID(bytes_le=…)验证)。
sub_31030080 / sub_31033B40(消息打包写会话存储)在 QueryEncrypt 成功后:
- 调
ITXEncryptvtable 第一个接口方法(相对接口指针偏移+12,紧接在IUnknown三个槽之后):在Common.dll工厂虚表off_301B9F70上对应sub_30095FA0(见 §16.7)→sub_30095CD0→sub_30001FA0—— 加密写盘(不是sub_30002340信封解密链)。 - 头部拼接
Util::Msg::GetMsgTime、GetMsgRand32,再sub_31040B60/sub_3103DE60写入index.dat/content.dat对应路径。
读档 / 解密复原应对 ITXEncrypt 上下一槽(工厂虚表里为偏移 +16):sub_30095FE0 → sub_30095E10 → sub_30002340(见 §16.7)。
sub_3102FDB0 对 index.dat / content.dat 仅 FS::CreateFileW — 不在此处调用 TXEncryptMgr。结合 §14 的高熵密文:
- 磁盘上的密文 = 上层已用
ITXEncrypt(或等价链路)处理后的字节,再写入ITXFile。 - 解密入口不在
FS::CreateFileW,而在 读出ITXBuffer之后、进入TranslateOldMsgToMsgPack/sub_3102E8F0之前 —— 走ITXEncrypt上sub_30095FE0槽位(相对IUnknown为+16)→sub_30002340(实现位于Common.dll;与+12写加密槽 不对称,见 §16.7)。
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 提供。
| 路线 | 条件 | 说明 |
|---|---|---|
| 完整移植 | 在 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/stdcall 与 HRESULT);仍需 有效 Matrix.dat 与密钥材料,否则 Init/QueryEncrypt 失败。 |
| 务实取证 | 在 Windows / Wine 里跑 QQ.exe + 钩子 截 ITXEncrypt +12 出入参 |
可比静态逆向更快拿到 明文缓冲与密钥长度。 |
对称算法骨架见 §16。QueryEncrypt → ITXEncrypt → sub_30002340 的静态钉扎见 §16.7(已完成)。下一步工程验证:在 buf==1 信封 + this+31 密钥 与 buddy/.../content.dat 切片之间 对齐帧边界 实测。
模块基址:0x30000000。导出 TXEncryptMgr:Init 0x300990D0,QueryEncrypt 0x300995B0,AddEncryptInfo 0x300998A0,CreateDataStorage 0x3009A110。
sub_30098DE0(Init 本体):若 ITXBuffer 提供的载荷长度 Size == 16:
sub_30001000:初始化标准 MD5 状态向量 —0x67452301、0xEFCDAB89、0x98BADCFE、0x10325476(与 RFC 1321 IV 一致)。sub_300016C0:依次MD5_update(flags, 4)、MD5_update(payload, 16)。sub_30001A80:MD5 结尾填充与摘要,把 16 字节摘要写回TXEncryptMgr全局状态(供后续QueryEncrypt/ Seal 使用)。
即:口令/会话种子经过「4 字节标志 + 16 字节二进制」再 MD5,不是裸用 16 字节当对称密钥。
反编译特征:
- 输入
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)。
在 a2 % 8 == 0 且 a2 >= 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)等多处调用。
sub_300996F0(AddEncryptInfo 主体)在注册 GUID 对应的链表节点后,调用 sub_300960A0(最后一参非 0)或 sub_30097FF0(最后一参为 0),从挂载的 ITXDataStorage 读出字段并完成 16 字节会话密钥材料写入对象偏移 this+31…:
典型 ITXData 键名(宽字符串):bValidInit、bSysLocalHash、bufRandKeyEnc、bufSvrSealEnc;另有 buffrLocalPasswdHash、buffLocalAnswerHash、bufLocalHash1 等分支(本地口令/密保路径)。
sub_30097FF0 中若缺失服务端封印:用 Util::Sys::Random 与 CoCreateGuid 对 this+31.. 做 XOR 混淆,并可经 sub_30095CD0 写入 bufRandKeyEnc;若存在 bufSvrSealEnc:打日志 PerfStand.DecryptMsg.Begin,并调用 ITXSvrSealCrypto::vtable+16(DecryptMsg 语义)把封印解密进缓冲。
sub_3000BA00:读注册表 HKLM\SOFTWARE\Microsoft\Cryptography\MachineGuid,经自定义 base36 风格映射表 "FGDEBC@A-JKHI-…" 折叠成 16 字节,作为 sub_30002340 的 sub_3000BA00(&v30) 密钥输入。
sub_3000C5C0:成功解密后,对明文逐字节 ^= byte_302078F0[2 * (i & 0x3F)] — 128 字节查找表再混淆一层。
已证实:对称基元为 MD5(密钥派生) + XXTEA(块密码) + sub_30002340(自定义信封)****。
读档解密与 IM.dll 写盘时调用的 +12 槽 不是同一条函数链:+12 → 加密(sub_30001FA0);解密信封 → sub_30095FE0 槽 → sub_30002340。详见 §16.7。
16.7 静态钉扎:TXEncryptMgr::QueryEncrypt → ITXEncrypt → sub_30002340 / sub_30001FA0(Common.dll.i64,ImageBase 0x30000000)
下列结论来自 反汇编 + 交叉引用,无运行依赖。
TXEncryptMgr::QueryEncrypt→sub_300995B0→sub_300990F0(0x300990F0)。sub_300990F0:在this+0x14起的 GUID 链表上按sub_30098F70查找节点;命中后取(list_node + 0x1C)处的 ATL 类厂对象指针,且
call dword ptr [eax]→call dword ptr [edx](0x3009919c–0x300991a7):等价于factory->lpVtbl[0](factory, &unk_301B9FC8, ppITXEncrypt)——AtlInternalQueryInterface(sub_30098810),对象映射表为off_301B9FB0(0x301B9FB0),内嵌 IID / 类对象 vtable 指针unk_301B9FC8(0x301B9FC8)。
- 类厂
vtable:off_301B9F70(0x301B9F70)。 [0]sub_30098810:AtlInternalQueryInterface包装。[3]sub_30095FA0、[4]sub_30095FE0:两条 实例方法桩(与IUnknown后第一个、第二个接口槽对齐讨论 §15.5)。
sub_30095FA0/sub_30095FE0汇编一致:cmp byte ptr [this+1Ch], 0(0x30095fa6/0x30095fe6);为假则HRESULT 0x8000FFFF。- 两函数均在
add eax, 1Fh后把this+0x1F(31)压栈作sub_30095CD0/sub_30095E10的 第三参 —— 即 16 字节密钥材料指针(与 §16.4this+31叙述一致)。
sub_30095FA0→sub_30095CD0。sub_30095CD0:从ITXBuffervtable+48/+52取指针与长度;核心变换sub_30001FA0(0x30001FA0)—— LCG(214013/2531011)+ 多轮按字节混淆,循环内调sub_30001C60(0x30001C60)。sub_30001C60:XXTEA「加密方向」单块混合(与sub_30001CF0解密成对),**xref 上不接 **sub_30002340****。
sub_30095FE0→sub_30095E10(0x30095E10)。sub_30095E10:ITXBuffervtable+48/+52取缓冲;若*pb == 1(0x30095e8b)则sub_30002340(v10+1, len-1, key, …)(0x30095ebc)。sub_30002340内部call sub_30001CF0(XXTEA 解密,0x30002393等)—— 与 §16.2–16.3 一致。
槽位(相对 **IUnknown,x86) |
代表函数 | 是否调用 sub_30002340 |
备注 |
|---|---|---|---|
+12 |
sub_30095FA0 |
否 | → sub_30095CD0 → sub_30001FA0(PRNG + 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 密钥)。
IDB:.../msg2.0/QQ2009/Bin/MsgMgr.dll.i64。本节与 §3–§5 的 IM.dll 主链路并列:MsgMgr 侧重 UI / 导入向导与 FS 初始化,无 Matrix.dat 字面量(strings -el / IDA 文本检索均为负)。
sub_61E4B590(地址以 QQ2009 IDB 为准):按对象内模式字段分支;在 分支 2 中构造SelfUin子目录,依次探测字面量Msg2.0.db、MsgSS.db、MsgEx.db,通过Util::Misc::CombinePath、FS::CombineQNC与FS::IsFileExist/FS::IsDirectoryExist选出存在的路径并写入对象内CTXStringW字段(供导入 UI 使用)。- 唯一代码引用来自
sub_61E4D460:在向导状态 20(界面串Import_Msg_Btn_Next)下调用sub_61E4B590,随后继续表情目录、定时器、导入进度等逻辑。 - 结论:此处
Msg2.0.db服务于 「消息导入」对话框,与IM.dll中挂载UserDataMsgStorage:并配合TXEncryptMgr打开在线消息库 不是同一条主链路。
- 参数为
wchar_t *用户目录;依次调用sub_61E5A690→sub_61E5CB10→sub_61E5FD30→sub_61E5D1D0→sub_61E59F40。 - 对该函数的 xref 主要来自 数据(导出 / COM 注册),符合「上层传入用户路径做一次批量初始化」的形态。
初始化函数 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 反编译 才能钉死。
对 IM.dll 内 FS::RemoveFileSystem 全部调用点核对字面量,仅出现 CheckMsg:、ExportMsg:、ImportMsg:、Hummer 临时挂载、UserCustomFace:* 等,未出现 对 UserDataMsgStorage: 的 RemoveFileSystem。QQ2009 上对 UserDataMsgStorage: 等前缀的 RemoveFileSystem 出现在 KernelUtil.dll(sub_60A3F970 开头、sub_60A37CB0,§19)。与 §17.3 的可配置退出清理不矛盾;细节在 Common 层。
以下地址均以 QQ2009 Common.dll.i64 为准,ImageBase 0x60400000(与 QQ2010 Common.dll 基址 不同,勿混用 §15–§16 的 0x300… 符号)。
| 导出 | 地址(约) | 行为 |
|---|---|---|
?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*。 |
sub_604AC510:在带EnterCriticalSection的向量里用CTXStringW::CompareNoCase匹配已注册的 虚拟盘前缀(如UserDataMsgStorage:)。- 命中项
entry+0x0C指向 具体挂载实现对象impl*;impl*首 dword 为 vtable。 - Set:
call [vtable + 0x6C],参数(impl*, int flag),HRESULT 语义,< 0视为失败(反汇编mov eax, [ecx+6Ch]/call eax)。 - Get:
call [vtable + 0x68],参数(impl*, int *out)。
本层无 Matrix.dat、DeleteFileW、RemoveFileSystem 字面量——只做「给这一条已注册挂载打一个 ExitDel 标志」。
FS::AddFileSystem(sub_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 挂载类混淆。
sub_604A6310(DeadQueue):只做 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):bClrearMsgExit → SetExitDelConfig(UserDataMsgStorage:, …);AddFileSystem 字面量不含该前缀
sub_61E5A690:读配置键bClrearMsgExit(字面量约0x61E8A6BC);成功则push flag、push offset "UserDataMsgStorage:"(约0x61E5B013),调用FS::SetExitDelConfig。- 对
?AddFileSystem@FS@@在 MsgMgr 内的 xref(如0x61E59FB2、0x61E5A71A、0x61E5CB94、0x61E5D242、0x61E5FDB4)核对:均为QQOldConfigDb:、QQOldUserDataDb:、QQOldUserDb:、QQOldCQQApplicationConfigDb:等与UserDataMsgStorage:无关的前缀。 - 将
UserDataMsgStorage:挂到 磁盘上Msg2.0.db全路径 的FS::AddFileSystem(2, …)在KernelUtil.dll的sub_60A3F970(§19),不在IM.dll/MsgMgr.dll。 IM.dll在挂载已生效后负责 子路径(如sub_60638A60CombineQNC+CreateDirectoryW;sub_6063B290CreateFileW打开index.dat/content.dat等)。
sub_60647120:FS::CombineQNC(L"UserDataMsgStorage:", L"Matrix.dat")→TXEncryptMgr::CreateDataStorage(与sub_606475A0等在路径含msg2.0.db时分支一致)。Matrix.dat走TXEncryptMgr小对象虚表(off_6057F940那条链),与 §18.3–18.4 里off_6057FEA0+sub_604A7480的「删this+8单路径」不是同一条析构语义。
- 若盘上
Matrix.dat与Msg2.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.dll:UserDataMsgStorage: / UserDataInfoStorage: 的真实 AddFileSystem;与 RemoveFileSystem
IDB:.../msg2.0/QQ2009/Bin/KernelUtil.dll.i64。本节钉 「虚拟前缀对应哪条物理路径」 的 注册点;与 §3(QQ2010 IM.dll 内的 MsgStorage 叙事)、§17–§18(MsgMgr / Common / IM.dll 消费侧)互补。
对 IM.dll(QQ2009) 的 ?AddFileSystem@FS@@ xref 核对:CheckMsg:、ExportMsg:、OldVer*:、UserCustomFace:、SNSFileSystem: 等 均无 字面量 UserDataMsgStorage:。
UserDataMsgStorage: 在 IM.dll 中 仅 与 CombineQNC / CreateDirectoryW / TXEncryptMgr::CreateDataStorage 等同现——前提是 进程更早 已完成 FS 注册。
19.2 sub_60A3F970:先 RemoveFileSystem,再 UserDataRoot:,再 Msg2.0.db → UserDataMsgStorage:,再 Info.db → UserDataInfoStorage:
函数开头对 UserDataRoot:、UserDataMsgStorage:、UserDataInfoStorage: 等 一批前缀 调用 FS::RemoveFileSystem(与 §17.4「IM.dll 不卸 UserDataMsgStorage:」不矛盾——卸载在这里做)。
随后 Util::Sys::GetGlobalDataUsersDir、CTXStringW::Format(L"%lu", UIN)、operator+(含 \\)拼出 用户数据目录;再:
FS::AddFileSystem(1, …, L"UserDataRoot:", 0, 0)(FILESYSTEM_TYPE= 1;字面量aUserdataroot_0)。operator+接上Msg2.0.db(字面量aMsg20Db)得到 OLE 文件全路径,再FS::AddFileSystem(2, <该宽路径>, L"UserDataMsgStorage:", 0, 0)(类型 2;字面量aUserdatamsgsto_0)。- 同理
Info.db(aInfoDb)+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 账号信息树)。
在构造 全局 / 用户目录 相关 CTXStringW 之后,对 UserDataRoot:、UserDataMsgStorage:、UserDataInfoStorage: 等 连续调用 RemoveFileSystem(字面量 aUserdataroot、aUserdatamsgsto 等),形态为 「切换账号 / 重建 FS 前清空注册」 一类逻辑。
Msg2.0.db与Matrix.dat仍是两个不同角色:前者是 OLE 挂载目标(§18.4:DeleteFileW常指向this+8→Msg2.0.db);后者走TXEncryptMgr小对象链(§18.6),ExitDel 路径不会按文件名删除Matrix.dat。UserDataInfoStorage:侧Matrix.dat不在Msg2.0.db目录内;归档时需 分别复制 两套前缀旁(或与Info.db/Msg2.0.db并列)的Matrix.dat(见 §15.3 增补段)。