解密QQ的MsgEx.db消息文件格式

本文详细解析了QQ消息的本地存储机制,包括消息文件的结构、加密方式及解密算法。通过一个C#实现的QQ消息类,展示了如何读取和输出不同类型的QQ历史消息。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

QQ的消息实际上是存放在本地的,位于"QQ安装目录/QQ号码/MsgEx.db"内。关于QQ消息文件格式的文章,网上有不少,但是没有一篇是完整并且可重现。结合QQ聊天记录察看器 5.1,做了一些研究,重现了读取并显示历史消息的完整过程。
一个很好的学习QQ相关算法的实例,是它的Linux版本LumaQQ
首先,MsgEx.db文件的大致结构可以参考QQ聊天记录查看器 5.3 华军版
IStorage的详细介绍可以在MSDN中查到,CHM就是使用了这个格式。为了方便的操作这个COM接口,我们可以直接使用Decompiling CHM (help) files with C#中提供的RelatedObjects.Storage.dll
消息的加密密码存放在Matrix.db中,提取出来之后就可以解密实际存放消息文本的Data.msj文件了
(值得注意的是,QQ使用的数据加密算法并不是上面帖子里提到的Blowfish,而是TEA算法,可以参考QQ的TEA填充算法C#实现)
QQ分若干种消息类型,诸如双人消息、群消息和系统公告等,格式有一些差异。
具体的细节,看看代码就清楚了。一个简单的QQ消息类的实现如下:
namespace Van.Utility.QQMsg
{
public enum QQMsgType
{
 BIM, C2C, Group, Sys, Mobile, TempSession //Disc
}
class QQMsgMgr
{
 private static readonly int s_MsgTypeNum = (int)QQMsgType.TempSession + 1;
 private static readonly string[] s_MsgName = new string[] {
    "BIMMsg", "C2CMsg", "GroupMsg", "SysMsg", "MobileMsg", "TempSessionMsg"
   };
 private IStorageWrapper m_Storage;
 private byte[] m_Password;
 private List<string>[] m_MsgList = new List<string>[s_MsgTypeNum];
 public void Open(string QQID)
 {
  Open(QQID, null);
 }
 public void Open(string QQID, string QQPath)
 {
  if (QQPath == null)
  {
   using (Microsoft.Win32.RegistryKey reg = Microsoft.Win32.Registry.LocalMachine.OpenSubKey(@"Software/Tencent/QQ"))
   {
    QQPath = reg.GetValue("Install") as string;
   }
   if (QQPath == null) return ;
  }
  for (int i = 0; i < m_MsgList.Length; ++i)
  {
   m_MsgList = new List<string>();
  }
  m_Storage = null;
  m_Password = null;
  m_Storage = new IStorageWrapper(QQPath + QQID + @"/MsgEx.db");
  m_Password = QQMsgMgr.GetGlobalPass(m_Storage);
  if (m_Password == null) m_Storage = null;
  foreach (IBaseStorageWrapper.FileObjects.FileObject fileObject in m_Storage.foCollection)
  {
   if (fileObject.FileType == 1)
   {
    for (int i = 0; i < m_MsgList.Length; ++i)
    {
     if (fileObject.FilePath == s_MsgName)
     {
      m_MsgList.Add(fileObject.FileName);
     }
    }
   }
  }
 }
 public void OutputMsg()
 {
  for (int i = 0; i < s_MsgTypeNum; ++i)
  {
   OutputMsg((QQMsgType)i);
  }
 }
 public void OutputMsg(QQMsgType type)
 {
  if (m_Storage == null) return ;
  if (m_Password == null) return ;
  int typeIndex = (int)type;
  if (typeIndex < 0 || typeIndex >= s_MsgTypeNum)
  {
   throw new ArgumentException("Invalid QQMsgType", "type");
  }
  string filePath = s_MsgName[typeIndex] + "//";
  Directory.CreateDirectory(filePath);
  foreach (string QQID in m_MsgList[typeIndex])
  {
   string fileName = filePath + QQID + ".msj";
   OutputMsg(type, QQID, fileName);
  }
 }
 public void OutputMsg(QQMsgType type, string QQID)
 {
  if (m_Storage == null) return ;
  if (m_Password == null) return ;
  int typeIndex = (int)type;
  if (typeIndex < 0 || typeIndex >= s_MsgTypeNum)
  {
   throw new ArgumentException("Invalid QQMsgType", "type");
  }
  string filePath = s_MsgName[typeIndex] + "//";
  Directory.CreateDirectory(filePath);
  string fileName = filePath + QQID + ".msj";
  OutputMsg(type, QQID, fileName);
 }
 private void OutputMsg(QQMsgType type, string QQID, string fileName)
 {
  string msgPath = s_MsgName[(int)type] + QQID;
  IList < byte[] > msgList = QQMsgMgr.DecryptMsg(m_Storage, msgPath, m_Password);
  Encoding encoding = Encoding.GetEncoding(936);
  using (FileStream fs = new FileStream(fileName, FileMode.Create))
  {
   using (StreamWriter sw = new StreamWriter(fs))
   {
    for (int i = 0; i < msgList.Count; ++i)
    {
     using (MemoryStream ms = new MemoryStream(msgList))
     {
      using (BinaryReader br = new BinaryReader(ms, Encoding.GetEncoding(936)))
      {
#if false
       fs.Write(msgList, 0, msgList.Length);
#else
       int ticks = br.ReadInt32();
       DateTime time = new DateTime(1970, 1, 1) + new TimeSpan(0, 0, ticks);
       switch (type)
       {
       case QQMsgType.BIM:
       case QQMsgType.C2C:
       case QQMsgType.Mobile:
        ms.Seek(1, SeekOrigin.Current);
        break;
       case QQMsgType.Group:
        ms.Seek(8, SeekOrigin.Current);
        break;
       case QQMsgType.Sys:
        ms.Seek(4, SeekOrigin.Current);
        break;
       case QQMsgType.TempSession:  //?
        ms.Seek(9, SeekOrigin.Current);
        break;
       }
       if (type == QQMsgType.TempSession)
       {
        int gLen = br.ReadInt32();
        string groupName = encoding.GetString(br.ReadBytes(gLen));
        if (groupName.Length > 0) sw.WriteLine("{0}", groupName);
       }
       int nLen = br.ReadInt32();
       string id = encoding.GetString(br.ReadBytes(nLen));
       sw.WriteLine("{0}: {1}", id, time.ToString());
       int cLen = br.ReadInt32();
       string msg = encoding.GetString(br.ReadBytes(cLen));
       msg.Replace("/n", Environment.NewLine);
       sw.WriteLine(msg);
       sw.WriteLine();
#endif
      }
     }
    }
   }
  }
 }
 public void OutputFileList()
 {
  if (m_Storage == null) return ;
  Dictionary<string, long> dic = new Dictionary<string, long>();
  foreach (IBaseStorageWrapper.FileObjects.FileObject fileObject in m_Storage.foCollection)
  {
   if (fileObject.FileType == 2 && fileObject.FileName == "Index.msj")
   {
    dic[fileObject.FilePath] = fileObject.Length / 4;
   }
  }
  for (int i = 0; i < m_MsgList.Length; ++i)
  {
   Console.WriteLine("{0}", s_MsgName);
   foreach (string ID in m_MsgList)
   {
    Console.WriteLine("/t{0}: {1}", ID, dic[s_MsgName + ID]);
   }
  }
 }
 private static IBaseStorageWrapper.FileObjects.FileObject GetStorageFileObject(IStorageWrapper iw, string path, string fileName)
 {
  foreach (IBaseStorageWrapper.FileObjects.FileObject fileObject in iw.foCollection)
  {
   if (fileObject.CanRead)
   {
    if (fileObject.FilePath == path && fileObject.FileName == fileName) return fileObject;
   }
  }
  return null;
 }
 private static byte[] Decrypt(byte[] src, byte[] pass, long offset)
 {
  RedQ.QQCrypt decryptor = new RedQ.QQCrypt();
  return decryptor.QQ_Decrypt(src, pass, offset);
 }
 private static IList < byte[] > DecryptMsg(IStorageWrapper iw, string path, byte[] pass)
 {
  List < byte[] > msgList = new List < byte[] > ();
  int num = 0;
  int[] pos = null;
  int[] len = null;
  using (IBaseStorageWrapper.FileObjects.FileObject fileObject = GetStorageFileObject(iw, path, "Index.msj"))
  {
   if (fileObject == null) return msgList;
   int fileLen = (int)fileObject.Length;
   num = fileLen / 4;
   pos = new int[num + 1];
   using (BinaryReader br = new BinaryReader(fileObject))
   {
    for (int i = 0; i < num; ++i)
    {
     pos = br.ReadInt32();
    }
   }
  }
  using (IBaseStorageWrapper.FileObjects.FileObject fileObject = GetStorageFileObject(iw, path, "Data.msj"))
  {
   if (fileObject != null)
   {
    int fileLen = (int)fileObject.Length;
    len = new int[num];
    pos[num] = fileLen;
    for (int i = 0; i < num; ++i)
    {
     len = pos[i + 1] - pos;
    }
    using (BinaryReader br = new BinaryReader(fileObject))
    {
     for (int i = 0; i < num; ++i)
     {
      fileObject.Seek(pos, SeekOrigin.Begin);
      byte[] data = br.ReadBytes(len);
      byte[] msg = Decrypt(data, pass, 0);
      msgList.Add(msg);
     }
    }
   }
  }
  return msgList;
 }
 private static byte[] GetGlobalPass(IStorageWrapper iw)
 {
  System.Security.Cryptography.MD5 md5 = System.Security.Cryptography.MD5.Create();
  string QQID = "254614441";
  byte[] dataID = new byte[QQID.Length];
  for (int i = 0; i < QQID.Length; ++i) dataID = (byte)(QQID);
  byte[] hashID = md5.ComputeHash(dataID);
  IBaseStorageWrapper.FileObjects.FileObject fileObject = GetStorageFileObject(iw, "Matrix", "Matrix.db");
  if (fileObject != null)
  {
   using (BinaryReader br = new BinaryReader(fileObject))
   {
    byte[] data = br.ReadBytes((int)fileObject.Length);
    long len = data.Length;
    if (len < 6 || data[0] != 0x51 || data[1] != 0x44) return null;
    if (len >= 32768) return null;
    bool bl = false;
    int i = 6;
    while (i < len)
    {
     bl = false;
     byte type = data[i++];
     if (i + 2 > len) break;
     int len1 = data + data[i + 1] * 256;
     byte xor1 = (byte)(data ^ data[i + 1]);
     i += 2;
     if (i + len1 > len) break;
     for (int j = 0; j < len1; ++j) data[i + j] = (byte)(~(data[i + j] ^ xor1));
     if (len1 == 3 && data == 0x43 && data[i + 1] == 0x52 && data[i + 2] == 0x4B)
     {
      bl = true;
     }
     i += len1;
     if (type > 7) break;
     if (i + 4 > len) break;
     int len2 = data + data[i + 1] * 256 + data[i + 2] * 256 * 256 + data[i + 3] * 256 * 256 * 256;
     byte xor2 = (byte)(data ^ data[i + 1]);
     i += 4;
     if (i + len2 > len) break;
     if (type == 6 || type == 7)
     {
      for (int j = 0; j < len2; ++j) data[i + j] = (byte)(~(data[i + j] ^ xor2));
     }
     if (bl && len2 == 0x20)
     {
      byte[] dataT = new byte[len2];
      for (int j = 0; j < len2; ++j) dataT[j] = data[i + j];
      return Decrypt(dataT, hashID, 0);
     }
     i += len2;
    }
    if (i != len) return null;
   }
  }
  return null;
 }
}
}
利用这个类,你就可以方便的导出QQ中的历史消息了。
从上面的分析可以看到,查看本地的历史消息是不需要你的QQ密码的,加密密钥来源于你的QQ号码的MD5散列。所以为了保证安全,最好不要在公共电脑或者别人的电脑上使用QQ并记录历史消息。在个人电脑上,最好将历史消息加密。
本文纯属技术交流,不是为了破解别人消息的,大家慎用,高手可以跟帖讨论。原文:http: //blog.youkuaiyun.com/vbvan/archive/2007/12/14/1937440.aspx

<think>好的,用户想知道如何直接打开.dat文件。首先,我需要确认.dat文件是什么类型的文件。DAT文件通常是数据文件,可能包含各种格式的数据,比如文本、二进制、视频、游戏存档等。不同的应用程序生成的.dat文件结构不同,所以处理方法也不一样。 接下来,用户可能不知道.dat文件的具体来源,这会影响打开方式。我应该建议用户先确定文件的来源,比如是某个软件生成的,还是系统文件,或者是邮件附件里的。比如,如果是视频软件的.dat文件,可能是缓存文件;如果是游戏的,可能是存档。 然后,常见的打开方法包括使用文本编辑器,如Notepad++或VS Code,因为有些.dat文件实际上是文本格式,用文本编辑器可以查看内容。但如果是二进制文件,可能会显示乱码,这时候需要特定的软件。比如,VCD中的.dat文件可以用媒体播放器打开。 另外,用户可能需要知道如何判断文件类型。可以建议用户尝试用十六进制编辑器查看文件头,或者用file命令(在Linux/Mac下)来识别。或者更改文件扩展名,比如改成.txt、.mpg等,试试能否打开。 还需要提醒用户注意安全,特别是来源不明的.dat文件,可能存在风险,不要随意运行。如果是重要文件,最好先备份。 最后,总结步骤:确认来源、尝试文本编辑器、使用关联软件、专业工具如Hex编辑器、修改扩展名、注意安全。这样用户能系统地尝试不同的方法,找到适合自己情况的解决方案。</think>.dat文件是通用的数据文件格式,可能包含文本、二进制、视频等多种数据。打开方法取决于文件来源和用途,以下是详细步骤指南: 1. **确认文件来源** - 查看文件路径:系统目录下的.dat文件可能是配置文件(如`C:\Windows\System32\drivers\etc\hosts.dat`) - 回忆文件来源:邮件附件、软件生成(如QQ聊天记录`MsgEx.db.dat`)或游戏存档(如《仙剑奇侠传》存档文件) 2. **尝试文本编辑器** - 推荐工具: - Notepad++(支持编码识别) - VS Code(自动检测编码) - WinHex(查看16进制) - 操作示例: ```bash file example.dat # Linux/Mac终端检测文件类型 ``` 3. **使用关联软件** - 常见对应关系: | 文件类型 | 对应软件 | |---------------|-------------------------| | VCD视频 | VLC Media Player | | 邮件附件 | 原邮件客户端 | | 游戏存档 | 对应游戏程序 | | 数据库缓存 | 相关应用(如QQ) | 4. **十六进制分析(进阶)** 使用010 Editor查看文件头: - 视频文件:前4字节可能是`52 49 46 46`(RIFF) - 图片文件:`FF D8 FF`开头可能是JPEG - ZIP文件:`50 4B 03 04`开头 5. **修改扩展名尝试** - 重命名方法: ```powershell ren example.dat example.txt # Windows命令提示符 ``` - 常见扩展名测试:.txt/.log(文本)、.mpg(视频)、.db(数据库) 6. **专业工具推荐** - 通用查看器:File Viewer Plus(支持300+格式) - 开发工具:Python解析(示例代码): ```python with open('data.dat', 'rb') as f: print(f.read(100)) # 查看前100字节 ``` **注意事项**: 1. 系统关键.dat文件(如`ntuser.dat`)不要随意修改 2. 病毒可能伪装成.dat文件,扫描安全后才打开 3. 重要数据操作前建议创建备份: ```bash cp example.dat example.dat.bak # Linux/Mac copy example.dat example.dat.bak # Windows ``` 如果以上方法均无效,可能需要联系文件提供方获取专用读取工具。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值