NDIS/TDI网络过滤
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过滤驱动
passthru.c
: 这是 主模块,负责驱动的初始化、卸载和调度核心功能。它是整个驱动的入口点,协调协议和小端口部分的交互。
miniport.c
: 这是 小端口(Miniport)模块,负责模拟一个虚拟网卡,并处理底层数据传输。 拦截数据包发送
protocol.c
: 这是 协议(Protocol)模块,负责与上层协议驱动(如 TCP/IP 协议栈)交互。 拦截数据包接收
然后和我们之前学的创建设备对象,创建符号连接不同
这里开始使用的是NdisMRegisterDevice创建设备对象
NDIS_MINIPORT_CHARACTERISTICS MChars;
这个就是 miniport用来拦截发包的函数,为什么一个为空呢??
上面的英文翻译过来是:
应指定 Send 或 SendPackets 处理程序。如果指定了 SendPackets 处理程序,则忽略 SendHandler
在NDIS层,已经没有进程,IRP的概念了,那么这个包属于哪个进程??这个咋整
在这里,PtReceiveComplete
,PtTransferDataComplete
,PtReceive
这三回调函数是用来拦截收包的
因为没有实践,先研究到这了….如果用到NDIS驱动开发,还会继续往下更新
WFP网络过滤驱动
WFP框架的组成
WFP:Windows Filter Platform
WFP框架本身已经实现了过滤的功能,缺的只是规则
WFP结构大致长这样
WFP系统是有数据过滤引擎的防火墙,没有规则。
编写用户层的程序给WFP引擎设置规则
编写核心态的callout驱动处理WFP抓到的网络数据包做深度处理
WFP中几个重要的概念:
Layers(层):
正如上面的图片所示
WFP 中的层表示网络协议栈中不同的数据处理阶段,它们包括:
Application Layer Enforcement (ALE) 层
Stream 层
Transport 层
Datagram 层
网络接口层
NDIS层
层选择的依据
你选择的层主要取决于想要实现的功能:
- 控制特定应用的网络连接:
- 使用 ALE_AUTH_CONNECT 或 ALE_AUTH_RECV_ACCEPT。(ALE层)
- 检查和修改传输层数据(TCP/UDP 数据包):( Transport 层)
- 使用 FWPM_LAYER_TRANSPORT。
- 深度解析数据流(如 HTTP 或 TLS 内容):( Stream 层 )
- 使用 FWPM_LAYER_STREAM。
- 控制 UDP 数据报: ( Datagram 层 )
- 使用 FWPM_LAYER_DATAGRAM_DATA。
- 在网络接口上处理入站和出站的原始 IP 数据包。 ( 网络接口层 )
- FWPM_LAYER_INBOUND_IPPACKET_V4/V6 和 FWPM_LAYER_OUTBOUND_IPPACKET_V4/V6:
例如 FWPM_LAYER_ALE_AUTH_CONNECT_V4
是 应用层授权连接层(ALE Authorization Connect Layer) , 专门用于 IPv4 的网络连接请求。选择这一层,可以在网络连接建立之前拦截并检查连接请求,决定是否允许或阻止连接。
在 FWPM_LAYER_ALE_AUTH_CONNECT_V4
层的 ClassifyFn
回调函数中,可以访问以下关键信息:
源和目标 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]
)。
- 源 IP 地址(
源和目标端口:
源端口
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;
协议类型:
- 协议类型(如 TCP、UDP 等),通过
incomingPacket->incomingValues[FWPS_FIELD_ALE_AUTH_CONNECT_V4_IP_PROTOCOL]
获取。
- 协议类型(如 TCP、UDP 等),通过
进程信息:
- 发起连接的进程 ID(PID)。
inMetaValues->processId
- 进程路径和名称:
inMetaValues->processPath
- 发起连接的进程 ID(PID)。
用户信息:
- 发起连接的用户 SID(安全标识符)。
- 用户名称(可以通过 SID 进一步查询)。
连接方向:
- 连接是入站(Inbound)还是出站(Outbound)。
其他网络层信息:
- 网络接口信息(如接口索引)。
- 其他与连接相关的元数据。
一个 层 中可以有多个 子层
可以这样添加子层
1 |
|
SubLayer_A
和 SubLayer_B
分别表示防火墙和日志记录规则。
weight
决定了子层的优先级。
层的条件:
我们可以查看这个文档
每个筛选层 (Fwpmu.h) 可用的筛选条件 - Win32 apps | Microsoft Learn
每一层都可以有自己的筛选条件
例如FWPM_LAYER_OUTBOUND_TRANSPORT_V4
这一层,在fieldKey设置条件类型,通过查询文档,我们选择一个远程port的条件
1 | conditions[conditionIndex].fieldKey = FWPM_CONDITION_IP_REMOTE_PORT; |
再比如应用层ALE,可以看到这个ALE_APP_ID
就是PID
, ALE_USER_ID
就是指用户帐户ID
层的条件的使用会在Filters
展现
Filters:(过滤条件)
用于处理包的一些规则集合
一个FWPM_FILTER0 filter
可以设置多个
1 | FWPM_FILTER0 filter = {0}; |
这里说一下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_TERMINATING
和FWP_ACTION_CALLOUT_UNKNOWN
的使用示例:
FWP_ACTION_CALLOUT_TERMINATING
:终止行为
1 | // Callout 函数实现 |
FWP_ACTION_CALLOUT_UNKNOWN
:动态决策,继续检查其他过滤器
1 | // Callout 函数实现 |
然后我们要把Filter添加到过滤引擎
1 | //FWPM_action0结构指定在所有筛选条件都为真时所采取的操作 |
Shims:
类似于自来水过滤器里面的小垫片,可以解析网络堆栈中的流,包,事件,调用过滤引擎并根据规则评估数据,或者进一步调用callout,最后决定对包的处理
Callouts函数:
由驱动暴露出来的一组接口函数, 它本质上是一个结构,包含了过滤回调函数(例如 ClassifyFn
、NotifyFn
和 FlowDeleteFn
)。 负责进一步分析或者修改包。比如classify数据处理函数,可以返回一组permit(允许),block(阻止),continue(继续),defer(延迟),needmoredata(需要更多数据),Drop connection(丢弃连接) 等
先注册Callout
(FWPS)
1 | FWPS_CALLOUT sCallout = { 0 }; |
然后管理 WFP 过滤引擎(FWPM)
1 | FWPM_CALLOUT mCallout = { 0 }; |
这样,就可以把一个Callout添加到过滤引擎了
Classify:
利用规则过滤包的过程。将包的各种属性与规则中的conditions进行比较
定义Callouts需要生成一个GUID,具体可以用GUID Generator进行生成, 这是因为 GUID(Globally Unique Identifier) 提供了一种全局唯一的标识方式,用于标识和管理 Callouts。与普通函数不同,WFP 的架构需要 GUID 来支持复杂的注册、识别和调度机制。
注册callout -R0注册
首先创建一个FWPS_CALLOUT结构体
1 | typedef struct FWPS_CALLOUT { |
CLASSIFY_FN的结构
1 | FWPS_CALLOUT_CLASSIFY_FN2 FwpsCalloutClassifyFn2; |
其中 inFixedValues,inMetaValues 具体能拿到什么还是要看具体注册在哪一层,如果是注册在NDIS层,那肯定拿不到IP地址,端口地址等,但是能拿Mac地址等
因此我们就可以写一个简单的向用户询问是否同意连接的代码
1 | index = FWPS_FIELD_ALE_FLOW_ESTABLISHED_V4_IP_LOCAL_ADDRESS; |
注册样例长这样
1 | FWPS_CALLOUT sCallout = { 0 }; |
WFP工作的整体流程
以下是 WFP 驱动过滤的总结流程:
- 数据包进入网络协议栈:当一个网络数据包到达时,它会进入网络协议栈进行处理。
- 协议栈调用 Shim(中间层):网络协议栈会调用 Shim 层,Shim 是过滤过程中的关键参与者。
- Shim 调用分类过程:Shim 在指定的网络层上启动分类过程,对数据包进行过滤规则匹配。
- 分类过程中匹配过滤器并执行动作:在分类过程中,系统会将数据包与过滤规则进行匹配,并根据匹配结果采取相应的动作(类似于 TDI 的 quick filter() 功能)。
- 匹配到 Callout 过滤器时调用对应 Callout 函数:如果分类过程中匹配到了 Callout 过滤器,则会调用相应的 Callout 函数进行自定义处理。
- Shim 根据最终过滤决策执行动作:Shim 根据分类的最终决策(例如丢弃数据包、允许通过等)对数据包进行处理。
FWPM和FWPS
FWPM
(Filtering Platform Management)是 Windows Filtering Platform (WFP) 中用于 配置和管理过滤引擎 的 API 集合
FWPM
提供了一组 API,用于在 用户态 配置和管理 WFP 过滤引擎。它的功能包括但不限于:
- 管理 Callout:
- 添加、删除和枚举 Callout。
- 例如:
FwpmCalloutAdd
、FwpmCalloutDeleteByKey0
、FwpmCalloutEnum0
。
- 管理 Filter:
- 添加、删除和枚举 Filter。
- 例如:
FwpmFilterAdd
、FwpmFilterDeleteByKey0
、FwpmFilterEnum0
。
- 管理 Sublayer:
- 添加、删除和枚举 Sublayer。
- 例如:
FwpmSubLayerAdd
、FwpmSubLayerDeleteByKey0
、FwpmSubLayerEnum0
。
- 管理 Provider:
- 添加、删除和枚举 Provider。
- 例如:
FwpmProviderAdd
、FwpmProviderDeleteByKey0
、FwpmProviderEnum0
。
- 管理会话(Session):
- 打开、关闭和配置过滤引擎会话。
- 例如:
FwpmEngineOpen
、FwpmEngineClose
、FwpmTransactionBegin
,FwpmTransactionAbort
FWPS
(Filtering Platform Session)是 Windows Filtering Platform (WFP) 中用于 内核态 的 API 集合。它与 FWPM
(用户态 API)相对应,主要用于在内核态实现网络数据包的过滤和处理。
FWPS
的作用
FWPS
提供了一组 API,用于在内核态实现以下功能:
- 注册和管理 Callout:
- 注册 Callout,并实现分类函数(
ClassifyFn
)、通知函数(NotifyFn
)等。 - 例如:
FwpsCalloutRegister
、FwpsCalloutUnregisterById
。
- 注册 Callout,并实现分类函数(
- 处理网络数据包:
- 在 Callout 的分类函数中,检查数据包的元数据和内容,并决定是否允许、阻止或修改数据包。
- 注入数据包:
- 将修改后的数据包重新注入网络堆栈。
- 例如:
FwpsInjectMacSendAsync
、FwpsInjectMacReceiveAsync
。
- 管理网络流:
- 跟踪和管理网络流(Flow)的生命周期。
- 例如:
FwpsFlowAssociateContext
、FwpsFlowRemoveContext
。
设置filter
1 | FWPM_FILTER0 filter = {0}; |
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 和回调函数。- 回调函数(如
MyClassifyFn
和MyNotifyFn
)将在数据包分类和通知时被调用。
Filter 创建:
AddFilter
定义了一个 Filter,将其action.calloutKey
设置为 Callout 的 GUID,从而将两者关联。
事务管理:
- 通过
FwpmTransactionBegin
和FwpmTransactionCommit
确保 Callout 和 Filter 的注册操作在一次原子事务中完成。
调用链:
- 当网络层触发 Filter 时,WFP 会调用与该 Filter 关联的 Callout 的
MyClassifyFn
函数来处理数据包。
WFP过滤驱动实战
首先上来就一堆报错,参考以下文章进行解决
wfp 驱动编译报错 解决方案._wfp 编译失败-CSDN博客
1.在项目属性中设置预定义处理器宏
进入 **C/C++ -> 预处理器 -> 预处理器定义
**。 添加: NDIS60
或者NDIS620
2.检查头文件包含顺序
确保头文件的包含顺序如下:
1 | #include <fwpsk.h> |
WFP 和 NDIS 头文件的依赖性很高,包含错误顺序可能导致未定义的标识符错误。
另外这俩顺序也要注意
1 |
在配置WFP开发环境时需要在链接器选项卡中的附加依赖项中增加fwpkclnt.lib,uuid.lib
这两个库文件
从以下这篇文章,我们分析一下WFP是咋注册的
一个完整的WFP驱动(详细注释)_wfp驱动 iseqal-CSDN博客
首先我们要注册一下Callouts,并且只能在内核层进行注册Callouts
1 | NTSTATUS WallRegisterCallouts() |
注册Callout并启用的主要逻辑在这:
首先用FwpsCalloutRegister
注册Callout
然后用FWPM
Add好已经注册的Callout,然后添加Filter
1 | NTSTATUS RegisterCalloutForLayer |