NDIS网络过滤驱动

为什么选择NDIS驱动过滤?

如果是绑定在协议驱动,那比较麻烦,可能说要各个协议驱动都加上一个过滤驱动设备,才能监视到。协议驱动能截获接收到的包,但无法截获所有发送的包

仅拦截特定协议: 协议驱动只能拦截特定协议的数据包,例如 TCP 或 UDP 数据包。 它无法拦截其它协议的数据包,例如 ICMP 数据包。 这使得它无法实现对所有网络流量的全面监控和过滤。

但是这些协议驱动最终都要把数据发送到NDIS中间层驱动,所以我们在这里拦截能够拦截到所有的

NDIS组成

小端口(Miniport)是 NDIS 驱动架构中的一个关键组件。它是一个底层驱动,负责与具体的硬件设备(如网卡)交互,同时为上层协议驱动提供服务。

协议部分用于与上层协议驱动交互,接收上层数据并传递给小端口。

小端口部分则与底层驱动交互,负责将数据发送到网络或从网络接收数据。

为了让协议部分和小端口部分协同工作,必须通过 关联 的方式将它们绑定在一起。

通过调用 NdisIMAssociateMiniport 函数将协议部分(由 ProtHandle 标识)与小端口部分(由 DriverHandle 标识)关联。

我们要写的就是中间层驱动,夹在协议驱动和小端口驱动之间,这样可以在协议驱动这里拦截收包,在小端口这里拦截发包

NDIS DriverEntry:

**1.初始化包装句柄: **

函数:NdisMInitialzeWrapper

初始化 NDIS 驱动程序的上下文环境,分配内存和资源,为后续驱动注册和操作做准备。

2.注册小端口特征并获得DriverHandle:

函数:NdisIMRegisterLayeredMiniport

注册小端口特性,定义底层微型端口(Miniport)的功能和行为。

3.注册协议特征并获得ProtHandle:

函数:NdisRegisterProtocl

  • 注册协议部分特性,定义协议的行为,如接收数据包、发送数据包和协议初始化等。
  • 返回一个 ProtHandle,用于标识协议部分。

4.将DriverHandle与ProtHandle关联:

函数:NdisIMAssciateMiniport

作用

将小端口和协议部分进行关联,创建一个完整的中间驱动程序。

中间驱动通过该步骤实现对上下层驱动之间的数据流的完全控制。

5.收包处理(协议部分的PtReceive())

函数:PtReceive

作用:

协议部分的接收数据回调函数,当数据包到达时触发。

对接收的数据包进行处理、分析或修改。

6.发包处理(小端口部分的MpSend())

函数:MpSend

作用:

小端口部分的发送数据回调函数,当需要发送数据时触发。

中间驱动可以对发送数据包进行修改或拦截。

7.协议分析(接收,拒绝,修改)

接收数据分析

  • PtReceive 函数中,解析数据包协议头部。
  • 判断是否需要接受该数据包,或拦截数据包进行进一步处理。

拒绝数据

在协议部分,通过返回特定状态拒绝数据包。或直接丢弃数据包。

修改数据

对接收到的数据包内容(如 IP 地址、端口号等)进行修改后,传递给上层或下层。

tips:

IP地址是从IP头拿,端口是从TCP,UDP头拿

从项目看NDIS过滤驱动

GoodstudyChina/FireWall–NDIS

1736515562715

passthru.c : 这是 主模块,负责驱动的初始化、卸载和调度核心功能。它是整个驱动的入口点,协调协议和小端口部分的交互。

miniport.c: 这是 小端口(Miniport)模块,负责模拟一个虚拟网卡,并处理底层数据传输。 拦截数据包发送

protocol.c : 这是 协议(Protocol)模块,负责与上层协议驱动(如 TCP/IP 协议栈)交互。 拦截数据包接收

然后和我们之前学的创建设备对象,创建符号连接不同

这里开始使用的是NdisMRegisterDevice创建设备对象

NDIS_MINIPORT_CHARACTERISTICS MChars;

1736519632106

这个就是 miniport用来拦截发包的函数,为什么一个为空呢??

上面的英文翻译过来是:

应指定 Send 或 SendPackets 处理程序。如果指定了 SendPackets 处理程序,则忽略 SendHandler

在NDIS层,已经没有进程,IRP的概念了,那么这个包属于哪个进程??这个咋整

在这里,PtReceiveCompletePtTransferDataCompletePtReceive这三回调函数是用来拦截收包的

1736520271953

因为没有实践,先研究到这了….如果用到NDIS驱动开发,还会继续往下更新

WFP网络过滤驱动

WFP框架的组成

WFP:Windows Filter Platform

WFP框架本身已经实现了过滤的功能,缺的只是规则

WFP结构大致长这样

1736562698489

WFP系统是有数据过滤引擎的防火墙,没有规则。

编写用户层的程序给WFP引擎设置规则

编写核心态的callout驱动处理WFP抓到的网络数据包做深度处理

WFP中几个重要的概念:

Layers(层):

正如上面的图片所示

WFP 中的层表示网络协议栈中不同的数据处理阶段,它们包括:

Application Layer Enforcement (ALE)

Stream 层

Transport 层

Datagram 层

网络接口层

NDIS层

层选择的依据

你选择的层主要取决于想要实现的功能:

  1. 控制特定应用的网络连接:
    • 使用 ALE_AUTH_CONNECTALE_AUTH_RECV_ACCEPT。(ALE层)
  2. 检查和修改传输层数据(TCP/UDP 数据包):( Transport 层)
    • 使用 FWPM_LAYER_TRANSPORT
  3. 深度解析数据流(如 HTTP 或 TLS 内容):( Stream 层 )
    • 使用 FWPM_LAYER_STREAM
  4. 控制 UDP 数据报: ( Datagram 层 )
    • 使用 FWPM_LAYER_DATAGRAM_DATA
  5. 在网络接口上处理入站和出站的原始 IP 数据包。 ( 网络接口层 )
  • FWPM_LAYER_INBOUND_IPPACKET_V4/V6FWPM_LAYER_OUTBOUND_IPPACKET_V4/V6:

例如 FWPM_LAYER_ALE_AUTH_CONNECT_V4应用层授权连接层(ALE Authorization Connect Layer) , 专门用于 IPv4 的网络连接请求。选择这一层,可以在网络连接建立之前拦截并检查连接请求,决定是否允许或阻止连接。

FWPM_LAYER_ALE_AUTH_CONNECT_V4 层的 ClassifyFn 回调函数中,可以访问以下关键信息:

  1. 源和目标 IP 地址:

    • 源 IP 地址(incomingPacket->incomingValues[FWPS_FIELD_ALE_AUTH_CONNECT_V4_IP_LOCAL_ADDRESS])。
    • 目标 IP 地址(incomingPacket->incomingValues[FWPS_FIELD_ALE_AUTH_CONNECT_V4_IP_REMOTE_ADDRESS])。
  2. 源和目标端口:

    • 源端口LocalIp = inFixedValues->incomingValue[FWPS_FIELD_ALE_AUTH_CONNECT_V4_IP_LOCAL_ADDRESS].value.uint32;

    • 目标端口inFixedValues->incomingValue[FWPS_FIELD_ALE_AUTH_CONNECT_V4_IP_REMOTE_ADDRESS].value.uint32;

  3. 协议类型:

    • 协议类型(如 TCP、UDP 等),通过 incomingPacket->incomingValues[FWPS_FIELD_ALE_AUTH_CONNECT_V4_IP_PROTOCOL] 获取。
  4. 进程信息:

    • 发起连接的进程 ID(PID)。inMetaValues->processId
    • 进程路径和名称:inMetaValues->processPath
  5. 用户信息:

    • 发起连接的用户 SID(安全标识符)。
    • 用户名称(可以通过 SID 进一步查询)。
  6. 连接方向:

    • 连接是入站(Inbound)还是出站(Outbound)。
  7. 其他网络层信息:

    • 网络接口信息(如接口索引)。
    • 其他与连接相关的元数据。

一个 中可以有多个 子层

可以这样添加子层

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
#include <fwpmu.h>

// 定义子层 GUID
const GUID SubLayer_A = { /* GUID for SubLayer A */ };
const GUID SubLayer_B = { /* GUID for SubLayer B */ };

// 添加子层
void AddSubLayers(HANDLE engineHandle) {
FWPM_SUBLAYER0 subLayerA = {0};
subLayerA.subLayerKey = SubLayer_A;
subLayerA.displayData.name = L"SubLayer A - Firewall Rules";
subLayerA.displayData.description = L"Handles firewall-related rules";
subLayerA.flags = 0;
subLayerA.weight = 100; // 优先级高

FWPM_SUBLAYER0 subLayerB = {0};
subLayerB.subLayerKey = SubLayer_B;
subLayerB.displayData.name = L"SubLayer B - Logging Rules";
subLayerB.displayData.description = L"Handles logging-related rules";
subLayerB.flags = 0;
subLayerB.weight = 200; // 优先级低

FwpmSubLayerAdd0(engineHandle, &subLayerA, NULL);
FwpmSubLayerAdd0(engineHandle, &subLayerB, NULL);
}

SubLayer_ASubLayer_B 分别表示防火墙和日志记录规则。

weight 决定了子层的优先级。

层的条件:

我们可以查看这个文档

每个筛选层 (Fwpmu.h) 可用的筛选条件 - Win32 apps | Microsoft Learn

每一层都可以有自己的筛选条件

例如FWPM_LAYER_OUTBOUND_TRANSPORT_V4这一层,在fieldKey设置条件类型,通过查询文档,我们选择一个远程port的条件

1736597791366

1
2
3
4
conditions[conditionIndex].fieldKey = FWPM_CONDITION_IP_REMOTE_PORT;
conditions[conditionIndex].matchType = FWP_MATCH_EQUAL;
conditions[conditionIndex].conditionValue.type = FWP_UINT16;
conditions[conditionIndex].conditionValue.uint16 = 80;

1736597913163

再比如应用层ALE,可以看到这个ALE_APP_ID就是PID, ALE_USER_ID就是指用户帐户ID

层的条件的使用会在Filters展现

Filters:(过滤条件)

用于处理包的一些规则集合

一个FWPM_FILTER0 filter可以设置多个

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
FWPM_FILTER0 filter = {0};
FWPM_FILTER_CONDITION0 filterConditions[3] = {0};
UINT conditionIndex;

// 设置 Filter 的层
filter.layerKey = *layerKey;

// 设置 Filter 的名称和描述
filter.displayData.name = (wchar_t*)filterName;
filter.displayData.description = (wchar_t*)filterDesc;

// 设置 Filter 的动作为调用 Callout 并终止处理
filter.action.type = FWP_ACTION_CALLOUT_TERMINATING;
filter.action.calloutKey = *calloutKey;

// 设置 Filter 的条件
filter.filterCondition = filterConditions;

// 设置 Filter 的子层
filter.subLayerKey = DD_PROXY_SUBLAYER;

// 设置 Filter 的权重为自动分配
filter.weight.type = FWP_EMPTY; // auto-weight

// 设置 Filter 的上下文
filter.rawContext = context;

// 设置第一个条件:远程端口等于指定值
conditionIndex = 0;
filterConditions[conditionIndex].fieldKey = FWPM_CONDITION_IP_REMOTE_PORT;
filterConditions[conditionIndex].matchType = FWP_MATCH_EQUAL;
filterConditions[conditionIndex].conditionValue.type = FWP_UINT16;
filterConditions[conditionIndex].conditionValue.uint16 = remotePort;
conditionIndex++;

// 设置 Filter 的条件数量
filter.numFilterConditions = conditionIndex;

// 添加 Filter 到过滤引擎
status = FwpmFilterAdd0(gEngineHandle, &filter, NULL, NULL);

这里说一下FWPM_FILTER0结构里面的action.type

从微软官方文档可以查询到:

含义
FWP_ACTION_BLOCK 阻止流量。0x00000001 |FWP_ACTION_FLAG_TERMINATING
FWP_ACTION_PERMIT 允许流量。0x00000002 |FWP_ACTION_FLAG_TERMINATING
FWP_ACTION_CALLOUT_TERMINATING 调用始终返回块或许可的标注。0x00000003 |FWP_ACTION_FLAG_CALLOUT |FWP_ACTION_FLAG_TERMINATING
FWP_ACTION_CALLOUT_INSPECTION 调用从不返回块或允许的标注。0x00000004 |FWP_ACTION_FLAG_CALLOUT |FWP_ACTION_FLAG_NON_TERMINATING
FWP_ACTION_CALLOUT_UNKNOWN 调用可返回块或允许的标注。0x00000005 |FWP_ACTION_FLAG_CALLOUT
动作类型 是否调用 Callout 是否终止 描述
FWP_ACTION_BLOCK 直接阻止流量
FWP_ACTION_PERMIT 直接允许流量。
FWP_ACTION_CALLOUT_TERMINATING 调用 Callout 函数,Callout 返回决定(允许/阻止),并停止进一步匹配过滤器。(只会匹配一个 Filter)
FWP_ACTION_CALLOUT_INSPECTION 调用 Callout 函数,Callout 只能观察流量,不改变流量行为,匹配后继续检查其他过滤器。(主要用于观察用途)
FWP_ACTION_CALLOUT_UNKNOWN 不确定 调用 Callout 函数,Callout 返回决定(允许/阻止),不明确后续动作,使用时需明确后果。

FWP_ACTION_CALLOUT_TERMINATINGFWP_ACTION_CALLOUT_UNKNOWN的使用示例:

FWP_ACTION_CALLOUT_TERMINATING:终止行为

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// Callout 函数实现
NTSTATUS MyClassifyFn(
const FWPS_INCOMING_VALUES0* inFixedValues,
const FWPS_INCOMING_METADATA_VALUES0* inMetaValues,
void* layerData,
const void* classifyContext,
const FWPS_FILTER0* filter,
UINT64 flowContext,
FWPS_CLASSIFY_OUT0* classifyOut
)
{
// 检查源 IP 地址是否匹配
FWP_VALUE0 srcIP = inFixedValues->incomingValue[FWPS_FIELD_OUTBOUND_TRANSPORT_V4_IP_LOCAL_ADDRESS].value;
FWP_VALUE0 destPort = inFixedValues->incomingValue[FWPS_FIELD_OUTBOUND_TRANSPORT_V4_IP_REMOTE_PORT].value;

if (srcIP.uint32 == targetIP && destPort.uint16 == 80) {
classifyOut->actionType = FWP_ACTION_BLOCK; // 阻止流量
} else {
classifyOut->actionType = FWP_ACTION_PERMIT; // 允许流量
}
classifyOut->rights &= ~FWPS_RIGHT_ACTION_WRITE; // 表示 Callout 决定了最终行为
return STATUS_SUCCESS;
}

FWP_ACTION_CALLOUT_UNKNOWN :动态决策,继续检查其他过滤器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// Callout 函数实现
NTSTATUS MyClassifyFn(
const FWPS_INCOMING_VALUES0* inFixedValues,
const FWPS_INCOMING_METADATA_VALUES0* inMetaValues,
void* layerData,
const void* classifyContext,
const FWPS_FILTER0* filter,
UINT64 flowContext,
FWPS_CLASSIFY_OUT0* classifyOut
)
{
// 检查流量的应用程序
if (IsTargetApplication(inFixedValues)) {
LogPacketInfo(layerData); // 记录数据包信息
}

// 不决定流量行为,继续交给其他过滤器
classifyOut->actionType = FWP_ACTION_CONTINUE;
return STATUS_SUCCESS;
}

然后我们要把Filter添加到过滤引擎

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
//FWPM_action0结构指定在所有筛选条件都为真时所采取的操作
mFilter.action.calloutKey = *calloutKey;
//交给callout处理 由callout来决定返回阻止(block)或者允许(permit)
mFilter.action.type = FWP_ACTION_CALLOUT_TERMINATING;

//描述
mFilter.displayData.name = L"WFP TEST";
mFilter.displayData.description = L"yxp's WFP TEST";


mFilter.layerKey = *layerKey; //这个值和FwpmCalloutAdd里面使用的值一样 过滤层标识符 此过滤层允许授权对传出tcp连接的连接请求,
//以及基于发送的第一个数据包授权传出非tcp通信量

//过滤条件数
mFilter.numFilterConditions = 0;
//过滤条件
mFilter.filterCondition = mFilter_condition;

mFilter.subLayerKey = FWPM_SUBLAYER_UNIVERSAL; //此子层承载所有未分配给任何其他子层的筛选器

//BFE将根据过滤条件自动分配权重
mFilter.weight.type = FWP_EMPTY;

//添加一个新的过滤器对象到系统中
status = FwpmFilterAdd(gEngineHandle, &mFilter, NULL, filterId);
if (!NT_SUCCESS(status))
goto exit;

Shims:

类似于自来水过滤器里面的小垫片,可以解析网络堆栈中的流,包,事件,调用过滤引擎并根据规则评估数据,或者进一步调用callout,最后决定对包的处理

Callouts函数:

由驱动暴露出来的一组接口函数, 它本质上是一个结构,包含了过滤回调函数(例如 ClassifyFnNotifyFnFlowDeleteFn)。 负责进一步分析或者修改包。比如classify数据处理函数,可以返回一组permit(允许),block(阻止),continue(继续),defer(延迟),needmoredata(需要更多数据),Drop connection(丢弃连接) 等

先注册Callout (FWPS)

1
2
3
4
5
6
7
8
9
10
11
12
FWPS_CALLOUT    sCallout = { 0 };
//这里未指定sCallout.flags
sCallout.calloutKey = *calloutKey; //一个唯一的GUID值,用来标记此callout
sCallout.classifyFn = classifyFn; //每当回调处理网络数据时,过滤器引擎将调用此函数,处理网络数据在这个回调函数里面处理.
sCallout.flowDeleteFn = flowDeleteNotifyFn; //每当终止由回调处理的数据流时,筛选器引擎将调用此函数
sCallout.notifyFn = notifyFn; //Callout 注册时的初始通知,筛选器动态绑定到 Callout,筛选器被动态删除,卸载callout时,会调用这个回调

//要使用哪个设备对象注册
status = FwpsCalloutRegister(gDevObj, //自己创建的设备对象
&sCallout, //该结构体有用的是指定了classifyFn回调,处理连接时的数据就会用到这个回调函数.
calloutId //返回的calloutID ,取消注册callout会用到这个值
);

然后管理 WFP 过滤引擎(FWPM)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
FWPM_CALLOUT    mCallout = { 0 };
FWPM_DISPLAY_DATA mDispData = { 0 };

mDispData.name = L"WFP TEST"; //可选的名字
mDispData.description = L"yxp's WFP TEST"; //可选的描述
//你感兴趣的内容
mCallout.applicableLayer = *layerKey; //过滤层标识符 此过滤层允许授权对传出tcp连接的连接请求,
//以及基于发送的第一个数据包授权传出非tcp通信量

//你感兴趣的内容的GUID
mCallout.calloutKey = *calloutKey; //该GUID值必须与FwpsCalloutRegister注册时的GUID值相同.
mCallout.displayData = mDispData;

//向过滤引擎添加callout
status = FwpmCalloutAdd(gEngineHandle,
&mCallout, //flags没有设置
NULL, //安全描述符 可以为NULL
NULL); //返回一个ID 与FwpsCalloutRegister函数返回的ID是相同的.

这样,就可以把一个Callout添加到过滤引擎了

Classify:

利用规则过滤包的过程。将包的各种属性与规则中的conditions进行比较

定义Callouts需要生成一个GUID,具体可以用GUID Generator进行生成, 这是因为 GUID(Globally Unique Identifier) 提供了一种全局唯一的标识方式,用于标识和管理 Callouts。与普通函数不同,WFP 的架构需要 GUID 来支持复杂的注册、识别和调度机制。

注册callout -R0注册

首先创建一个FWPS_CALLOUT结构体

1
2
3
4
5
6
7
typedef struct FWPS_CALLOUT {
GUID calloutKey; //标注驱动程序定义的 GUID ,用于唯一标识标注。
UINT32 flags;//指定特定于标注的参数的标志
FWPS_CALLOUT_CLASSIFY_FN classifyFn; //指向标注驱动程序的 分类Fn2 标注函数的指针。 每当有网络数据要由标注处理时,筛选器引擎都会调用此函数
FWPS_CALLOUT_NOTIFY_FN notifyFn;//指向标注驱动程序的 notifyFn 函数的指针。 筛选器引擎调用此函数以通知标注驱动程序与标注关联的事件。
FWPS_CALLOUT_FLOW_DELETE_NOTIFY_FN flowDeleteFn;//指向标注驱动程序的 flowDeleteFn 标注函数的指针。 每当由标注处理的数据流终止时,筛选器引擎就会调用此函数。
} FWPS_CALLOUT;

CLASSIFY_FN的结构

1
2
3
4
5
6
7
8
9
10
11
FWPS_CALLOUT_CLASSIFY_FN2 FwpsCalloutClassifyFn2;

void FwpsCalloutClassifyFn2(
[in] const FWPS_INCOMING_VALUES0 *inFixedValues,//WFP传进来的本层特有的数据,如端口,IP地址(看具体哪一层)
[in] const FWPS_INCOMING_METADATA_VALUES0 *inMetaValues,//本层相关的扩展数据,如PID和路径
[in, out, optional] void *layerData,//指向原始数据的指针,rawData=(PNET_BUFFER_LIST)layerData
[in, optional] const void *classifyContext,//指向筛选器引擎与标注驱动程序关联的上下文数据的指针。
[in] const FWPS_FILTER2 *filter,//指向 FWPS_FILTER2 结构的指针。 此结构描述指定筛选器操作的标注的筛选器。
[in] UINT64 flowContext,//包含与数据流关联的上下文的 UINT64 类型变量。 如果没有上下文与数据流关联,则此参数为零。
[in, out] FWPS_CLASSIFY_OUT0 *classifyOut//指向 FWPS_CLASSIFY_OUT0 结构的指针,允许或者阻止等
)

其中 inFixedValues,inMetaValues 具体能拿到什么还是要看具体注册在哪一层,如果是注册在NDIS层,那肯定拿不到IP地址,端口地址等,但是能拿Mac地址等

因此我们就可以写一个简单的向用户询问是否同意连接的代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
index = FWPS_FIELD_ALE_FLOW_ESTABLISHED_V4_IP_LOCAL_ADDRESS;
LocalIPADDRv4 = inFixedValues->incomingValue[index].value.uint32;

index = FWPS_FIELD_ALE_FLOW_ESTABLISHED_V4_IP_LOCAL_PORT;
LocalPort = inFixedValues->incomingValue[index].value.uint16;

index = FWPS_FIELD_ALE_FLOW_ESTABLISHED_V4_IP_REMOTE_ADDRESS;
remoteIPADDRv4 = inFixedValues->incomingValue[index].value.uint32;

index = FWPS_FIELD_ALE_FLOW_ESTABLISHED_V4_IP_REMOTE_PORT;
remotePort = inFixedValues->incomingValue[index].value.uint16;

DbgPrint("BaseTDI:LocalIP%bx LocalPort %d\n remoteIP %bx remotePort %d", LocalIPADDRv4, LocalPort, remoteIPADDRv4, remotePort);

DbgPrint("BaseTDI:PID%d,PID'sPATH%s", inMetaValues->processId, inMetaValues->processPath->data);

if (monitoringEnabled) {
// 访问规则代码,在这里通知用户态程序
AskUser(LocalIP, LocalPort, remoteIP, remotePort, PID);

if (允许) {
classifyOut->actionType = FWP_ACTION_PERMIT; // 允许发送或接收
} else {
classifyOut->actionType = FWP_ACTION_BLOCK; // 不允许发送或接收
}
}

注册样例长这样

1
2
3
4
5
6
7
FWPS_CALLOUT    sCallout = { 0 };

//这里未指定sCallout.flags
sCallout.calloutKey = *calloutKey; //一个唯一的GUID值,用来标记此callout
sCallout.classifyFn = (FWPS_CALLOUT_CLASSIFY_FN3)classifyFn; //每当回调处理网络数据时,过滤器引擎将调用此函数,处理网络数据在这个回调函数里面处理.
sCallout.notifyFn = (FWPS_CALLOUT_NOTIFY_FN3)notifyFn;//卸载callout时,会调用这个回调
sCallout.flowDeleteFn = (FWPS_CALLOUT_FLOW_DELETE_NOTIFY_FN)flowDeleteNotifyFn; //每当终止由回调处理的数据流时,筛选器引擎将调用此函数

WFP工作的整体流程

以下是 WFP 驱动过滤的总结流程:

  1. 数据包进入网络协议栈:当一个网络数据包到达时,它会进入网络协议栈进行处理。
  2. 协议栈调用 Shim(中间层):网络协议栈会调用 Shim 层,Shim 是过滤过程中的关键参与者。
  3. Shim 调用分类过程:Shim 在指定的网络层上启动分类过程,对数据包进行过滤规则匹配。
  4. 分类过程中匹配过滤器并执行动作:在分类过程中,系统会将数据包与过滤规则进行匹配,并根据匹配结果采取相应的动作(类似于 TDI 的 quick filter() 功能)。
  5. 匹配到 Callout 过滤器时调用对应 Callout 函数:如果分类过程中匹配到了 Callout 过滤器,则会调用相应的 Callout 函数进行自定义处理。
  6. Shim 根据最终过滤决策执行动作:Shim 根据分类的最终决策(例如丢弃数据包、允许通过等)对数据包进行处理。

FWPM和FWPS

FWPM(Filtering Platform Management)是 Windows Filtering Platform (WFP) 中用于 配置和管理过滤引擎 的 API 集合

FWPM 提供了一组 API,用于在 用户态 配置和管理 WFP 过滤引擎。它的功能包括但不限于:

  • 管理 Callout
    • 添加、删除和枚举 Callout。
    • 例如:FwpmCalloutAddFwpmCalloutDeleteByKey0FwpmCalloutEnum0
  • 管理 Filter
    • 添加、删除和枚举 Filter。
    • 例如:FwpmFilterAddFwpmFilterDeleteByKey0FwpmFilterEnum0
  • 管理 Sublayer
    • 添加、删除和枚举 Sublayer。
    • 例如:FwpmSubLayerAddFwpmSubLayerDeleteByKey0FwpmSubLayerEnum0
  • 管理 Provider
    • 添加、删除和枚举 Provider。
    • 例如:FwpmProviderAddFwpmProviderDeleteByKey0FwpmProviderEnum0
  • 管理会话(Session)
    • 打开、关闭和配置过滤引擎会话。
    • 例如:FwpmEngineOpenFwpmEngineCloseFwpmTransactionBeginFwpmTransactionAbort

FWPS

(Filtering Platform Session)是 Windows Filtering Platform (WFP) 中用于 内核态 的 API 集合。它与 FWPM(用户态 API)相对应,主要用于在内核态实现网络数据包的过滤和处理。

FWPS 的作用

FWPS 提供了一组 API,用于在内核态实现以下功能:

  • 注册和管理 Callout
    • 注册 Callout,并实现分类函数(ClassifyFn)、通知函数(NotifyFn)等。
    • 例如:FwpsCalloutRegisterFwpsCalloutUnregisterById
  • 处理网络数据包
    • 在 Callout 的分类函数中,检查数据包的元数据和内容,并决定是否允许、阻止或修改数据包。
  • 注入数据包
    • 将修改后的数据包重新注入网络堆栈。
    • 例如:FwpsInjectMacSendAsyncFwpsInjectMacReceiveAsync
  • 管理网络流
    • 跟踪和管理网络流(Flow)的生命周期。
    • 例如:FwpsFlowAssociateContextFwpsFlowRemoveContext

设置filter

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
FWPM_FILTER0 filter = {0};
FWPM_FILTER_CONDITION0 filterConditions[3] = {0};
UINT conditionIndex;

// 设置 Filter 的层
filter.layerKey = *layerKey;

// 设置 Filter 的名称和描述
filter.displayData.name = (wchar_t*)filterName;
filter.displayData.description = (wchar_t*)filterDesc;

// 设置 Filter 的动作为调用 Callout 并终止处理
filter.action.type = FWP_ACTION_CALLOUT_TERMINATING;
filter.action.calloutKey = *calloutKey;

// 设置 Filter 的条件
filter.filterCondition = filterConditions;

// 设置 Filter 的子层
filter.subLayerKey = DD_PROXY_SUBLAYER;

// 设置 Filter 的权重为自动分配
filter.weight.type = FWP_EMPTY; // auto-weight

// 设置 Filter 的上下文
filter.rawContext = context;

// 设置第一个条件:远程端口等于指定值
conditionIndex = 0;
filterConditions[conditionIndex].fieldKey = FWPM_CONDITION_IP_REMOTE_PORT;
filterConditions[conditionIndex].matchType = FWP_MATCH_EQUAL;
filterConditions[conditionIndex].conditionValue.type = FWP_UINT16;
filterConditions[conditionIndex].conditionValue.uint16 = remotePort;
conditionIndex++;

// 设置 Filter 的条件数量
filter.numFilterConditions = conditionIndex;

// 添加 Filter 到过滤引擎
status = FwpmFilterAdd0(gEngineHandle, &filter, NULL, NULL);

4. 层和子层的区别总结

特性 层(Layer) 子层(Sublayer)
定义 网络协议栈中的一个特定阶段 层内的逻辑分组
作用 决定 Filter 的作用范围 组织和管理 Filter 的执行顺序
数量 固定的,由 WFP 预定义 可自定义,一个层可以包含多个子层
顺序 按照协议栈的顺序处理 按照权重顺序处理
示例 FWPM_LAYER_ALE_AUTH_CONNECT_V4 用户自定义的子层(如 DD_PROXY_SUBLAYER
权重 通过权重决定子层的执行顺序

可以多次调用Callout注册

同一个 Callout 可以被多个 Filter 关联。 在这种情况下,MyClassifyFn 会被多次调用,每次调用时 filter 参数会指向不同的 Filter。

callout与filter的绑定流程

Callout 注册:

  • RegisterCallout 注册 Callout,并指定 GUID 和回调函数。
  • 回调函数(如 MyClassifyFnMyNotifyFn)将在数据包分类和通知时被调用。

Filter 创建:

  • AddFilter 定义了一个 Filter,将其 action.calloutKey 设置为 Callout 的 GUID,从而将两者关联。

事务管理:

  • 通过 FwpmTransactionBeginFwpmTransactionCommit 确保 Callout 和 Filter 的注册操作在一次原子事务中完成。

调用链:

  • 当网络层触发 Filter 时,WFP 会调用与该 Filter 关联的 Callout 的 MyClassifyFn 函数来处理数据包。

WFP过滤驱动实战

首先上来就一堆报错,参考以下文章进行解决

wfp 驱动编译报错 解决方案._wfp 编译失败-CSDN博客

1.在项目属性中设置预定义处理器宏

进入 **C/C++ -> 预处理器 -> 预处理器定义**。 添加: NDIS60或者NDIS620

2.检查头文件包含顺序

确保头文件的包含顺序如下:

1
2
3
#include <fwpsk.h>
#include <ndis.h>
#include <fwpmk.h>

WFP 和 NDIS 头文件的依赖性很高,包含错误顺序可能导致未定义的标识符错误。

另外这俩顺序也要注意

1
2
#include <ntifs.h>
#include <ntddk.h>

在配置WFP开发环境时需要在链接器选项卡中的附加依赖项中增加fwpkclnt.lib,uuid.lib这两个库文件

1736650516335

从以下这篇文章,我们分析一下WFP是咋注册的

一个完整的WFP驱动(详细注释)_wfp驱动 iseqal-CSDN博客

首先我们要注册一下Callouts,并且只能在内核层进行注册Callouts

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
NTSTATUS WallRegisterCallouts()
{
NTSTATUS status = STATUS_SUCCESS;
BOOLEAN bInTransaction = FALSE;
BOOLEAN bEngineOpened = FALSE;
FWPM_SESSION session = { 0 };

//置此标志时,会话结束时将自动删除在会话期间添加的任何对象
session.flags = FWPM_SESSION_FLAG_DYNAMIC;

//开启WFP引擎,获得一个引擎句柄
status = FwpmEngineOpen(NULL,
RPC_C_AUTHN_WINNT,
NULL,
&session,
&gEngineHandle);
if (!NT_SUCCESS(status))
goto exit;
bEngineOpened = TRUE;
// 在当前会话中开始显式事务
status = FwpmTransactionBegin(gEngineHandle, 0);
if (!NT_SUCCESS(status))
goto exit;
bInTransaction = TRUE;
//注册回调函数
status = RegisterCalloutForLayer(
&FWPM_LAYER_ALE_AUTH_CONNECT_V4, //此过滤层允许授权对传出tcp连接的连接请求,
//以及基于发送的第一个数据包授权传出非tcp通信量
&GUID_ALE_AUTH_CONNECT_CALLOUT_V4,
WallALEConnectClassify,
(FWPS_CALLOUT_NOTIFY_FN)WallNotifyFn,
WallFlowDeleteFn,
&gAleConnectCalloutId,
&gAleConnectFilterId);
if (!NT_SUCCESS(status))
{
DbgPrint("RegisterCalloutForLayer失败!\n");
goto exit;
}
//确认所有内容并提交,让回调函数正式发挥作用
status = FwpmTransactionCommit(gEngineHandle);
if (!NT_SUCCESS(status))
goto exit;
bInTransaction = FALSE;
exit:
if (!NT_SUCCESS(status))
{
if (bInTransaction)
{
FwpmTransactionAbort(gEngineHandle);
}
if (bEngineOpened)
{
FwpmEngineClose(gEngineHandle);
gEngineHandle = 0;
}
}
return status;
}

注册Callout并启用的主要逻辑在这:

首先用FwpsCalloutRegister注册Callout

然后用FWPM Add好已经注册的Callout,然后添加Filter

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
NTSTATUS RegisterCalloutForLayer
(
IN const GUID* layerKey,
IN const GUID* calloutKey,
IN FWPS_CALLOUT_CLASSIFY_FN classifyFn,
IN FWPS_CALLOUT_NOTIFY_FN notifyFn,
IN FWPS_CALLOUT_FLOW_DELETE_NOTIFY_FN flowDeleteNotifyFn,
OUT UINT32* calloutId, //注册callout时返回此ID
OUT UINT64* filterId
)
{
NTSTATUS status = STATUS_SUCCESS;
FWPS_CALLOUT sCallout = { 0 };
FWPM_FILTER mFilter = { 0 };
FWPM_FILTER_CONDITION mFilter_condition[1] = { 0 };
FWPM_CALLOUT mCallout = { 0 };
FWPM_DISPLAY_DATA mDispData = { 0 };
BOOLEAN bCalloutRegistered = FALSE;

//这里未指定sCallout.flags
sCallout.calloutKey = *calloutKey; //一个唯一的GUID值,用来标记此callout
sCallout.classifyFn = classifyFn; //每当回调处理网络数据时,过滤器引擎将调用此函数,处理网络数据在这个回调函数里面处理.
sCallout.flowDeleteFn = flowDeleteNotifyFn; //每当终止由回调处理的数据流时,筛选器引擎将调用此函数
sCallout.notifyFn = notifyFn; //Callout 注册时的初始通知,筛选器动态绑定到 Callout,筛选器被动态删除,卸载callout时,会调用这个回调

//要使用哪个设备对象注册
status = FwpsCalloutRegister(gDevObj, //自己创建的设备对象
&sCallout, //该结构体有用的是指定了classifyFn回调,处理连接时的数据就会用到这个回调函数.
calloutId //返回的calloutID ,取消注册callout会用到这个值
);
if (!NT_SUCCESS(status))
goto exit;

bCalloutRegistered = TRUE;


mDispData.name = L"WFP TEST"; //可选的名字
mDispData.description = L"yxp's WFP TEST"; //可选的描述
//你感兴趣的内容
mCallout.applicableLayer = *layerKey; //过滤层标识符 此过滤层允许授权对传出tcp连接的连接请求,
//以及基于发送的第一个数据包授权传出非tcp通信量

//你感兴趣的内容的GUID
mCallout.calloutKey = *calloutKey; //该GUID值必须与FwpsCalloutRegister注册时的GUID值相同.
mCallout.displayData = mDispData;

//向过滤引擎添加callout
status = FwpmCalloutAdd(gEngineHandle,
&mCallout, //flags没有设置
NULL, //安全描述符 可以为NULL
NULL); //返回一个ID 与FwpsCalloutRegister函数返回的ID是相同的.
if (!NT_SUCCESS(status))
goto exit;


//FWPM_action0结构指定在所有筛选条件都为真时所采取的操作
mFilter.action.calloutKey = *calloutKey;
//交给callout处理 由callout来决定返回阻止(block)或者允许(permit)
mFilter.action.type = FWP_ACTION_CALLOUT_TERMINATING;

//描述
mFilter.displayData.name = L"WFP TEST";
mFilter.displayData.description = L"yxp's WFP TEST";


mFilter.layerKey = *layerKey; //这个值和FwpmCalloutAdd里面使用的值一样 过滤层标识符 此过滤层允许授权对传出tcp连接的连接请求,
//以及基于发送的第一个数据包授权传出非tcp通信量

//过滤条件数
mFilter.numFilterConditions = 0;
//过滤条件
mFilter.filterCondition = mFilter_condition;

mFilter.subLayerKey = FWPM_SUBLAYER_UNIVERSAL; //此子层承载所有未分配给任何其他子层的筛选器

//BFE将根据过滤条件自动分配权重
mFilter.weight.type = FWP_EMPTY;

//添加一个新的过滤器对象到系统中
status = FwpmFilterAdd(gEngineHandle, &mFilter, NULL, filterId);
if (!NT_SUCCESS(status))
goto exit;
exit:
if (!NT_SUCCESS(status))
{
if (bCalloutRegistered)
{
FwpsCalloutUnregisterById(*calloutId);
}
}
return status;
}