一次冒险:如何实现防火墙挂钩驱动程序?






4.83/5 (63投票s)
2004年10月28日
9分钟阅读

695642

10999
防火墙挂钩驱动程序是一种开发简单数据包过滤应用程序的完全未知的方法。通过本文,我想告诉您这个驱动程序是如何工作的,以及您在应用程序中使用它需要做什么。
引言
可能,防火墙挂钩驱动程序是开发人员在 Windows 系统中开发数据包过滤应用程序时可以使用的一个文档最少的(undocumented)方法。微软没有提供任何关于它的文档,您唯一能学到东西的地方就是 DDK 头文件(ipFirewall.h)。事实上,当我安装 Windows 2000 DDK 时,当我发现这个 .h(以及它的内容!)时,我非常惊讶,因为没有任何关于防火墙挂钩存在的文档可以阅读。在 DDK 的后续版本中,微软增加了一些关于它的文档:“此方法存在,但不建议实现”。
然而,因为它是一种实现防火墙解决方案的简单方法,我认为了解防火墙挂钩驱动程序的工作原理很有意思。
防火墙挂钩驱动程序
我不明白为什么微软不推荐开发防火墙挂钩驱动程序。诚然,当您想开发一个完整的防火墙解决方案时,我从不推荐它,但对于小型应用程序来说,它可能是一个不错的选择。基本上,防火墙挂钩驱动程序可以做与过滤挂钩驱动程序相同的工作(请参阅我的文章 为 Windows 2000/XP 开发防火墙,以获取更多信息),但限制更少。
如果您还记得,过滤挂钩驱动程序只允许在系统中安装一个过滤函数。如果一个应用程序已经使用了此功能,您的应用程序将无法工作。使用防火墙挂钩驱动程序,您就没有这个问题。您可以安装所有需要的过滤函数。每个过滤函数都有一个分配的优先级,因此系统会按照优先级顺序依次调用一个函数,直到某个函数返回“DROP PACKET
”。如果所有函数都返回“ALLOW PACKET
”,数据包将被允许。您可以将其想象成一个过滤函数的链。当其中一个函数返回“DROP PACKET
”时,此链就会被中断。链中每个函数 的顺序由其优先级值给出。
在图中,我表示了以下过程
- 数据包在您的主机上接收。IP 驱动程序具有按优先级排序的过滤函数列表(优先级最高的函数是过滤函数 1)。
- 首先,IP 驱动程序将数据包传递给最高优先级的过滤函数,并等待返回值。
- 过滤函数 1 返回“
ALLOW PACKET
”。 - 由于过滤函数 1 允许了该数据包,IP 驱动程序将其传递给下一个过滤函数:过滤函数 2。
- 在这种情况下,过滤函数 2 返回“
DROP PACKET
”。因此,IP 驱动程序丢弃该数据包,并且不再继续调用下一个过滤函数。
您在过滤挂钩驱动程序中可能遇到的另一个问题是,对于发送的数据包,您无法访问数据包内容。但是,您可以通过防火墙挂钩驱动程序访问所有数据。在防火墙挂钩过滤函数中接收的数据结构比过滤挂钩驱动程序中接收的更复杂。它更类似于您可以在 NDIS 驱动程序中找到的数据包结构,其中整个数据包由一系列缓冲区组成。但请耐心等待,我们稍后可以了解更多。
与过滤挂钩驱动程序一样,防火墙挂钩驱动程序只是一个内核模式驱动程序,用于安装回调函数(但防火墙挂钩驱动程序在 IP 驱动程序中安装回调)。事实上,安装防火墙挂钩驱动程序的过程与安装过滤挂钩驱动程序的过程类似。在 ipFirewall.h 文件中,您可以找到以下行
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\\<a href="file://Ip/">Device\\Ip</a>
#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)
这些行让您对如何安装回调函数有一个大致了解。您只需用回调函数的数据填充一个 IP_SET_FIREWALL_HOOK_INFO
结构,然后通过发送 IOCTL IOCTL_IP_SET_FIREWALL_HOOK
到 IP 设备来安装它。如果您有驱动程序开发经验,并且之前处理过文档齐全的过滤挂钩驱动程序,那么这很简单。关于此结构的一个重要参数是 Priority
字段。每个字段包含过滤函数的优先级,此值越大,优先级越高。
PDEVICE_OBJECT ipDeviceObject=NULL;
IP_SET_FIREWALL_HOOK_INFO filterData;
//.....
// Init structure filterData.
FirewallPtr = filterFunction;
filterData.Priority = 1;
filterData.Add = TRUE;
//....
// Send the commando to ip driver
IoCallDriver(ipDeviceObject, irp);
如果您想卸载过滤函数,可以使用相同的代码,但将 FALSE
值放入 filterData.Add
。
过滤函数
防火墙挂钩驱动程序的过滤函数比过滤挂钩驱动程序中使用的要复杂。因此,复杂性增加,因为关于该函数及其参数没有文档。该函数具有以下签名
FORWARD_ACTION cbFilterFunction(VOID **pData,
UINT RecvInterfaceIndex,
UINT *pSendInterfaceIndex,
UCHAR *pDestinationType,
VOID *pContext,
UINT ContextLength,
struct IPRcvBuf **pRcvBuf);
通过耐心和使用调试方法(以及解释参数名称 :)),我获得了关于这些参数的以下信息
pData | *pData 指向一个(struct IPRcvBuf * )结构,其中包含数据包缓冲区。 |
RecvInterfaceIndex | 接收数据的接口。 |
pSendInterfaceIndex | 指向包含数据发送到的索引值的无符号 int 的指针。尽管它是一个指针,但更改此值并不能使数据包被重定向:(。 |
pDestinationType | 指向无符号 int 的指针,其中包含目标类型:本地网络、远程、广播、多播等。 |
pContext | 指向一个 FIREWALL_CONTEXT_T 结构,您可以在其中找到有关数据包的信息,例如数据包是传入还是传出数据包。 |
ContextLength | pContext 指向的缓冲区的尺寸。其值始终为 sizeof(FIREWALL_CONTEXT_T) 。 |
pRcvBuf | *pRcvBuf 始终指向 NULL 。 |
这些信息可能会在未来的 Windows 版本中发生变化,因为没有官方文档可用。我只保证这些字段的含义是我在 Windows 2000 和 Windows XP 的测试中获得的。
对于每个数据包,都会调用我们的函数,并且根据其返回值,数据包将被丢弃或被传递。在过滤函数中可以返回的值是
FORWARD | 数据包被允许。 |
DROP | 数据包被丢弃。 |
ICMP_ON_DROP | 数据包被丢弃,并将一个 ICMP 数据包发送到远程计算机。 |
解链缓冲区
在防火墙挂钩过滤函数中,您不会像在过滤挂钩驱动程序中那样直接接收包含数据包头部和数据包内容的缓冲区。经过一些测试,我了解了缓冲区的内部结构。如前所述,发送/接收的数据包在 pData
参数中传递。*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 (but
// i never have seen it).
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
// but because I haven't a value different from 0, I can affirm it.
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.
// It's the only thing I can tell you about this field.
ULONG ipr_promiscuous;
};
为了我们的目的,我们只需要了解 ipr_next
、ipr_buffer
和 ipr_size
字段。ipr_buffer
字段包含数据包的 ipr_size
字节。但是,整个数据包不一定在一个缓冲区中,系统可以链式连接多个缓冲区。因此,使用了 ipr_next
字段。此字段指向包含数据包数据的下一个结构。当数据结构中的 ipr_next
字段指向 NULL
时,我们就拥有了整个数据包。因此,在防火墙挂钩驱动程序中,我们发现了一个链式缓冲区结构,正如我们在 NDIS 驱动程序中看到的那样。在我的测试中,对于所有接收到的数据包,函数都只接收到一个缓冲区,其中包含所有数据;对于发送的数据包,我发现了几个链式缓冲区,每个缓冲区包含有关一个协议的信息。我的意思是,如果我发送一个 ICMP 数据包,例如,有三个链式缓冲区:一个包含 IP 头,一个包含 ICMP 头,另一个包含数据。然而,正如我们必须与 NDIS 驱动程序一样,我们不能依赖系统如何填充缓冲区。
在下图,您可以看到防火墙挂钩驱动程序中数据包的构建示例
作为示例,您可以在以下代码中看到如何从链式缓冲区获取带有数据包内容的线性缓冲区
char *pPacket = NULL;
int iBufferSize;
struct IPRcvBuf *pBuffer = (struct IPRcvBuf *) *pData;
// First, I 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);
}
}
对于所有好奇的人(以及在您问我之前 :P),您可以自行修改数据包数据,但风险自负。没有工具可以执行此类软件,要执行类似操作,我建议您实现一个 NDIS IM 驱动程序或 TDI 过滤驱动程序。我没有对其进行太多测试,但我不会非常依赖修改数据包内容的防火墙挂钩驱动程序的稳定性。为什么?因为我们不知道 IP 驱动程序如何管理这些缓冲区以及修改它们会有什么风险。简而言之,我的建议是:看看,但不要去碰。
加入时间!
好了,现在我们知道了过滤函数的语法,也知道了传递给它的数据包的格式。现在,我们只需要知道如何将这两者结合起来以获得一个数据包过滤应用程序。我使用的方法是尝试定义一个类似于过滤挂钩驱动程序中使用的过滤函数,因为它更容易理解。由于这些驱动程序中传递给过滤函数的参数不同,对于防火墙挂钩,我实现了一个中间函数(实际的防火墙挂钩过滤函数),它封装了过滤函数。我的意思是,作为防火墙挂钩过滤函数,我使用一个处理数据包、将其复制到线性缓冲区,然后将其传递给过滤函数的函数。有了这段代码,我想您会更好地理解它
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.
// This won't be the fastest code but
// will help us to understand better the method.
// ...........
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;
}
代码
您可以快速识别本文的应用。是的,GUI 和我用于过滤挂钩驱动程序的 GUI 完全相同。为什么?因为我开发了一个简单的包过滤应用程序,我用它来测试我开发的所有防火墙方法。这样,我就有了一个通用的 GUI,为所有方法提供相同的用户功能,但在底层,它们工作得非常不同。我有这个应用程序的不同版本(经过最少的修改),用于测试我的过滤挂钩驱动程序、防火墙挂钩驱动程序、LSP DLL、TDI 过滤驱动程序、NDIS 驱动程序……因此,我认为这些方法很容易理解。GUI 应用程序只需稍作修改,您就只想理解使用的新方法。
就像我在其他文章中一样,这个应用程序只实现数据包过滤。很多人问我是否要添加一些额外功能,如数据包日志记录、安装为服务……但我想遵循提供方法而不是解决方案的思路。如果您想添加这些额外功能中的任何一个,我非常乐意 :)。您可以联系我咨询您需要的所有问题。
结论
好了,这篇文章写完了,现在属于您了。希望您能从中获得和我一样的收获。尽情享受吧!!
历史
- 2004年10月28日:初稿
许可证
本文没有明确的许可条款,但可能包含文章文本或下载文件中的使用条款。如有疑问,请通过下方的讨论区联系作者。您可以在 此处 找到作者可能使用的许可证列表。