13、网络过滤与防火墙:TDI过滤、WFP框架、网络数据包拦截、连接监控、应用层协议识别
网络过滤,说白了就是给操作系统装个「安检门」。数据包进来出去,都得过一遍你的代码。我早年做安全产品时,这块踩过的坑能写满一个笔记本。今天咱们就聊聊Windows下的网络过滤技术——从老牌的TDI到现在的WFP,再到实际的数据包拦截和协议识别。
13.1 TDI过滤:老司机才懂的往事
TDI(Transport Driver Interface)是Windows 2000/XP时代的产物。那时候WFP还没出生,大家做防火墙全靠TDI过滤。嗯,现在新项目基本不用它了,但维护老系统时你可能会碰到。
TDI过滤的核心思路是:在传输层驱动和设备之间插入一层。你拦截IRP(I/O请求包),检查里面的连接信息。
核心要点:TDI过滤只能看到传输层信息(IP、端口、协议类型),看不到应用层数据。说白了,它只能管「谁连谁」,管不了「发了什么内容」。
我当年接手过一个遗留项目,用的就是TDI过滤。那代码写得……嗯,一言难尽。最头疼的是TDI版本兼容问题——Windows 2000和XP的TDI行为不完全一样,稍不注意就蓝屏。
// TDI过滤中拦截连接请求的典型代码片段
NTSTATUS TdiFilterDispatchCreate(PDEVICE_OBJECT DeviceObject, PIRP Irp) {
PIO_STACK_LOCATION irpSp = IoGetCurrentIrpStackLocation(Irp);
PFILE_FULL_EA_INFORMATION eaInfo = NULL;
// 获取扩展属性,里面包含连接信息
if (irpSp->Parameters.Create.EaLength > 0) {
eaInfo = (PFILE_FULL_EA_INFORMATION)irpSp->Parameters.Create.EaBuffer;
// 解析EA,提取目标IP和端口
// 这里省略具体解析逻辑
}
// 检查是否允许连接
if (!IsConnectionAllowed(eaInfo)) {
Irp->IoStatus.Status = STATUS_ACCESS_DENIED;
IoCompleteRequest(Irp, IO_NO_INCREMENT);
return STATUS_ACCESS_DENIED;
}
// 放行,交给下层驱动
return IoCallDriver(DeviceObject->AttachedDevice, Irp);
}
避坑指南:我曾经在TDI过滤中忘记处理IRP的完成例程,结果导致系统频繁蓝屏。记住:拦截IRP后,要么自己完成它,要么传给下层,千万别「吞掉」IRP不处理。
13.2 WFP框架:现代Windows网络过滤的标准答案
从Windows Vista开始,微软推出了WFP(Windows Filtering Platform)。这玩意儿比TDI强太多了。它把网络栈分成了多个层(Layer),你可以在任意层注册回调函数,拦截、修改、放行数据包。
WFP的架构,我画个图你就明白了:
WFP的层是分级的。你可以在ALE层拦截连接,在传输层拦截数据包,在网络层拦截IP包。我个人习惯在ALE层做连接监控,在传输层做数据包内容检查。
13.3 网络数据包拦截:从注册回调开始
WFP拦截数据包,核心就两步:注册回调、处理数据包。回调函数里你会拿到一个FWPS_INCOMING_VALUES结构,里面装着数据包的各种信息。
// WFP数据包拦截回调函数
NTSTATUS WfpClassifyCallback(
const FWPS_INCOMING_VALUES* inFixedValues,
const FWPS_INCOMING_METADATA_VALUES* inMetaValues,
void* layerData,
const void* classifyContext,
const FWPS_FILTER* filter,
UINT64 flowContext,
FWPS_CLASSIFY_OUT* classifyOut
) {
// 获取远程IP和端口
UINT32 remoteAddr = inFixedValues->incomingValue[
FWPS_FIELD_ALE_AUTH_CONNECT_V4_REMOTE_ADDRESS
].value.uint32;
UINT16 remotePort = inFixedValues->incomingValue[
FWPS_FIELD_ALE_AUTH_CONNECT_V4_REMOTE_PORT
].value.uint16;
// 检查是否在黑名单中
if (IsBlacklisted(remoteAddr, remotePort)) {
classifyOut->actionType = FWP_ACTION_BLOCK;
return STATUS_SUCCESS;
}
// 放行
classifyOut->actionType = FWP_ACTION_PERMIT;
return STATUS_SUCCESS;
}
个人经验:WFP回调里不要做耗时操作。我曾经在回调里查数据库,结果网络延迟飙升到几百毫秒。正确的做法是:回调里只做快速判断,复杂逻辑扔到工作线程去处理。
13.4 连接监控:谁在连谁,一目了然
连接监控说白了就是记录「谁(进程)在什么时间连了谁(IP:端口)」。WFP的ALE层提供了FWPS_FIELD_ALE_AUTH_CONNECT_V4_ALE_APP_ID字段,可以拿到进程ID。
| 监控字段 | 含义 | 获取方式 |
|---|---|---|
| 本地IP/端口 | 本机绑定的地址 | FWPS_FIELD_ALE_AUTH_CONNECT_V4_LOCAL_* |
| 远程IP/端口 | 目标地址 | FWPS_FIELD_ALE_AUTH_CONNECT_V4_REMOTE_* |
| 进程ID | 发起连接的进程 | FWPS_FIELD_ALE_AUTH_CONNECT_V4_ALE_APP_ID |
| 协议类型 | TCP/UDP | FWPS_FIELD_ALE_AUTH_CONNECT_V4_IP_PROTOCOL |
你想想看,有了这些信息,做个网络连接日志工具是不是很简单?我做过一个企业级监控工具,就是靠这些字段记录所有外连请求,然后上报给管理端。
13.5 应用层协议识别:不止看端口
很多人以为识别协议就是看端口——80是HTTP,443是HTTPS。嗯,太天真了。现在很多应用都用非标准端口,或者端口复用。真正的协议识别得看数据包内容。
WFP在传输层可以拿到数据包负载。你可以在回调里检查前几个字节,判断协议特征。
// 简单的HTTP协议识别
BOOLEAN IsHttpPacket(const UCHAR* payload, ULONG length) {
if (length < 4) return FALSE;
// HTTP请求以 GET/POST/PUT 等开头
if (memcmp(payload, "GET ", 4) == 0 ||
memcmp(payload, "POST", 4) == 0 ||
memcmp(payload, "PUT ", 4) == 0) {
return TRUE;
}
// HTTP响应以 HTTP/ 开头
if (memcmp(payload, "HTTP", 4) == 0) {
return TRUE;
}
return FALSE;
}
注意:协议识别不能只看前几个字节。有些协议会加密(比如HTTPS),你看到的全是乱码。这时候只能靠端口、TLS握手特征等间接判断。
我遇到过最坑的情况是:某个软件用HTTP协议但端口是8080,另一个软件用自定义协议也用8080。光看端口根本分不清。后来我加了深度包检测(DPI),检查HTTP头部的Host字段,才把两者区分开。
13.6 性能优化:别让过滤成为瓶颈
网络过滤最怕什么?性能问题。你想想看,每个数据包都要过你的回调,稍微慢一点,整个系统的网络吞吐就下来了。
我总结了几条优化经验:
- 快速路径优先:大部分数据包应该直接放行,只有可疑的才深入检查
- 避免内存分配:回调里不要用
ExAllocatePool,预分配好缓冲区 - 使用读写锁:黑名单等配置数据用
ERESOURCE保护,别用全局锁 - 批量处理:日志记录攒一批再写,别每条连接都写一次磁盘
血的教训:我曾经在WFP回调里打印调试日志,结果导致系统网络吞吐从1Gbps掉到100Mbps。记住:发布版本里绝对不要有DbgPrint,那玩意儿慢得要命。
13.7 实战:搭建一个简单的连接防火墙
说了这么多,咱们动手写个简单的。这个防火墙只做一件事:拦截所有发往某个IP段的连接。
// 注册WFP过滤器
NTSTATUS RegisterBlockFilter(HANDLE engineHandle) {
FWPM_FILTER filter = {0};
FWPM_FILTER_CONDITION cond[1] = {0};
// 条件:远程IP在192.168.1.0/24段
cond[0].fieldKey = FWPS_FIELD_ALE_AUTH_CONNECT_V4_REMOTE_ADDRESS;
cond[0].matchType = FWP_MATCH_RANGE;
cond[0].conditionValue.rangeValue->valueLow.uint32 = 0xC0A80100; // 192.168.1.0
cond[0].conditionValue.rangeValue->valueHigh.uint32 = 0xC0A801FF; // 192.168.1.255
filter.layerKey = FWPM_LAYER_ALE_AUTH_CONNECT_V4;
filter.action.type = FWP_ACTION_BLOCK;
filter.filterCondition = cond;
filter.numFilterConditions = 1;
filter.displayData.name = L"Block 192.168.1.x";
return FwpmFilterAdd(engineHandle, &filter, NULL, NULL);
}
这段代码注册了一个过滤器,所有发往192.168.1.x网段的TCP连接都会被拦截。实际产品中,你肯定需要动态管理规则,而不是硬编码。
好了,网络过滤这块内容不少,但核心思路就这些。从TDI到WFP,从数据包拦截到协议识别,每一步都有坑,但也都有解法。做驱动开发就是这样——细节决定成败。