Windows7以上使用WFP驱动框架实现IP数据包截取(一)

 

WFP(Windows Filtering Platform)驱动框架,也许很多人都不熟悉,然而提到 TDI 驱动,可能许多人都熟悉。
WFP是在WIN7以上系统中TDI 的替代框架,提供了更加强大的内核网络数据包的过滤,拦截,修改等诸多功能。
其实在很早的一篇文章介绍win7的内核网络驱动框架时候,简单提到了WFP。
详见WIN7系统内核网络堆栈实现简述 - 信易达 - 博客园 (cnblogs.com)

 

这篇文章较详细介绍WFP,并且设计一个利用WFP框架截获所有的IP层数据包到应用层来处理的驱动。
这种驱动可以在应用层分析处理所有的IP网络数据包,
要做应用层的防火墙,以及细化到进程和单个IP或端口的流量监控与拦截,以及NAT, VPN都是非常方便的,
因为大部分的数据分析处理都在应用层来完成。

 

WFP(Windows Filtering Platform)顾名思义,是过滤层框架,它工作在内核的 TCP/IP协议栈的全部层。
介绍WFP之前,先看看 WIndows中 TCP/IP协议栈中的数据包是如何流动的。


首先从应用层程序开始,应用层程序使用 BSD socket建立socket套接字,以TCP协议为例,最简单的通讯:

int socket= socket(AF_INET,SOCK_STREAM,0);
connect( socket,server_addr...);
send(socket, ...)
recv(socket, ...);

socket网络通讯进入内核层之后,首先进入winsock辅助功能驱动程序(afd.sys),
afd.sys驱动程序承上启下的管理应用层套接字(比如套接字的文件句柄,对象等信息)和下层的通讯层,
比如TDI, TLNPI 都是afd.sys的客户。
我们知道,传输层网际层核心部分是 tcpip.sys 驱动干的事情。
但是 从afd.sys 到 tcpip.sys驱动之间还有一个驱动接口负责承上启下。
在WINXP年代,这个承上启下的工作的驱动程序就是 TDI.sys。
而到了 WIN7以后的系统。由于内核网络结构做了全新设计,这个工作就交给了 TLNPI(Transport Layer Network Provider Interface),
这是一个没有文档化的接口,就是接口细节除了微软自己,别人并不了解。
然而为了能在 win7以上系统中使用TDI的功能,微软提供了一个叫 TDX.sys的驱动,它实现了 TLNPI的接口功能跟tcpip.sys通讯,
并且TDX创建了跟WINXP的TDI兼容的各种设备对象,比如\Device\TCP, \Device\Udp等等。
当windows内核检测到我们创建了 TDI的过滤驱动,上层的全部socket通讯就会自动路由到TDX.sys驱动,
于是TDX.sys驱动解析处理古老的TDI请求包,翻译转化成TLNPI请求跟 tcpip.sys通讯。
这就是为什么 WIN7以上平台甚至最新的WIN10平台都能完美支持古老的TDI的原因,但是神知道哪天微软一不高兴会把TDX给砍掉了。
tcpip.sys主要负责传输层网络层数据包的处理,到了下层就是NDIS层。
NDIS又分成 NDIS协议驱动,NDIS中间驱动, NDSI小端口驱动(就是网卡驱动) 三层。
tcpip.sys相对于NDIS来说就是NDIS的协议驱动。

 

因此,可以把windows内核网络流向大致如下分层:

WINSOCK辅助功能驱动(afd.sys)
|
TLNPI(传输层网络传输提供者接口。为了兼容TDI,提供了TDX驱动; 这一层同时提供WSK内核套接字)
|
TCPIP(tcpip.sys ,核心处理网络数据包驱动,包括传输层(TCP,UDP)和网际层(IP))
|
NDIS (NDIS驱动层,具体可以分成三层(NDIS协议驱动,NDIS中间驱动,NDIS小端口驱动),
最终通过NDIS小端口驱动也就是网卡驱动发送数据包到物理网络中)

其实这种分层也是按照TCPIP协议的标准四层协议来划分的:
应用层和afd.sys以及TLNPI相当于 TCPIP的应用层,
tcpip.sys包含了 TCPIP的传输层和网际层,
NDIS则是TCPIP的链路层。

这里似乎没WFP没什么事,但是刚才说了, WFP是过滤层,它挂载到整个内核网络通讯协议栈的全部层中,
也就是上边的 AFD, TLNPI 有WFP框架的影子,在这个层中 WFP 负责处理应用层数据包,
以及一些特殊控制比如connect控制,accept控制,listen控制,bind控制等。在这一层中,因为跟应用层程序关联紧密,
因此这一层都能知道是哪个进程发起了网络通讯操作。

在TCPIP.sys,也有WFP影子,在tcpip.sys,主要负责传输层和网际层的数据包的拦截控制,
这一层(其实是两层:传输层和网际层),数据包的处理已经脱离了具体进程,因此是不能获取到是哪些进程发起或接受的数据包。

而在 WIN8.1以上的系统 WFP也能过滤到网卡数据包,也就是说在WIN8.1以上系统中,WFP还掺和进去NDIS驱动层。

 

因此根据TCPIP通讯协议栈来分, WFP框架也分成三层固定过滤层:

一,A)数据流层(Stream Data)接收应用层的原始数据包,已经去掉个TCPIP各种头信息)
B)各种bind,connect,accept,listen等控制信息层(ALE) .
如果划分细致点,可以把他们当成两层来理解。

二,传输层, 接收发送的TCP,UDP数据包,已经包含 TCP,UDP等头

三, 网际层, 接收发送IP数据包, 已经包含IP头信息。

WIN8.1以上系统包含第四层:
四:链路层,包含以太网卡MAC地址的链路层数据包。

 

可以看到,WFP 的固定的过滤层, 都全部渗透到了TCPIP的四层协议栈中。
因此WFP可以对TCPIP协议栈进行全方位的过滤拦截。功能是非常全面和强大的,这是WINXP的TDI所无法比拟的!
Linux平台有类似的netfilter,但是它是工作在IP层,过滤拦截的都是IP包,比起 WFP也有逊色。

WFP的每个固定过滤层都挂载到TCPIP协议栈对应层,可以简单理解是在TCPIP栈的每层HOOK了一个“钩子函数”。
但是这个 ”钩子函数“ 还可以运行许多 ”子钩子函数“,从而构成一个丰富复杂的WFP过滤系统。

WFP的每个固定层包含了许多固定过滤点,也可以说成是过滤子层,简单的说,就是每层中可以设置许多不同类型的固定的 ”子钩子函数“。
比如 Stream Data 层,可以设置 FWPM_LAYER_STREAM_V4(V6) 类型的钩子函数。
(V6是IP协议的第6版本,就是 IPV6,每个固定过滤点都有对应的V6版本,下文为了简单省去 V6 )。
这样应用层调用send,sendto等函数发送的数据内容就会在 FWPM_LAYER_STREAM_V4设置的钩子函数中截获到。
同样从物理网络层接收到的数据经过层层剥离最后只剩下应用层数据内容,也会进入 FWPM_LAYER_STREAM_V4 设置的钩子函数中。

 

再比如ALE层,这一层包含的固定过滤点有点多。
比如应用层程序调用 bind函数来绑定到本地地址,WFP的ALE层就会对应有两个 固定过滤点调用:
1, FWPM_LAYER_ALE_BIND_REDIRECT_V4,这个是 在绑定前,允许你自己修改绑定的本地地址,在win7以上系统支持。
2, FWPM_LAYER_ALE_RESOURCE_ASSIGNMENT_V4, 分配本地资源认证,
就是你可以允许绑定,或者不允许,不允许的话,bind返回失败。

再如connect函数调用,也会对应两个固定过滤点:
1, FWPM_LAYER_ALE_CONNECT_REDIRECT_V4,在连接前,允许你修改连接的目的地址,
这个功能非常适合用来做代理,win7以上支持。
2,FWPM_LAYER_ALE_AUTH_CONNECT_V4, 连接认证,同上,不允许的话,connect函数失败。

listen,accept等函数都有对应的ALE固定过滤点,这里就不一一列举。

 

FWPM_LAYER_ALE_CONNECT_REDIRECT_V4 这个过滤点,对我们实现代理服务器非常有用。
就是我们可以在这个过滤点的钩子函数中,把连接的目的地址设置成我们服务器的IP地址,
这样应用层所有的网络请求都会转发到我们的代理服务器。这就达到了代理的效果。
自然这里还需要解决一个技术问题:就是代理服务器还需要知道我们最终连接的哪个IP地址。
可以借鉴 SOCKS5 代理协议的做法,在真正传输数据前,先告诉代理服务器我们需要连接的IP地址。

但是在FWPM_LAYER_ALE_CONNECT_REDIRECT_V4 钩子函数中还没真正跟代理服务器建立起连接,
于是先把传输的目的IP地址等信息记录下来。再挂载FWPM_LAYER_STREAM_V4 过滤点。
对于TCP,第一个发送数据包时候,在头部增加一段数据记录目的IP等信息。这样代理服务器端接收到第一个数据包就能知道朝哪里连接。
(然而这种做法似乎对连接建立后,服务端首先发送数据包给客户端的TCP连接会有些问题,
具体可以再想其他解决办法,估计还是没使用LSP方案解决起来轻松)
对于 UDP,则每个数据包前增加目的IP等信息,这样代理服务器端对每个UDP数据包都能知道朝哪发送。

当然FWPM_LAYER_ALE_CONNECT_REDIRECT_V4 的代理功能在应用层使用 LSP一样能实现,只是这样让你有更多的选择。
至于内核层发送的数据包要做代理转发,比如使用TDI或者WSK套接字发送的数据包,
这时候你就只能使用FWPM_LAYER_ALE_CONNECT_REDIRECT_V4的方案来解决了。


我们再来看看TCP数据在整个协议栈传输流动的时候,WFP在其中充当的过滤点角色,
这样我们更能了解WFP的作用。

TCP : 分为 client , server

首先连接建立过程:
client端调用 socket, connect 建立连接,
server调用 socket, bind, listen, accept 等函数接收客户端连接。

 

client:

bind: FWPM_LAYER_ALE_BIND_REDIRECT_V4 这个支持win7以上系统,(不管有没有显式调用bind函数,绑定操作都会发生)
bind: FWPM_LAYER_ALE_RESOURCE_ASSIGNMENT_V4
connect: FWPM_LAYER_ALE_CONNECT_REDIRECT_V4 , win7以上系统
connect: FWPM_LAYER_ALE_AUTH_CONNECT_V4

接着发送SYN数据包,

SYN: FWPM_LAYER_OUTBOUND_TRANSPORT_V4 , SYN进入到传输层
SYN: FWPM_LAYER_OUTBOUND_IPPACKET_V4, SYN进入到IP层

接收到 SYN_ACK数据包,

SYN-ACK: FWPM_LAYER_INBOUND_IPPACKET_V4, 首先进入IP层
SYN-ACK: FWPM_LAYER_INBOUND_TRANSPORT_V4 ,进入到传输层
FWPM_LAYER_ALE_FLOW_ESTABLISHED_V4,到达ALE层,内核建立起连接,同时回复ACK包给服务端。
ACK: FWPM_LAYER_OUTBOUND_TRANSPORT_V4, 回复的ACK包进入传输层
ACK: FWPM_LAYER_OUTBOUND_IPPACKET_V4, 回复的包进入IP层

以上就是客户端在建立连接时候,WFP能挂载的所有挂载点和流程。
以下是服务端端:

 

server:

bind: FWPM_LAYER_ALE_BIND_REDIRECT_V4 这个支持win7以上系统
bind: FWPM_LAYER_ALE_RESOURCE_ASSIGNMENT_V4
listen: FWPM_LAYER_ALE_AUTH_LISTEN_V4, listen函数认证

接下来就是accept开始接收客户端的请求,首先接收到的是SYN数据包,

SYN: FWPM_LAYER_INBOUND_IPPACKET_V4, SYN包首先进入IP层
SYN: FWPM_LAYER_INBOUND_TRANSPORT_V4 , SYN包进入传输层
SYN: FWPM_LAYER_ALE_AUTH_RECV_ACCEPT_V4, SYN进入ALE层,确认建立连接,同时给客户端回复SYN-ACK数据包。
SYN-ACK: FWPM_LAYER_OUTBOUND_TRANSPORT_V4,回复的SYN-ACK数据包进入传输层
SYN-ACK: FWPM_LAYER_OUTBOUND_IPPACKET_V4, 回复的SYN-ACK数据包进入IP层

然后就开始接收客户端发来的最后一个ACK数据包,

ACK: FWPM_LAYER_INBOUND_IPPACKET_V4, ACK包首先进入IP层
ACK: FWPM_LAYER_INBOUND_TRANSPORT_V4 , ACK进入传输层
FWPM_LAYER_ALE_FLOW_ESTABLISHED_V4, ACK进入ALE层,这个时候就已经建立起了连接,

accept函数返回,接收到了新的客户端连接的socket。
以上是服务端和客户端建立三次握手连接的流程。

 

当客户端连接服务端一个不在侦听的端口的情况下:

下边是服务端的流程

SYN: FWPM_LAYER_INBOUND_IPPACKET_V, 进入IP层
SYN: FWPM_LAYER_INBOUND_TRANSPORT_V4_DISCARD,

进入传输层,但是这个是时候,是进入WFP传输层的丢弃过滤点 , 同时给客户端回复RST数据包。

RST: FWPM_LAYER_OUTBOUND_TRANSPORT_V4,
RST: FWPM_LAYER_OUTBOUND_IPPACKET_V4

接下来就是client和server端的数据传输:
send(发送数据包):

data:FWPM_LAYER_STREAM_V4, 发送数据到 Stream DATA层
TCP: FWPM_LAYER_OUTBOUND_TRANSPORT_V4, 到达传输层
IP: FWPM_LAYER_OUTBOUND_IPPACKET_V4, 达到IP层。

recv(接收数据包):
把上边的过程反过来。

服务端还要处理一种情况,就是当最初的连接授权发生变化的时候, 数据包还会进入到

FWPM_LAYER_ALE_AUTH_RECV_ACCEPT_V4, 认证正确
FWPM_LAYER_ALE_AUTH_RECV_ACCEPT_V4_DISCARD, 没通过认证。

 

至于这个连接授权发生变化的情况,请看如下链接,这里就不再继续,
https://msdn.microsoft.com/en-us/library/windows/desktop/bb613462(v=vs.85).aspx

同样,UDP的WFP流程请看如下链接,这里也不再继续。
https://msdn.microsoft.com/en-us/library/bb451831(v=vs.85).aspx,

从上边的例子,我们可以看到,WFP的过滤点,几乎是挂载到整个通讯的方方面面,可以说无处不在。

说了这么多,大家最关心的就是如何建立一个WFP的过滤驱动模型。
首先创建一个WFP会话句柄,如下伪代码:

FWPM_SESSION0 session = { 0 };
session.flags = FWPM_SESSION_FLAG_DYNAMIC;
HANDLE engin_handle;
status = FwpmEngineOpen0(
NULL,
RPC_C_AUTHN_WINNT,
NULL,
&session,
&engine_handle
);

会话类型可以是静态或者动态的,如上创建的就是一个动态会话句柄,动态会话句柄的好处,
就是当关闭这个句柄后,所有在这个会话中创建的对象都自动被删除,
当FwpmEngineOpen0第4个参数传递NULL指针时候,创建的是静态会话句柄,在静态会话创建的子层等对象,可以共享给所有其他会话,
但是关闭时候,必须删除每个在静态会话中创建的对象。

接着开始会话,开始之后就可以在会话中创建子层,创建过滤点等。

status = FwpmTransactionBegin0( engine_handle, NULL);

 

分三步创建过滤点:
1, 注册 callout,如下代码

FWPS_CALLOUT0 scallout = { 0 };
scallout.calloutKey = *calloutKey; // 一个GUID值,用来唯一标志这个过滤点,可以使用ExUuidCreate动态生成。
scallout.classifyFn = callifyFn0; //这个就是我们非常关心的过滤函数,就是钩子函数,在这个函数中做任何想做的事情。
scallout.notifyFn = callout_Notify; //事件通知函数
//注册callout, device_object 是我们在驱动中创建的一个设备对象。
status = FwpsCalloutRegister0( device_object, &scallout, NULL );

2, 添加 callout到会话中,如下代码

FWPM_CALLOUT0 mcallout = { 0 };
FWPM_DISPLAY_DATA0 displayData = { 0 };

displayData.name = L"Fanxiushu WFP Filter Callout"; //名字和描述,可随意,
displayData.description = L"Fanxiushu WFP Filter Callout";
mcallout.calloutKey = *calloutKey; 同上,一个GUID值,用来唯一标志这个过滤点
mcallout.displayData = displayData;
mcallout.applicableLayer = *layerKey; //
layerKey 这个就是微软提供的固定过滤点的值,比如过滤 connect可以设置 FWPM_LAYER_ALE_AUTH_CONNECT_V4
status = FwpmCalloutAdd0( engine_handle, &mcallout, NULL, NULL); //添加callout到会话中

3, 添加filter,如下代码:

FWPM_FILTER0 filter = { 0 };
filter.layerKey = *layerKey; /// 同上,这个就是微软提供的固定过滤点的值。
filter.action.calloutKey = *calloutKey; ///同上,唯一标志这个过滤点的GUID
filter.filterCondition = NULL;
filter.subLayerKey = *guid_layer; //过滤点在哪个子层中,如果不设置则处于系统默认的子层中,
filter.weight.type = FWP_UINT64; //
filter.weight.uint64 = &weight; //设置权重,权重越大,越先执行
。。。。
status = FwpmFilterAdd0( engine_handle, &filter, NULL, NULL);

这样一个过滤点就创建成功了,要删除过滤点调用

FwpsCalloutUnregisterByKey0( calloutKey); //calloutKey 唯一标志这个过滤点的GUID值。

 

在一个会话中可以同时创建多个过滤点。
有时候可能还需要在会话中,创建自定义的子层,把过滤点放到我们自定义的子层中,创建子层的代码如下:

FWPM_SUBLAYER0 layer = { 0 };
layer.subLayerKey = *guid_layer; // 一个GUID值,用来唯一标志这个子层,可以使用ExUuidCreate动态生成。
。。。
status = FwpmSubLayerAdd0( engine_handle, &layer, NULL); //

要删除子层,调用 FwpmSubLayerDeleteByKey0(guid_layer);

当添加完所有过滤点,创建完子层之后,需要结束会话,结束会话代码如下:
//完成回话

status = FwpmTransactionCommit0( engine_handle);

关闭会话调用

FwpmEngineClose0(engine_handle);

 

至此,一个拥有多个过滤点的WFP会话开始就运行起来了。

我们最关心的就是 callifyFn0 过滤函数(钩子函数),函数原型如下:

static void NTAPI callifyFn0(
IN const FWPS_INCOMING_VALUES0* inFixedValues,
IN const FWPS_INCOMING_METADATA_VALUES0* inMetaValues,
IN OUT void* layerData, //如果是在传输层,IP层,数据层,指向 NET_BUFFER_LIST类型的参数
IN const FWPS_FILTER0* filter,
IN UINT64 flowContext,
OUT FWPS_CLASSIFY_OUT0* classifyOut);

 

可见参数非常多,具体含义可查阅MSDN相关文档。

写这篇文章的目的不单是介绍WFP框架,还要实现一个 IP层数据包的截获,并且把所有IP数据包传递到应用层处理。
经过上边的介绍,截获IP层数据包,直接使用 IP层的过滤点。
包括输入和输出两个方向,还包括转发包
(这种情况是内核开启了NAT等功能,从网卡发到IP层的数据包直接被转发出去,不再向上投递),
IPV4和IPV6都支持,一共就是6个过滤点,分别过滤值如下:

FWPM_LAYER_INBOUND_IPPACKET_V4,//本机接收的数据包
FWPM_LAYER_INBOUND_IPPACKET_V6,
FWPM_LAYER_OUTBOUND_IPPACKET_V4,//从本机发出去的数据包
FWPM_LAYER_OUTBOUND_IPPACKET_V6,
FWPM_LAYER_IPFORWARD_V4,直接在IP层转发
FWPM_LAYER_IPFORWARD_V6,

 

看起来似乎不复杂,确实原理不负责,但具体实现起来事情有点多。

未完待续。。。。

posted @ 2022-11-04 15:18  信易达  阅读(3028)  评论(0编辑  收藏  举报