为 Windows 2000/XP 开发防火墙






4.86/5 (153投票s)
一篇关于为 Windows 2000/XP 开发防火墙的文章
引言
如果您决定为 Linux 开发防火墙,您会发现大量的免费信息和源代码。然而,对于对 Windows 平台防火墙感兴趣的人来说,会遇到一些困难,不仅是寻找信息,而且寻找免费源代码几乎是不可能的任务!所以,我决定写这篇文章,描述一种为 Windows 2000/XP 开发防火墙的简单方法,以帮助对此主题感兴趣的人。
背景
在 Windows 2000 DDK 中,Microsoft 包含了一种名为 Filter-Hook Driver 的新型网络驱动程序。使用它,您可以设置一个函数来过滤到达/离开接口的所有流量。由于关于此主题的文档很少,并且不包含示例,因此我在本文中将介绍成功使用它的必要步骤。我希望这篇文章能帮助您理解这种简单的方法。
Filter-Hook Driver
正如我之前所说,Filter-Hook Driver 是 Microsoft 在 Windows 2000 DDK 中引入的。事实上,它不是一种新的网络驱动程序类;它只是扩展 IP Filter Driver(随 Windows 2000 及更高版本一起包含?)功能的一种方式。实际上,Filter-Hook Driver 不是网络驱动程序;它是一个内核模式驱动程序。基本上,在 Filter-Hook Driver 中,我们实现一个回调函数,然后将此回调注册到 IP Filter Driver。这样做时,当数据包被发送或接收时,IP Filter Driver 会调用我们的回调函数。那么……执行此操作的主要步骤是什么?我们可以将其总结为以下步骤:
- 创建 Filter-Hook Driver。为此,您必须创建一个内核模式驱动程序。您可以选择名称、DOS 名称和其他驱动程序特性,没有强制要求,但我建议使用描述性名称。
- 如果我们想安装过滤功能,首先必须获取指向 IP Filter Driver 的指针。所以,这是第二步。
- 我们已经有了指针,现在就可以安装过滤函数了。我们可以通过发送一个特定的 IRP 来做到这一点。在此“消息”中传递的数据包括指向过滤函数的指针。
- 过滤数据包!!!!
- 当我们决定完成过滤时,必须注销过滤函数。我们可以通过将 `null` 指针“注册”为过滤函数来做到这一点。
哦,哦,只有五个步骤,看起来非常简单,但是……我如何编写内核模式驱动程序?我如何获取指向 IP Filter Driver 的指针?我如何……是的,请稍等,我现在将解释所有这些步骤:P,并展示源代码示例。
创建内核模式驱动程序
Filter-Hook driver 是一个内核模式驱动程序,所以如果我们想创建一个,就必须编写一个内核模式驱动程序。本文不是“5 分钟内核模式驱动程序开发指南”,所以我假设读者对此主题有一定的了解。Filter-Hook driver 的结构是典型的内核模式驱动程序结构。
- 驱动程序入口点,我们在其中创建设备,设置标准例程以处理 IRP(Dispatch、load、unload、create……),并创建用于与用户应用程序通信的符号链接。
- 管理 IRP 的标准例程。在开始编码之前,我建议您思考您将从设备驱动程序向应用程序“导出”哪些 IOCTL。在我的示例中,我实现了四个 IOCTL 代码:`START_IP_HOOK`(注册过滤函数)、`STOP_IP_HOOK`(注销过滤函数)、`ADD_FILTER`(安装新规则)和 `CLEAR_FILTER`(释放所有规则)。
- 对于我们的驱动程序,我们必须实现一个额外的函数:`filter` 函数。
我建议您使用一个程序来生成内核模式驱动程序的结构,这样您只需要将代码放入生成的函数中。例如,在此项目中我使用了 QuickSYS。您可以在以下代码中看到我自己的驱动程序结构实现。
NTSTATUS DriverEntry(IN PDRIVER_OBJECT DriverObject,
IN PUNICODE_STRING RegistryPath)
{
//....
dprintf("DrvFltIp.SYS: entering DriverEntry\n");
//we have to create the device
RtlInitUnicodeString(&deviceNameUnicodeString, NT_DEVICE_NAME);
ntStatus = IoCreateDevice(DriverObject,
0,
&deviceNameUnicodeString,
FILE_DEVICE_DRVFLTIP,
0,
FALSE,
&deviceObject);
if ( NT_SUCCESS(ntStatus) )
{
// Create a symbolic link that Win32 apps can specify to gain access
// to this driver/device
RtlInitUnicodeString(&deviceLinkUnicodeString, DOS_DEVICE_NAME);
ntStatus = IoCreateSymbolicLink(&deviceLinkUnicodeString,
&deviceNameUnicodeString);
//....
// Create dispatch points for device control, create, close.
DriverObject->MajorFunction[IRP_MJ_CREATE] =
DriverObject->MajorFunction[IRP_MJ_CLOSE] =
DriverObject->MajorFunction[IRP_MJ_DEVICE_CONTROL] = DrvDispatch;
DriverObject->DriverUnload = DrvUnload;
}
if ( !NT_SUCCESS(ntStatus) )
{
dprintf("Error in initialization. Unloading...");
DrvUnload(DriverObject);
}
return ntStatus;
}
NTSTATUS DrvDispatch(IN PDEVICE_OBJECT DeviceObject, IN PIRP Irp)
{
// ....
switch (irpStack->MajorFunction)
{
case IRP_MJ_CREATE:
dprintf("DrvFltIp.SYS: IRP_MJ_CREATE\n");
break;
case IRP_MJ_CLOSE:
dprintf("DrvFltIp.SYS: IRP_MJ_CLOSE\n");
break;
case IRP_MJ_DEVICE_CONTROL:
dprintf("DrvFltIp.SYS: IRP_MJ_DEVICE_CONTROL\n");
ioControlCode = irpStack->Parameters.DeviceIoControl.IoControlCode;
switch (ioControlCode)
{
// ioctl code to start filtering
case START_IP_HOOK:
{
SetFilterFunction(cbFilterFunction);
break;
}
// ioctl to stop filtering
case STOP_IP_HOOK:
{
SetFilterFunction(NULL);
break;
}
// ioctl to add a filter rule
case ADD_FILTER:
{
if(inputBufferLength == sizeof(IPFilter))
{
IPFilter *nf;
nf = (IPFilter *)ioBuffer;
AddFilterToList(nf);
}
break;
}
// ioctl to free filter rule list
case CLEAR_FILTER:
{
ClearFilterList();
break;
}
default:
Irp->IoStatus.Status = STATUS_INVALID_PARAMETER;
dprintf("DrvFltIp.SYS: unknown IRP_MJ_DEVICE_CONTROL\n");
break;
}
break;
}
ntStatus = Irp->IoStatus.Status;
IoCompleteRequest(Irp, IO_NO_INCREMENT);
// We never have pending operation so always return the status code.
return ntStatus;
}
VOID DrvUnload(IN PDRIVER_OBJECT DriverObject)
{
UNICODE_STRING deviceLinkUnicodeString;
dprintf("DrvFltIp.SYS: Unloading\n");
SetFilterFunction(NULL);
// Free any resources
ClearFilterList();
// Delete the symbolic link
RtlInitUnicodeString(&deviceLinkUnicodeString, DOS_DEVICE_NAME);
IoDeleteSymbolicLink(&deviceLinkUnicodeString);
// Delete the device object
IoDeleteDevice(DriverObject->DeviceObject);
}
我们已经完成了驱动程序的主体代码,所以我们继续编写 Filter-Hook Driver 的代码。
注册过滤函数
在上面的代码中,您看到了一个名为 `SetFilterFunction(..)` 的函数。我实现了这个函数来在 IP Filter Driver 中注册一个函数。我将描述遵循的步骤:
- 首先,我们必须获取指向 IP Filter Driver 的指针。这需要驱动程序已安装并正在运行。我的用户应用程序在加载此驱动程序之前加载并启动 IP Filter Driver,以确保这一点。
- 其次,我们必须构建一个 IRP,指定 `IOCTL_PF_SET_EXTENSION_POINTER` 作为 IO 控制码。我们必须传递一个 `PF_SET_EXTENSION_HOOK_INFO` 结构作为参数,该结构包含有关过滤函数指针的信息。如果您想卸载该函数,则必须遵循相同的步骤,但将 `NULL` 作为过滤函数指针传递。
- 将构建好的 IRP 发送到设备驱动程序。
这里是这个驱动程序的一个大问题。每次只能安装一个过滤函数,所以如果其他应用程序安装了一个,您就无法安装您的函数。我将在以下几行展示这个函数的代码。
NTSTATUS SetFilterFunction
(PacketFilterExtensionPtr filterFunction)
{
NTSTATUS status = STATUS_SUCCESS, waitStatus=STATUS_SUCCESS;
UNICODE_STRING filterName;
PDEVICE_OBJECT ipDeviceObject=NULL;
PFILE_OBJECT ipFileObject=NULL;
PF_SET_EXTENSION_HOOK_INFO filterData;
KEVENT event;
IO_STATUS_BLOCK ioStatus;
PIRP irp;
dprintf("Getting pointer to IpFilterDriver\n");
//first of all, we have to get a pointer to IpFilterDriver Device
RtlInitUnicodeString(&filterName, DD_IPFLTRDRVR_DEVICE_NAME);
status = IoGetDeviceObjectPointer(&filterName,STANDARD_RIGHTS_ALL,
&ipFileObject, &ipDeviceObject);
if(NT_SUCCESS(status))
{
//initialize the struct with functions parameters
filterData.ExtensionPointer = filterFunction;
//we need initialize the event used later by
//the IpFilterDriver to signal us
//when it finished its work
KeInitializeEvent(&event, NotificationEvent, FALSE);
//we build the irp needed to establish fitler function
irp = IoBuildDeviceIoControlRequest(IOCTL_PF_SET_EXTENSION_POINTER,
ipDeviceObject,
if(irp != NULL)
{
// we send the IRP
status = IoCallDriver(ipDeviceObject, irp);
//and finally, we wait for
//"acknowledge" of IpFilter Driver
if (status == STATUS_PENDING)
{
waitStatus = KeWaitForSingleObject(&event,
Executive, KernelMode, FALSE, NULL);
if (waitStatus != STATUS_SUCCESS )
dprintf("Error waiting for IpFilterDriver response.");
}
status = ioStatus.Status;
if(!NT_SUCCESS(status))
dprintf("Error, IO error with ipFilterDriver\n");
}
else
{
//if we cant allocate the space,
//we return the corresponding code error
status = STATUS_INSUFFICIENT_RESOURCES;
dprintf("Error building IpFilterDriver IRP\n");
}
if(ipFileObject != NULL)
ObDereferenceObject(ipFileObject);
ipFileObject = NULL;
ipDeviceObject = NULL;
}
else
dprintf("Error while getting the pointer\n");
return status;
}
您可以看到,当我们完成设置 `filter` 函数的过程后,必须解除对我们在获取设备驱动程序指针时获得的文件的引用。我使用一个事件来在 IpFilter Driver 完成 IRP 处理时获得通知。
过滤函数
我们已经看到如何开发驱动程序以及如何安装 `filter` 函数,但我们对这个函数还一无所知。我已经说过,当主机接收或发送数据包时,总是会调用这个函数。根据这个函数的返回值,系统决定如何处理数据包。这个函数的原型必须是:
typedef PF_FORWARD_ACTION
(*PacketFilterExtensionPtr)(
// Ip Packet Header
IN unsigned char *PacketHeader,
// Packet. Don't include Header
IN unsigned char *Packet,
// Packet length. Don't Include length of ip header
IN unsigned int PacketLength,
// Index number for the interface adapter
//over which the packet arrived
IN unsigned int RecvInterfaceIndex,
// Index number for the interface adapter
//over which the packet will be transmitted
IN unsigned int SendInterfaceIndex,
//IP address for the interface
//adapter that received the packet
IN IPAddr RecvLinkNextHop,
//IP address for the interface adapter
//that will transmit the packet
IN IPAddr SendLinkNextHop
);
`PF_FORWARD_ACTION` 是一个枚举类型,其值(在 Microsoft 文档中)可以是:
PF_FORWARD
指定 IP 过滤驱动程序立即将转发响应返回给 IP 堆栈。对于本地数据包,IP 将它们向上转发到堆栈。如果数据包的目的地是另一台计算机并且启用了路由,IP 会相应地路由它们。
PF_DROP
指定 IP 过滤驱动程序立即将丢弃响应返回给 IP 堆栈。IP 应该丢弃数据包。
PF_PASS
指定 IP 过滤驱动程序过滤数据包并将结果响应返回给 IP 堆栈。IP 过滤驱动程序如何过滤数据包取决于它如何使用包过滤 API 进行设置。如果过滤钩子确定它不处理数据包,而是允许 IP 过滤驱动程序过滤数据包,则返回此 pass 响应。
尽管 DDK 文档只包含这 3 个值,但如果您查看 `pfhook.h`(Filter-Hook Driver 需要包含的文件),您会发现还有一个值。这个值是 `PF_ICMP_ON_DROP`。我猜测这个值对应于丢弃数据包并使用 ICMP 数据包通知源有关错误。正如您在过滤函数定义中看到的,数据包及其标头是以指针形式传递的。因此,您可以修改标头或有效载荷,然后转发数据包。这非常有用,例如,用于网络地址转换(NAT)。如果我们更改目标地址,IP 会路由数据包。在我的实现中,`filter` 函数将每个数据包与用户应用程序引入的规则列表进行比较。此列表实现为在运行时与每个 `START_IP_HOOK` IOCTL 一起构建的链表。您可以在我的源代码中看到这一点。
代码
在本文的第一个版本中,我包含了一个简单的示例,并且——由于有些人请求我帮助他们开发实际应用程序——我用一个更复杂的示例进行了更新。新示例是一个简单的包过滤应用程序。使用这个新程序,您可以设置您的过滤规则,就像在一些商业防火墙中所做的那样。与第一个版本一样,此应用程序包含两个组件:
- 用户应用程序:这是一个 MFC 应用程序,负责管理过滤规则。此应用程序将规则发送到应用程序,并决定驱动程序何时开始过滤。过滤流量的三个步骤:
- 定义您需要的规则。使用 `Add` 和 `Delete` 命令,您可以添加或删除过滤规则。
- 安装规则。定义规则后,单击“**安装**”按钮将它们发送到驱动程序。
- 开始过滤。您只需单击“开始”按钮即可开始过滤。
- Filter-Hook Driver:根据从用户应用程序接收到的过滤规则过滤 IP 流量的驱动程序。
Filter-Hook Driver 必须与用户应用程序可执行文件位于同一目录中。
为什么使用这种方法开发防火墙?
这不是开发 Windows 防火墙的唯一方法。还有其他方法,例如 NDIS Firewall、TDI Firewall、Winsock Layered Firewall、Packet Filtering API 等等。因此,我将列出 Filter-Hook Driver 的一些优点和缺点,以便您决定您的未来防火墙是否必须使用此驱动程序。
- 使用此方法过滤具有极大的灵活性。您可以过滤所有 IP 流量(及以上)。但是,您无法过滤较低层标头。例如,您无法过滤以太网帧。您需要一个 NDIS 过滤器来实现这一点,这开发起来更复杂,但更灵活。
- 这是一种简单的方法。使用此方法安装防火墙和实现过滤函数是简单的过程。但是,Packet Filtering API 更简单,尽管它不太灵活。您无法访问数据包内容,也无法使用 Packet Filtering API 修改它们。
结果:Filter-Hook Driver 在任何方面都不是最好的,但它没有任何缺点。然而,为什么这种方法不被商业产品使用?答案很简单。尽管此驱动程序没有任何缺点,但它也有一个很大的缺点。如前所述,每次只能安装一个 `filter` 函数。我们可以开发一个出色的防火墙,并且可以被数千用户下载和安装,但如果其他应用程序使用此过滤器(并且在之前安装了过滤函数),我们的程序将无法工作。
此方法还有另一个 Microsoft 未记录的缺点。尽管 DDK 文档说明您可以在过滤函数中访问数据包内容,但这并不真实。您可以访问接收到的数据包的内容,但对于发送的数据包,您只能读取 IP 和 TCP、UDP 或 ICMP 标头。我不明白为什么…… Microsoft 在 Windows XP 中引入了另一种没有此限制的驱动程序:firewall-hook driver。它的安装非常相似,但 Microsoft 不建议使用它,因为“它在网络堆栈中运行得太高了”。也许这种驱动程序将在以后的 Windows 版本中消失。
结论
好了,就到这里。我知道这不是开发防火墙的最佳方法(我前面提到了那个大缺点),但我认为这对于那些正在寻找信息或对此感兴趣的人来说是一个不错的开端。我希望您有所理解,并且现在想开发一个出色的防火墙。
历史
- 2002 年 12 月 21 日 - 首次发布
- 2003 年 11 月 4 日 - 更新