NDIS网络封包的研究
最近在研究一个修改网络封包的问题,研究了4天了,有点心得,害怕以后想不起来,所以放在博客上做个笔记。
我所在的学校和大多数高校一样,采用了城市热点的Dr.com计费系统,不过说句实话,城市热点的那个DRCOM写的真的不怎么样。它采用的SPI的方式,主要过滤 Send 和 SendTo 两个函数,分别过滤TCP和UDP协议。
城市热点的DRCOM实现方法其实不是很复杂,他先是和服务器交换数据,得到一个16字节的隧道码,然后TCP连接的时候,第一个数据包前面就加上16个字节的这个数据,然后经过城市热点的交换机时,交换机把这个数据去掉,然后实现正常的交换机功能。再说他的DRCOM,采用SPI的方法,SPI严格上应该算是应用层,虽然相对来说比较底层,但是毕竟是应用层,在SPI里实现加数据,造成很多软件都无法正常工作,我猜想原因应该是这些程序使用了特殊的发送数据的方法,或者其他原因,具体原因我也没研究,不过很明显,很多程序无法正常使用,比如NOD32就无法正常更新,操作系统也无法正常更新。
我从网上下了一个DRCOM的新版本,他写的FOR VISTA,安装上后发现新版本采用了NDIS的方式来附加数据,应该就是IMD驱动,直接作用在NIC和协议驱动之间,也是很多防火墙惯用的方式,这个版本的DRCOM不会造成杀软和操作系统无法更新的问题了。
不过就着打破砂锅问到底的方式,我在 sourceforge.net 网站上找到了DRCOM的LINUX版本,也是一个高手破解出来写的,有个WIN32版本可惜没有提供代码,发邮件也不回,为此只得就着LINUX代码研究。
经过2天研究把和DRCOM服务器交换数据的协议研究出来了,然后就是最关键的在数据包里加入16字节隧道码,我也采用的NDIS方式,微软的DDK自带了一个PASSTHRU的驱动程序就是一个IMD驱动的范本,不过他没有实现任何功能,只是对发送出去的数据包重新申请了一个数据包发送,并没有修改或者过滤。
要实现过滤,首先必须得到要发送的数据,很不幸,NDIS里不是用一整块内存来储存数据的,虽然不知道为什么,不过记得很久以前在哪里看到过一个文章简单说明了原因,因为采用链表的形式是为了封包的时候方便,直接将IP头,ETH头和TCP头还有数据,分别存在一个节点里,然后用链表的形式挂接,这样避免了频繁的申请和回收内存。 不管怎么说,首先我们先要获得数据,我搜索了很多NDIS的资料,在安焦上找到一个文章,他是介绍的防火墙的,里面就有得到数据包的方法
//获得数据大小
NdisQueryPacket(MyPacket, NULL, NULL, NULL, &PacketSize);
// KdPrint(("数据大小:%d\n", PacketSize));
//申请数据缓冲区
Status = NdisAllocateMemoryWithTag(&PacketContent, 2048, TAG);
if (Status != NDIS_STATUS_SUCCESS)
return;
NdisZeroMemory(PacketContent, 2048);
NdisQueryBufferSafe(MyPacket->Private.Head, &pBuf, &BufLength, HighPagePriority);
NdisMoveMemory(PacketContent, pBuf, BufLength);
i = BufLength;
pNext = MyPacket->Private.Head;
for(;;)
{
if(pNext == MyPacket->Private.Tail)
break;
pNext = pNext->Next; //指针后移
if(pNext == NULL)
break;
NdisQueryBufferSafe(pNext,&pBuf,&BufLength,32);
NdisMoveMemory(PacketContent+i,pBuf,BufLength);
i+=BufLength;
}
他先获得了包的大小,估计是想动态申请内存,不过不知道为什么,后来直接写的2000,我后来改成的2048,不过我试过一次动态申请,结果蓝屏了,也没研究原因。可以看到,从数据包的链表里,提取数据然后储存在 PacketContent 里。直到链表到末尾。
这样提取出来的数据实际是 ETH头部+IP头部+TCP头部+数据,也就是实际在网络传输的数据形式。
因为我要过滤的是TCP协议,UDP协议是每个包都加了一个隧道码,很简单,TCP则是每个连接的第一个包才加,后面都不加,相对比较复杂,我开始想的是直接判断连接,但是网卡数据都是以包的形式,不存在连接问题,后来想想类似程序发现最明显的就是防火墙,因为防火墙在第一次连出的时候都会拦截,可惜这种商业级的防火墙代码一个都没找到,找到的都是简单的过滤型防火墙,无奈只有自己想,我想的方法是过滤PSH包,因为TCP连接建立时先是三次握手,这个玩过DDOS的基本都知道。
连接发起者发送SYN包到对方。
对方接收到后判断对应端口是否可以连接,如果可以发送SYN+ACK标志的包。
接到后发送ACK包,至此连接就建立成功。
当一方需要发送数据的时候,TCP的标志位是PSH+ACK, 对方收到后返回ACK包。
我想的就是过滤PSH包。
if (ipPacket->tcp.th_flag == 0x0018)
如果是PSH包则表示发送数据,开始我只是修改了数据的第一位,修改为大写字母A,结果发现接收的时候接收不到,想了很久没想到原因,后来抓包发现数据实际是到达了,但是因为数据修改了,校验和就不正确了,所以接收方就将数据包丢弃了,而发送方不知道,则做超时处理,并大概在6秒以后重新发送,如此往复(从这点看出TCP的稳定性确实非UDP可比,不会出现丢包的问题,不过又从一个侧面来看,似乎也是一个很强大的拒绝服务攻击方式)。 计算校验和又费了我很多时间,开始参照了一些DDOS工具的校验和算法,但是每次算出来的都不对。后来发现实际TCP校验和的算法应该是 PSD伪头部+TCP头部+数据 整体来进行 checksum 代码如下:
ptcp_header = (PTCP_HEADER)(PacketContent + sizeof(ETHHDR) + sizeof(IP_HEADER));
tcphdrlen = HTONS(ipPacket->ip.total_len) - sizeof(IP_HEADER);
memcpy(pCheckSum, ptcp_header, tcphdrlen);
KdPrint(("SizeofTCP: %d TcpHdrLen %d\n", sizeof(TCP_HEADER), tcphdrlen));
//修改目的地址和目的端口,校验和
((PTCP_HEADER)pCheckSum)->th_sum = 0;
//填充TCP伪首部
psd_header.saddr = ipPacket->ip.sourceIP; //源地址
psd_header.daddr = ipPacket->ip.destIP; //目的地址 psd_header.mbz = 0;
psd_header.ptcl = 6/*IPPROTO_TCP*/;//协议类型
psd_header.tcpl = HTONS(sizeof(TCP_HEADER)); //TCP首部长度
//计算TCP首部校验和
NdisZeroMemory(SendBuf, 2048);
NdisMoveMemory(SendBuf, &psd_header, sizeof(psd_header));
NdisMoveMemory(SendBuf+sizeof(psd_header), pCheckSum , tcphdrlen);
tcp_header.th_sum = checksum((USHORT*)SendBuf,sizeof(psd_header)+tcphdrlen);
tcp_header.th_sum = HTONS(HTONS(tcp_header.th_sum) - DataSize);
//KdPrint(("网络代码计算校验和: [0x%.4x] 原校验和: [0x%.4x]\n", tcp_header.th_sum, ipPacket->tcp.th_sum));
ipPacket->tcp.th_sum = tcp_header.th_sum;
因为 Intel 的字节序和网络字节序是反的,所以多次用到HTONS这个宏,因为这个是驱动,没有办法用 winsock2.h 所以只有网上找了个宏来用。
开始算出来还是不对,不过后来发现每次修改后的数据在ethereal显示的正确的校验和是算出来的校验和减去数据的长度,所以后面减去了一个 DataSize 测试发现修改数据可以实现了,下面要改改,改为添加16字节数据,先是将数据拷贝到数据包里,然后重新计算TCP校验和,不过问题又出来了,测试发现又出现接不到数据的问题了,抓包发现这次数据是添加了,但是ethereal显示还是以前的数据长度,又仔细看了下数据包,发现IP头有个值表示的是整个数据包的长度除开ETH头也就是说长度是 IP头+TCP头+数据,所以又修改了这个值,结果还是不行,继续抓包分析。发现IP头的校验又不正确了。 参见某些DDOS的代码,计算出来还是不对,而且差距甚远。。。。。郁闷了很久,他的代码如下:
memcpy(SendBuf,&ip_header,sizeof(ip_header));
memcpy(SendBuf+sizeof(ip_header),&tcp_header,sizeof(tcp_header));
memset(SendBuf+sizeof(ip_header)+sizeof(tcp_header),0,4);
datasize=sizeof(ip_header)+sizeof(tcp_header);
ip_header.checksum=checksum((USHORT *)SendBuf,datasize);
他是将IP头和TCP头进行计算,可惜不对。
网上找了很多代码都是这个方法,想了很久想不到为什么错误。 后来因为一些事情出去了一下,回来时候大脑稍微清醒点了,突然想到一个问题,以前修改数据的时候抓包,发现只是TCP校验不正确,这就证明IP头的校验和和数据无关,后来为此我对TCP头重新计算,这个时候可以正常接发,所以证明也和TCP头无关,那么也就是说只和ETH头和IP头有关,但是在DDOS工具里是看不到ETH头的,也就证明只和IP头有关,试着写了个代码,实现我的想法是正确的。
//-----------计算IP校验---------
pip_header = (PIP_HEADER)(PacketContent + sizeof(ETHHDR));
pip_header->checksum = 0;
pip_header->checksum = checksum((USHORT*)pip_header, sizeof(IP_HEADER));
//KdPrint(("网络代码计算校验和: [0x%.4x] 原校验和: [0x%.4x]\n", ip_header.checksum, ipPacket->ip.checksum));
//---------------------------
他只和IP头有关,修改了长度后直接把校验和置零然后重新计算即可。下面就是发送的代码,我只是重新申请了数据包:
KdPrint(("数据 %s 大小 %d\n", pData, i-sizeof(PACKET)));
NdisAllocateBuffer(&Status,&pDataBuffer, pAdapt->SendPacketPoolHandle, PacketContent, i);
NdisChainBufferAtFront(MyPacket, pDataBuffer);
NdisFreeMemory(PacketContent, 2048, 0);
开始找转发的方法时候安焦上找到一个NDIS实现NAT的文章,他的方法有几步是错的,郁闷了我很久,至少我按他的方法实现要蓝屏。 后面就是自带的一句
NdisSend(&Status, pAdapt->BindingHandle, MyPacket);
然后下面都是他写的代码了,判断是否发送完成之类的,就不帖了。 这个的确实现了发出的数据里添加16字节数据,不过有个目前还有个问题,就是对方返回的一个ACK包,应该这个ACK包里有数据的校验,校验应该是返回给上层了的,因为我数据包修改了,校验和就不对,以前修改内容的时候就没出现这个问题,初步估计是校验只是校验了数据长度,至于是哪个位置是校验目前还不知道,还没研究出来。 当发送方收到校验时候会在两台机子之间交换一个很奇怪的包,应该是检测网络或者重置网卡用的,具体什么用我也还没研究出来,不过交换速度很快,瞬间就交换了上千个数据包,这不得不让人又想到了很万恶的拒绝服务攻击的方式。。。。 本文纯粹的学习笔记,希望给同样想写类似程序的人一个帮助。