freeBuf
主站

分类

漏洞 工具 极客 Web安全 系统安全 网络安全 无线安全 设备/客户端安全 数据安全 安全管理 企业安全 工控安全

特色

头条 人物志 活动 视频 观点 招聘 报告 资讯 区块链安全 标准与合规 容器安全 公开课

官方公众号企业安全新浪微博

FreeBuf.COM网络安全行业门户,每日发布专业的安全资讯、技术剖析。

FreeBuf+小程序

FreeBuf+小程序

终端安全: 一文从浅入深介绍windows WFP框架 
2022-12-10 22:29:07
所属地 北京

Windows 中的 WFP 框架是一个用于网络流量管理的工具,它可以帮助用户控制数据包在网络中的流动,从而提高网络安全性和性能。
本文将深入介绍 Windows 中的 WFP 框架,并通过一个简单的实例(基于 WFP 框架的 DNS 流量解析工具)让大家更好地理解它。

WFP 框架

WFP 框架是Windows Filtering Platform的简称,即 Windows 过滤平台。
它是一个用于网络流量管理的工具,可以让用户定义数据包进出网络的规则,从而控制数据包在网络中的流动。
WFP 框架可以实现以下功能:

  1. 防火墙:
    WFP 框架可以帮助用户构建一个防火墙,用于拦截来自不受信任的来源的数据包,并阻止它们进入网络

  2. 网络流量监控:
    WFP 框架可以帮助用户监控网络流量,让用户可以了解网络上发生的一切事情。

  3. 应用程序控制:
    WFP 框架可以帮助用户控制应用程序访问网络的权限,从而提高网络安全性。

hello world

以下代码是最小的实现代码,请注意:

  1. 他不完整,比如缺少一些全局变量+没有错误异常处理

  2. 没有卸载函数

不过,作为教学用途,这段代码已经足够了。
作者认为,新人不需要看太多代码,过多的代码会让人感到困惑。
所以,上面的代码已经是经过精简的版本了

auto MiniInstall() -> bool {
    FWPM_SESSION0 wfpSession = {0};            // WFP 会话
    FWPM_SUBLAYER0 wfpFirewallSubLayer = {0};  // WFP 子层
    FWPM_PROVIDER0 ProviderInfo = {0};         // WFP 提供程序信息

    bool inTransaction = false;                    // 是否在事务中
    wfpSession.flags = FWPM_SESSION_FLAG_DYNAMIC;  // 设置会话标志
    auto ntStatus = FwpmEngineOpen0(NULL, RPC_C_AUTHN_WINNT, NULL, &wfpSession,
                                    &gEngineHandle);  // 打开 WFP 引擎
    do {
        if (NT_SUCCESS(ntStatus) == false) {  // 检查是否成功打开引擎
            break;
        }
        if (NT_SUCCESS(ntStatus = FwpmTransactionBegin0(gEngineHandle, 0)) ==
            false) {  // 开始事务
            break;
        }
        inTransaction = true;
        ProviderInfo.displayData.name =
            L"My First Wfp Provider by huoji";  // 设置提供程序名称
        ProviderInfo.displayData.description =
            L"Huoji Wfp Provider";  // 设置提供程序描述
        ProviderInfo.providerKey = GUID_WFP_PROVIDER;  // 设置提供程序的 GUID
        ProviderInfo.serviceName = L"duck";  // 设置提供程序的服务名
        if (NT_SUCCESS(ntStatus = FwpmProviderAdd0(gEngineHandle, &ProviderInfo,
                                                   NULL)) ==
            false) {  // 添加 WFP 提供程序
            break;
        }
        wfpFirewallSubLayer.subLayerKey =
            GUID_SUBLAYER;  // 2e207682-d95f-4525-b966-969f26587f03
        wfpFirewallSubLayer.displayData.name =
            L"First Wfp Transport Wfp Sub-Layer";  // 设置子层名称
        wfpFirewallSubLayer.displayData.description =
            L"Sub-Layer for use by First Wfp callouts "; // 设置子层描述
             wfpFirewallSubLayer.flags = 0;  // 设置子层标志
        wfpFirewallSubLayer.weight = 0;      // 设置子层权重
        if (NT_SUCCESS(ntStatus = FwpmSubLayerAdd0(
                           gEngineHandle, &wfpFirewallSubLayer, NULL)) ==
            false) {  // 添加 WFP 子层
            break;
        }
        FWPS_CALLOUT0 sCallout = {0};
        FWPM_CALLOUT0 mCallout = {0};
        sCallout.classifyFn = (FWPS_CALLOUT_CLASSIFY_FN0)
            NewESTConnect;  // 第一次握手的回调.后面会细说
        sCallout.notifyFn = (FWPS_CALLOUT_NOTIFY_FN0)
            ConnectNotify;  // 通知回调 不用管.后面会细说
        sCallout.flowDeleteFn = (FWPS_CALLOUT_FLOW_DELETE_NOTIFY_FN0)
            ConnectFlowDelete;  // 流删除回调
        const auto calloutType = FWP_ACTION_CALLOUT_INSPECTION;  // 回调类型
        if (NT_SUCCESS(ntStatus = FwpmCalloutAdd0(gEngineHandle, &mCallout,
                                                  NULL, NULL)) ==
            false) {  // 添加 WFP 回调函数
            break;
        }
        FWPM_FILTER0 Filter = {0};

        Filter.layerKey = FWPM_LAYER_ALE_FLOW_ESTABLISHED_V4;  // 设置过滤器的层
        Filter.displayData.name =
            L"Transport First ALE Classify";  // 设置过滤器名称
        Filter.displayData.description =
            L"Intercepts inbound or outbound connect attempts";  // 设置过滤器描述
        Filter.action.type = calloutType;                // 设置回调类型
        Filter.action.calloutKey = SF_EST_CALLOUT_GUID;  // 设置回调函数的 GUID
        Filter.subLayerKey = SF_SUBLAYER;  // 设置过滤器所属的子层
        Filter.weight.type = FWP_EMPTY;    // 设置过滤器权重
        if (NT_SUCCESS(ntStatus = FwpmFilterAdd0(gEngineHandle, &Filter, NULL,
                                                 NULL)) ==
            false) {  // 添加 WFP 过滤器
            break;
        }
        if (NT_SUCCESS(ntStatus = FwpmTransactionCommit0(gEngineHandle)) ==
            false) {  // 提交事务
            break;
        }
        inTransaction = false;
    } while (false);
    // 错误处理代码...cleanup的代码..
}

代码拆解:

首先,我们定义了一个 WFP 提供程序的 GUID:

DEFINE_GUID(GUID_WFP_PROVIDER, 0x19b351df, 0x1055, 0x46e1, 0x93, 0x42, 0xff,
            0xfe, 0x4a, 0xd0, 0xdd, 0xc1);

然后,在MiniInstall函数中,我们初始化了一些变量,包括 WFP 会话,WFP 子层和 WFP 提供程序信息:

FWPM_SESSION0 wfpSession = {0};
FWPM_SUBLAYER0 wfpFirewallSubLayer = {0};
FWPM_PROVIDER0 ProviderInfo = {0};

接着,我们设置会话标志,并打开 WFP 引擎:

wfpSession.flags = FWPM_SESSION_FLAG_DYNAMIC;
auto ntStatus = FwpmEngineOpen0(NULL, RPC_C_AUTHN_WINNT, NULL, &wfpSession,
                                &gEngineHandle);

在打开 WFP 引擎后,我们开始事务,并添加一个新的 WFP 提供程序:

if (NT_SUCCESS(ntStatus = FwpmTransactionBegin0(gEngineHandle, 0)) ==
    false) {
    break;
}
inTransaction = true;
ProviderInfo.displayData.name = L"My First Wfp Provider by huoji";
ProviderInfo.displayData.description = L"Huoji Wfp Provider";
ProviderInfo.providerKey = GUID_WFP_PROVIDER;
ProviderInfo.serviceName = L"duck";
if (NT_SUCCESS(ntStatus = FwpmProviderAdd0(gEngineHandle, &ProviderInfo,
                                           NULL)) == false) {
    break;
}

然后,我们添加一个子层,并为子层添加一些回调函数:

wfpFirewallSubLayer.subLayerKey =
    GUID_SUBLAYER;
wfpFirewallSubLayer.displayData.name =
    L"First Wfp Transport Wfp Sub-Layer";
wfpFirewallSubLayer.displayData.description =
    L"Sub-Layer for use by First Wfp callouts";
wfpFirewallSubLayer.flags = 0;
wfpFirewallSubLayer.weight = 0;
if (NT_SUCCCCESS(ntStatus = FwpmSubLayerAdd0(
	gEngineHandle, &wfpFirewallSubLayer, NULL)) ==
	false) {
	break;
}
FWPS_CALLOUT0 sCallout = {0};
FWPM_CALLOUT0 mCallout = {0};

sCallout.classifyFn = (FWPS_CALLOUT_CLASSIFY_FN0)
	NewESTConnect;
sCallout.notifyFn = (FWPS_CALLOUT_NOTIFY_FN0)
	ConnectNotify;
sCallout.flowDeleteFn = (FWPS_CALLOUT_FLOW_DELETE_NOTIFY_FN0)
	ConnectFlowDelete;
const auto calloutType = FWP_ACTION_CALLOUT_INSPECTION;
if (NT_SUCCESS(ntStatus = FwpmCalloutAdd0(gEngineHandle, &mCallout,
	NULL, NULL)) == false) {
	break;
}

最后,我们添加过滤器,并提交事务:

FWPM_FILTER0 Filter = {0};

Filter.layerKey = FWPM_LAYER_ALE_FLOW_ESTABLISHED_V4;
Filter.displayData.name = L"Transport First ALE Classify";
Filter.displayData.description =
L"Intercepts inbound or outbound connect attempts";
Filter.action.type = calloutType;
Filter.action.calloutKey = SF_EST_CALLOUT_GUID;
Filter.subLayerKey = SF_SUBLAYER;
Filter.weight.type = FWP_EMPTY;
if (NT_SUCCESS(ntStatus = FwpmFilterAdd0(gEngineHandle, &Filter, NULL, NULL)) == false) {
break;
}
if (NT_SUCCESS(ntStatus = FwpmTransactionCommit0(gEngineHandle)) ==
false) {
break;
}
inTransaction = false;

恭喜你第一个hello world就这样完成了

Callout层

Windows Filtering Platform (WFP)中,Callout 层是用来拦截和修改网络流量的一个重要机制。它提供了一种机制,可以在 WFP 拦截流量时,调用用户自定义的回调函数,以实现自定义的过滤逻辑。
注册layer函数在上面的代码中

mCallout.calloutKey = *calloutKey; //callout的GUID
mCallout.applicableLayer = *layerKey; //层的GUID

        if (NT_SUCCESS(ntStatus = FwpmCalloutAdd0(EngineHandle, &mCallout, NULL,
                                                  NULL)) == false) {
            break;
        }

比如 为了拦截DNS流量.本例子中提供的layer Guid是:

FWPM_LAYER_DATAGRAM_DATA_V4

他在整个WFP生命周期中的位置是:

客户端 (发送方) 执行活动打开

bind: FWPM_LAYER_ALE_BIND_REDIRECT_V4 (Windows 7 / Windows Server 2008 R2 仅)
bind:FWPM_LAYER_ALE_RESOURCE_ASSIGNMENT_V4
sendto:FWPM_LAYER_ALE_CONNECT_REDIRECT_V4 (Windows 7/Windows Server 2008 R2 仅)
sendto:FWPM_LAYER_ALE_AUTH_CONNECT_V4
FWPM_LAYER_ALE_FLOW_ESTABLISHED_V4 
数据:FWPM_LAYER_DATAGRAM_DATA_V4 <-这里
UDP 消息:FWPM_LAYER_OUTBOUND_TRANSPORT_V4
IP 数据报:FWPM_LAYER_OUTBOUND_IPPACKET_V4
	
服务器
IP 数据报:FWPM_LAYER_INBOUND_IPPACKET_V4
UDP 消息:FWPM_LAYER_INBOUND_TRANSPORT_V4
UDP 消息:FWPM_LAYER_ALE_AUTH_RECV_ACCEPT_V4
FWPM_LAYER_ALE_FLOW_ESTABLISHED_V4 
数据:FWPM_LAYER_DATAGRAM_DATA_V4 <-这里

而为了建立进程与报文的关联,必须得在FWPM_LAYER_ALE_FLOW_ESTABLISHED_V4这个层建立EST的Callout过滤(用于监听握手)

DEFINE_GUID(SF_EST_CALLOUT_GUID, 0x95ECC39E, 0xC9F7, 0x436A, 0xA6, 0x61, 0x54,
            0x40, 0x1A, 0xDF, 0x0D, 0x0D);

让我们逐个实现关键的层,最终目的是解析DNS流量并且判断是哪个进程发的。

TCP/UDP 握手监听FWPM_LAYER_ALE_FLOW_ESTABLISHED_V4

在WFP中UDP也有握手,也有timeout,这个是微软为了方便处理而虚拟出来的东西。
layerGuid是FWPM_LAYER_ALE_FLOW_ESTABLISHED_V4
这是本层的函数原型:

// 新的握手,注意UDP是windows自己抽象出来的一层吊东西.UDP不应该有连接的
// 之前的AuthConnectIpv4没办法抓UDP的
auto NewESTConnect(const FWPS_INCOMING_VALUES0* inFixedValues,
                   const FWPS_INCOMING_METADATA_VALUES0* inMetaValues,
                   void* layerData, const FWPS_FILTER1* filter,
                   uint64_t flowContext, FWPS_CLASSIFY_OUT0* classifyOut)
    -> void

通常情况下,在本层中的作用是建立ctx,给后续层用.微软用的是链表.我这边用hash表,hash表你需要自己自定义,这边不做过多介绍。

https://github.com/0x9dec1980/Windows-samples/blob/554b4fa23c8e10edbe55aa3e6a64ae73a636e3b8/WDK-Samples/WDK%207.1.0%20(7600.16385.1)/src/network/trans/ddproxy/sys/DD_proxy.c#L197

到了本层 我们应该重点关注几个信息:

  1. 进程id只有本层拿得到

  2. flowHandle是贯穿整个WFP框架的.微软不会和minifilter一样给你个函数啥的自己获取.而是要你自己通过这个flowhandle获取一些信息

  3. 注意IRQL.如果你的ctx结构体太大.如果太大需要用连续内存.exallocpool不保证一定连续(具体看微软文档).所以使用MmAllocateContiguousMemory

const auto flowHandle = inMetaValues->flowHandle;
const auto processId = inMetaValues->processId;

之后我们就可以获取一些基本的信息.包括ip地址、端口号、协议、方向等

const auto RemoteIp =
            inFixedValues
                ->incomingValue
                    [FWPS_FIELD_ALE_FLOW_ESTABLISHED_V4_IP_REMOTE_ADDRESS]
                .value.uint32;
        const auto LocalIp =
            inFixedValues
                ->incomingValue
                    [FWPS_FIELD_ALE_FLOW_ESTABLISHED_V4_IP_LOCAL_ADDRESS]
                .value.uint32;
        const auto RemotePort =
            inFixedValues
                ->incomingValue
                    [FWPS_FIELD_ALE_FLOW_ESTABLISHED_V4_IP_REMOTE_PORT]
                .value.uint16;
        const auto LocalPort =
            inFixedValues
                ->incomingValue
                    [FWPS_FIELD_ALE_FLOW_ESTABLISHED_V4_IP_LOCAL_PORT]
                .value.uint16;
        IPPROTO Protocol = static_cast<IPPROTO>(
            inFixedValues
                ->incomingValue[FWPS_FIELD_ALE_FLOW_ESTABLISHED_V4_IP_PROTOCOL]
                .value.uint32);
        const auto Direction =
            inFixedValues
                ->incomingValue[FWPS_FIELD_ALE_FLOW_ESTABLISHED_V4_DIRECTION]
                .value.uint8;
		.......

再然后 通过FwpsFlowAssociateContext0绑定流。这样这个流就会贯穿整个生命周期。当结束时候会自己走到之前设置的 follow delete 函数中。

auto ntStatus = FwpsFlowAssociateContext0(
            flowHandle, FWPS_LAYER_ALE_FLOW_ESTABLISHED_V4,
            AleConnectCalloutIdV4, reinterpret_cast<uint64_t>(netWorkCtx));
        if (NT_SUCCESS(ntStatus) == false) {
            break;
        }
		//TODO: 一个flow handle与ctx的hash表或者链表,以后其他层要查

此外本层还可以决定需不需要终止链接:

if (classifyOut->rights & FWPS_RIGHT_ACTION_WRITE) {
        classifyOut->actionType =
            isNeedBlock ? FWP_ACTION_BLOCK : FWP_ACTION_CONTINUE;
    }

那么,我们的流就已经创建好了。

流清理/链接断开

这个不算层,这个算之前的清理,作用是清理流。

auto ConnectFlowDelete(uint16_t layerId, uint32_t calloutId,
                       uint64_t flowContext) -> void {
    // 流删除
    auto netWorkCtx = reinterpret_cast<_NetWorkCtx*>(flowContext);
    if (netWorkCtx != nullptr && isNeedCleanUp == false) {
        DebugPrint("[%s]ctx flow delete remote ip: %d.%d.%d.%d Port: %d \n",
                   __FUNCTION__, netWorkCtx->Ipv4_remoteIp & 0xff,
                   (netWorkCtx->Ipv4_remoteIp >> 8) & 0xff,
                   (netWorkCtx->Ipv4_remoteIp >> 16) & 0xff,
                   (netWorkCtx->Ipv4_remoteIp >> 24) & 0xff,
                   netWorkCtx->Ipv4_remotePort);
        freeCtx(netWorkCtx);
        MmFreeContiguousMemory(netWorkCtx);
    }
}

也可以当做 TCP/UDP握手断开

TCP 数据传输层 FWPM_LAYER_STREAM_V4

本层用于监控TCP的数据传输
一开始,请先设置type为FWP_ACTION_PERMIT
classifyOut->actionType = FWP_ACTION_PERMIT
关于这个:
在 WFP 中,过滤器的操作类型可以是FWP_ACTION_BLOCK或者FWP_ACTION_PERMIT等。
当 WFP 拦截到一个网络流量时,会根据过滤器的操作类型来决定对这个流量的处理方式。
如果过滤器的操作类型是FWP_ACTION_PERMIT,则 WFP 会允许这个流量通过;
如果过滤器的操作类型是FWP_ACTION_BLOCK,则 WFP 会拦截这个流量,并阻止它的传输。
本层中需要用flow handle去查之前连接绑定的ctx

if (inMetaValues == nullptr || inFixedValues == nullptr || isNeedCleanUp) {
        return;
    }
    const auto ctx = getCtxByflowHandle(inMetaValues->flowHandle);
    if (ctx == nullptr) {
        return;
    }

然后我们就可以取数据了

const auto streamPacket =
            reinterpret_cast<FWPS_STREAM_CALLOUT_IO_PACKET*>(layerData);
        if (streamPacket == nullptr || streamPacket->streamData == nullptr) {
            break;
        }
        if (streamPacket->streamData->dataLength == 0 ||
            ctx->Ipv4_remoteIp == ctx->Ipv4_localIp) {
            break;
        }

这段代码中,使用了一个const auto streamPacket变量,并将layerData强制转换成FWPS_STREAM_CALLOUT_IO_PACKET类型。

FWPS_STREAM_CALLOUT_IO_PACKET是 WFP 中的一种数据结构,它用来表示流量的相关信息。在这里,streamPacket变量保存了一个指向该类型的指针,用来操作这条流量。
获取到streampacket后,就能拷贝到缓冲区中。读取网络流量了,这边使用FwpsCopyStreamDataToBuffer进行拷贝:

streamBuffer = reinterpret_cast<BYTE*>(
            Tools::AllocContiguousMemory(streamPacket->streamData->dataLength));
        if (streamBuffer == nullptr) {
            break;
        }
        const bool isOutBound = direction == FWP_DIRECTION_OUTBOUND;
        size_t byte_copied = 0;
        FwpsCopyStreamDataToBuffer(streamPacket->streamData, streamBuffer,
                                   streamPacket->streamData->dataLength,
                                   &byte_copied);
        if (byte_copied == streamPacket->streamData->dataLength) {
				//TODO:..
        }

有了网络流量包,你可以拿来做任何你想做的,比如匹配到恶意代码后,终止网络连接:

if (isNeedBlock) {
                if (classifyOut->rights & FWPS_RIGHT_ACTION_WRITE) {
                    classifyOut->actionType = FWP_ACTION_BLOCK;
                }
            }

这里不是我们的重点,因为本篇文章主要介绍DNS流量,是UDP。

UDP数据传输层FWPM_LAYER_DATAGRAM_DATA_V4

本层跟TCP差不多,但是跟TCP层的不同是流量的获取。
udp的netbuffer是这样获取的:

const auto NetBufferList = (PNET_BUFFER_LIST)layerData;
        if (NetBufferList == nullptr) {
            break;
        }
        const auto NetBuffer = NET_BUFFER_LIST_FIRST_NB(NetBufferList);
        if (NetBuffer == nullptr) {
            break;
        }
        auto UdpContentLength = NET_BUFFER_DATA_LENGTH(NetBuffer);
        if (UdpContentLength == 0) {
            break;
        }

然后你需要判断一下方向:

const auto direction =
            inFixedValues->incomingValue[FWPS_FIELD_DATAGRAM_DATA_V4_DIRECTION]
                .value.uint8;

如果是输出方向,则需要计算出UDP头长度:

if (direction == FWP_DIRECTION_OUTBOUND) {
            UdpHeaderLength = inMetaValues->transportHeaderSize;
            if (UdpHeaderLength == 0 || UdpContentLength < UdpHeaderLength) {
                break;
            }
            UdpContentLength -= UdpHeaderLength;
            isOutBound = true;
        }

然后通过NdisGetDataBuffer获取包的buffer:

UdpContent = Tools::AllocPoolMemory(UdpContentLength + UdpHeaderLength,
                                            NETWORK_TAG);
        if (UdpContent == nullptr) {
            break;
        }
        ndisBuffer = NdisGetDataBuffer(
            NetBuffer, UdpContentLength + UdpHeaderLength, UdpContent, 1, 0);
        if (ndisBuffer == nullptr) {
            break;
        }

之后同理,可以做一些操作了:

const bool isNeedBlock =
            isOutBound
                ? OnUdpPacketSend(
                      ctx, (void*)((uint64_t)ndisBuffer + UdpHeaderLength),
                      UdpContentLength)
                : OnUdpPacketRecv(
                      ctx, (void*)((uint64_t)ndisBuffer + UdpHeaderLength),
                      UdpContentLength);
        if (isNeedBlock && (classifyOut->rights & FWPS_RIGHT_ACTION_WRITE)) {
            classifyOut->actionType = FWP_ACTION_BLOCK;
        }

解析DNS并且拿到域名

DNS端口是53,在不考虑其他DNS的情况比如Doh的情况下,代码如下:

if (ctx->Ipv4_remotePort == 53) {
        NetWorkResolver::ResolveDnsPacket(buffer, bufferSize);
    }

分析DNS包结构

这边主要说的是请求包,而且不考虑question count大于1的情况:

比如查询www.baidu.com
除去包头,只看body的情况下:

03(www的长度) 77 77 77(w的ascii码) 0x5(baidu的长度) .....

编写dns解包的代码

整个函数的执行过程简要地描述如下:

  1. 检查 DNS 数据包的大小,如果小于 DNS 头部的大小,则退出函数。

  2. 检查 DNS 头部中的标志位IsResponse,如果为 true,则表示数据包是一个响应数据包,函数也退出。

  3. 计算出 DNS 数据部分的大小,如果 DNS 数据部分的大小大于整个数据包的大小,则退出函数。

  4. 为 DNS 数据部分分配内存,并解析出 DNS 查询请求包中的域名。

  5. 检查域名是否提取成功,如果提取成功,则调用DebugPrint函数打印域名。

  6. 释放分配的内存,并退出函数

对应的完整解包代码如下:

auto ResolveDnsPacket(void* packet, size_t packetSize) -> void {
    if (packetSize < sizeof(DNS_HEADER)) {
        return;
    }
    DNS_HEADER* dnsHeader = (DNS_HEADER*)packet;
    if (dnsHeader->IsResponse) {
        return;
    }
    size_t dnsDataLength = packetSize - sizeof(DNS_HEADER);
    // Check if DNS data length is greater than packet size
    if (dnsDataLength >= packetSize) {
        return;
    }
    // resolver DNS question packet and get domain Name
    char* dnsData = (char*)packet + sizeof(DNS_HEADER);
    char* domainName =
        reinterpret_cast<char*>(Tools::AllocContiguousMemory(allocDomainNameLength));
    if (domainName == nullptr) {
        return;
    }
    bool isSuccess = true;
    size_t domainNameLength = 0;
    while (dnsDataLength > 0) {
        const auto length = *dnsData;
        if (length == 0) {
            break;
        }
        if (length >= packetSize || length > 256) {
            break;
        }
        // Check if domain name length is greater than domainName array
        // length
        if (domainNameLength + 1 > allocDomainNameLength) {
            isSuccess = false;
            break;
        }
        auto domainNameStr = *(dnsData + 1);
        // 检查第一个字符是否是可读字符
        if (isprint(domainNameStr) == false) {
            isSuccess = false;
            break;
        }
        if (domainNameLength != 0) {
            domainName[domainNameLength] = '.';
            domainNameLength++;
        }
        memcpy(domainName + domainNameLength, dnsData + 1, length);
        domainNameLength += length;
        dnsDataLength -= *dnsData + 1;
        dnsData += *dnsData + 1;
    }
    if (isSuccess) {
        // TODO: OnDnsQueryEvent;
        DebugPrint("ResolverDnsPacket: %s \n", domainName);
    }
    MmFreeContiguousMemory(domainName);
}

函数首先检查 DNS 数据包的大小,如果小于 DNS 头部的大小,则退出函数

if (packetSize < sizeof(DNS_HEADER)) {
        return;
    }

接下来,函数检查 DNS 头部中的标志位IsResponse,如果为 true,则表示数据包是一个响应数据包,函数也退出:

DNS_HEADER* dnsHeader = (DNS_HEADER*)packet;
    if (dnsHeader->IsResponse) {
        return;
    }

接下来,函数计算出 DNS 数据部分的大小,如果 DNS 数据部分的大小大于整个数据包的大小,则退出函数:

size_t dnsDataLength = packetSize - sizeof(DNS_HEADER);
    // Check if DNS data length is greater than packet size
    if (dnsDataLength >= packetSize) {
        return;
    }

接下来,函数为 DNS 数据部分分配内存,并解析出 DNS 查询请求包中的域名:

// resolver DNS question packet and get domain Name
    char* dnsData = (char*)packet + sizeof(DNS_HEADER);
    char* domainName =
        reinterpret_cast<char*>(Tools::AllocContiguousMemory(allocDomainNameLength));
    if (domainName == nullptr) {
        return;
    }
    bool isSuccess = true;
    size_t domainNameLength = 0;
    while (dnsDataLength > 0) {
        const auto length = *dnsData;
        if (length == 0) {
            break;
        }
        if (length >= packetSize || length > 256) {
            break;
        }
        // Check if domain name length is greater than domainName array
        // length
        if (domainNameLength + 1 > allocDomainNameLength) {
            isSuccess = false;
            break;
        }
        auto domainNameStr = *(dnsData + 1);
        // 检查第一个字符是否是可读字符
        if (isprint(domainNameStr) == false) {
            isSuccess = false;
            break;

接着,函数检查域名是否提取成功,如果提取成功,则调用DebugPrint函数打印域名:

DebugPrint("ResolverDnsPacket: %s \n", domainName);

本段代码有一点点需要改进的地方,自行改进,不要复制粘贴,不出意外应该就看得到DNS包了:

卸载

卸载无非使用FwpsCalloutUnregisterById0FwpmProviderDeleteByKey0FwpmEngineClose0这几个函数,不再过多介绍,主要的问题是流的删除。
如果你不删除流就卸载了,驱动的object还会在内存中,你将无法再次启动驱动
删除流的函数是FwpsFlowRemoveContext0,
但是他会抽风。网上有两个是16年几个老外问的问题,FwpsFlowRemoveContext0会抽风,但是没人答复要怎么办。
这边建议卸载不掉等1秒再尝试卸载,因为是驱动的卸载函数,irql不会大于APC 可以使用KeDelayExecutionThread或者用信号量等待。当然我没实际遇到过这个问题。

值得注意的

一个坑,网上介绍WFP的时候几乎没看到提及的:

https://github.com/kahotv/PrintCallouts/blob/edae8d00f350d700adffba572e9161ee37636c73/src/driver/callouts.cpp#L116

# 网络安全技术
本文为 独立观点,未经允许不得转载,授权请联系FreeBuf客服小蜜蜂,微信:freebee2022
被以下专辑收录,发现更多精彩内容
+ 收入我的专辑
+ 加入我的收藏
相关推荐
  • 0 文章数
  • 0 关注者
文章目录