Windows下截获网络数据
——winpcap源代码分析
王守彦
1简介
fencer在本刊的前期中,介绍用于截获网络数据的通用库libpcap的高层实现,及在linux/unix 下底层实现。Winpcap是libpcap的windows版本,它们虽然提供了同样的高层接口,但底层实现却截然不同。对于linux/unix,网络数据的截获作为系统的一项基本功能,大多直接实现于内核中,用户只要调用简单的接口函数,就能截获通过网络适配器的数据,windows 系统本身不提供截获网络数据的接口,但提供一套和网络适配器交互的网络驱动器接口规范(NDIS),可以通过NDIS实现网络数据的截获。本文作为前文的补充,详细分析winpcap 的结构和如何通过NDIS实现网络数据的截获和过滤。本文分为三个部分:winpcap结构、NDIS概述和winpcap包截获驱动器源代码分析。
2Winpcap结构
图1 winpcap结构comforter
图1描述了winpcap的结构,其中是一个使用winpcap的外部程序,winpcap 包括libpcap通用接口、packet.dll动态链接库和NDIS包截获驱动器三个部分。NDIS包截获驱动器用于从网络适配器(Network Adapter)截获数据,并提供一套标准的接口函数,如read,write,ioctl等;packet.dll提供对NDIS包截获驱动器接口函数的封装,对上层(libpcap)提供更为方便的接口;libpcap调用packet.dll中的函数,提供和linux/unix平台上libpcap相同
的接口,实现代码的通用性。
实际上在windows编程中使用packet.dll提供的接口函数和使用libpcap函数一样方便,它们都是对其它函数的封装,本文不做分析,而是着重分析NDIS包截获驱动器的实现。
3NDIS概述
包截获驱动器通过NDIS(网络驱动器接口规范)与网络适配器交互,NDIS是win32网络代码的一部分。NDIS管理各种网络适配器,并负责适配器与上层协议的通信。NDIS可以实现这几种驱动器形式:小端口驱动器、中间驱动器和协议驱动器。Winpcap的包截获驱动器实际上是一个协议驱动器。
NDIS协议驱动器可以分配,拷贝来自发送应用程序申请发送的数据包,也可以提供一个协议接口来接受来自下级驱动程序的包,并转移接受到的数据给适当的客户端应用程序。这正好是包截获驱动器要实现的功能。
4包截获驱动器源代码分析
winpcap提供win98和winNT/2000的驱动,它们使用的都是统一的NDIS接口,只是提供的接口函数不同,这里以winNT/2000代码为例。( .\winpcap\packetntx\driver\ )
4.1 入口函数DriverEntry()(packet.c)
DriverEntry()函数在这个驱动程序被加载启动时调用,是整个驱动程序的入口,其主要功能是初始化并注册一个无连接协议驱动器,为每个网络适配器创建相应的设备用于截获通过的数据。首先通过NdisRegisterProtocol()注册一个协议驱动器,这里最重要的参数就是ProtocolChar,这个结构中存放这个协议驱动器的版本、名字等,更重要的是它存放了这个协议驱动器的各种处理函数:
ProtocolChar.OpenAdapterCompleteHandler = NPF_OpenAdapterComplete;
ProtocolChar.CloAdapterCompleteHandler = NPF_CloAdapterComplete;
ProtocolChar.SendCompleteHandler = NPF_SendComplete;
ProtocolChar.TransferDataCompleteHandler = NPF_TransferDataComplete;
ProtocolChar.RetCompleteHandler = NPF_RetComplete;
ProtocolChar.RequestCompleteHandler = NPF_RequestComplete;
ProtocolChar.ReceiveHandler = NPF_tap;
ProtocolChar.ReceiveCompleteHandler = NPF_ReceiveComplete;
ProtocolChar.StatusHandler = NPF_Status;
ProtocolChar.StatusCompleteHandler = NPF_StatusComplete;
#ifdef NDIS50
ProtocolChar.BindAdapterHandler = NPF_BindAdapter;
ProtocolChar.UnbindAdapterHandler = NPF_UnbindAdapter;
ProtocolChar.PnPEventHandler = NPF_PowerChange;
ProtocolChar.ReceivePacketHandler = NULL;
这些处理函数只能被系统调用,即在网络设备或用户有某种动作后,这些函数会被自动调用,以下是这些函数的用途:
BindAdapterHandler:这是一个必须的函数,NDIS调用这个函数来请求这个协议驱动 程序绑定到一个由传过来的名字参数指向的下层NIC或虚拟NIC的句柄。
UnbindAdapterHandler:这是一个必须的函数,ProtocolUnbindAdapter由NDIS关闭一个由传过来的名字参数指向的下层NIC或虚拟NIC句柄的绑定时调用。ProtocolUnbind Adapter调用NdisCloseAdapter并在绑定安全关闭后释放分配的资源。 card reader
OpenAdapterCompleteHandler:这是一个必须的函数。如果一个协议驱动程序调用了 NdisOpenAdapter并返回NDIS_STATUS_PENDING,ProtocolOpenAdapterComplete在完成这个绑定操作以后被调用。
演讲口才培训 CloseAdapterCompleteHandler:这是一个必须的函数。如果一个协议驱动程序调用了 NdisCloseAdapter并返回NDIS_STATUS_PENDING,ProtocolCloseAdapterComplete在完成这个解除绑定操作以后被调用。
ReceiveHandler:这是一个必须的函数。ProtocolReceive随一个指向一个lookahead 缓冲被调用,如果这个缓冲接受到的网络包包含的比完整的要少,P
rotocolReceive调用 NdisTransferData随着一个协议分配的包描述符号指定的一个协议分配的缓冲来获得剩余 的收到的包。
ReceiveCompleteHandler:这是一个必须的函数。ProtocolReceiveComplete被调用来 标记任何收到的先前标记给ProtocolReceive的包现在能够进行post处理。
TransferCompleteHandler:除非协议使用NdisMIndicateReceivePacket标记包来专有 的绑定自己给下层的NIC驱动程序,这个函数是一个必须的函数。
ProtocolTransferDataComplete在先前的一个NdisTransferData调用返回一个
NDIS_STATUS_PENDING状态并残留的数据已经被协议提供的已经链接到一个给定的包描述
符的缓冲中的时候被调用。
ReceivePacketHandler:这是一个可选的函数,如果协议驱动程序将绑定到
一个NIC驱动上通过调用NdisMIndicateReceivePacket来标识一组一个或多个包,ProtocolReceive Packet函数应被提供。
SendCompleteHandler:这是一个必须的函数。在每个包通过一个调用给NdisSend传输 时返回NDIS_STATUS_PENDING状态,当发送操作完成后调用ProtocolSendComplete。如果一组包需要发送,ProtocolSendComplete在所有包都被发送给NdisSendPackets后只被调用一次,无论是否返回悬挂状态。
ResetCompleteHandler:这是一个必须的函数。ProtocolResetComplete在一个协议初 始化重置操作的时候被调用。由调用NdisReset返回NDIS_STATUS_PENDING状态开始,完成于ResetCompleteHandler。 汽车空调如何维护
RequestCompleteHandler:这是一个必须的函数。ProtocolRequestComplete在一个协 议初始化或设置操作中被调用。由调用NdisRequest返回NDIS_STATUS_PENDING状态开始,完成于RequestCompleteHandler。
StatusHandler:这是一个必须的函数。ProtocolStatus被调用来处理由下层NDIS驱动标识的状态的改变。
StatusCompleteHandler:这是一个必须的函数。ProtocolStatusComplete由NDIS调用,随着ProtocolStatus,报告NDIS或NIC驱动初始化重置操作开始或结束。
欢欣鼓舞的意思
PnPEventHandler:这是一个必须的函数。NDIS调用ProtocolPnPEvent来指出一个及插及用的事件或一个电源管理的事件。参考章节2.6.
UnloadHandler:这是一个可选的函数。NDIS调用ProtocolUnload在响应给一个用户请求来卸载安装一个协议驱动程序中。NDIS在对每个绑定的适配器调用ProtocolUnbind Adapter后调用ProtocolUnload一次。ProtocolUnload执行设备检查和清除操作。
在完成协议驱动器的注册后,就需要为新驱动器指定入口函数,也就是外部对该驱动器的操作函数,如open,close,reak,write,ioctl等:
DriverObject->MajorFunction[IRP_MJ_CREATE] = NPF_Open;
DriverObject->MajorFunction[IRP_MJ_CLOSE] = NPF_Close;
DriverObject->MajorFunction[IRP_MJ_READ] = NPF_Read;
DriverObject->MajorFunction[IRP_MJ_WRITE] = NPF_Write;
DriverObject->MajorFunction[IRP_MJ_DEVICE_CONTROL] = NPF_IoControl;
DriverObject->DriverUnload = NPF_Unload;
接下来就是获取可用的网络适配器列表:bindP = getAdaptersList(),并为每个网络适配器创建用于截获数据的设备:
vos for (; *bindT != UNICODE_NULL; bindT += (macName.Length + sizeof(UNICODE_NULL)) / sizeof(WCHAR))
{
RtlInitUnicodeString(&macName, bindT);
createDevice(DriverObject, &macName, NdisProtocolHandle);
}
createDevice()调用IoCreateDevice()创建一个设备,在创建时就同时把操作函数的指针复制给了该设备的设备对象,而相关的适配器名和协议驱动器句柄(aProtoHandle)是创建后赋值给其设备对象的:
RtlInitUnicodeString(&devExtP->AdapterName,amacNameP->Buffer);
devExtP->NdisProtocolHandle=aProtoHandle;
到这里为止,就完成了包截获设备驱动器的初始化,用户可以通过操作函数对指定的网络设备进行操作,如打开一个包截获设备就是调用了NPF_Open(),读数据就是NPF_Read()
等。同时在系统内部也自动进行一些操作,最重要就是当网络适配器接收到数据将自动调用NPF_tap()将数据放入缓冲区中,由上层读取。这样包截获设备就能运行起来了,下面对几个主要函数做进一步分析。
4.2 NPF_tap() (read.c)
当有数据包到达网络适配器时,处于底层的NIC驱动器就会调用NPF_tap(),其指针是在注册协议驱动器是作为ProtocolChar的一部分,其形式是系统指定的,来看其函数的各个参数:
NDIS_STATUS NPF_tap(IN NDIS_HANDLE ProtocolBindingContext, IN NDIS_HANDLE MacReceiveContext, IN PVOID HeaderBuffer,IN UINT HeaderBufferSize,IN PVOID
LookaheadBuffer, IN UINT LookaheadBufferSize,IN UINT PacketSize) ProtocolBindingContexts是接收数据包的包截获驱动器的OPEN_INSTANCE结构指针。MacReceiveContexts是产生这个请求的NIC驱动器的句柄。
HeaderBuffer是这个数据包报头在NIC驱动器地址空间的地址指针,这里的报头指MAC报头。HeaderBufferSize是数据包报头的大小。
yoboLookaheadBuffer是数据包内容在NIC驱动器地址空间的地址指针。LookaheadBufferSize是数据包内
容的大小。
PacketSize是包括报头的数据包大小。
对这个数据包首先进行的是过滤处理,然后在写入包截获驱动器的缓冲中。注意到在libpcap的不同平台的版本上都进行了过滤处理,而且都是按照BPF规则,这里根据包内容是否在紧接报头之后,分别执行bpf_filter( )和bpf_filter_with_2_buffers( )进行过滤。
Winpcap包截获驱动器使用缓冲保存数据,等待上层读取并删除数据,它使用两个环形缓冲,一个用于保存MAC报头,另一个用于保存包内容,这样做的原因是一般情况下协议驱动器对报头和内容有不同的处理。缓冲满时将自动丢弃新到来的数据包。完成过滤后,下一步就是把数据包从NIC驱动器地址空间读到自己的缓冲中,首先把报头和内容分别保存:
ToCopy = Open->Size - LocalData->P;
NdisMoveMappedMemory(LocalData->Buffer + LocalData->P,HeaderBuffer, ToCopy);
NdisMoveMappedMemory(LocalData->Buffer + 0 , (PUCHAR)HeaderBuffer +
ToCopy, fres - ToCopy);
LocalData->P = fres-ToCopy;
针对在在NIC驱动器地址空间中报头和内容可能不是紧邻的,数据需要分别处理。之后分配NDIS_PACKET结构,并把这个结构挂到包截获驱动器环形缓冲的适当位置:
温州翻译 NdisAllocatePacket(&Status, &pPacket, Open->PacketPool);
最后复制数据到该NDIS_PACKET结构:
NdisTransferData(
&Status,
Open->AdapterHandle,
MacReceiveContext,
dote LookaheadBufferSize,
fres - HeaderBufferSize - LookaheadBufferSi
ze,
pPacket,
&BytesTransfered );
4.2 NPF_Open() (openclos.c)
这个函数用于打开一个包截获设备,在前文我们提到winpcap为每个网络适配器创建了相应的包截获设备,但这些设备并没有和对应的适配器交互,所以打开这些包截获设备实际上就是让它们和适配器交互。这里需要对open实例做必要的初始化,然后调用NdisOpenAdapter():
NdisInitializeEvent(&Open->WriteEvent);
NdisInitializeEvent(&Open->IOEvent);
NdisInitializeEvent(&Open->DumpEvent);
NdisInitializeEvent(&Open->IOEvent);
NdisAllocateSpinLock(&Open->MachineLock);
……………………
Open->bpfprogram = NULL; //reset the filter
Open->mode = MODE_CAPT;
Open->Nbytes.QuadPart = 0;
Open->Npackets.QuadPart = 0;
Open->Nwrites = 1;
Open->Multiple_Write_Counter = 0;
Open->MinToCopy = 0;
Open->TimeOut.QuadPart = (LONGLONG)1;
……………………
NdisOpenAdapter(
&Status,
&ErrorStatus,
&Open->AdapterHandle,
&Open->Medium,
MediumArray,