一个简单基于WFP模型的防火墙设计实现
一年前,为了练习英文文档阅读水平,也为了自己有个用的,就在假期凭借着WDK的help documents写了这个玩意儿出来。碰巧与毕业设计挂上了,所以没有及时放出,惭愧。现在毕业了,发出来和大家分享交流。为了高效阅读,文档中的字句是我摘出来的,去掉了一些多余的废话!当然了,这依然是入门作品。大牛们飘过吧。
至于怎么编译安装,请不要问我这个问题,如果用心了,自然会编译安装。祝大家学习快乐。
好好学习,天天向上!
Windows平台下基于WFP模型的网络防火墙设计实现
模块设计与划分
本项目实现的具体功能
(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,在发布版本中,此模块不被编译。
控制规则设计
代码:
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规则
网络驱动模块的核心功能实现
代码:
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与驱动的交互
代码:
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必须保持一致。