|
1.加密和解密结构在客户端和服务器是一样的,不会存在客户端的加密对应服务器的解密,服务器的加密对应客户端的解密 2.加密后数据的长度不会进行改变 3.加密方法没有采用比较有名的算法(例如DES),只是进行简单的置换和异或 4.算法采用了两套解密方式(重叠使用),方式一:逐字节置换,方式二:逐四字节进行异或(当然加密和解密顺序是相反的) 5.异或秘钥是根据上次发送的信息生成的,这样可以达到一环扣一环,只监听一次数据包是不可能解出明文的,必须还要有上次通信的内容 6.固定异或秘钥0xA55AA55A,估计应该是提升版本的时候用吧 7.最初异或秘钥有客户端生成,发送给服务器 8.服务器和客户端都保留有两个秘钥,发送秘钥和接收秘钥 首先评论一下该算法,算法的复杂度不是很高,有效的降低了因为加密解密造成的CPU消耗。聊一些乱七八糟的东西,发送网络数据包为什么 还要进行加密啊?哈哈,一方面是防止嗅探监听进行破解,更重要的是针对外挂制作者吧。分析加解密代码以前,我曾抓包分析发送密文, 就像网上常见的教程那样,先发送一个“1”,再分送一个“1”,再发送一个“2”,再发送一个“11”,观察数据包长度和内容的变化,由此猜测加密 方式,后来看到秘钥是变化的,因为前后发送两个“1”的密文是不同的,由此我猜测服务器端和客户端是否约定好了一个很大的秘钥数组,轮 着来,后来连续抓包发送的二十个“1”,妈的,崩溃,猜测失败。哈哈,当然现在看来这种只靠猜的方法是不可能猜出来加密解密的方式的, 屡次猜测失败的情况下最终放弃,最近在看网狐内核代码,看到了加密解密这段,不由得赞叹当初的白费功夫。虽然看不到C++的源代码,但 是可以反汇编看到客户端的代码,我也挺熟悉OllyDbg的,大学期间常用它破解一些小程序,还有就是免杀常用到它,但是我没有达到用它分 析算法的境界,相信牛人分析这些都是小菜一碟,我也只是泛泛学学而已。后来我尝试抓包腾讯的聊天数据包,加密之后的数据包长得样子和 网狐擦不多,估计是算结构有雷同之处。 好了,进入正题,我们来一步一步分析加解密代码,我们分析的时候肯定是先分析加密的程序,但是读代码的时候我是先看的解密代码,这样就 好容易了很多,当然如果你一理解了该算法,无论从前看还是从后看都是非常容易的,无非就是置换+异或嘛。我们先来整体了解一下加密流程 再进行细节分析吧,这样方便读者阅读,有了方向感和目标感,否则不太理解为什么这样做,这样做的好处是什么。首先我们看一下发包结构, 最终发送的结构体是TCP_Buffer,我们观看一下它的一个成员cbBuffer,好家伙将近16KB,当然这个结构体不会整体发送过去,只会发送前边一 部分数据,具体数据有多长,TCP_Info的最后面的成员wPacketSize会决定,这个性质决定了TCP_Info结构体不被加密,看看这个结构体的其他 两个元素,cbCheckCode,校验和,TCP协议不是已经有检验和了吗,为什么多次一举呢,大概是防止外挂程序恶意修改分包数据的内容吧,因为 TCP协议在底层,外挂程序大都在应用层,如果不在应用层进行校验和,本地外挂轻而易举就可修改封包内容,如果不是本地修改而是数据包在 网络发送中修改就没有那么简单了(其实也不是很复杂,修改封包之后再修改校验和就可以了),TCP协议的校验和大概就是防止数据包在网络 中的修改吧,cbVersion,版本号,这个应该是防止新老版本通信的策略,或者还有其他用途,由于版本是6603.所以版本号都是0x66。TCP_Command ,主命令,子命令没有什么好说的,命令太多,进行分组。 我们看看EncryptBuffer函数,首先要调整代码长度,调整到长度正好是4的整数倍,不够的后面补0 下面这段代码就是干的这个活, WORD wEncryptSize = wDataSize - sizeof(TCP_Info), wSnapCount = 0; if ((wEncryptSize % sizeof(DWORD)) != 0) { wSnapCount = sizeof(DWORD) - wEncryptSize % sizeof(DWORD); memset(pcbDataBuffer + sizeof(TCP_Info) + wEncryptSize, 0, wSnapCount); } 然后把所有字节加到一起,强转为BYTE类型, BYTE cbCheckCode = 0; for (i = sizeof(TCP_Info); i < wDataSize; i++) { cbCheckCode += pcbDataBuffer; pcbDataBuffer = MapSendByte(pcbDataBuffer); } 还有一句是pcbDataBuffer = MapSendByte(pcbDataBuffer);这是个字节映射, 先看看这个映射函数m_cbSendRound是自定义偏移,个另一方的m_cbRecvRound是对应的, 就是说这边发完数据包那边接收之后两个值是一样的,这样就可以达到秘钥一直在变,为了方便理解 我们把它们暂时看做是0,接着聊聊两个映射数组,这两个数组可不是随便写的,该数组有256个元素, 各不相同,为什么有两个数组?一个数组不是挺好的吗?加解密有一个数组,这样才是可逆的啊,期初 我一直犯嘀咕,仔细看下这里面还有猫腻,比如我要加密一个字节“0x00”,我们看看MapSendByte函数, 从g_SendByteMap里取第0x00个元素,就是0x70,这就是加密之后的数值,怎么解密呢,再看g_RecvByteMap 数组,第0x70个元素正好是0x00,是不是巧合?再试一个数据,“0x01”,g_SendByteMap的第1个元素值是 0x2F,加密值就是0x2F,g_RecvByteMap数组的第0x2F个元素正好是“0x01”,看到了吧,很神奇的数组吧, 我们回过头看看为什么会有两个数组呢?保证程序的高效性,如果只有一个数组,解密的时候我们就不 得不写一个循环进行破译,现在有了专门破译的数组,效率岂不是快了数百倍,为了实现动态的映射, 加入了m_cbSendRound偏移量,没有关系的,有人可能会有疑问,接收端怎么知道这个偏移量,会不会 两边不一样,哈哈,不用担心,加密段和解密端两边预定好一开始的偏移量(0),他们还约定每发送一 个字节偏移量+3,这样就可以保证两边的偏移量始终一样。万一丢包了怎么办?哈哈发生丢包就不可以正常 解密了,所以这种算法只适合TCP协议,UDP是不合适的。 BYTE CServerSocketItem::MapSendByte(BYTE const cbData) { BYTE cbMap = g_SendByteMap[(BYTE)(cbData+m_cbSendRound)]; m_cbSendRound += 3; return cbMap; } 接着再看异或加密,每次操作4个字节,和谁进行异或呢?我举个例子,比如对一下八个自己23 43 54 65 53 56 78 34 异或加密,先把这八个字按照四个字节一块儿的分两节,23 43 54 65 和 53 56 78 34,对后面的四个字节异或时,有 前边四个自己进行固定的映射,映射成4个字节xx xx xx xx,再由这4个字节和53 56 78 34进行异或,生成密文,那么 前边这四个23 43 54 65和谁进行异或呢,和他前边的字节进行固定映射之后进行异或,这样说总的有个头吧,没错, 这个数据包的头异或的目标是上次数据包的尾,那最初的最初和谁异或呢?这个是客户端随机生成的,他会在第一次数 据包里面发送给服务器。这真是一环扣一环呀,所以说只抓获一次数据包是无法全部解密的,总是四字节的异或,要是 不够四字节怎么办,不够就补0呀,知道当初为什么这样做了吧。知道了加密方式,解密方式就不用讲了。综合来看这套 加密机智还是相当不错的,算法不算太复杂,但是很好用呀,想想当初只看密文猜测加密方式和秘钥,简直是扯淡。就说 这么多吧,要是有什么说错的地方欢迎大家指正。
|
|