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[] m_MsgList = new List[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(); } 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 dic = new Dictionary(); 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.csdn.net/vbvan/archive/2007/12/14/1937440.aspx
|