扫一扫,访问微社区

新浪微博登陆

只需一步, 快速开始

登录 | 加入雨荷 | 找回密码

数据恢复,winhex视频,数据恢复培训,硬盘数据恢复,raid0,raid1,raid5数据恢复,雨荷数据

 找回密码
 加入雨荷

扫一扫,访问微社区

新浪微博登陆

只需一步, 快速开始

总共6071条微博

动态微博

查看: 6493|回复: 23

转载 解密QQ消息文件格式

  [复制链接]

该用户从未签到

新浪微博达人勋

发表于 2008-12-22 18:02:51 | 显示全部楼层 |阅读模式
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, 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[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,
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 = (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并记录历史消息。在个人电脑上,最好将历史消息加密。





最近花了几天时间跟踪了一下“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[nLen];
int rLen;
char Content[rLen];
};


初始内容也是通过加密存储的。解密方法很简单:将长度的低位字节和高位字节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 编辑 ]
  • TA的每日心情
    开心
    2017-2-2 08:54
  • 签到天数: 64 天

    [LV.6]站长亲人

    新浪微博达人勋

    发表于 2008-12-22 22:36:44 | 显示全部楼层
    从哪里搞到的这么好的资料?:37|
  • TA的每日心情
    慵懒
    2013-10-17 22:20
  • 签到天数: 7 天

    [LV.3]偶尔蹭饭

    新浪微博达人勋

    发表于 2008-12-23 00:34:59 | 显示全部楼层

    该用户从未签到

    新浪微博达人勋

     楼主| 发表于 2008-12-23 07:23:35 | 显示全部楼层

    回复 2# rainhe 的帖子

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

    该用户从未签到

    新浪微博达人勋

    发表于 2008-12-23 11:03:39 | 显示全部楼层
    这个得顶一下!即使是转的,也是好贴!
  • TA的每日心情
    开心
    2013-4-7 21:16
  • 签到天数: 14 天

    [LV.3]偶尔蹭饭

    新浪微博达人勋

    发表于 2009-1-23 00:28:38 | 显示全部楼层
    好资料:36| :36| :36| :36| :36|
  • TA的每日心情
    擦汗
    2015-4-1 22:53
  • 签到天数: 55 天

    [LV.5]站长亲戚

    新浪微博达人勋

    发表于 2009-2-23 20:46:55 | 显示全部楼层

    该用户从未签到

    新浪微博达人勋

    发表于 2009-5-11 14:20:32 | 显示全部楼层
    狂顶啊!!!  太好了

    该用户从未签到

    新浪微博达人勋

    发表于 2009-6-1 14:07:17 | 显示全部楼层
    一看到那满篇的代码就头晕了,没明白,不过下面的介绍还是蛮清楚的

    该用户从未签到

    新浪微博达人勋

    发表于 2009-8-19 11:31:41 | 显示全部楼层
  • TA的每日心情
    开心
    2015-9-29 13:35
  • 签到天数: 3 天

    [LV.2]偶尔串门

    新浪微博达人勋

    发表于 2009-10-31 05:04:15 | 显示全部楼层

    该用户从未签到

    新浪微博达人勋

    发表于 2009-11-9 12:32:40 | 显示全部楼层
    这个得顶一下!即使是转的,也是好贴!
  • TA的每日心情
    擦汗
    2012-10-25 16:12
  • 签到天数: 58 天

    [LV.5]站长亲戚

    新浪微博达人勋

    发表于 2010-2-22 23:43:43 | 显示全部楼层
    这个是老版本了,09。10版的就不是了

    该用户从未签到

    新浪微博达人勋

    发表于 2010-3-4 10:41:42 | 显示全部楼层

    该用户从未签到

    新浪微博达人勋

    发表于 2010-5-8 22:07:35 | 显示全部楼层
    发表回复
    您需要登录后才可以回帖 登录 | 加入雨荷 新浪微博登陆

    本版积分规则

    Copyright © 2001-2015 All Rights Reserved雨荷数据安全技术论坛 陕ICP备08105630

    QQ|申请友链|小黑屋|手机版|Archiver|雨荷数据安全技术论坛 ( 陕ICP备17009169号

    快速回复 返回顶部 返回列表