一个想法

zerray | 六月 23rd, 2005 - 22:29

网络防火墙是监控主机接收和发送的数据包,设定各种各样的rule,进而保证系统的安全。

病毒防火墙(实时监控)是根据病毒特征码来检查是否有病毒正要运行。

那么可不可以把网络防火墙的思想用在防病毒上呢?

例如实现一个 API 防火墙,对于每个正在运行的程序,监控它要调用的 API ,对于一些有可能造成破坏的 API 调用弹出提示,由用户决定是否允许调用。这样,一些像 RegCreateKey,WriteProcessMemory,CreateRemoteThread 等病毒常用的函数需要经过用户允许才能调用,对于病毒的发现和预防都很有用。实现的话用 Hook API 就可以吧?不知道对系统资源消耗有多大。

这只是我自己的空想,可能没什么价值,也可能已经有这样的软件了。欢迎感兴趣的朋友们跟我讨论:)

如何实现防火墙钩子驱动

zerray | 六月 20th, 2005 - 21:33

介绍

也许,防火墙钩子驱动是在 Windows 系统下开发数据包过滤程序的方法中最没有文档可查的一个了。微软没有给出关于它的任何文档,你唯一能够找到点儿什么的地方是 DDK 头文件(ipFirewall.h)。事实上,当我安装了 Windows 2000 DDK,我对于发现这个 .h 文件(和它的内容)非常惊奇,因为没有文档提到防火墙钩子的存在。在下一个版本的 DDK 中,微软加了一些关于它的文档:“这个方法存在但不建议使用”。

然而,因为它是个实现防火墙的简单方法,我认为了解防火墙钩子驱动如何工作将是很有趣的。

防火墙钩子驱动

我不明白为什么微软不建议使用防火墙钩子驱动的开发。对于开发一个完整的防火墙方案我的确不建议用它,但对于小程序,它可是个好的选择。基本上,防 火墙钩子驱动能够做过滤钩子驱动(见我的文章《在 Windows 2000/XP下开发防火墙》)所做的相同工作,但限制更少。

可能你还记得,过滤钩子函数仅允许在系统中安装一个过滤函数。如果有程序已经使用了这个功能,你的程序就没用了。使用防火墙钩子驱动,你不会遇到这 个问题。你可以安装你需要的所有过滤函数。每个过滤函数被赋予一个优先级,于是系统会一个接一个的(以优先级顺序)调用过滤函数知道一个函数返回 “DROP PACKET(丢弃包)”。如果所有的函数都返回“ALLOW PACKET(允许包)”,这个包就被允许通过了。你可以把它想象称一个过滤函数链。当其中一个返回了“DROP PACKET”链就被切断了。链中每个函数的顺序是由它的优先级值数给出的。

在上图中,我表示了以下过程:

  1. 你的主机收到了一个包。IP 驱动拥有以优先级排列的过滤函数列表(具有更高优先级的函数是 Filter Function 1)。
  2. 首先,IP 驱动将包传递给优先级最高的过滤函数并等待返回值。
  3. Filter function 1 返回“ALLOW PACKET”。
  4. 因为 Filter function 1 允许了包,IP 驱动将包传递给了下一个过滤函数:Filter function 2。
  5. 在图示情况下,Filter function 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\\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)

这几行代码告诉你了安装回调函数的大致方法。你只需用你的回调函数的数据填写 IP_SET_FIREWALL_HOOK_INFO 结构并安装它发送 IOCTL IOCTL_IP_SET_FIREWALL_HOOK 到 IP 设备。简单,如果你与驱动程序打过交道或是曾做过那个有文档的过滤钩子函数。这个结构的一个重要参数是优先级域。每个优先级域包含了该过滤函数的优先级, 其值越来越大。

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);
如果你要卸载过滤函数,可以用同样的代码,只要把 filterData.Add 的值改成 FALSE 就行了。

过滤函数

防火墙钩子驱动的过滤函数比过滤钩子驱动的更复杂。因为没有关于该函数及其参数的文档,所以复 杂度增加了。该函数具有以下特征:

FORWARD_ACTION cbFilterFunction(VOID **pData,
                                     UINT RecvInterfaceIndex,
                                     UINT *pSendInterfaceIndex,
                                     UCHAR *pDestinationType,
                                     VOID *pContext, 
                                     UINT ContextLength,
                                     struct IPRcvBuf **pRcvBuf);

用耐心和调试方法(还有参数名字的解释:)),我得出了以下关于参数的信息:

pData *pData 指向一个带有包缓冲的 (struct IPRcvBuf *) 结构。
RecvInterfaceIndex 数据接收的接口。
pSendInterfaceIndex 指向无符号整型的指针,包含数据发送的索引值。虽然它是个指针,改变它的值并不能让包改变路径:(。
pDestinationType 指向无符号整型的指针,指出目标类型:本地网络,远程,广播,多播,等。
pContext 指向 FIREWALL_CONTEXT_T 结构的指针,使你可以像流入和流出的包一样取得包的信息。
ContextLength 指向的缓冲区的大小。其值总是 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;
}

代码

你能很快认出这篇文章的程序。是的,图形界面和我在过滤钩子驱动中用的完全一样。为什么?因为我写了个简单的数据包过滤程序用来测试我写的所有防火 墙方法。这样,我有了一个通用的图形界面给它们,来提供相同的功能,但在底层,它们工作的不一样。我有这个程序的不同版本(只做最小的改动)来测试我的过 滤钩子驱动,防火墙钩子驱动,LSP DLL,TDI 过滤驱动,NDIS 驱动……所以,我想这个方法是好理解的。几乎不更改图形界面,这样你只想了解使用的新方法。

像我其它的文章一样,这个程序只实现了包的过滤。很多人问我关于加入一些更多的功能,像包记录,安装为服务……但我想要按照提供方法而不是答案的思 想。如果你想要加入一些这样的功能,我很高兴:)。你可以联系我,问所有你想要的问题。

结论

好了,一旦我写完了这篇文章,它就是你的了。我希望你能从中学到与我一样多的东西。Enjoy it!!



在Windows 2000/XP下开发防火墙

zerray | 六月 20th, 2005 - 21:24

介绍

如果你决定在 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 版本中消失。

结论

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

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