简介:面向Windows 2000/XP平台驱动级防火墙开发的学习者,这份资料提供了内核网络过滤与访问控制的具体代码实现,解决了早期系统环境下防火墙核心模块从零搭建缺乏参考的问题。压缩包内共5个文件,包含两个核心源码工程(分别对应过滤驱动与防火墙钩子),另附CHM帮助文档、HTM说明页与文本说明,整体仅122KB,轻量紧凑,便于快速下载和查阅。目前已有73人学习下载。两个源码工程演示了数据包拦截、规则匹配、驱动与应用层通信等关键环节,通过精简代码帮助理解过滤引擎与钩子的协作方式,可为理解早期系统防火墙工作原理提供直观示例;配套帮助文档涵盖编译步骤与使用方法,能帮助读者按图索骥完成代码编译和功能验证。资料目录结构清晰,聚焦功能核心而非冗余代码,适合有一定Windows驱动基础、进行课程设计、逆向分析或二次开发的工程师参考。
1. Windows 2000/XP 防火墙源码包:一套能编译能上机的过滤驱动骨架
前两年我从一位老开发手里接管了模拟项目X,主题是 Windows 2000/XP 下开发防火墙。说起来是一份课程设计规格,可里面真正值钱的是 DrvFltIp 和 FirewallFHK 两套源码,外加一份 CHM 格式的使用说明。很多人拿到手看一眼 CHM 就搁置了,以为只是抄来的演示工程,其实它把 NDIS 中间层过滤和 TDI 层钩子两条技术路径都点到了。做网络驱动开发的从业者拿它入门非常合适——你要找的不是能直接商用的成品,而是一套看得见内核过滤链路、能编译、能加载、能改规则的样板。这篇笔记我会按拆包顺序讲清楚它怎么落地,以及最容易翻车的位置。
2. 两套源码的技术骨架:DrvFltIp 与 FirewallFHK 各负责哪一段
2.1 解压后先分清四个组成部分
把压缩包解开后,第一眼别急着看代码,先按文件用途分个类。资源包里大致由四块组成:CHM 格式的使用说明、readme.htm 概述页、DrvFltIp 源码压缩包、FirewallFHK 源码压缩包,外加一个来自某代码分享站点的导出文本。最后那个文本基本只有下载页的存档价值,没有代码含量,可以直接放一边。
我当时的处理顺序是:先看 readme.htm 了解整体设计意图,再翻 CHM 里的编译与安装章节,最后才碰两份源码。readme 里明确写了这套防火墙不是单驱动方案,而是“NDIS 包过滤 + TDI 连接过滤的复合体”,这一点直接解释了为什么要有两套源码,而不是一个大而全的工程。
下面这张表可以帮你建立阅读优先级:
| 文件 | 作用 | 阅读优先级 |
|---|---|---|
| 使用说明.CHM | 编译环境、加载顺序、常见错误 | 高 |
| readme.htm | 模块划分、调用关系、设计思路 | 中 |
| DrvFltIp_source.zip | NDIS 中间层驱动,负责 IP 包级过滤 | 高 |
| FirewallFHK_src.zip | TDI 钩子驱动,负责连接级控制 | 高 |
| 站点导出文本 | 下载来源记录,无代码价值 | 低 |
两个源码包的目录结构都不复杂,DrvFltIp 里就是经典的驱动入口文件、NDIS 回调文件、IOCTL 处理文件加一个 SOURCES 构建脚本;FirewallFHK 则是 TDI 过滤设备对象、钩子分发函数和连接表管理。没有生产工程里那些复杂的构建自动化,反而适合拿来逐行读。
2.2 NDIS 中间层过滤与 TDI 钩子的分工
要理解这套源码,得先搞清楚 Windows 2000/XP 时代做个人防火墙的两条主流路线。当年还没有后来的 WFP 框架,开发者想介入网络数据流,基本上只能二选一,或者像这套源码一样两套都做。
第一类是 NDIS 中间层驱动。它挂在协议驱动和网卡小端口驱动之间,能拿到层面最原始的帧数据。网卡收上来的包会先经过中间层,协议栈要发出去的包也会经过这里。优点是视野完整,链路层帧头都在,缺点是在这个位置做判断代价高,每一个包都要过一遍回调逻辑,处理不好直接影响收发性能。DrvFltIp 干的正是这件事:注册协议回调,在 IP 包进出时按规则放行或丢弃。
第二类是 TDI 钩子。TDI 是传输驱动接口,Winsock 的底层调用最终会经过这里。所谓钩子,就是在 TDI 客户端和传输驱动之间插一层拦截逻辑,重点观察的是连接建立、断开这类连接级事件,而不是逐包过滤。它的优点是逻辑简单,不需要解析 IP 头,适合做基于进程或端口的访问控制;缺点也很明显,只能管住本机协议栈主动发起的连接,对伪造源地址的报文基本没有防御力。
FirewallFHK 走的就是这条 TDI 路线。它把自己挂到连接创建的必经之路上,每个 TCP 连接请求在真正发出之前都会先过它的检查函数。如果规则不允许,它直接返回失败,应用层拿到的结果就像“连接被拒绝”。这样的设计在现代角度看偏粗放,但对于 2000/XP 时代的个人防火墙已经够用。
2.3 从 CHM 使用说明里挖出加载顺序
CHM 文档里最有价值的不是功能列表,而是驱动加载顺序。它明确建议先加载 FirewallFHK,再加载 DrvFltIp。原因是 DrvFltIp 作为 NDIS 中间层驱动,在初始化协议注册时,可能会受到 TDI 层已有过滤设备的影响。虽然两者理论上可以独立工作,但按照文档推荐的顺序能避免很多莫名其妙的初始化失败。
文档里还区分了 Free Build 和 Checked Build 两种编译模式。Free Build 是发布版,适合正常使用;Checked Build 是调试版,会保留大量断言和调试输出。如果你要跟踪过滤逻辑,必须用 Checked Build 重新编译一遍,否则源码里的 DbgPrint 基本不会出现在你预期的输出位置。
这里有个容易翻车的细节:文档里给的 NDIS 版本宏示例,直接复制过去编译会报错,因为 DDK 版本差异导致头文件中部分宏的声明位置不同。这种现象在老驱动源码里很常见,不必怀疑代码本身有问题,按后面的避坑清单调整即可。
2.4 两条调用链的入口和交点
读这套源码时,我习惯先追 DriverEntry,再往下走分发函数。DrvFltIp 的入口函数做了三件关键的事:调用协议注册接口把自己挂入网络栈、创建设备对象供应用层打开、初始化 IOCTL 分发表。之后每当有包到达,NDIS 库会回调 PtReceive 或 PtSend 对应的处理函数。
FirewallFHK 的入口则不同,它要找到 TDI 客户端注册操作的位置,把自己的过滤操作表插入到连接建立流程之前。插入成功之后,所有 TDI 连接创建请求都会先经过它的检查函数。两条链路在中间层并不直接通信,它们各自独立完成过滤判断,互不干扰。这也是这套设计的一个优点:包过滤层和连接过滤层可以分别调试,定位问题时不用两边同时打断点。
从这里可以看出,这套源码包的定位很明确:它是教学级的防火墙内核框架,不是功能完整的商业产品。有了这个认知,接下来编译和加载才不会走偏。
3. 编译与加载:从 DDK 环境搭建到驱动服务上线
3.1 工具链与版本匹配
编译这套源码,首先要解决的是环境版本问题。它面向的是 2000/XP 时代的 DDK,我当时用的是 Windows XP 对应的 DDK 版本,配合 VC6 的编译器环境。源码包里没有 .sln 或 .dsp 这类 IDE 工程文件,只有 .c、.h 和 makefile,说明设计者默认你使用命令行 build 工具。
不建议用太新的 WDK 去硬编这套老代码,新版头文件里的结构体定义和老代码的预期类型差距较大,改起来得不偿失。如果你的机器上只有新版 WDK,也可以用,但要做好修改源代码的准备,主要改动集中在 NDIS 版本宏和 TDI 相关结构体的兼容声明上。
DDK 安装完成后,开始菜单里会出现“Build Environments”一组快捷方式,分别对应不同系统和构建类型。我一般会打开两个命令行窗口,一个用于 Free Build,一个用于 Checked Build,避免来回切换时环境变量互相污染。这是后续一切编译工作的基础。
3.2 用 Free Build 环境编译两个包
在 Free Build 环境下编译 DrvFltIp,命令很直接:
cd /d D:\fw_dev\DrvFltIp build -cZ-c 参数表示清空之前的中间文件重新编译,Z 是让编译在遇到警告时不至于中断第一次构建。初次编译时建议保留这两个参数,能减少旧缓存文件带来的干扰。编译完成后,输出文件会出现在 objfre_wxp_x86 之类的目录下,文件名是 drvfltip.sys。
FirewallFHK 的编译同理:
cd /d D:\fw_dev\FirewallFHK build -cZ如果你调试版也想要一份,就切到 Checked Build 环境再执行一次。需要注意,两次 build 会生成不同的输出目录,一个是 objfre 前缀,一个是 objchk 前缀,复制文件时不要拿错版本。加载到系统里的 .sys 文件,debug 版和 release 版不建议混用,它们是不同的构建产物,行为会有差异。
编译过程中最常见的错误是 NDIS 头文件版本不匹配,表现为某些宏未定义。这是老树根问题,根源在于 DDK 版本与源码预期不一致,按第 4 章的补充宏方案处理即可。
3.3 生成服务并启动驱动
驱动编译产物需要复制到系统驱动目录,再注册成内核服务,才能被系统加载。在 2000/XP 上不强制要求驱动签名,直接用命令行操作就行。复制和注册的典型操作如下:
copy /Y drvfltip.sys C:\Windows\system32\drivers\ sc create DrvFltIp type= kernel binPath= C:\Windows\system32\drivers\drvfltip.sys sc start DrvFltIpFirewallFHK 是同样的套路:
copy /Y FirewallFHK.sys C:\Windows\system32\drivers\ sc create FwFhk type= kernel binPath= C:\Windows\system32\drivers\FirewallFHK.sys sc start FwFhksc create 里的 type= kernel 表示这是一个内核驱动服务,binPath 必须指定到实际存在的 .sys 路径。start 成功后会返回“服务已启动”,但如果驱动初始化失败,sc start 会报错,甚至直接触发蓝屏。所以第一次加载驱动时,务必在开发测试机上做,不要在生产环境直接试。
3.4 开机自动启动与应用层发现
做成演示版本,可以设置为开机自动加载,命令是改服务的 start 参数:
sc config DrvFltIp start= auto sc config FwFhk start= auto加载顺序仍然建议 FwFhk 在前。如果两个服务都设为自动,系统启动时不保证顺序,这会导致 DrvFltIp 初始化时遇到一个还不存在的 TDI 过滤环境,首次加载失败。最常见的处理方式是在 DrvFltIp 的 DriverEntry 里加一个等待逻辑,或者干脆不依赖自动启动,由应用层在启动后主动拉起两个服务。
用户态要打开控制设备,使用 CreateFile 访问设备路径即可。设备名在驱动源码里定义,一般是 \.\DrvFltIp 这类符号链接。设备对象创建成功,说明驱动至少已经跑到了 IoCreateDevice 之后,过滤回调链是否工作还要进一步的规则验证。
到这里,驱动的编译、安装、启动链路就通了。下一步要解决的是运行时出现的各种异常,这部分我单独拿出来讲。
4. 避坑清单:蓝屏、规则不生效与残留服务的五个案例
4.1 加载驱动后系统蓝屏
现象:驱动编译成功,复制到 drivers 目录,重启系统后蓝屏,错误码类似 0x0000007E。
原因:这个问题我排查了很久,最后定位到头文件版本混用。项目里同时引用了多套 NDIS 头文件,编译器预处理时,部分宏被解释成了旧版定义,导致 Ndis 注册协议的内部调用约定不一致。驱动代码本身没有逻辑错误,是环境把代码编译扭曲了。
解决:彻底清理中间文件,切换到一个明确的 DDK 环境变量上下文,重新执行 build -cZ。重点检查 sources 文件里是否有重复的 include 路径,有的话删除冗余项。
4.2 编译时报“未知 NDIS 以太网类型常量”
现象:编译过程中报某个以太网协议类型常量未定义,定位到的是 IP 包判断代码,但代码本身明显没有拼写错误。
原因:DDK 版本较低,头文件里没有包含后来补充的协议类型常量定义。老 SDK 里的 ntddndis.h 只覆盖了基础协议类型,IPv4 这种延伸常量需要手动声明。
解决:在源码顶部手动补充宏定义:
#ifndef NDIS_ETH_TYPE_IPV4 #define NDIS_ETH_TYPE_IPV4 0x0800 #endif同时把该宏通过 sources 文件的 C_DEFINES 字段传给其他编译单元,避免多个 .c 文件分别报错。
4.3 规则配置正确但过滤不生效
现象:应用层配置了一条规则,明确要阻止某个 TCP 端口入站,但实际对外发起连接仍然成功。
原因:过滤回调挂错了方向。DrvFltIp 的 PtReceive 和 PtSend 是两个独立入口,如果规则只在 PtSend 逻辑里判断了方向,那么入站包根本不会走到这条检查路径。FirewallFHK 里的连接方向判断也是一样的道理。
解决:给规则结构增加方向字段,入站检查走 PtReceive 分支,出站走 PtSend 分支。检查逻辑顺序要放在包数据被提交给上层协议之前,否则即使放行与否判断正确,包可能已经进入了协议栈处理,拦截就没有意义了。
4.4 内核调试输出看不到
现象:驱动里写了 DbgPrint,但在调试器里完全看不到输出,也不报错。
原因:默认情况下,2000/XP 系统的内核调试输出是需要开启的,光写 DbgPrint 不保证能被调试器捕获。系统级的调试输出过滤条件默认不满足,日志内容被吞掉了。
解决:在驱动加载前,开启内核调试输出捕获,或者使用 DbgPrintEx 指定输出级别,并在调试器端设置过滤掩码。最省事的办法是用 Checked Build 版本,因为它默认的 printf 类输出策略比 Free Build 宽松,更容易定位问题。
4.5 卸载后提示“服务已被标记删除”
现象:驱动卸载流程执行完毕后,再次注册同一个服务名时报错,提示服务已存在或被标记删除。
原因:驱动卸载例程里做了 IoDeleteDevice 和 IoDeleteSymbolicLink,但服务注册表信息还残留在系统中。服务控制管理器认为该名字已被占用,导致后续重新创建失败。
解决:卸载服务时先停止再删除:
sc stop DrvFltIp sc delete DrvFltIp如果已经被标记删除,重启一次系统再清理注册表对应键值。不要用暴力工具强删服务项,否则下次驱动加载时设备对象可能因为名称残留创建失败,反而触发蓝屏。从那以后,我每次卸载驱动都固定走 stop + delete 这条标准流程,不再贪图省事直接删文件。
5. 进阶一环:给规则表加上用户态控制通道与验证口径
5.1 设备控制通道
想让这套驱动真正可用,至少要给它加一个规则下发通道。通用做法是走 IOCTL,驱动端实现一个控制设备,用户态用 DeviceIoControl 下发规则。规则结构体大致是这样:
typedef struct _FW_RULE { ULONG dir; // 1=入站,2=出站 ULONG protocol; // 6=TCP,17=UDP ULONG port; // 目标端口 ULONG action; // 0=放行,1=丢弃 } FW_RULE;用户态下发:
HANDLE h = CreateFile("\\\\.\\DrvFltIp", GENERIC_READ | GENERIC_WRITE, 0, NULL, OPEN_EXISTING, 0, NULL); DeviceIoControl(h, IOCTL_FW_ADD_RULE, &rule, sizeof(rule), NULL, 0, &ret, NULL);驱动端对应的 IOCTL 分支:
case IOCTL_FW_ADD_RULE: pRule = (PFW_RULE)Irp->AssociatedIrp.SystemBuffer; status = FwRuleInsert(pRule); break;DeviceIoControl 的缓冲区模型分为几种,这里建议用 METHOD_BUFFERED。因为这种模式下输入输出都从 SystemBuffer 取,省去双向映射的缓冲区管理,对新手最友好。如果定义 IOCTL 时用了 METHOD_IN_DIRECT,还要同步设置用户态缓冲区锁页,又是另一层复杂度。
5.2 规则持久化与重启恢复
驱动重启后规则表清零,这是必然现象,因为规则存在内核内存里。不少受这套源码启发的人会想直接在驱动里写注册表保存规则,实际做起来风险比收益大:驱动挂载期间写入注册表,遇到断电或崩溃很容易造成键值损坏,而且驱动代码里做 INI/注册表操作,出问题后定位成本远高于应用层。
我习惯的做法是应用层单独保存规则副本,每次启动时先把驱动服务拉起来,再通过 IOCTL 逐条下发。驱动只保留运行时状态,重启后从干净状态开始,避免历史脏数据干扰过滤逻辑。这对课程设计级的项目已经足够。
5.3 验证口径
引擎是否生效,只看驱动加载成功还不够。我常用的验证闭环是四步:确认驱动版本、下发一条规则、从对端发包、检查计数器变化。
驱动里维护一个简单的过滤计数器,每次拦截一个包就递增。用户态通过查询指令取回计数,就能确认规则是否真的在路径上生效。没有内核调试器时,这个计数口径能帮你快速判断“规则没进表”和“规则进了表但不触发”两种完全不同的问题。
从那以后,我每次引入新过滤规则都强制走一遍确认驱动版本、下发规则、端到端发包、查计数的闭环,确保策略没有错位。希望这套拆包思路能帮到你,少在加载和回调方向上栽不必要的跟头。
本文还有配套的精品资源,点击获取