Windows7以上使用WFP驱动框架实现IP数据包截取(二)
接上文。
上文所说只要挂载其中的6个WFP过滤点,就可以截获IP层的所有数据包。
再把截获的数据包转发到应用层,应用层处理之后,再发给内核驱动,经过这样的过程,就完成一个数据包的处理过程。
IP数据包到达应用层之后,我们就可以随心所欲的实现某些功能。比如做流量分析,可以细化到端口和具体IP等,
可以做NAT转发,可以做VPN,把某些特殊数据包发给VPN服务器转发等等。
当然这些功能,我们在驱动层也可以实现,但是肯定没有在应用层来的灵活和简便。
利用WFP截获IP层的数据包到应用层,再来做其他业务逻辑处理的框架,也不是我的独创的想法。
其实有个开源项目 WinDivert 就是完成这个功能,具体你可以到网上搜索相关信息,而且它开发历史较长,基本上多年前就实现了这个功能。
这篇文章不介绍WinDivert,而是利用windows的 WFP 的特性,实现我们自己的处理框架。
同时还把每个数据包跟某个具体进程ID联系起来,这个在WFP的IP层无法办到,还必须挂载
FWPM_LAYER_ALE_RESOURCE_ASSIGNMENT_V4/V6 过滤点, 用于在bind操作中获取进程ID和端口的联系。
这样每个IP层数据包就能获取这个数据包从哪个进程发出或接收的。
首先,我们在驱动的入口函数DriverEntry中必须创建一个控制设备,用于跟应用层通讯,
这个是必须的,因为初始化WFP也需要我们创建一个设备。
创建和初始化全局变量和WFP。
这里我们把WFP初始化分成两部分:
第一部分初始化静态会话(在DriverEntry中初始化)创建一个静态的子层,把后边创建的所有过滤点都加到这个子层中。
创建 FWPM_LAYER_ALE_RESOURCE_ASSIGNMENT_V4/V6过滤点,用于记录进程ID和端口的关联(用MAP等结构存储关联)。
调用FwpsInjectionHandleCreate0等函数创建投递句柄,这个是为了重新投递数据包时候使用。
调用NdisAllocateNetBufferListPool创建NET_BUFFER_LIST的内存池,用于重新投递数据包时候,创建NET_BUFFER_LIST结构。
以及其他全局变量的初始化。
第二部分的WFP初始化,是属于动态创建过滤点,由应用层控制。
当应用层程序打开我们的控制设备,发送创建WFP的IOCTL(IOCTL_WFP_FILTER_CREATE)命令到驱动.
驱动开始创建WFP会话,并且挂载
FWPM_LAYER_INBOUND_IPPACKET_V4/V6, FWPM_LAYER_OUTBOUND_IPPACKET_V4/V6, F WPM_LAYER_IPFORWARD_V4/V6
过滤点。
同时我们可以设置一下过滤条件,比如只采集某些感兴趣的数据包,其他包直接PASS。
为了简单,我们可以只把过滤条件设置某些IP段的数据包截获,或者截获全部的IP包。
我们在驱动创建wfp_context_t数据结构,结构声明大致如下:
struct wfp_context_t { LIST_ENTRY list; //挂到全局队列,对应到 wfp->fsobj_head; BOOLEAN is_valid; /// KSPIN_LOCK spin_lock; /// KIRQL kirql; /// LONG ref_count; /// 引用计数 LIST_ENTRY irps_head; /// IRP 队列 LONG pending_irp_count; //从队列取出,但是还没完成的间隙, LIST_ENTRY pkts_head; // IP包队列 LONG wait_pkts_count; ///队列中包个数 过滤条件 fs_filter_t white_filter; /// 符合此条件的采集 fs_filter_t black_filter; /// 符合此条件的不采集 /WFP相关 UINT32 priority; BOOLEAN is_wfp_active; BOOLEAN is_associate_processid; // 数据包是否需要关联到进程ID wfp_callout_t callout; // WFP 相关 ......... };
也就是当应用层发送IOCTL_WFP_FILTER_CREATE命令,都会创建wfp_context_t结构,直到应用层的退出,
再销毁此结构和相关WFP等对象。
因此,我们在应用层同时打开这个控制设备的多个文件HANDLE,每个HANDLE都包含一个WFP过滤,
也就是同一个IP数据包可以流过每个应用层的过滤,但是如果同时打开多个文件对象,会造成网络效率很低。
接下来核心处理就在WFP挂载的 ”钩子函数“,我们的目的是把我们感兴趣的数据包转发到应用层来处理。
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);
在此函数中,首先做一些基础判断,如下
//基本检测 if ( !layerData || !(classifyOut->rights & FWPS_RIGHT_ACTION_WRITE) ) { //如果数据为空,或者不允许行使否决权 /// return; } NTSTATUS status = STATUS_SUCCESS; PNET_BUFFER_LIST buffers = (PNET_BUFFER_LIST)layerData; if (NET_BUFFER_LIST_NEXT_NBL(buffers) != NULL) // 如果有多个 NET_BUFFER_LIST,不处理 return; } ............
接下来获取我们的 wfp_context_t数据结构
wfp_context_t* ctx = usage_wfp_context((PVOID)(ULONG_PTR)filter->context);//之所以可以在 FWPS_FILTER0的context获取,是因为在调用FwpmFilterAdd0 函数时候,把我们的数据结构指针保存到对应的结构中。
接下来,必须要做的一个检测,就是数据包重复检测。为何会造成数据包的重复,举个例子说明。
比如我们在应用层同时打开三个控制设备的文件HANDLE来创建WFP,
驱动里WFP为我们的某个挂载点(比如 FWPM_LAYER_OUTBOUND_IPPACKET_V4)挂载三个钩子函数,
按照顺序分别是 fn1, fn2, fn3 。同时假设系统中这个挂载点就只有这三个钩子函数。
按照通常的思路,fn1,fn2,fn3组成一个钩子函数链条, 在fn1投递的自定义数据包会自动投递到 fn2,fn3,
在fn2投递的自定义数据包会自动投递到fn3中。
但是WFP不是按照这个思路进行的,
当我们调用 FwpsInjectNetworkSendAsync0函数,投递我们自己定义的IP数据包时候,WFP框架干的事情,
不论我们是在fn1,fn2,还是fn3,还是在别的地方调用这个函数投递,WFP框架都会查看这个挂载点的所有钩子函数,
然后全部重新投递一遍,也就是这个数据包接着会被投递到 fn1,fn2,fn3三个函数中,
至于投递的顺序,不再是注册挂载点时候的注册顺序,是使用一个权重来衡量投递顺序。
权重的计算方式也比较复杂,但是在同一个子层中,
根据 FwpmFilterAdd0参数设置的权重来计算,权重越大越先投递。
这就是我们为什么要创建一个静态的子层,然后把所有挂载点都放到这个子层中,免得不同的子层又要重新计算权重。
在钩子函数中,为了避免接收到重复的数据包,我们必须要做些处理,
首先在调用 FwpmFilterAdd0函数的时候,设置不同的权重。
设置一个全局UINT变量, priority。初始化为0 ,
每次创建一个新wfp_context_t结构时候,递增 priority。
把priority存储到 wfp_context_t结构中,同时传递给 FwpmFilterAdd0参数如下
UINT64 weight = 0xFFFFFFFF - priority; .... filter.weight.type = FWP_UINT64; // filter.weight.uint64 = &weight; //设置权重,权重越大,越先执行 ..... FwpmFilterAdd0( engine_handle, &filter, NULL, NULL);
这样,每次新建的wfp_context_t的WFP过滤钩子函数都老老实实的放到后边被调用。
而在 callifyFn0 钩子函数中做如下判断:
FWPS_PACKET_INJECTION_STATE packet_state; UINT32 priority=0; packet_state = FwpsQueryPacketInjectionState0( is_ipv4 ? wfp->injection_v4_handle : wfp->injection_v6_handle , buffers, (HANDLE*)&priority); if (packet_state == FWPS_PACKET_INJECTED_BY_SELF || packet_state == FWPS_PACKET_PREVIOUSLY_INJECTED_BY_SELF) { // DPT("--- Reinject: ctx=%p\n", ctx); if (priority >= ctx->priority) { //有可能打开多个应用层客户端,也就是多个过滤器,这里根据 权重来确定哪些是被重复发送的数据包 / classifyOut->actionType = FWP_ACTION_CONTINUE; unusage_wfp_context(ctx); return; } / }
然后,我就需要处理数据包,看看哪些是我们感兴趣,可以发到应用层处理,哪些不感兴趣,直接投递下去。
虽然如上边的判断,我们只处理 NET_BUFFER_LIST链条的第一个NET_BUFFER_LIST,
但是根据NET_BUFFER_LIST的特点,它可能包含多个 NET_BUFFER(每个NET_BUFFER的数据就是IP数据包),
这就使得处理变得更复杂。
因此我们必须遍历所有的NET_BUFFER,查看是否满足我们的过滤条件,满足的就是我们要截获到应用层的IP数据包,
如果整个 NET_BUFFER_LIST都没找到符合过滤条件的,直接PASS。
直到遇到第一个满足过滤条件的,把这个满足条件的NET_BUFFER解析并重新打包投递到队列,等待应用层程序取走,
然后接着遍历,再遇到不符合过滤条件的,需要重新组包,然后再调用WFP函数重新投递下去,如此循环,直到遍历完成。
这里假设:
判断是否满足我们的过滤条件的函数是 filter_passed,
重新投递NET_BUFFER到WFP框架的函数是 inject_packet,
满足我们过滤条件的NET_BUFFER,投递到应用层队列的函数是 post_net_buffer_to_user,
第一个 buffer = NET_BUFFER_LIST_FIRST_NB(buffers);
这个遍历解析过程伪代码如下所示:
PNET_BUFFER buf_first = buffer; do{ ///判断过滤条件 if ( filter_passed(ctx, buf_first, is_ipv4, is_inbound)) { //可以传递到应用层 break; } } while (buf_first = NET_BUFFER_NEXT_NB(buf_first)); if (!buf_first) { // 全部都不通过, classifyOut->actionType = FWP_ACTION_CONTINUE; unusage_wfp_context(ctx); return; } PNET_BUFFER buf_next = buffer; while (buf_next != buf_first) {//前面的全部要重新投递下去 status = inject_packet(ctx, ifIndex, ifSubIndex, buf_next, NULL, 0, is_ipv4, is_inbound, is_forward); if (!NT_SUCCESS(status)) { //投递失败,这种情况干脆直接阻止整个包继续投递 goto EXIT; } buf_next = NET_BUFFER_NEXT_NB(buf_next); /// } ///剩下的包 while (buf_next) { BOOLEAN passed = TRUE; if (buf_next != buf_first) { // passed = filter_passed(ctx, buf_first, is_ipv4, is_inbound); } if (passed) { //传递到应用层 ippacket_header_t hdr; hdr.type = is_forward ? 1 : 0; hdr.is_recv = is_inbound ? 1 : 0; hdr.is_ipv4 = is_ipv4 ? 1 : 0; hdr.if_index = ifIndex; hdr.if_subindex = ifSubIndex; hdr.length = NET_BUFFER_DATA_LENGTH(buf_next); hdr.process_id = -1; /// 进程ID status = post_net_buffer_to_user(ctx, buf_next, &hdr ); //在此函数中,同时查找和端口对应的进程ID } else { //重新投递 status = inject_packet(ctx, ifIndex, ifSubIndex, buf_next, NULL, 0, is_ipv4, is_inbound, is_forward); } if (!NT_SUCCESS(status)) { // 失败,这种情况干脆直接阻止整个包继续投递 /// goto EXIT; } buf_next = NET_BUFFER_NEXT_NB(buf_next); /// }
这样,核心的数据包处理过程基本完成。
这里需要注意一点,按照正常思路,WFP提供给IP层的钩子函数的NET_BUFFER_LIST指向的数据包应该就是指向 IP头开始的,
但是这里有个例外,就是 FWPM_LAYER_INBOUND_IPPACKET_V4/V6 挂载点,指向的不是IP头开始,而是隐藏了IP头,
因此我正确处理,我们必须调用 NdisRetreatNetBufferDataStart 设置到IP头,
处理完之后调用 NdisAdvanceNetBufferDataStart 恢复原来位置。
大致代码如下:
PNET_BUFFER buffer = NET_BUFFER_LIST_FIRST_NB(buffers); //这里只考虑第一个包,因为process_ip_packet函数也只处理了第一个包 status = NdisRetreatNetBufferDataStart(buffer, inMetaValues->ipHeaderSize, 0, NULL); ....... // process_ip_packet NdisAdvanceNetBufferDataStart(buffer, inMetaValues->ipHeaderSize, FALSE, NULL); //还原
驱动力该做的事情基本完成了,这里却有个效率的问题,就跟我以前文章介绍NDIS驱动,照样把NDIS驱动的数据包传递到应用层处理。
他们都面临一个同样的问题:
网路数据包很多, IO率非常高,一不留神,就会造成网络通讯效率极其低下。
为了尽量提高IO吞吐量,我们必须在应用层使用完成端口来处理这些数据包,同时处理数据包的线程提升到更高的优先级别。
并且尽量使用一个线程来处理IO数据包,原因很简单,多线程多CPU并行处理,
看起来是很快,但是同时会造成TCP网络栈的数据包乱序的概率大大增大,从而造成TCPIP组包的负担,使得通讯效率反而下降。
至于如何改进效率,有兴趣可自行研究测试,这里就不再继续。