在Windows 2000/XP下开发防火墙

Sample Image

介绍

如果你决定在 Linux 下开发一个防火墙,你可以找到很多资料和源代码,全部免费。但对 Windows 平台下的防火墙感兴趣的人们就有些困难了,不仅是找资料难,找免费的源代码更是个不可能的任务!

所以,我决定写这片文章来简述一下在 Windows 2000/XP 下开发防火墙的简单方法,借以帮助那些对此感兴趣的人们。

背景

在 Windows 2000 DDK 中,微软包含了一种新的网络驱动叫做过滤钩子驱动( Filter-Hook Driver )。使用它,你可以建立一个函数来过滤所有到达或离开接口的数据包。

因为关于这个主题的文档很少,并且没有例子,所以我写了这篇文章来介绍成功使用它所需的步骤。我希望这篇文章能帮你理解这个简单的方法。

过滤钩子驱动

正如我前面所说,过滤钩子驱动是微软在 Windows 2000 DDK 中引入的。实际上,它不是一个新的网络驱动类,它只是扩展 IP 过滤驱动(包含在 Windows 2000 及以后的版本中)功能的一种方法。

事实上,过滤钩子驱动不是网络驱动,而是内核模式驱动。基本的,我们在过滤钩子驱动中实现一个回调函数,然后把这个回调函数与 IP 过滤驱动注册到一起。这样做了之后,IP 过滤驱动会在一个数据包被发送或接受时调用我们的回调函数。那么……这么做主要的步骤有哪些呢?

我总结了以下五个步骤:

  1. 建立一个过滤器钩子驱动。这一步,你必须建立内核模式驱动,选择一个名字,DOS 名和其它驱动字符,没什么特别的要求但我建议你用描述性的名字。
  2. 如果我们要安装过滤函数,首先必须取得一个指向 IP 过滤驱动的指针。所以,这是第二个步骤。
  3. 我们已经有了指针,现在可以安装过滤函数了。我们可以通过发送指定的 IRP 来做这件事。在传递的“消息”数据中包含指向过滤函数的指针。
  4. 过滤数据包!!!
  5. 当我们决定停止过滤时,必须注销过滤函数。我们可以通过注册过滤函数到空指针来实现注销。

哦,只有五步,而且看起来很简单,但是……要怎样建立内核模式驱动呢?怎么取得指向 IP 过滤驱动的指针?怎么……是的,请稍等,我现在就来解释这些步骤:P,展示源码例子。

建立内核模式驱动

过滤钩子驱动是内核模式驱动,所以如果我们想做,就得做个内核模式驱动。这篇文章并不是“5分钟学会怎样开发内核模式驱动”,所以我假设读者们已经有了这方面的知识。

过滤钩子驱动的结构是典型的内核模式驱动结构:

  1. 建立设备的驱动入口(注:建立设备是驱动入口的定语),设置标准例程来处理 IRP(分派,加载,卸载,创建……)和建立与其它应用程序通讯的符号连接。
  2. 管理 IRP 的标准例程。在你开始编写代码之前,我建议,考虑你要从设备驱动引出到应用程序哪些 IOCTL。在我的例子中,我实现了四个 IOCTL 代码:START_IP_HOOK(注册过滤函数)、STOP_IP_HOOK(注销过滤函数)、ADD_FILTER(安装新规则)和 CLEAR_FILTER(清除所有的规则)。
  3. 还必须为我们的驱动实现另一个函数:过滤函数。

我建议你使用程序生成内核模式驱动的结构,然后你只要把添加代码到生成的函数里就行了。例如,我在这个项目中使用的 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);
}
我们已经做好了驱动的主要代码,接下来是过滤钩子驱动的代码。

注册过滤函数

在上面的代码中,你已经看到了一个叫做 SetFilterFunction(..) 的函数。实现这个函数是用来注册函数到 IP 过滤驱动的。有下面的几步:

  1. 首先,我们必须取得一个指向 IP 过滤驱动的指针。这要求 IP 过滤驱动已经安装并运行了。我的用户应用程序,在加载这个驱动之前就加载并启动了 IP 过滤驱动,来保证这一点。
  2. 第二,我们必须建立一个 IRP 指定 IOCTL_PF_SET_EXTENSION_POINTER 作为 IO 控制代码。我们必须以参数的形式传递一个 PF_SET_EXTENSION_HOOK_INFO 结构,其中包含了指向过滤函数的指针。如果你要卸载这个函数,你必须按照同样的步骤并用 NULL 代替指向过滤函数的指针。
  3. 发送建立的 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;
}

你会发现当我们完成了建立过滤函数的处理,我们必须废除( de-reference )在我们取得指向设备驱动的指针时获得的文件对象。我用了一个将在 IP 过滤驱动完成对 IRP 的处理时提示的事件。

过滤函数

我们已经看到了如何开发驱动和如何安装过滤函数,但我们还不知道过滤函数是什么样的。

我已经说过了这个函数总是在主机接受或发送一个数据包时被调用。根据这个函数的返回值,系统来决定对这个包作些什么。

这个函数的原型必须是这样的:

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 是一个枚举类型,可以取值为:

  • PF_FORWARD

    指定 IP 过滤驱动立即向 IP 栈返回 forward 响应。对于本地包,IP 将他们直接入栈。如果包的目的地是另一台计算机并且允许路由,IP 将它们相应的路由。

  • PF_DROP

    指定 IP 过滤驱动立即向 IP 栈返回 drop 响应。IP 应该丢弃数据包。

  • PF_PASS

    指定 IP 过滤驱动过滤包并向 IP 栈返回结果响应。IP 过滤驱动如何处理过滤数据包取决于数据包过滤 API 的设置。

    如果过滤钩子决定不处理这个包而交给 IP 过滤驱动来过滤这个包,返回这个 pass 响应。

虽然 DDK 文档只包含了这三个值,但你查看 pfhook.h (过滤钩子驱动需要包含)就会看到另一个值。这个值是 PF_ICMP_ON_DROP 。我估计这个值对应的是丢弃包并以一个 ICMP 包反馈错误信息。

正如你在过滤函数的定义中看到的,数据包及它的报头是以指针传递的。所以,你可以修改报头或负载然后传递该包。这是很有用的,例如做网络地址转换( NAT )。如果你改变了目标地址,IP 会路由该包。

在我的实现中,过滤函数将每个包和一个由用户应用程序引入的规则列表比较。这个列表是用链表实现的,由每个 START_IP_HOOK IOCTL 在运行时建立。你可以在我的代码中看到这些。

代码

在这篇文章的第一个版本中包含了一个简单的例子,但由于有人想让我帮他开发实际的程序,我把它更新成了一个更复杂的例子。新的例子时一个小的数据包过滤程序。用这个新程序你可以实现你自己的过滤规则,就像你在一些商业防火墙软件中做的那样。

在第一个版本中,程序包含两个组件:

  • 用户应用程序:一个 MFC 应用程序用来管理过滤规则。这个应用程序发送规则到驱动程序并决定驱动什么时候开始过滤。过滤数据分三步:
    • 定义你需要的规则。用添加和删除命令你可以添加或删除过滤规则。
    • 安装规则。当你定义好了规则,点击安装按钮,发送它们到驱动程序。
    • 开始过滤。你只需要点击开始按钮来开始过滤。
  • 过滤钩子驱动:根据由用户应用程序接收到的过滤规则来过滤 IP 数据包的驱动。

过滤钩子驱动必须和用户应用程序的可执行文件处在同一个目录中。

为什么用这种方法开发防火墙?

这不是在 Windows 开发防火墙的唯一方法,还有其它的像 NDIS 防火墙,TDI 防火墙,Winsock 层的防火墙,数据包过滤 API,……所以我总结了一些过滤钩子驱动的优点和缺点,以供你在将来需要用到时参考。

  • 用这种方法有很大的灵活性。你可以过滤所有的 IP 数据包(以及上层的数据包)。但你不能过滤更底层的报头,例如,你不能过滤以太网帧。你需要用 NDIS 过滤来实现,更复杂也更灵活。
  • 这是一个简单的方法。安装防火墙和实现过滤函数用这种方法都是很简单的。但数据包过滤 API 更为简单,虽然没有它灵活。你不能访问数据包的内容,也不能用数据包过滤 API 修改它。

结果:过滤钩子驱动并不是最好的,但它也没有什么坏特性。可是为什么这个方法没有用在商业产品中呢?

答案很简单。虽然这个驱动没有不好的特性,但它有一个很大的缺点。正如我之前提到的,每次只能安装一个过滤函数。我们可以开发一个强大的防火墙,它可能被上千的用户下载并安装,但如果其它的应用程序使用了过滤(并在之前安装了过滤函数),我们的程序就什么都做不了了。

这个方法还有另一个缺点没有被微软的文档提到。虽然 DDK 文档说你可以在过滤函数中访问数据包内容,但那不是真的。你可以访问收到的包的内容,但对于发送的包,你只能读 IP 和 TCP,UDP 或 ICMP 报头。我不明白为什么……

微软在 Windows XP 中引入了另一种没有这个限制的驱动:防火墙钩子驱动。它的安装很类似,但微软并不建议使用它,以为“它消耗太多的网络栈”。也许这个驱动会在以后的 Windows 版本中消失。

结论

好了,到结尾了。我只到这并不是开发防火墙的最好方法(我在前面提到了它的缺点)。但我认为这对正在查找资料和对此感兴趣的人们是个好的开始。

我希望你能得到些帮助,并开始想要开发个强大的防火墙了。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值