解密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>[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 == nullreturn;
            }

            for (int i = 0; i < m_MsgList.Length; ++i)
            {
                m_MsgList[i] = 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[i])
                        {
                            m_MsgList[i].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 == nullreturn;
            if (m_Password == nullreturn;

            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 == nullreturn;
            if (m_Password == nullreturn;

            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[i]))
                        {
                            using (BinaryReader br = new BinaryReader(ms, Encoding.GetEncoding(936)))
                            {
#if false
                                fs.Write(msgList[i], 0, msgList[i].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 == nullreturn;

            Dictionary<stringlong> dic = new Dictionary<stringlong>();
            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[i]);
                foreach (string ID in m_MsgList[i])
                {
                    Console.WriteLine("/t{0}: {1}", ID, dic[s_MsgName[i] + 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 == nullreturn 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[i] = 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[i] = pos[i + 1] - pos[i];
                    }
                    using (BinaryReader br = new BinaryReader(fileObject))
                    {
                        for (int i = 0; i < num; ++i)
                        {
                            fileObject.Seek(pos[i], SeekOrigin.Begin);
                            byte[] data = br.ReadBytes(len[i]);
                            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[QQID.Length];
            for (int i = 0; i < QQID.Length; ++i) dataID[i] = (byte)(QQID[i]);
            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[i] + data[i + 1] * 256;
                        byte xor1 = (byte)(data[i] ^ 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[i] == 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[i] + data[i + 1] * 256 + data[i + 2] * 256 * 256 + data[i + 3] * 256 * 256 * 256;
                        byte xor2 = (byte)(data[i] ^ 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并记录历史消息。在个人电脑上,最好将历史消息加密。

你可能感兴趣的:(String,qq,解密,null,byte,encoding)