Windows平台下基于WFP模型的网络防火墙设计实现

本文转载自:http://bbs.pediy.com/showthread.php?t=173871
Windows平台下基于WFP模型的网络防火墙设计实现


项目概述:
本项目由两大模块构成,分别为驱动模块和UI模块。

首先,从规则库(存放在注册表中)中读取访问控制规则,然后利用WFP(Windows Filter Platform)技术[6]在Windows内核的TCP/IP协议栈中建立WFP钩子,并将回调函数设置到驱动模块里,用来截获拦截TCP/IP封包并做相应处理(包括一些协议的解析)。最后通过事先制定好的访问规则检查认证确定连接是否允许通过。

实现UI 模块主要任务是UI的制作。这里采用了MFC编程框架,实现的主体界面有四个,分别为主控对话框、程序联网规则编辑对话框、IP规则编辑对话框以及DNS解析规则编辑对话框。主控对话框实现一些全局性的控制工作,指挥驱动模块采取相应动作,比如各个子功能的开启和关闭控制等。其余三个编辑对话框则实现相应控制规则的添加、编辑、更新和储存操作。UI模块和驱动模块需要交换的数据主要是规则库,而且是单向的,因此,本项目把注册表当成UI模块和驱动模块通讯的桥梁,其余实现UI对驱动的实时控制操作则用系统提供的设备(驱动)控制函数实现。

由于UI只负责配置规则以及管理驱动,配置完成后自己就可以结束了。而驱动工作不需要进程,所以在这样的架构下,本系统实现了无进程工作的目的。

开发平台与开发工具

目标运行平台:Windows Visita及后续版本
开发平台:Windows 7
开发工具:WDK、VS2008、Windbg(内核调试器)、VMware(虚拟机)

WFP 过滤驱动

WFP(Windows Filter Platform)是为网络过滤应用开发平台提供支持的API和系统服务的集合。WFP允许开发者编写代码和操作系统的网络协议栈交互。网络数据可以在到达目的地之前被过滤和修改。通过提供简单的开发平台,WFP被用于取代以前的TDI过滤,NDIS过滤,以及LSP(Winsock Layered Service )。在Visita及以后的系统火墙钩子,过滤钩子驱动将不再适用。相应的功能将用WFP实现。WFP的基本架构如图所示:


其中,Filter Engine是WFP的核心,它提供所有基于TCP/IP网络数据的过滤操作。在TCP/IP的协议栈的关键点有过滤层,通过这些过滤层网络数据被传递到Filter Engine进行处理。如果一个过滤层的过滤器条件被满足,Filter Engine将对获得的数据包采取相应的动作。Callout扩展了Filter Engine的功能,它由一系列callout 函数和一个用以唯一标识该callout的GUID KEY值组成。而一个callout内核驱动程序要实现若干个这样的callout并向Filter Engine注册这些callout,从而实现应用所要求的功能[6]。本项目最主要的部分是实现一个callout驱动程序(驱动模块)对本机的网络连接进行过滤,控制。

模块设计与划分

本项目实现的具体功能

(1)监控管理,包括程序联网控制、IP过滤控制、DNS过滤控制以及总监控开关。

(2)规则管理,包括程序联网规则、IP过滤规则、DNS过滤规则的添加、编辑、删除等。

(3)与建立连接相关数据包的捕获和管理(驱动实现)。

(4)无进程工作。

模块架构
模块架构图如图所示:


UI部分模块架构
本项目的UI部分以对话框为单位划分模块,分为一个主控对话框、三个规则编辑对话框以及其它一些辅助对话框。在模块架构图中描述的进程联网控制,IP过滤、DNS过滤三大功能就被分别封装在这些对话框中。下面详细介绍这些对话框。

1 主控对话框:由类CCactiWallDlg实现。其中包含各种监控开关的控制以及打开规则设置对话框的按钮。在开启/关闭按钮的事件响应函数里调用系统API DeviceIoControl向驱动模块发送相应的控制码打开或者关闭相关的监控。

2 进程规则设置对话框:由类CProcessSettingDlg实现。主要功能包括列表显示当前已经设置的进程联网规则(程序文件名、规则状态、路径),添加规则,编辑规则和删除规则。其中用到了二个辅助对话框CProcessRuleAddDlg和CProcessLogDlg。CProcessRuleAddDlg用于添加、编辑规则,而CProcessLogDlg则是用于从日志文件中生成规则,方便用户操作。

3 IP过滤规则设置对话框:由类CIpSettingDlg实现。主要功能包括列表显示当前已经设置的IP规则(规则名称、规则、方向、源IP、目的IP、协议类型、源端口、目的端口、ICMP类型和代码),添加规则,编辑规则和删除规则。用到了一个辅助对话框CIpRuleAddDlg,用于从用户输入接收规则并将其转换为内部数据结构存入规则库。

4 DNS过滤规则设置对话框:由类CDnsSettingDlg实现。主要功能包括列表显示当前已经设置的DNS规则(DNS名称、规则),添加规则,编辑规则和删除规则。用到了一个辅助对话框CDnsRuleAddDlg,用来接收用户输入的DNS字符串并将其添加到规则库。
除以上4个对话框外,UI模块还用到了crc32编码模块,用以生成字符串的hash值,作数据结构转换用。

驱动部分模块架构

本项目的驱动模块用面向过程的语言C编写。设计过程中运用了一些面向对象编程的思想,将功能相对独立的部分集中到一个模块里实现,使得模块之间的耦合度大大降低,这就类似于C++里的对象的概念。本模块分为7个子模块,下面分别作简单介绍:

1 common.c :为所有其它模块提供基础支持。比如Hash字符串、获取系统时间、路径格式转换、创建/销毁内核线程等功能都被放到此模块中实现,为其它模块服务。

2 crc32.c :提供crc32编码支持。

3 init.c :驱动程序初始化部分。包括创建设备、设置需要处理应用层下发请求的派遣例程、根据从规则库读到的全局配置信息有选择地调用其它模块的初始化例程。

4 wall.c :驱动中最核心的部分。用于从规则库中读取配置信息和用户指定的监控规则信息、向Filter Engine的连接层注册callout用以捕获建立连接的动作、建立数据包链表用来缓冲数据包、根据读取的规则信息对缓冲的数据包进行相应处理(允许或者禁止通过)等。

5 rules.c :封装用户规则的数据结构以及对规则的相关操作,包括进程规则、IP规则以及DNS规则都集中在此模块中。Wall.c会检测此模块建立的规则库从而决定对所处理数据包采取何种动作。具体规则的数据结构将在4.4节详细说明。

6 callouts.c :具体实现需要的callout,定义各个callout的GUID KEY,用以扩展Filter Engine的功能。

7 memtrace.c :内存跟踪模块,对内核内存的分配和释放进行跟踪记录,防止内存泄露。在调试版本的驱动中此模块接管系统的内存分配和释放API,在发布版本中,此模块不被编译。

控制规则设计


本项目的规则库存放在注册表中,根键目录为:HKEY_LOCAL_MACHINE\Software\lzcj\CactiWall,以下介绍的各配置项均在此根键下,设此项为ROOT。

需要设计的控制规则类型

1全局规则,内容包括总监控开启/关闭、程序联网监控开启/关闭、IP过滤监控开启/关闭、DNS过滤开启/关闭以及进程联网日志文件存放位置。

2 进程联网规则,内容包括程序全路径、访问网络规则(允许/禁止)、全路径的小写形式的crc32值、以及规则之外程序联网的默认动作。

3 IP过滤规则,内容包括规则名称、数据包方向(上行,下行,双向)、动作(允许/禁止)、源IP、目的IP、协议类型、源端口、目的端口、ICMP类型和代码以及用户规则之外的默认动作。

4 DNS过滤规则,内容包括DNS特征串、动作(允许解析/禁止解析)以及规则之外的默认动作。 

控制规则数据结构定义

1、全局规则
位置:[ROOT\globalrules]

表1 全局规则


2、进程联网规则
位置:[ROOT\processrules]

表2 进程规则


位置[ROOT\processrules\XXXX],
其中,XXXX为程序全路径字符串(UNICODE形式)的crc32值的字串形式。

表3 进程规则


3、 IP过滤规则
位置:[ROOT\iprules]

表4 IP规则


位置[ROOT\iprules\XXXX],
其中,XXXX为规则名称的crc32值的字串形式。

表5 IP规则


表中对于Rule的32值的用法如下:
代码:
enum
{
    AnyAddr=0,
    UniqueAddr,
    RangeAddr,
    UnknownAddr
};
enum
{
    RulesDirectionAny=0,
    RulesDirectionUp,
    RulesDirectionDown,
    RulesDirectionUnknown
};
enum
{
    RulesProtocolAny = 0
};
union{
        UINT32   u32;
        struct
        {
            UINT32  RemoteAddrType :2;  //取值为AnyAddr,UniqueAddr,RangeAddr
            UINT32  LocalAddrType :2;   //取值为AnyAddr,UniqueAddr,RangeAddr
            UINT32  RemotePortType :2;  //取值为AnyAddr,UniqueAddr,RangeAddr
            UINT32  LocalPortType :2;   //取值为AnyAddr,UniqueAddr,RangeAddr
            UINT32  ProtocolType :8;//网络协议类型,和RFC文档的代码保持一致
            UINT32  Direction :2;//00:任意方向01:上行 10:下行 11:未定义
            UINT32  Access  :1;//是否允许访问,1为允许
            UINT32  IcmpType :5;
            UINT32  IcmpCode :5;
            UINT32  Reserved :3;
        }Bits;
}rule;
对于表中给出的可选字段作如下说明:
当****Addr(Port)Type取值为AnyAddr时,字段****Addr(Port)和****Addr(Port)2均无效。表示为任意地址
当****AddrType(Port)取值为UniqueAddr时,仅字段****Addr(Port)有效,表示唯一地址。
当****AddrType(Port)取值为RangeAddr时,字段****Addr(Port)和****Addr(Port)2均有效,表示为****addr——****addr2之间的地址范围。

4、DNS 过滤规则
位置:[ROOT\dnsrules]

表6  DNS规则


位置[ROOT\dnsrules\XXXX],
其中,XXXX为DNS名称的crc32值的字串形式。

表7  DNS规则


网络驱动模块的核心功能实现


网络数据的截获

在callouts.c中实现了两个callout用于捕获进行连接的数据包。一个用于向外的连接,一个用于向内的连接。WDK中对callout函数原型的定义如下:
代码:
void 
NTAPI
WallALEConnectClassify(
   IN const FWPS_INCOMING_VALUES* inFixedValues,
   IN const FWPS_INCOMING_METADATA_VALUES* inMetaValues,
   IN OUT void* layerData,
   IN const void* classifyContext,
   IN const FWPS_FILTER* filter,
   IN UINT64 flowContext,
   OUT FWPS_CLASSIFY_OUT* classifyOut
   )
void 
NTAPI
WallALERecvAcceptClassify(
   IN const FWPS_INCOMING_VALUES* inFixedValues,
   IN const FWPS_INCOMING_METADATA_VALUES* inMetaValues,
   IN OUT void* layerData,
   IN const void* classifyContext,
   IN const FWPS_FILTER* filter,
   IN UINT64 flowContext,
   OUT FWPS_CLASSIFY_OUT* classifyOut
   );
通过这些函数中参数iniFixedValues、inMetaValues、layerData、协议族(V6 OR V4 )以及数据包方向,可以唯一标识一个数据包。在wall.c中的WallAllocateAndInitPendedPacket函数用以分配和初始化一个内定的数据包结构结点。这两个callout中都分别调用了这个函数分配和初始化数据包结点(line 72、316),然后将对当前数据包的操作挂起(line 88、332),并将刚分配的结点放入连接数据包链表(由wall.c创建维护)中缓冲(line 103、348),进而进行后续过滤处理。本项目采用的过滤方式是异步的,如果在callout中直接对数据包采取动作,则是同步方式。在同步方式中,参数classifyOut将告诉Filter Engine是callout决定采取何种动作。

控制规则的获取
控制规则的获取功能集中在wall.c中,分为全局规则的获取、进程规则的获取、IP规则的获取以及DNS规则的获取。对应的函数分别为:
VOID WallLoadGlobalConfig();

NTSTATUS WallLoadProcessConfig();

NTSTATUS WallLoadIpConfig();

NTSTATUS WallLoadDnsConfig();

它们的主要任务是读取注册表并将相关信息转换为内部的数据结构,并将规则信息添加到由rules.c创建和维护的规则表中。这里规则表的存储结构以及实现算法将在5.2.4中作简单介绍。当然,在相应监控关闭的情况下,为了节省内存资源,还得卸载一些配置,和上边后三个对应存在这样的调用:
VOID WallUnloadProcessConfig();

VOID WallUnloadIpConfig();

VOID WallUnloadDnsConfig();

控制网络数据包
核心代码在wall.c的线程函数WallInspectWallPackets( IN PVOID Context )中,该函数从连接数据包链表中摘下结点,并根据读取到的规则规则对其进行处理。关键代码如下所示(line 1312):
代码:
packet = (PWALL_PENDED_PACKET)listEntry;
        if( gbBlockAll )
            packet->authConnectDecision = FWP_ACTION_BLOCK;
        else if( gbEnableProcessMonitor && !WallIsProcessTrafficPermit(packet))
            packet->authConnectDecision = FWP_ACTION_BLOCK;
        else if ( gbEnableIpMonitor && !WallIsIpTrafficPermit(packet))
            packet->authConnectDecision = FWP_ACTION_BLOCK;
        else if( gbEnableDnsMonitor && !WallIsDnsTrafficPermit( packet ))
            packet->authConnectDecision = FWP_ACTION_BLOCK;
        else
            packet->authConnectDecision = FWP_ACTION_PERMIT;
控制规则在驱动中的数据结构

前面已经描述过,本项目有三种联网监控功能,即程序联网监控、IP监控以及DNS监控。同于这三者在匹配规则时的要求略有不同,同时考虑到一些性能方面的因素,对它们采用了不同的数据结构来建立规则表。规则表的建立,初始化和维护被封装在rules.c中。

1,程序联网规则表用哈希表实现,存储方式为数组。用到的结构体定义如下:
代码:
typedef struct _PROCESS_RULES_ELEM
{
    UINT32   crcPath;
    UINT32   rule;      //32位值,各个位的功能参看下边的宏定义
}PROCESS_RULES_ELEM,*PPROCESS_RULES_ELEM;
typedef struct _PROCESS_RULES_TABLE
{
    UINT8                count;
    PROCESS_RULES_ELEM  rules[ MAX_PROCESS_RULES_NUM ];
}PROCESS_RULES_TABLE,*PPROCESS_RULES_TABLE;
由于程序路径要完全匹配,规则中存放字串形式的全路径不仅会占用更多的存储空间,而且比较效率也会急剧下降[4]。,因而这里选择存放全路径字串的哈希值。选择的编码算法是crc32,该算法的哈希结果为一个32位整数值[13]。驱动得到网络数据包对应进程全路径以后,通过运算将其转换为crc32值,然后与规则库的存放的crc32值比较即可。这样可以极大地节约内存,降低匹配时间开销。

为了提高在规则表中的查找、存储速度,将程序全路径的crc32值作为哈希key,进行某种运算后,直接生成该项在数组中的序号。解决哈希冲突的方法是:开放定址法,增量为1。核心算法代码例举如下:
代码:
NTSTATUS
AddProcessRule( IN UINT32 crcPath,IN UINT32 rule )
{
    UINT8   xorsum = 0;
    UINT32  key,i;
    LOG("into\n");
    if( gProcessRulesTable.count >= MAX_PROCESS_RULES_NUM )
        return STATUS_PROCESS_RULES_FULL;
    if ( IsProcessRuleExist( crcPath ))
        return STATUS_PROCESS_RULES_EXISTED;
    if( crcPath == 0 )crcPath = ZERO_CRC_VALUE;
    key = crcPath;
  //哈希函数
    for( i = 0;i < 32;i++)
    {
        xorsum ^= key & 0xff;
        key >>= 1;
    }
  //处理哈希冲突
    for( i = xorsum;;i = (i + 1 ) % MAX_PROCESS_RULES_NUM )
    {
        if( gProcessRulesTable.rules[i].crcPath == 0 )
            break;
    }
    gProcessRulesTable.rules[i].crcPath = crcPath;
    gProcessRulesTable.rules[i].rule = rule;
    gProcessRulesTable.count++;
    return STATUS_SUCCESS;
}
2 IP联网规则表用动态链表实现。相关的数据结构如下:
代码:
typedef struct _IP_RULES_ELEM
{
    LIST_ENTRY  list;
    UINT32   crcRuleName;   //IP规则名称的位crc值(对应注册表中相应的项)
    union{
        UINT32   u32;
        struct
        {
            UINT32  RemoteAddrType :2;  //取值为AnyAddr,UniqueAddr,RangeAddr
            UINT32  LocalAddrType :2;   //取值为AnyAddr,UniqueAddr,RangeAddr
            UINT32  RemotePortType :2;  //取值为AnyAddr,UniqueAddr,RangeAddr
            UINT32  LocalPortType :2;   //取值为AnyAddr,UniqueAddr,RangeAddr
            UINT32  ProtocolType :8;//网络协议类型
            UINT32  Direction :2;//00:任意方向01:上行 10:下行 11:未定义
            UINT32  Access  :1;//是否允许访问,为允许
            UINT32  IcmpType :5;
            UINT32  IcmpCode :5;
            UINT32  Reserved :3;
        }Bits;
    }rule;
    UINT32   LocalAddr;
    UINT32   LocalAddr2;
    UINT32   RemoteAddr;
    UINT32   RemoteAddr2;
    UINT16   LocalPort;
    UINT16   LocalPort2;
    UINT16   RemotePort;
    UINT16   RemotePort2;
}IP_RULES_ELEM,*PIP_RULES_ELEM;//数据项结点结构定义
typedef struct _IP_RULES_LIST
{
    LIST_ENTRY          list;
    LONG                count;
    KSPIN_LOCK          lock;
}IP_RULES_LIST,*PIP_RULES_LIST;  //链表头结点结构定义
IP规则的匹配是顺序的,而且拒绝动作的优先级更高(即一旦某规则是拒绝,就没必要再继续匹配其它规则了),但为了优化,将带有范围(地址范围,端口范围)的规则放在距链表头近的地方被匹配的概率会更高些。

3 DNS规则表也是用链表实现,具体结构如下:
代码:
typedef struct _DNS_RULES_ELEM
{
    LIST_ENTRY          list;
    UINT32              crcRuleName;
    PMY_UNICODE_STRING  dnsName;
    UINT32              rule;      //32位值,各个位的功能参看下边的宏定义
}DNS_RULES_ELEM,*PDNS_RULES_ELEM;
typedef struct _DNS_RULES_LIST
{
    LIST_ENTRY          list;
    LONG                count;
    KSPIN_LOCK          lock;
}DNS_RULES_LIST,*PDNS_RULES_LIST;
由于DNS规则的匹配需要用到子串匹配,这里用crc32值去匹配是不可行的,故在规则结点中存入了DNS名称的字串指针(字串内存是在建立规则表时动态从内核堆里分配,在卸载DNS配置的时候要释放这类堆内存)。

UI与驱动的交互


构造控制规则库
构造规则库主要是将UI控制中用户输入的字串信息转换为内部数据结构,存入规则库(注册表)中。具体实现在各个对话框类里。下边仅简单例举一下进程控制规则的构造。

该功能在对话框类CProcessRuleAddDlg里实现。关键代码如下:
代码:
void CProcessSettingDlg::OnBnClickedButtonAddrule()
{
    // TODO: 在此添加控件通知处理程序代码
    CProcessRuleAddDlg  dlg;
    dlg.m_bNewRule = TRUE;
    if( IDOK == dlg.DoModal())
    {
        //添加新规则
        if( false == RegAddProcessRule( dlg.m_ProcessPath.GetBuffer(),
                          dlg.m_bAllowed ))
        {
            AfxMessageBox(_T("该程序已存在!"));
            return;
        }
        UpdateRuleList();
    }
}
其中函数RegAddProcessRule( ProcessSettingDlg.cpp line 164)是对话框类中封装的用于向注册表添加进程规则的接口。

UI与驱动的通信

从图3.1的模块架构图中可以看出,UI模块与驱动通信有两种方式,一种是直接向驱动发送控制命令,另一种是间接通过注册表向驱动提供配置的规则信息。前边已经详细介绍过。这里着重介绍一下UI模块是怎样向驱动发送控制命令的。这样的命令集中在主控对话框里。下边是开启总监控的消息处理函数。
代码:
void CCactiWallDlg::OnBnClickedButtonStartMon)
{
    // TODO: 在此添加控件通知处理程序代码
    LSTATUS  status = ERROR_SUCCESS;
    DWORD    value = 0;
    DWORD    cbSize = sizeof( DWORD );
    DWORD    retLength = 0;
    BOOL     bOk = TRUE;
#if LOADDEVICE
    bOk = DeviceIoControl(m_hDevice,IOCTL_MONITOR_ON,NULL,0,NULL,0,&retLength,NULL);
    if( !bOk )
    {
        AfxMessageBox(_T("控制码发送失败!"),MB_OK | MB_ICONSTOP );
        return;
    }
#endif
  //以下代码修改注册表中关于软件运行状态的项目,此处略……
}
最关键的就是:
bOk = DeviceIoControl(m_hDevice,IOCTL_MONITOR_ON,NULL,0,NULL,0,&retLength,NULL);
其中m_hDevice是设备文件句柄(代表驱动),IOCTL_MONITOR_ON是控制码,定义在ctlcode.h,这个头文件在编译驱动的时候也会用到,驱动和UI必须保持一致。

你可能感兴趣的:(技术类,windows,防火墙)