haobinnan 发表于 2008-12-22 18:02:51

转载 解密QQ消息文件格式

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>;

      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, QQID);

            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 + "\\";
            Directory.CreateDirectory(filePath);

            foreach (string QQID in m_MsgList)
            {
                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 + "\\";
            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.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 + 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;
                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;
                  pos = fileLen;
                  for (int i = 0; i < num; ++i)
                  {
                        len = pos - 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, string QQID)
      {
            System.Security.Cryptography.MD5 md5 = System.Security.Cryptography.MD5.Create();
            byte[] dataID = new byte;
            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 != 0x51 || data != 0x44) return null;
                  if (len >= 32768) return null;

                  bool bl = false;
                  int i = 6;
                  while (i < len)
                  {
                        bl = false;
                        byte type = data;
                        if (i + 2 > len) break;
                        int len1 = data + data * 256;
                        byte xor1 = (byte)(data ^ data);
                        i += 2;
                        if (i + len1 > len) break;
                        for (int j = 0; j < len1; ++j) data = (byte)(~(data ^ xor1));
                        if (len1 == 3 && data == 0x43 && data == 0x52 && data == 0x4B)
                        {
                            bl = true;
                        }
                        i += len1;

                        if (type > 7) break;
                        if (i + 4 > len) break;
                        int len2 = data + data * 256 + data * 256 * 256 + data * 256 * 256 * 256;
                        byte xor2 = (byte)(data ^ data);
                        i += 4;
                        if (i + len2 > len) break;
                        if (type == 6 || type == 7)
                        {
                            for (int j = 0; j < len2; ++j) data = (byte)(~(data ^ xor2));
                        }
                        if (bl && len2 == 0x20)
                        {
                            byte[] dataT = new byte;
                            for (int j = 0; j < len2; ++j) dataT = data;
                            return Decrypt(dataT, hashID, 0);
                        }
                        i += len2;
                  }
                  if (i != len) return null;
                }
            }
            return null;
      }
    }
}

利用这个类,你就可以方便的导出QQ中的历史消息了。

从上面的分析可以看到,查看本地的历史消息是不需要你的QQ密码的,加密密钥来源于你的QQ号码的MD5散列。所以为了保证安全,最好不要在公共电脑或者别人的电脑上使用QQ并记录历史消息。在个人电脑上,最好将历史消息加密。




最近花了几天时间跟踪了一下“QQ聊天记录查看器 5.3 华军版”,总算把聊天记录的存储方法弄清了。大家不要笑我,只是好奇而已,呵呵。

1.聊天记录存储方式

QQ聊天记录保存在MsgEx.db文件中。以前很早的版本是保存在Msg.db中,文件结构也与现在不同,我们就不分析了。

MsgEx.db采用Storage结构化存储。关于Storage复合文档的知识请查阅Microsoft相关文档,我们不做赘述。

大家可以用VC自带的DocFile View工具查看该文件的内容,可以看到文件结构大致如下:


|----MsgEx.db
|   |----C2CMsg
|   |----QQ号码
|         |----Data.msj
|         |----Index.msj
|   |----IMInfo
|   |----info.dat
|   |----Matrix
|   |----Matrix.db
|   |----SysMsg
|   |----10000
|         |----Data.msj
|         |----Index.msj
|   |----DiscMsg
|   |----GroupMsg
|   |----MobileMsg
|---------TempSessionMsg


消息内容都存储在每个号码下面的Data.msj中,通过Index.msj索引。消息内容是经过加密处理的,必须经过解密才能看到。

2.解密方法

消息内容采用BlowFish分组加密。每8个字节为一个分组。密钥Key通过QQ号码生成,具体算法稍后讨论。

解密方法:

a.取前8个字节,通过BlowFish解密, 得到decryptKey;

b.decryptKey与后面8个字节XOR,对结果再进行一次BlowFish解密;

c.将decryptKey与前8个字节XOR,得到第一组结果;

d.decryptKey与后面8个字节XOR,重复b,c两步;

e.最终全部数据解密完毕。

最后会剩下一组8字节无法解密,这个实际上是冗余数据,似乎是用来作为校验。

3.具体步骤

以上解密时,BlowFish的密钥是一个全局公用密钥Key。Key要通过QQ号码生成,具体步骤是:

a.将QQ号码进行MD5变换,得到Md5Key

b.取Matrix.db的数据,对其进行解码。简单说一下Matrix.db文件的结构:

Matrix.db采用分块存储,每个Record包含类型、名字长度、名字、内容长度、内容几个字段组成。用数据结构表示就是:


struct Record{
char rType;
short nLen;
char Name;
int rLen;
char Content;
};


初始内容也是通过加密存储的。解密方法很简单:将长度的低位字节和高位字节XOR,得到key;将内容逐个与key进行XOR,就得到结果。对名字和内容分别进行解密即可。解密后会看到STL, TIP, CRK, CPH, CAH等字段,不清楚具体的啥含义,感兴趣的同学可以自己去研究研究。我们要用到的是CRK字段,长度为32字节(如果本地聊天记录加密,可能会有变化,没试过)。将得到的CRK字段作为pData。

c.用Md5Key对pData进行BlowFish解密,得到全局密钥Key

4.结论

以上讨论的都是本地聊天记录没有加密的情况。如果选择了加密,没有密码是肯定解不出来滴,大伙就不用费心了

[ 本帖最后由 haobinnan 于 2008-12-22 18:04 编辑 ]

rainhe 发表于 2008-12-22 22:36:44

从哪里搞到的这么好的资料?:37|

san123456789 发表于 2008-12-23 00:34:59

:37| :37| :37| :37|

haobinnan 发表于 2008-12-23 07:23:35

回复 2# rainhe 的帖子

对大家有用就好:165| :165| :165|

reeker 发表于 2008-12-23 11:03:39

这个得顶一下!即使是转的,也是好贴!

lwb_hao 发表于 2009-1-23 00:28:38

好资料:36| :36| :36| :36| :36|

金龙卡 发表于 2009-2-23 20:46:55

狠好,虽然看不懂:5|

wwan 发表于 2009-5-11 14:20:32

狂顶啊!!!太好了

清茶 发表于 2009-6-1 14:07:17

一看到那满篇的代码就头晕了,没明白,不过下面的介绍还是蛮清楚的

dgtan 发表于 2009-8-19 11:31:41

小楠,厲害!...

一帘幽梦 发表于 2009-10-31 05:04:15

你好厉害,支持

maxshot 发表于 2009-11-9 12:32:40

这个得顶一下!即使是转的,也是好贴!

cjteam 发表于 2010-2-22 23:43:43

这个是老版本了,09。10版的就不是了

xddn 发表于 2010-3-4 10:41:42

学习了,谢谢

smscxj 发表于 2010-5-8 22:07:35

看不懂...............
页: [1] 2
查看完整版本: 转载 解密QQ消息文件格式