李连杰出生在哪里:基于NDIS中间层的驱动包截获技术-【科技原创】电子科技大学

来源:百度文库 编辑:偶看新闻 时间:2024/04/29 11:52:27
基于NDIS中间层的驱动包截获技术 09-03-25 作者:   编辑:校方人员

NDIS(Network Driver Interface Specification)是网络驱动程序接口规范的简称。它横跨传输层、网络层和数据链路层,定义了网卡或网卡驱动程序与上层协议驱动程序之间的通信接口规范,屏蔽了底层物理硬件的不同,使上层的协议驱动程序可以和底层任何型号的网卡通信。NDIS为网络驱动程序创建了一个完整的开发环境,只需调用NDIS函数,而不用考虑操作系统的内核以及与其他驱动程序的接口问题,从而使得网络驱动程序可以从与操作系统的复杂通讯中分离,极大地方便了网络驱动程序的编写。另外,利用NDIS的封装特性,可以专注于一层驱动的设计,减少了设计的复杂性,同时易于扩展驱动程序栈。

1  NDIS驱动模型简介

NDIS支持的网络驱动程序类型:

网卡驱动程序(NIC Drivers):网卡驱动程序是网卡与上层驱动程序通信的接口,它负责接收来自上层的数据包,或将数据包发送到上层相应的驱动程序,同时它还完成处理中断等工作。

中间驱动程序(Intermediate Protocol Drivers):中间驱动程序位于网卡驱动程序和协议驱动程序之间,它向上提供小端口(Minport)函数集,向下提供协议(protocol)函数集,因此对于上层驱动程序而言,它是小端口驱动程序。对于底层的驱动程序,它是协议驱动程序。

协议驱动程序(Upper Level Protocol Drivers):协议驱动程序执行具体的网络协议,如IPX/SPXTCP/IP等。协议驱动程序为应用层客户程序提供服务,接收来自网卡或中间驱动程序的信息。

防火墙的开发一般采用的是中间驱动程序。通过NDIS中间层驱动,我们可以截获来自网卡的所有原始数据包。图1则是NDIS中间层驱动的工作过程图:


1  NDIS中间层驱动工作过程图

NDIS中间层驱动程序是工作在MINIPROTPROTOCOL接口之间的,驱动程序必须向下导出一个PROTOCOL接口,向上导出一个MINIPORT接口。将自己创建的驱动程序插入到网卡驱动程序与传输驱动程序之间。如此一来,当下层的网卡驱动程序接收到数据后会通过MINIPORT接口发送到导出的PROTOCOL接口上,NDIS中间层驱动程序便接收到了来自网卡的数据并调用准备好的回调函数处理数据包信息。接着NDIS中间层驱动在处理数据包完毕后再继续把数据通过导出的MINIPROT接口向PROTOCOL接口发送,这样就完成了一个截获数据包的过程[1]

2  NDIS中间层驱动的工作流程

在开始学习NDIS中间层驱动之前,我们有必要了解下NDIS是怎样工作的。当然这就包括了它的接收数据包的流程了。那么我们来看看NDIS接收数据包流程到底是怎样的:

1.低层的网卡驱动调用NdisMIndicateReceive或者NdisMEthIndicateReceive函数通知上一层已经它们已经收到数据。

2.接着系统调用自定义的PtReceive或者PtReceivePacket函数,到底系统会调用哪个函数跟机器的网卡有关。接着在函数中调用NdisGetReceivedPacket函数接受低层传上来的数据,如果我们得到了一个完整的packet包,我们就申请一个缓冲区存放下层传上来的数据,接着调用NdisMIndicateReceivePacket通知上层设备。如果此时MyPacketstatusNDIS_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]


2  NDIS中间层驱动工作流程图

通过流程图可以知道在PtReceive或者PtReceivePacket中可以得到我们所希望的数据,然后在以上2个函数中加入自己的处理代码,就可以达到截获数据并进行相应处理的目的了。

在驱动程序中导出接口

我们首先必须在驱动程序中向系统注册导出虚拟接口。这些工作将在DriverEntry函数中完成,代码如下:

DriverEntry(

              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);

PChars.ReceivePacketHandler = PtReceivePacket;  //设置接收数据包的回调函数

       //NDIS注册MINIPORT接口

              NdisRegisterProtocol(&Status,

                                                &ProtHandle,

                                                &PChars,

                                                sizeof(NDIS_PROTOCOL_CHARACTERISTICS));

              //通知NDIS生成所注册的2个接口

              NdisIMAssociateMiniport(DriverHandle, ProtHandle);

       }

如此一来,驱动程序可以看成是工作在网卡层与协议层之间了,当底层网卡有数据到来时会先经过驱动程序处理后再往上层设备发送的。那么我们就可以在自己的回调函数中处理来自网络的数据了。

回调函数的工作

在向系统注册的回调函数中,比较重要的就是PtReceivePtReceivePacket函数了。为了程序的通用性,2个回调函数的大致处理流程是一样的。我们仅拿PtReceive函数来做例子。PtReceive函数的原型如下:

NDIS_STATUS

PtReceive(

       IN  NDIS_HANDLE  ProtocolBindingContext,

       IN  NDIS_HANDLE    MacReceiveContext,

       IN  PVOID   HeaderBuffer,            

       IN  UINT      HeaderBufferSize,                

       IN  PVOID   LookAheadBuffer,      

       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  UINT      HeaderBufferSize,                 //以太头数据大小

       IN  PVOID   LookAheadBuffer,       //数据体部分

       IN  UINT      LookAheadBufferSize,          //LookAheadBuffer数据大小

       IN  UINT      PacketSize                                         //数据包大小

)

{

       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->isSecondary)

                      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, NDIS_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函数就是包分析函数,在该函数中我们就可以对传来的数据进行处理,过滤和拦截了。

数据包的分析与处理

在以上代码中,其实在MyPacket这个结构中就储存了所希望得到的数据包地址,但是如何得到数据呢?我们在得到数据的过程中需要了解NDIS_PACKETNDIS_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.HeadPrivate.Tail[3][4]。而NDIS_BUFFER中就记录了我们数据包的地址和下一个NDIS_BUFFER的地址。操作有很多种方法,但是由于这些结构体本来对我们是不透明的,所以最安全的方法是用微软提供的一系列函数来操作NDIS_PACKETNDIS_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里面的数据为原始数据包的数据,也就是包括了包头等一系列信息,需要自己分析包头信息来获取希望的数据。

结束语

本文只通过简单的一些示例代码阐述了如何利用驱动来截获数据包的方法。大部分防火墙就是通过该技术截获网络数据并判断数据的合法性实现保护的。但是要写出很具有通用性的代码还需要更广泛的知识作为基础。在这里仅给大家抛砖引玉,至于关于NDIS中间驱动更详细的信息读者们可以去参考微软提供的WDK文档。

 

     

[1] 朱耀辉《Windows防火墙与数据封包截获技术》 北京.电子工业出版社

[2] 《关于passthruSend/Receive的流程图》(http://bbs.driverdevelop.com/htm_data/10/0305/40727.html

[3] NDIS_PACKET结构讨论[]》(http://feikoo.bokee.com/viewdiary.10774705.html

[4] NDIS_PACKET结构讨论[]》(http://feikoo.bokee.com/viewdiary.10774711.html