开发基于Firewall-Hook的防火墙

      Security 2006-1-12 16:18

  网络数据包拦截技术是包过滤的基础,它不仅仅可以用来开发包过滤防火墙,还可以用来开发其它的产品,与网络安全密切相关。网络数据包拦截技术很多,基本上网络架构上的任何一层都可以拦截。

核心态下的网络数据包拦截

  大多数的包过滤防火墙软件都是利用网络驱动程序在内核态来实现的。利用驱动程序拦截数据包可以在网络栈的不同位置实现,主要有以下几种方法:
 (1)TDI过滤驱动程序(TDI Filter Driver)
   当应用程序要发送或接收网络数据包的时候,都是通过与协议驱动所提供的接口来进行的。协议驱动提供了一套系统预定义的标准接口来和应用程序之间进行交互。在Windows2000/NT下,IP,TCP,UDP是在一个驱动程序里实现的,叫做tcp.sys,这个驱动程序创建了几个设备:DeviceRawIp,DeviceUdp,DeviceTcp,DeviceIp,DeviceMULTICAST。应用程序所有的网络数据操作都是通过这几个设备进行的。因此,我们只需要开发一个过滤驱动来截获这些交互的接口,就可以实现网络数据包的拦截。TDI层的网络数据拦截还可以得到操作网络数据包的进程详细信息。
 (2)NDIS中间层驱动程序(NDIS Intermediate Driver)
  NDIS中间层驱动介于协议层驱动和Miniport驱动之间,它能够截获所有的网络数据包(如果是以太网,那就是以太帧)。NDIS中间层驱动的应用很广泛,不仅仅是个人防火墙,还可以用来实现VPN,NAT,PPPOverEthernet以及VLan。中间层驱动的概念是在WindowsNT SP4之后才有的,因此对于Windows9x来说无法直接利用中间层驱动的功能。Windows DDK提供了两个著名的中间层驱动例子:Passthru以及Mux。开发人员可以在Passthru的基础上进行开发,Mux则实现了VLan功能。目前个人防火墙的产品还很少用到这种技术,主要的原因在于中间层驱动的安装过于复杂,尤其是在Windows NT下。Windows2000下可以通过程序实现自动安装,但是如果驱动没有经过数字签名的话,系统会提示用户是否继续安装。中间层驱动功能强大,应该是今后个人防火墙技术的趋势所在,特别是一些附加功能的实现。
 (3)NDIS Hook Driver
  NDIS Hook Driver是目前大多数个人防火墙所使用的方法。Hook的概念在Windows9x下非常流行,而且实现也很容易。在Windows9x下,驱动程序(VxD)通过使用Hook_Device_Service可以挂接NDIS所提供的所有服务。在Windows NT/2000下面实现Hook有两种不同的思路:通过修改NDIS.SYS的Export Table以及向系统注册假协议(Fake Protocol)。然而,这种方法对平台的依赖性比较大,需要在程序中判断不同的操作系统版本而使用不同的结构定义。
 (4)Filter-Hook Driver
  Filter-Hook Driver是从Windows2000开始系统所提供的一种驱动程序。事实上,它不是一种新的网络驱动,它只是扩展了IP过滤驱动(IP Filter Driver)的功能。
  实际上,Filter-Hook Driver并不是网络驱动,它是一种内核模式驱动(Kernel Mode Driver)。 大致上的实现方法是这样的:在Filter-Hook Driver中提供回调函数(Callback Function),然后使用IP Filter Driver注册回调函数。这样当数据包发送和接收时,IP Filter Driver会调用回调函数。也就是说,该驱动程序主要是利用ipfiltdrv.sys所提供的功能来拦截网络数据包。Filter-Hook Driver的结构非常简单,易于实现。但是正因为其结构过于简单,并且依赖于ipfiltdrv.sys,Microsoft并不推荐使用Filter-Hook Driver。
 此外,这种驱动有一个巨大的缺点:每次只能有一个过滤函数可以安装。我们开发了强大的防火墙,如果其它应用程序已经使用了过滤器(安装了过滤函数)那么我们的程序将不会正常工作。
 (5)Firewall-Hook Driver
  Firewall-Hook Driver是从Windows2000开始系统提供的另一种驱动程序。它能够完成所有Filter-Hook Driver能处理的工作,而且没有过滤函数的个数限制,也就是说,可以安装多个过滤函数,按照过滤函数的优先级顺序来传递数据包。
  这种驱动也很容易实现,但是仍然存在一些问题,例如它的效率不高,而且有可能会和ICS(Internet Connection Sharing)或者其他的个人防火墙产生冲突,因此,微软也不推荐使用Firewall-Hook Driver。
  在这几种方法中,利用NDIS中间驱动可以在网卡驱动程序和传输驱动程序之间插入一层自己的处理,从而可以从截获网络封包并重新进行封包、加密、网络地址转换及过滤等操作。由于NDIS中间驱动程序位于网卡和传输驱动程序之间所以它可以截获较为底层的封包从而可以完成更为低级的操作以及用来编写网络安全软件安全系数也高。但是NDIS中间层驱动的开发较为困难,对于一些小型的个人防火墙应用程序来说,简单易用的Firewall-Hook Driver也许是更好的选择。

驱动程序的设计

  基于Firewall-Hook Driver的包过滤驱动程序位于核心态,运行效率高,主要用于在IP过滤驱动中拦截所有的网络数据包,根据过滤规则判别是否接收或发送数据包。同时处理上层应用程序发送的IRP,接收应用程序发送的过滤规则等。
安装过滤函数之前,先将过滤函数的地址填入IP_SET_FIREWALL_HOOK_INFO结构的FirewallPtr指针中,Add设置为TRUE,并指定该过滤函数优先级Priority,然后向IP设备发送IOCTL IOCTL_IP_SET_FIREWALL_HOOK控制码,这样就完成了过滤函数的安装。卸载过滤函数的时候只用把Add设置为FALSE就行了,其他参数和安装时一样。
每个过滤函数可以设置一个优先级,系统调用这些函数的时候按照优先级的顺序进行,直到某个函数返回“丢弃包”为止。如果所有的过滤函数都返回“允许包”,那么这个包才能顺利通过系统。可以把这些过滤函数想象成一个过滤链,所有的函数按照优先级排列,如果一个函数返回“丢弃包”,这条过滤链就断开了。



  如图所示,IP DRIVER维护一条过滤函数链,Filter Function 1的优先级最高,Filter Function 2其次,Filter Function 3的优先级最低。当一个数据包到达主机的时候,IP DRIVER首先将包发给优先级最高的过滤函数(Filter Function 1),并等待它返回。Filter Function 1返回“允许包”,因此,IP DRIVER又将包发给下一个过滤函数(Filter Function 2),此时,Filter Function 2返回“丢弃包”,所以,IP DRIVER就将这个包丢弃了,而不会再发给过滤函数链上的下一个函数(Filter Function 3)。
  在过滤函数中,根据IP包的头可以判断包的具体协议类型(ICMP,TCP,UDP等),然后根据不同的协议,检查是否满足过滤规则。如果是TCP包,要检查源IP地址,源端口,目的IP地址,目的端口;如果是TCP包,则只要检查源IP地址和源端口。然后按照规则对数据包进行处理(允许或丢弃)。
  最后,在卸载驱动程序之前,要清除所有加载的过滤规则,释放系统资源,并卸载安装的过滤函数。

注册过滤函数

  注册过滤函数实际上就向IP DRIVER注册一个回调函数,当IP DRIVER收到数据包的时候会自动调用这个过滤函数。注册过滤函数的方法是未公开的(Undocumented)。
  在DDK的头文件ipfirewall.h里可以找到相关的定义。

 //
 // Indicates whether it is a transmitted or received packet.
 //
 typedef enum _IP_DIRECTION_E {
     IP_TRANSMIT,
     IP_RECEIVE
     } DIRECTION_E, *PDIRECTION_E;
 
 typedef struct _FIREWALL_CONTEXT_T {
   DIRECTION_E  Direction;
   void         *NTE;
   void         *LinkCtxt;
   NDIS_HANDLE  LContext1;
   UINT         LContext2;
   } FIREWALL_CONTEXT_T, *PFIREWALL_CONTEXT_T;
 
 typedef struct _IP_SET_FIREWALL_HOOK_INFO
 {
  // Packet filter callout.
  IPPacketFirewallPtr FirewallPtr;
 
  // Priority of the hook
  UINT Priority;
 
  // if TRUE then ADD else DELETE
  BOOLEAN Add;
 } IP_SET_FIREWALL_HOOK_INFO, *PIP_SET_FIREWALL_HOOK_INFO;  
 
 #define DD_IP_DEVICE_NAME   L"\\Device\\Ip"
 
 #define _IP_CTL_CODE(function, method, access) \
            CTL_CODE(FSCTL_IP_BASE, function, method, access)

 #define IOCTL_IP_SET_FIREWALL_HOOK  \
            _IP_CTL_CODE(12, METHOD_BUFFERED, FILE_WRITE_ACCESS)

  通过这些定义,可以推测出注册过滤函数的方法:
  (1)首先,我们必须得到IP Driver的指针,这要求驱动已经安装并执行。
  (2)然后,我们必须建立用IOCTL_IP_SET_FIREWALL_HOOK作为控制代码的IRP。我们必须传递IP_SET_FIREWALL_HOOK_INFO 参数,该参数结构中包含了指向过滤函数的指针FirewallPtr和过滤函数的优先级Priority。如果你要卸载该函数,你必须在同样的步骤里将Add的值设置为FALSE。
  (3)向IP DRIVER设备驱动发送创建的IRP。
  设置过滤函数的相关代码如下:

 PDEVICE_OBJECT ipDeviceObject=NULL;
 PFILE_OBJECT ipFileObject=NULL;
 IP_SET_FIREWALL_HOOK_INFO filterData;
 
 KEVENT event;
 PIRP irp;
 
 // Get pointer to Ip device
 RtlInitUnicodeString(&filterName, DD_IP_DEVICE_NAME);
 IoGetDeviceObjectPointer(&filterName, STANDARD_RIGHTS_ALL, &ipFileObject,  &ipDeviceObject);
 
 // Init structure filterData.
 FirewallPtr = filterFunction;
 filterData.Priority = 1;
 filterData.Add = TRUE;
 
 KeInitializeEvent(&event, NotificationEvent, FALSE);

 // Build Irp to establish filter function
 irp = IoBuildDeviceIoControlRequest(IOCTL_IP_SET_FIREWALL_HOOK,  ipDeviceObject, (PVOID) &filterData, sizeof(IP_SET_FIREWALL_HOOK_INFO), NULL,  0, FALSE, &event, &ioStatus);
 
 // Send the commando to ip driver
 IoCallDriver(ipDeviceObject, irp);

过滤函数

  向系统注册的过滤函数的定义如下:

 // Definiton for a firewall routine callout.
 FORWARD_ACTION cbFilterFunction(VOID **pData,  //can be pMdl or pRcvBuf
                                     UINT RecvInterfaceIndex,
                                     UINT *pSendInterfaceIndex,
                                     UCHAR *pDestinationType,
                                     VOID *pContext,
                                     UINT ContextLength,
                                     struct IPRcvBuf **pRcvBuf);

  过滤函数的各个参数也是未公开的,因此只能通过调试的方法推测出它们的含义。
  *pData指向一个struct IPRcvBuf *结构,其中保存着包的数据。RecvInterfaceIndex是接收数据的接口适配器编号。pSendInterfaceIndex指向一个无符号整形(UINT),其中保存着发送数据的接口适配器编号,但是通过修改它的值并不能实现包的路由。pDestinationType也指向一个无符号整形(UINT),其中保存着目标网络类型(本地网络、远程网络、广播、多播等)。pContext指向一个FIREWALL_CONTEXT_T结构,从这个结构中可以知道包的传递方向。ContextLength是pContext指向的缓冲区大小,它的值总是sizeof(FIREWALL_CONTEXT_T)。最后一个参数指针*pRcvBuf总是指向NULL的。
  过滤函数的返回值是FORWARD_ACTION,它的定义为:

 //
 // Enum for values that may be returned from filter routine.
 //

 typedef enum _FORWARD_ACTION
 {
  FORWARD = 0,
  DROP = 1,
  ICMP_ON_DROP = 2
 } FORWARD_ACTION;

  如果过滤函数返回FORWARD,那么IP DRIVER继续将数据包发给下一个过滤函数。如果所有过滤函数都返回FORWARD,那么IP DRIVER将向IP栈传递数据。对于本地数据包,IP向上送入栈。如果目标是另外的机器并且允许路由,将通过IP路由发送。如果过滤函数返回DROP,那么IP DRIVER丢弃IP包。如果过滤函数返回ICMP_ON_DROP,那么IP DRIVER丢弃数据包,并向远程主机发送一个ICMP包。
  在基于Firewall-Hook的过滤函数中,程序收到的缓冲区并非直接是包的首部(Packet Header)和包的内容(Packet Content)。*pData指向IPRcvBuf 结构,它的定义如下:

 struct IPRcvBuf
 {
  // Point to the next buffer in the chain
  struct IPRcvBuf *ipr_next;
 
  // Always 0
  UINT ipr_owner;
 
  // Buffer data
  UCHAR *ipr_buffer;
 
  // Buffer data size
  UINT ipr_size;
 
  // In my tests always a pointer to NULL.
  // Maybe the system could use MDLs instead of IPRcvBuf structures
  PMDL ipr_pMdl;
 
  // Always a pointer to NULL.
  UINT *ipr_pClientCnt;
 
  // Always a pointer to NULL.
  UCHAR *ipr_RcvContext;
 
  // Always 0. I suppose this field is a offset into buffer data
  UINT ipr_RcvOffset;
 
  // In Windows 2003 DDK the name of this field have changed to flags.
  // In my tests I always get 0 value for local traffic and 2 for remote.
  ULONG ipr_promiscuous;
 };

  对于过滤函数来说,我们只关心其中的三个成员ipr_next,ipr_buffer和ipr_size。ipr_buffer中包含了ipr_size个字节的包数据。然而,一个完整的包不一定在一个缓冲区中,系统可以根据ipr_next的值,将多个缓冲区的内容拼接成一个包。ipr_next指向下一个含有包数据的IPRcvBuf结构,当ipr_next为NULL时则说明已经可以完整的拼接包的数据了。这种链接缓冲区和NDIS驱动里的情形类似。在测试的时候我们发现,在每个链接缓冲区中包含一个协议的信息。比如发送一个ICMP包,那么就有三个链接缓冲区,一个包含IP头,一个包含ICMP头,另外一个包含数据,如图。




  将链接缓冲区中的包拼接成线形缓冲区的代码如下:

 char *pPacket = NULL;
 int iBufferSize;
 struct IPRcvBuf *pBuffer = (struct IPRcvBuf *) *pData;
 
 // Calculate the total size of the packet
 iBufferSize = buffer->ipr_size;
 while(pBuffer->ipr_next != NULL)
 {
  pBuffer = pBuffer->ipr_next;
  iBufferSize += pBuffer->ipr_size;
 }
 
 // Reserve memory to the lineal buffer.
 pPacket = (char *) ExAllocatePool(NonPagedPool, iBufferSize);
 if(pPacket != NULL)
 {
  unsigned int iOffset = 0;
  pBuffer = (struct IPRcvBuf *) *pData;
 
  // we are going to copy each buffer of the chain in the lineal buffer.
  memcpy(pPacket, pBuffer->ipr_buffer, pBuffer->ipr_size);
  while(pBuffer->ipr_next != NULL)
  {
   iOffset += pBuffer->ipr_size;
   pBuffer = pBbuffer->ipr_next;
   memcpy(pPacket + iOffset, pBuffer->ipr_buffer, pBbuffer->ipr_size);
  }
 }

  最后,将拼接好的线形缓冲区传递给真正的过滤函数(FilterPacket)来进行处理。

 FORWARD_ACTION cbFilterFunction(VOID **pData,
           UINT RecvInterfaceIndex,
           UINT *pSendInterfaceIndex,
           UCHAR *pDestinationType,
           VOID *pContext,
           UINT ContextLength,
           struct IPRcvBuf **pRcvBuf)
 {
  FORWARD_ACTION result = FORWARD;
  char *pPacket = NULL;
  int iBufferSize;
  struct IPRcvBuf *pBbuffer =(struct IPRcvBuf *) *pData;
  PFIREWALL_CONTEXT_T fwContext = (PFIREWALL_CONTEXT_T)pContext;
 
  IPHeader *pIpHeader;
 
  // Convert chained buffer to lineal buffer as we see before.
 
  // ...........
 
  pIpHeader = (IPHeader *)pPacket;
 
  // Call the real filter function and return result
  result = FilterPacket(pPacket,
      // length in bytes = ipp->headerLength * (32 bits/8)
      pPacket + (pIpHeader ->headerLength * 4),
      iBufferSize - (pIpHeader ->headerLength * 4),
      (fwContext != NULL) ? fwContext->Direction: 0,
      RecvInterfaceIndex,
      (pSendInterfaceIndex != NULL) ? *pSendInterfaceIndex : 0);
 
  return result;
 }

  当主机接收或发送一个数据包,过滤函数会被调用,系统会根据函数的返回值决定如何处理这个数据包。过滤函数根据用户程序的要求将每个包与规则列表进行比较,这个列表是连接列表,由应用程序传递给驱动程序。过滤函数的原型为:

 FORWARD_ACTION FilterPacket(unsigned char *PacketHeader,
        unsigned char *Packet,
        unsigned int PacketLength,
        DIRECTION_E direction,
        unsigned int RecvInterfaceIndex,
        unsigned int SendInterfaceIndex)

  指针*PacketHeader指向IP数据包的包头。*Packet指向不包括头的数据包。PacketLength是包的长度(不包括IP头的长度)。direction是结构FIREWALL_CONTEXT_T中的成员,它是一个DIRECTION_E结构,表示数据包传递的方向。RecvInterfaceIndex是接收数据的接口适配器编号。SendInterfaceIndex是发送数据的接口适配器编号。


原文地址:http://www.codeproject.com/internet/FwHookDrv.asp

程序下载地址:XFireWall
标签集:TAGS:
回复Comments() 点击Count()

回复Comments

{commenttime}{commentauthor}

{CommentUrl}
{commentcontent}