引言
截包的需求一般来自于过滤、转换协议、截取报文分析等。
过滤型的应用比较多,典型为包过滤型防火墙。
转换协议的应用局限于一些特定环境。比如第三方开发网络协议软件,不能够与原有操作系统软件融合,只好采取“嵌入协议栈的块”(BITS)方式实施。比如IPSEC在Windows上的第三方实现,无法和操作系统厂商提供的IP软件融合,只好实现在IP层与链路层之间,作为协议栈的一层来实现。第三方PPPOE软件也是通过这种方式实现。
截取包用于分析的目的,用“抓包”描述更恰当一些,“截包”一般表示有截断的能力,“抓包”只需要能够获取即可。实现上一般作为协议层实现。
本文所说的“应用层截包”特指在驱动程序中截包,然后送到应用层处理的工作模式。
截包模式
用户态下的网络数据包拦截方式有
1.Winsock Layered Service Provider;
2.Windows 2000 包过滤接口;
3.替换系统自带的WINSOCK动态连接库;
利用驱动程序拦截网络数据包的方式有
1.TDI过滤驱动程序(TDI Filter Driver)
2.NDIS中间层驱动程序(NDIS Intermediate Driver)
3.Win2k Filter-Hook Driver
4.NDIS Hook Driver
用户态下拦截数据包有一些局限性,“很显然,在用户态下进行数据包拦截最致命的缺点就是只能在Winsock层次上进行,而对于网络协议栈中底层协议的数据包无法进行处理。对于一些木马和病毒来说很容易避开这个层次的防火墙。”
我们所说的“应用层截包”不是指上面描述的在用户态拦截数据包。而是在驱动程序中拦截,在应用层中处理。要获得一个通用的方式,应该在IP层之下进行拦截。综合比较,本文选用中间层模式。
为什么要在应用层处理截取的报文
一般来说,网络应用如防火墙,协议类软件都是工作在内核,我们为什么要反过来,提出要在应用层处理报文呢?理由也可以找出几点(哪怕是比较牵强):
众所周知,驱动程序开发有一定的难度,对于一个经验丰富的程序员来说,或许开发过程中不存在技术问题,但是对初学者,尤其是第一次接触的程序员简直是痛苦的经历。
另外,开发周期也是一个不得不考虑的问题。程序工作在内核,稳定性/兼容性都需要大量测试,而且可供使用的函数库相对于应用层来说相当少。在应用层开发,调试修改相对要容易地多。
不利的因素也有:
性能影响,在应用层工作,改变了工作模式,每当驱动程序截到数据,送到应用层处理后再次送回内核,再向上传递到IP协议。因此,性能影响非常大,效率非常低,在100Mbps网络上,只有80%的性能表现。
综合来看,在特定的场合应用还是比较适合的:
台式机上使用,台式机的网络负载相当小,不到100Mbps足以满足要求,尤其是主要用于上网等环境,网络连接的流量不到512Kbps,根本不用考虑性能因素。作为单机防火墙或其他一些协议实现,分析等很容易基于这种方式实现。
方案
模型
上图描述了应用层截包的模型,主要的流程如下:
接收报文过程:
1.网络接口收到报文,中间层截取,通过2送到应用层处理;
2.应用层处理后,送回中间层处理结果;
3.中间层根据处理结果,丢弃该报文,或者将处理后的报文通过1送到IP协议;
4.IP协议及上层应用接收到报文;
发送报文过程:
1.上层应用发送数据,从而IP协议发送报文;
2.报文被中间层截取,通过2送到应用层处理;
3.应用层处理后,送回中间层处理结果;
4.中间层根据处理结果,丢弃该报文,或者将处理后的报文发送到网络上;
实现细节探讨
IO与通讯
有一个很容易的方式,在驱动程序和应用程序之间用一个事件。
在应用程序CreateFile的时候,驱动程序IoCreateSynchronizationEvent一个有名的事件,然后应用程序CreateEvent/OpenEvent此有名事件即可。
注意点:
1,不要在驱动初始化的时候创建事件,此时大多不能成功创建;
2,让驱动先创建,那么此后应用程序打开时,只能读(Waitxxxx),不能写(SetEvent/ResetEvent)。反之,如果应用程序先创建,则应用程序和驱动程序都有读写权限;
3,用名字比较理想,注意驱动中名字在\BaseNamedObjects\下,例如应用程序用“xxxEvent”,那么驱动中就是“\BaseNamedObjects\xxxEvent”;
4,用HANDLE的方式也可以,但是在WIN98下是否可行,未知。
5,此后,驱动对读请求应立即返回,否则就返回失败。不然将失去用事件通知的意义(不再等待读完成,而是有需要(通知事件)时才会读);
6,应用程序发现有事件,应该在一个循环中读取,直到读取失败,表明没有数据可读;否则会漏掉后续数据,而没有及时读取;
处理线程优先级
应用层处理线程应该提高优先级,因为该线程为其他上层应用程序服务,如果优先级比其他线程优先级低的话,将会发生类似死锁的等待状态。
另外,提高优先级的时候必须注意,线程尽量缩短运行时间,不要长期占用CPU,否则其他线程无法得到服务。优先级不必提高到REALTIME_PRIORITY_CLASS级,此时线程不能做一些磁盘IO之类的操作,而且也影响到鼠标、键盘等工作。
驱动程序也可以动态地提高线程的优先级。
缓存
在驱动程序接收到报文后,至少应该有一个缓冲以便临时存储,等待应用层处理。缓冲不必很大,只要能在应用层得到时间片之前缓冲区不溢出就可以了,实践中大约能存储几十个报文就够了。
缓冲的使用方式,是一个先进先出的队列。考虑方便实现为静态存储的环形队列,也就是说,不必每次分配内存,而是一次性分配好一大块内存,环形的使用。
初始,head==tail==0;
tail和head都是无限增长的。
Tail – head <= size;
放入一个报文时, tail=tail + packetlen;
取出一个报文时,head=head + packetlen;
tail== head表明空;
tail>head表明有数据;
tail + input packet length - head >size表明满;
取数据时:
ppacket GetPacket()
{
ASSERT(tail>=head);
if(tail==head)
return NULL;
//else
ppacket = &start[head % SIZE];
if(head % size + ppacket->length > size )
//数据不连续(一部分在尾部,一部分在头部);
else
//数据是连续的
return ppacket;
}
放入数据:
bool InputPacket(ppacket)
{
if(tail + input packet length - head >size) //满
return false;
//copy packet to &start[tail % SIZE]
//if(tail % SIZE + packet length > SIZE)
//数据不连续(一部分在尾部,一部分在头部);
//else
//数据是连续的
tail = tail + packet length;
return true;
}
上面这种方式采用数组的方式组织,为每个报文提供一个最大报文长度的空间。因为缓冲区数目有限,因此这种方式可以满足需要。如果要考虑到减少空间的浪费,那么可以按每个报文的实际长度存储,上面的算法不能够适应这种方式。
应用层和驱动程序的通信
在网卡接收/IP发送过程中,驱动程序缓存报文,用事件通知应用层有报文需要处理。那么应用层可以通过IO方式或者共享内存方式取得此报文。
实践说明,在100Mbps速率下,以上两种方式都可以满足需要,最为简便的方式就是使用有缓冲的IO方式。
应用层处理完毕,也可以使用以上两种方式之一来向驱动程序递交结果。不过,IO方式因为一次只能发送一个报文,100Mbps网络速度下降为70%~80%网络速度,10Mbps不会有影响。也就是说,主机发出的最大速度只有70%的网络速度,这和应用程序发送不超过MTU的UDP数据报的速度是一样的。对TCP来说,由于是双向通信,损失更加大一些,大约40%~60%速度。
这时候,使用共享内存方式,因为减少了系统调用的开销,可以避免速度下降。
报文发送的速度控制
当IP协议发送报文的时候,一般来说,我们的中间层驱动必须把这些报文缓存起来,告诉IP软件发送成功,然后让应用层处理完毕之后再做决定。显然,存储报文的速度远远超过网卡能够发送的速度,然而IP软件(特别是UDP)将以我们存储的速度发送报文。造成缓存迅速耗尽。后续的报文只好丢弃。这样一来,UDP发送将不能正常工作。TCP由于可以自行适应网络状况,依然可以在这种情况下工作,速度在70%左右。在Passthru里,可以转发至低层驱动,然后用异步或同步方式返回,从而达到网卡的发送速度一致。
因此,必须有一个办法避免这种状况。中间层驱动把这些报文缓存起来,告诉IP软件发送状态未决(Pending)。等到最后处理完毕,告诉IP软件发送完成。从而协调了发送速度。这种方式带来一个问题,就是驱动程序必须在发送超时的情况下放弃对这些缓冲报文的所有权。具体来说,就是MiniportReset被调用的时候,就有可能是NDIS察觉到发送超时,从而放弃所有未完成的发送操作,如果没有正确处理这种情况,将会导致严重问题。如果中间层在Miniport初始化的时候通过调用NdisMSetAttributesEx函数设置了NDIS_ATTRIBUTE_IGNORE_PACKET_TIMEOUT标志,那么中间层驱动程序将不会得到报文超时通知,中间层必须自行处理缓存的报文。
与Passthru协同工作
当上层应用不再需要截包时,驱动程序应该完全是Passthru行为。这就要