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

                                                                                                                by fanxiushu 2017-10-23 转载或引用请注明原是作者。
接上文。
上文所说只要挂载其中的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组包的负担,使得通讯效率反而下降。

至于如何改进效率,有兴趣可自行研究测试,这里就不再继续。

范秀树  2017-10-25夜。


猜你喜欢

转载自blog.csdn.net/fanxiushu/article/details/78347137