NDIS(Network Driver Interface Specification) 是网络驱动程序接口规范的简称。它横跨传输层、网络层和数据链路层 , 定义了网卡或网卡驱动程序与上层协议驱动程序之间的通信接口规范 , 屏蔽了底层物理硬件的不同 , 使上层的协议驱动程序可以和底层任何型号的网卡通信。 NDIS 为网络驱动程序创建了一个完整的开发环境,只需调用 NDIS 函数 , 而不用考虑操作系统的内核以及与其他驱动程序的接口问题,从而使得网络驱动程序可以从与操作系统的复杂通讯中分离,极大地方便了网络驱动程序的编写。另外,利用 NDIS 的封装特性,可以专注于一层驱动的设计,减少了设计的复杂性,同时易于扩展驱动程序栈。
1 NDIS 驱动模型简介
NDIS 支持的网络驱动程序类型:
网卡驱动程序 (NIC Drivers) :网卡驱动程序是网卡与上层驱动程序通信的接口,它负责接收来自上层的数据包,或将数据包发送到上层相应的驱动程序,同时它还完成处理中断等工作。
中间驱动程序 (Intermediate Protocol Drivers) : 中间驱动程序位于网卡驱动程序和协议驱动程序之间,它向上提供小端口 (Minport) 函数集,向下提供协议 (protocol) 函数集,因此对于上层驱动程序而言,它是小端口驱动程序。对于底层的驱动程序,它是协议驱动程序。
协议驱动程序 (Upper Level Protocol Drivers) :协议驱动程序执行具体的网络协议,如 IPX/SPX 、 TCP/IP 等。协议驱动程序为应用层客户程序提供服务,接收来自网卡或中间驱动程序的信息。
防火墙的开发一般采用的是中间驱动程序。通过 NDIS 中间层驱动,我们可以截获来自网卡的所有原始数据包。图 1 则是 NDIS 中间层驱动的工作过程图:
NDIS 中间层驱动程序是工作在 MINIPROT 和 PROTOCOL 接口之间的,驱动程序必须向下导出一个 PROTOCOL 接口,向上导出一个 MINIPORT 接口。将自己创建的驱动程序插入到网卡驱动程序与传输驱动程序之间。如此一来,当下层的网卡驱动程序接收到数据后会通过 MINIPORT 接口发送到导出的 PROTOCOL 接口上, NDIS 中间层驱动程序便接收到了来自网卡的数据并调用准备好的回调函数处理数据包信息。接着 NDIS 中间层驱动在处理数据包完毕后再继续把数据通过导出的 MINIPROT 接口向 PROTOCOL 接口发送,这样就完成了一个截获数据包的过程 [1] 。
2 NDIS 中间层驱动的工作流程
在开始学习 NDIS 中间层驱动之前,我们有必要了解下 NDIS 是怎样工作的。当然这就包括了它的接收数据包的流程了。那么我们来看看 NDIS 接收数据包流程到底是怎样的:
1 .低层的网卡驱动调用 NdisMIndicateReceive 或者 NdisMEthIndicateReceive 函数通知上一层已经它们已经收到数据。
2 .接着系统调用自定义的 PtReceive 或者 PtReceivePacket 函数,到底系统会调用哪个函数跟机器的网卡有关。接着在函数中调用 NdisGetReceivedPacket 函数接受低层传上来的数据,如果我们得到了一个完整的 packet 包,我们就申请一个缓冲区存放下层传上来的数据,接着调用 NdisMIndicateReceivePacket 通知上层设备。如果此时 MyPacket 的 status 是 NDIS_STATUS_RESOURCES ,我们就在本函数中释放我们分配的缓冲区;否则我们在上层发送 4 的时候,在 MPReturnPacket 中释放该缓冲区。
3 .如果在 PtReceive 或者 PtReceivePacket 函数中无法得到一个完整的 packet ,那么就调用 NdisMEthIndicateReceive 等函数通知系统。
4 .当上层设备得到了一个完整的数据并且处理完毕以后,它会调用 NdisReturnPacket ,然后 NDIS 会调用我们的 MPReturnPacket 。如果申请的缓冲区没释放,则在 MPReturnPacket 函数中释放该缓冲区。然后同样的向下层调用 NdisReturnPacket 。下层会释放他们自己申请的缓冲区。
5 .如果 3 发生,那么系统会调用 PtReceiveComplete 函数。在 PtReceiveComplete 函数中我们应该调用 NdisMEthIndicateReceiveComplete ,通知系统收到了完整的数据。
6 .当上层协议驱动得知底层已经收到了完整的数据报文以后,可能会调用 NdisTransferData ,要求下层把剩余的数据传上来。然后系统调用 MPTransferData 例程。在 MPTransferData 中,调用 NdisTransferData 。必须注意的是该函数的返回值:如果返回 success ,说明剩余的数据立刻就传上来了。此时会立即返回。 7 步骤就不会调用;如果返回 pending ,表明底层在此阻塞,底层会在稍后的时候调用 7 。
7 .当底层 miniport 驱动做好了一个完整的 packet ,它会调用 NdisTransferDataComplete 。同样的,系统会调用我们的 PtTransferDataComplete 函数。这样,整个接收数据的流程就结束了 [2] 。
通过流程图可以知道在 PtReceive 或者 PtReceivePacket 中可以得到我们所希望的数据,然后在以上 2 个函数中加入自己的处理代码,就可以达到截获数据并进行相应处理的目的了。
3 在驱动程序中导出接口
我们首先必须在驱动程序中向系统注册导出虚拟接口。这些工作将在 DriverEntry 函数中完成,代码如下:
DriverE n try(
IN PDRIVER _OBJECT DriverObject,
IN PUNICODE_ STRING RegistryPath
)
{
NDIS_STATUS Status;
NDIS_ PROTOCOL _CHARACTERISTICS PChars; // 保存有关导出 PROTOCOL 接口的回调函数地址的结构
NDIS_MINIPORT_ CHARACTERISTICS MChars; // 保存有关导出 MINIPORT 接口的回调函数地址的结构
PNDIS_ CONFIGURATION _PARAMETER Param;
NDIS_ STRING Name ;
NdisMInitializeWrapper(&NdisWrapperHandle, DriverObject, RegistryPath, NULL); // 初始化 NdisWrapperHandle
// 设置其他的回调函数
MChars . SendPacketsHandler = MPSendPackets ; // 设置发送数据包的回调函数
// 向 NDIS 注册我们的 MINIPORT 接口
Status = NdisIMRegisterLayeredMiniport ( NdisWrapperHandle ,
& MChars ,
sizeof( MChars ),
& DriverHandle );
PC h ars. ReceivePacketHandler = PtReceivePacket ; // 设置接收数据包的回调函数
// 向 NDIS 注册 MINIPORT 接口
NdisRegisterProtocol (&Status,
& ProtHandle ,
& PChars ,
sizeof( NDIS _PROTOCOL_CHARACTERISTICS));
// 通知 NDIS 生成所注册的 2 个接口
NdisIMAssociateMiniport (DriverHandle, ProtHandle);
}
如此一来,驱动程序可以看成是工作在网卡层与协议层之间了,当底层网卡有数据到来时会先经过驱动程序处理后再往上层设备发送的。那么我们就可以在自己的回调函数中处理来自网络的数据了。
4 回调函数的工作
在向系统注册的回调函数中,比较重要的就是 PtReceive 和 PtReceivePacket 函数了。为了程序的通用性, 2 个回调函数的大致处理流程是一样的。我们仅拿 PtReceive 函数来做例子。 PtReceive 函数的原型如下:
NDIS_ STATUS
PtReceive (
IN NDIS_HANDLE ProtocolBindingContext ,
IN NDIS_HANDLE MacReceiveContext,
IN PVOID HeaderBuffer,
IN UINT HeaderBufferSize ,
IN PVOID LookAhea d Buffer,
IN UINT LookAheadBufferSize ,
IN UINT PacketSize
)
在该函数中,第三个参数的指向帧头的起始缓冲区,第五个参数指向数据体的起始缓冲区,第七个参数的值为缓冲区大小。如果 PacketSize 大于 LookAheadBufferSize ,表明数据还未全部拷贝上来。如果这 2 个参数相等,那么说明数据全部在 LookAheadBuffer 变量指向的缓冲区内。来看看下面的代码:
NDIS _STATUS
PtReceive (
IN NDIS _HANDLE ProtocolBindingContext ,
IN NDIS_HANDLE MacReceiveContext ,
IN PVOID HeaderBuffer , // 以太头数据
IN UI N T HeaderBufferSize , // 以太头数据大小
IN PVOID LookAheadBuffer , // 数据体部分
IN UINT LookAheadBufferSize , // LookAheadBuffer 数据大小
IN UINT Pack e tSize // 数据包大小
)
{
PADAPT pAdapt =(PADAPT)ProtocolBindingContext;
PNDIS_PACKET MyPacket , Packet;
NDIS_ STATUS Status = NDIS_STATUS_ SUCCESS , DataStatus ;
if(! pAdapt -> MiniportHandle )
{
Status = NDIS_ STATUS _ FAILURE ;
}
else do
{
if( pAdapt ->isSe c ondary)
ASSERT (0);
// 从下层驱动获取数据包
Packet = NdisGetReceivedPacket (pAdapt-> BindingHandle , MacReceiveContext );
if( Packet != NULL )
{
// 如果数据包不为空那么就为下层即将
// 发送上来的数据包分配空间
NdisDprAllocatePacket (& Status
, &MyPacket, pAdapt ->RecvPacketPoolHandle);
if( Status == NDIS_ STATUS _SUCCESS)
{
// 拷贝原下层数据包到我们分配的缓冲中
MyPacket ->Private. Head = Packet-> Private .Head;
MyPacket->Private.Tail = Packet->Private.Tail;
NDIS_SET_ORIGINAL_PACKET(
MyPacket, N DIS_GET_ORIGINAL_PACKET(Packet));
NDIS_SET_PACKET_HEADER_SIZE(MyPacket, HeaderBufferSize);
NdisGetPacketFlags(MyPacket) = NdisGetPacketFlags(Packet);
NDIS_SET_PACKET_STATUS(MyPacket, DIS_STATUS_RESOURCES);
ASSERT(NDIS_GET_PACKET_STATUS(MyPacket) == NDIS_STATUS_RESOURCES);
// 拷贝数据包完成
// 数据包分析处理函数
PacketAnalysis(MyPacket);
// 处理代码
// 通知 NDIS 已复制数据包到缓冲区中
NdisMIndicateReceivePacket(pAdapt->MiniportHandle, &MyPacket, 1);
// 释放数据包
NdisDprFreePacket(MyPacket);
break;
在 PtReceive 函数中我们要做的就是为从下层传上来的数据分配缓冲区,然后将收到的数据拷贝到分配的缓冲区中,接着调用 NdisMIndicateReceivePacket 函数将数据传给上一层。 PacketAnalysis 函数就是包分析函数,在该函数中我们就可以对传来的数据进行处理,过滤和拦截了。
5 数据包的分析与处理
在以上代码中,其实在 MyPacket 这个结构中就储存了所希望得到的数据包地址,但是如何得到数据呢?我们在得到数据的过程中需要了解 NDIS _ PACKET 和 NDIS _ BUFFER 这两个结构。下面给出这两个结构的定义:
// NDIS_PACKET 结构的定义
typedef struct _NDIS_PACKET
{
NDIS_PACKET_PRIVATE Private;
// 这个其实是一个链表结构, Private.Head 指向第一个链表, Private.Tail 指向最后一个
// 以下有关于这个结构的解释
union
{
struct // For Connection-less miniports
{
UCHAR MiniportReserved[2*sizeof(PVOID)];
UCHAR WrapperReserved[2*sizeof(PVOID)];
};
struct
{
// For de-serialized miniports. And by implication conn-oriented miniports.
// This is for the send-path only. Packets indicated will use WrapperReserved
// instead of WrapperReservedEx
UCHAR MiniportReservedEx[3*sizeof(PVOID)];
UCHAR WrapperReservedEx[sizeof(PVOID)];
};
struct
{
UCHAR MacReserved[4*sizeof(PVOID)];
};
};
ULONG_PTR Reserved[2]; // For compatibility with Win95
UCHAR ProtocolReserved[1];
} NDIS_PACKET, *PNDIS_PACKET, **PPNDIS_PACKET;
// NDIS_PACKET_PRIVATE 的定义
typedef struct _NDIS_PACKET_PRIVATE
{
UINT PhysicalCount; // number of physical pages in packet.
UINT TotalLength; // Total amount of data in the packet.
PNDIS_BUFFER Head; // 链表指针,指向下一个
PNDIS_BUFFER Tail; // 链表指针,指向前面一个
// if Head is NULL the chain is empty; Tail doesn/'t have to be NULL also
PNDIS_PACKET_POOL Pool; // so we know where to free it back to
UINT Count;
ULONG Flags;
BOOLEAN ValidCounts;
UCHAR NdisPacketFlags; // See fPACKET_xxx bits below
USHORT NdisPacketOobOffset;
} NDIS_PACKET_PRIVATE, * PNDIS_PACKET_PRIVATE;
//NDIS_BUFFER 定义 其实就是一个内存描述符
typedef struct _NDIS_BUFFER {
struct _NDIS_BUFFER *Next; // 指向下一个节点的指针
PVOID VirtualAddress; // 指向报文首地址
PNDIS_BUFFER_POOL Pool;
UINT Length; // 报文数据长度
UINT Signature;
} NDIS_BUFFER, * PNDIS_BUFFER;
我们要的数据就储存在 NDIS_BUFFER 这个结构中的 VirtualAddress 成员里面,这个指针指向数据包的首地址。关系图如图 3 所示:
NDIS_PACKET 是一个描述 NDIS_BUFFER 链表的结构,在 NDIS_PACKET 中的成员 Private 中有指向第一个 NDIS_BUFFER 的指针和指向最后一个 NDIS_BUFFER 的指针分别是 Private.Head 和 Private.Tail[3][4] 。而 NDIS_BUFFER 中就记录了我们数据包的地址和下一个 NDIS_BUFFER 的地址。操作有很多种方法,但是由于这些结构体本来对我们是不透明的,所以最安全的方法是用微软提供的一系列函数来操作 NDIS_PACKET 和 NDIS_BUFFER 。这些函数都可以在 DDK 中查得到。
获取数据包内容的代码如下 :
NDIS_STATUS status ;
PNDIS_BUFFER NdisBuffer ;
UINT TotalPacketLength = 0 , copysize = 0 , DataOffset = 0 , PhysicalBufferCount , BufferCount ;
PUCHAR mybuffer = NULL ,tembuffer = NULL ;
// 假设这个是在 PtReceive 等函数中得到的 PACKET
NdisQueryPacket(packet // 先得到第一个 NDISBUFFER 的指针
, &PhysicalBufferCount
, &BufferCount
,&NdisBuffer //NdisBuffer 就是指向链表头
, &TotalPacketLength
);
其实也可以直接 NdisBuffer = packet->Private.Head ; 就可以取得第一个 BUFFER 了
status = NdisAllocateMemory( &mybuffer, 2048, 0, HighestAcceptableMax ); // 分配内存块
if( status != NDIS_STATUS_SUCCESS )
return NDIS_STATUS_FAILURE ;
NdisZeroMemory( mybuffer, 2048 ) ;
NdisQueryBufferSafe( // 取得 NDIS_BUFFER 描述符中数据的首地址和大小
NdisBuffer,
&tembuffer,
©size,
NormalPagePriority
// 将数据复制到内存中
NdisMoveMemory(mybuffer, tembuffer, copysize) ;
DataOffset = copysize ;
while(1)
{
也可以这样操作而不用 NdisGetNextBuffer
if(NdisBuffer->Next == packet->Private.Tail )
break ;
NdisBuffer = NdisBuffer->Next ;
if(pmdl == NULL )
break ;
// 获得下一个 NDIS_BUFFER 的的指针
NdisGetNextBuffer(NdisBuffer , &NdisBuffer ) ;
如果指针是 NULL 那么表示到链表尾了
if( NdisBuffer == NULL )
break ;
NdisQueryBufferSafe(
NdisBuffer,
&tembuffer,
©size,
NormalPagePriority
) ;
NdisMoveMemory( mybuffer + DataOffset , tembuffer, copysize) ;
DataOffset += copysize ;
// 我们要的数据就全部都在申请的内存 mybuffer 中,数据大小为 DataOffset
我们想要的截获数据包的功能就达到了,如果想要过滤数据包,那么就只需要对数据包的内容进行判断就可以了。但是需要注意的是 mybuffer 里面的数据为原始数据包的数据,也就是包括了包头等一系列信息,需要自己分析包头信息来获取希望的数据。
6 结束语
本文只通过简单的一些示例代码阐述了如何利用驱动来截获数据包的方法。大部分防火墙就是通过该技术截获网络数据并判断数据的合法性实现保护的。但是要写出很具有通用性的代码还需要更广泛的知识作为基础。在这里仅给大家抛砖引玉,至于关于 NDIS 中间驱动更详细的信息读者们可以去参考微软提供的 WDK 文档。