当SYN数据包的发送速度大幅提高后,校验和计算成了自然成了不可忽视的一部分。
传统的Checksum算法网上随便一搜一大堆,甚至不少攻击器也是用此算法。该算法并没有什么缺点,具有普遍性,可计算任意数据任意长度的Checksum。对于SYN包那样很短并且每次都是固定长度的包,这种算法显然有些累赘了。我们不妨仔细分析下Checksum算法的原理:
USHORT ChechkSum(USHORT *buffer, int size)
{
DWORD chkSum = 0;
while(size > 1)
{
chkSum += *buffer++;
size -= sizeof(USHORT);
}
if(size)
chkSum += *(UCHAR*)buffer;
chkSum = (chkSum >> 16) + (chkSum & 0xffff);
chkSum += (chkSum >> 16);
return (USHORT)(~chkSum);
}
下面不考虑长度为奇数的情况。先看循环里面的代码,事实上只是把这些数据当作一个USHORT[],计算这个数组的和而已。即使数据的内容不一样,但只要这个累加的和不变,那后面不管对其做什么运算,函数结果肯定是一样的。简单的说:不同的数据可能有相同的和,必然也有相同的校验和。比如1+2+3=3+2+1=2+2+2,和都是6,所以他们也有相同的校验和。
既然是这样的算法,我们就可以大作优化了。比如一个固定数组:USHORT A[10]。他的和一共有65535*10+1种情况。这个应该很好理解:如果10个数全是0,那和也是0;反之全是最大数65535,那么和是655350;其余情况就在这之间波动。由此可见,N字长的数据校验和其实也只有65535*N+1种情况。在本程序中计算TCP校验和的数据有20字长(40字节),其结果也只有1,310,699种。如果你在思考的话一定也想到了经典的空间换时间算法。没错,正是如此!即使事先把这几个数的校验和全部计算好保存起来,也仅需2.5M的内存空间。不过问题是,以后如何去利用表里的这些缓存数据?
因为这是一张“和”与“校验和”的映射表,所以当“和”确定后即可获得“校验和”。因此在预先填充数据包的时候,可变的字段一定要置0,然后计算出这个半成品包的“和”当作“基值”。以后当可变部分填上数据时,比如IP头填上了id字段,那么这个IP头的“和”就是:基值+id,校验和当然就是:表[基值+id]。o(1)的复杂度,够快吧!
当然,如果可变字段超过2个字节就不能这么简简单单的加上就可以了,还要对其做高位合并计算。比如IP头的SrcIP(4个字节)填上了数据,那么:
表[基值 + SUM(SrcIP)]
才是其正确的校验和,具体的SUM运算可以用个宏来定义:
#define HI(VALUE) (VALUE >> 16)
#define LOW(VALUE) (VALUE & 0xFFFF)
#define SUM(VALUE) (HI(VALUE) + LOW(VALUE))
缓存表的初始化:
USHORT TableChkSum[MAX_TBL_SIZE];
/**************************************************
* 函数:InitChkSumTable
* 注释:初始化Checksum映射表
* 缓存: 字段和(sum) -> 校验和(checksum)
**************************************************/
VOID InitChkSumTable()
{
UINT i;
UINT chkSum;
for(i=0; i<MAX_TBL_SIZE; i++)
{
chkSum = SUM(i);
chkSum += HI(chkSum);
TableChkSum[i] = (USHORT)~chkSum;
}
}
(2009/1/2)