Guided tour inside WinDefender’s network inspection driver
本文介绍了 Windows Defender 如何通过 WFP(Windows 过滤平台)在内核中实现其网络检测功能,设备对象的安全描述符如何保护它免受潜在漏洞的影响,并详细说明了我发现的一些漏洞。作为本文的补充,我还发布了一个小工具用于测试这些不同的漏洞。
研究意义
对于任何逆向工程师来说,理解事物的运作方式本身就很有趣,而闭源软件更是让这一过程充满挑战。Windows 系统经历了很大的演变,杀毒软件也需要与时俱进。随着 Patchguard 的出现,以及操作系统越来越注重安全性,内核钩子基本上已经消失。然而,Windows 提供了多种方式来收集对象(进程、线程、文件、注册表等)的信息,如通知回调、过滤器、事件等。人们可能会好奇杀毒驱动程序是如何收集信息以及收集什么类型的信息。研究 Windows Defender 是很有意义的,因为它可能会使用 Windows 提供的所有最新功能。最后,n4r1b[1] 关于 Windows Defender 内核组件(如 WdFilter 和 WdBoot)的博客系列非常有趣,这促使我对这些组件进行深入研究。
基于 WFP 的驱动程序
Windows 过滤平台允许在网络协议栈的不同层设置过滤器,并提供了丰富的功能来处理网络流量:数据篡改、注入、应用策略、重定向、审计等。
MSDN 页面 About Windows Filtering Platform[2] 详细描述了其所有功能和运作方式。
过滤概念
本文描述的驱动程序文件版本为 4.18.2102.3-0。
如前所述,网络检测驱动程序 WdNisDrv 主要依赖于 WFP 模型。该架构相当复杂,但基本上驱动程序需要在特定层或子层上注册过滤器,指定过滤条件,然后为该过滤器提供一组称为 callout 的回调。
WFP 架构[3]
可以通过执行以下命令来转储系统上当前配置的不同过滤器:
netsh wfp show filters
它将输出一个非常详细的 XML 文件,其中包含系统上当前配置的过滤器、回调、条件、层等信息。
要了解数据包在遍历网络协议栈时经过的完整层列表,可以参考 TCP 数据包流[4] 页面。该页面方便地将 TCP 标志与建立连接时的层进行了映射。UDP 版本也是可用的。
在 XML 文件中搜索 "windefend" 这个词时,可以检索到不同层的配置。例如,查看 FWPM_LAYER_STREAM_V4 层时,可以发现关联并注册了 windefend_stream_v4 回调。
标志 FWP_CALLOUT_FLAG_CONDITIONAL_ON_FLOW 表示需要将"上下文"与数据流关联,回调才能被调用。其关联的过滤器表明,如果回调被注销,它将允许流量通过,并且回调可以阻止或允许数据流的处理。它实际上是与 FWPM_LAYER_ALE_FLOW_ESTABLISHED_V4 层协同工作的。
MSDN 指出,该层用于在建立 TCP 连接时发出通知。回调将负责创建一个"用户定义"的 FLOW_CONTEXT 结构,这对于调用数据流层回调是必需的。关联的过滤器具有以下需要满足的条件才能触发回调:
-
流的方向必须是出站(FWP_DIRECTION_OUTBOUND);
-
IP 协议必须是 TCP 或 UDP;
-
回调只能检查数据包,不能终止流量(FWP_ACTION_CALLOUT_INSPECTION)。
总结一下 IPv4 数据包在网络检测驱动程序中的流程:当建立连接时,数据包将通过 FWPM_LAYER_ALE_FLOW_ESTABLISHED_V4 层。然后,如果前一层创建了流上下文,数据包将通过数据流/数据报层过滤器。关闭连接时,数据包经过相同的层,流上下文被删除。在每一层都会执行一组通过回调注册的回调函数。
回调
回调的注册是通过调用 FwpsCalloutRegister2 实现的,该函数以 FWPS_CALLOUT2_ 结构作为参数。它由一个与过滤器关联的 GUID 和三个不同的回调组成:通知、分类和删除。过滤驱动程序不使用第一个。继续 IPv4 数据包流程,驱动程序仅为 FWPM_LAYER_ALE_FLOW_ESTABLISHED_V4 注册了一个分类函数。它具有以下原型:
void FWPS_CALLOUT_CLASSIFY_FN2(
const FWPS_INCOMING_VALUES0 *inFixedValues,
const FWPS_INCOMING_METADATA_VALUES0 *inMetaValues,
void *layerData,
constvoid *classifyContext,
const FWPS_FILTER2 *filter,
UINT64 flowContext,
FWPS_CLASSIFY_OUT0 *classifyOut
)
如前所述,该函数负责创建流上下文,该上下文将与两个端点之间通信过程中交换的所有数据包相关联。它使用不同的参数来填充以下结构。
FWPM_LAYER_STREAM_V4 层的回调注册了分类和删除函数。分类函数的主要目标是确定是否应该丢弃数据包。它通过查看 FLOW_CONTEXT 结构的 FilterFlag 成员来实现这一点。该标志基于网络保护的当前配置。删除函数的主要目标是释放、释放和删除与连接相关的所有内容,如 FLOW_CONTEXT。
一个非常重要的点是,这些函数中的每一个都会创建一个通知供用户层服务 WdNisSvc 处理。这个进程由 NisSrv.exe 可执行文件支持,负责分析来自接收到的通知的网络流量。根据一个旧的符号文件(PDB 现在已不可用)和快速分析,它包含各种解析器(HTTP、json...)。
通知
基本上,用户层服务向 WdNisDrv 驱动程序发送特定的 IOCTL 来请求连接通知。驱动程序使用取消安全 IRP 队列来跟踪请求,并在调用回调时完成它们。连接通知以头部开始,后面跟着一个取决于通知类型的联合体。
typedefstruct {
unsignedlonglong CreationTime;
unsignedlonglong NotificationType;
} _CONNECTION_NOTIFICATION_HEADER;
typedefstruct {
_CONNECTION_NOTIFICATION_HEADER Header;
union {
_FLOW_NOTIFICATION FlowNotification;
_STREAM_DATA_NOTIFICATION StreamDataNotification;
_ERROR_NOTIFICATION ErrorNotification;
_FLOW_DELETE_NOTIFICATION FlowDeleteNotification;
};
} _CONNECTION_NOTIFICATION;
例如,当建立连接时,该层的分类函数会创建以下通知(进程路径会附加到该结构中):
typedefstruct {
unsignedlonglong FlowHandle;
unsignedshort Layer;
unsignedint CalloutId;
unsignedint IpProtocol;
unsignedchar FilterFlag;
union {
SOCKADDR_IN IPv4;
SOCKADDR_IN6 IPv6;
SOCKADDR_STORAGE_LH IPvX;
} LocalAddress;
union {
SOCKADDR_IN IPv4;
SOCKADDR_IN6 IPv6;
SOCKADDR_STORAGE_LH IPvX;
} RemoteAddress;
unsignedint ProcessId;
unsignedlonglong ProcessCreationTime;
unsignedchar IsProcessExcluded;
unsignedint ProcessPathLength;
} _FLOW_NOTIFICATION;
流数据通知的结构相对简单。其最重要的字段是数据本身(附加在通知后面),以及 StreamFlags 成员,该成员用于指示数据包是出站还是入站,并包含其他标志位(PSH、URG)。
typedefstruct {
unsignedlonglong PktExchanged;
unsignedlonglong FlowHandle;
unsignedshort Layer;
unsignedint CalloutId;
unsignedshort StreamFlags;
unsignedshort IsStreamOutbound;
unsignedint StreamSize;
} _STREAM_DATA_NOTIFICATION;
因此,在发送任何数据之前,服务就已经知道是哪个程序建立了连接,以及它与哪个 IP 地址和端口进行通信。然后,它可以在该层跟踪已发送和接收的数据包。
我开发了一个小工具,可以实时查看这些通知。
在收集了有关过滤器、回调和通知的各种信息后,就可以理解检查驱动程序如何检索网络信息以及它获取哪些信息来跟踪数据流。下表总结了所有不同的回调、过滤器、条件和标志。
配置和附加功能
Set/Get/Add-MpPreference
通过"Windows 安全中心"应用程序配置 Windows Defender 时,暴露的选项非常有限。但是,可以使用 Defender PowerShell 模块来访问更多配置选项(过滤级别、排除项等)。该模块由 ProtectionManagement.mof 描述,并在同名 dll 中实现。Get-MpPreference 用于检索当前配置,Add-MpPreference 和 Set-MpPreference 用于设置配置。
如图所示,有四种类型的排除项,但最有趣的是 ExclusionIpAddress,因为这个选项在 GUI 中是不可用的。本质上,当修改配置时,会向驱动程序发送一个 IOCTL。例如,当排除一个 IP 地址时,SOCKADDR_STORAGE 结构列表会被传递给驱动程序并添加到AVL 树[5]中;然后,在建立连接时,目标地址将与该树进行比对。进程也是如此,但方案略有不同,以提供更大的灵活性。在初始化期间,WdNisDrv 驱动程序注册一个回调,该回调将通过_CallbackWdProcessNotificationCallback_被 WdFilter 驱动程序"通知"。另一个驱动程序通过 PsSetCreateProcessNotifyRoutine 实现进程通知回调。同样创建一个 AVL 树,每个进程通知都会更新该树,无论是创建新进程、终止进程还是排除状态发生变化。在建立连接时,进程 ID 将与该树进行比对。
该模块通过 EnableNetworkProtection 参数提供了配置网络保护级别的功能,有 3 个不同级别。它具有以下枚举值:Disabled、Enabled、AuditMode。_Enabled_会阻止被认为是恶意的 IP 地址和域名,而_AuditMode_不会阻止,只是简单地创建与本应被阻止的连接相关的 Windows 事件。日志名称为"Microsoft-Windows-Windows Defender/Operational",相关事件为 1125 和 1126。它们显示了访问的目标地址和访问它的程序。这两种模式都会为用户层服务生成连接通知以供解析。
相关 IOCTLs
如前所述,可以通过 I/O 控制代码配置驱动程序。以下是 IOCTL 及其功能列表:
-
0x226005:设置过滤状态
-
0x226009:设置进程例外(可执行文件路径列表)
-
0x226011:在协议栈中注入流或数据报
-
0x226015:设置 IP 地址排除
-
0x22A00E:请求连接通知
如可以看到,一个 IOCTL 允许直接将数据包或数据报注入到网络栈中。例如,如果需要在流层中注入一些数据,应该打开一个连接并检索其 FlowHandle。这个值连同数据和流层的_calloutid_通过 IRP 的输入缓冲区传递,并通过调用 FwpsStreamInjectAsync0 来注入数据包。下图展示了我开发的工具在开放连接中注入数据包的情况。没有流通知传递给用户层服务。
通过调用 FwpsInjectTransportSendAsync0 也可以在数据报层实现相同功能。这个 ioctl 的目的尚不清楚,但它肯定具有重要功能。
安全性快速概述
一个 ACL 之隔的混乱
在初始化过程的早期,驱动程序调用 WdmlibIoCreateDeviceSecure 例程来创建设备对象。它在_DEVICE_OBJECT _flag_成员上设置 DO_EXCLUSIVE 位。随后,它继续在设备对象上应用安全描述符。
unpack('<IIIII', SHA1.new("WDNISSVC".encode('utf-16le')).digest())
(3668810961, 2468724468, 4084584310, 3029221373, 430494444)
安全描述符基本上规定只有 WdNisSvc 服务才能在设备对象上打开句柄。
为了测试不同的 IOCTL,我开发了一个脚本,该脚本主要是移除设备的独占打开权限,并且在安全描述符的控制成员中只保留 SE_SELF_RELATIVE 标志。这将有效地移除设备上的所有保护。然而,由于设备的独占特性,每当收到 IRP_MJ_CLEANUP(很可能是通过调用_CloseHandle_)时,驱动程序就会停止过滤。
我发现的以下安全漏洞如果不停用上述机制是无法触及的。为了重现以下问题,必须使用 Windbg Preview 创建内核调试会话。然后,当驱动程序加载并初始化后,可以执行脚本来移除保护。
dx Debugger.State.Scripts.mod_sec.Contents.open_dev()*
IP 地址排除中的整数溢出
处理 IP 地址排除 IOCTL 的函数需要一个 QWORD 作为计数整数,后跟一个描述 IP 地址的 SOCKADDR_STORAGE 数组。
所需字节数的计算和检查方式如下:(IO_STACK_LOCATION.Parameters.DeviceIoControl.InputBufferLength < count * sizeof(sockaddr_storage) + sizeof(count))。
由于没有考虑溢出,如果我们设置 count 变量最高 7 位中的至少一位,我们就可以溢出并通过检查。然而,count 仍然在循环中用作要插入的 SOCKADDR_STORAGE 的数量。当代码试图访问未映射的内存页面时,这将导致蓝屏。
数据报注入中的越界读取
处理 IOCTL 的函数对输入缓冲区进行一些初步检查,以验证长度是否正确。缓冲区包含一个"注入头"。
首先,它检查输入缓冲区是否足够大以容纳 30 字节长的头部结构。然后将头部大小加上头部内指定的大小,并检查输入缓冲区的大小是否等于或大于结果。但是,可以将此大小字段设置为零并通过检查。在这种情况下,当调用流注入函数时,会进行两次越界读取:一次在注入头部之后的偏移量 0x1E 处,第二次在偏移量 0xB6 处。
在调试时,可以通过查看输入缓冲区长度和正在进行的访问来观察这个漏洞。
第一次甚至第二次越界读取导致蓝屏的可能性很小。第一个值用于确定数据包是否包含一些额外数据,而第二个值用于告知其大小。然而,这些值似乎不会导致另一个漏洞。
通过连接通知的栈泄漏
分析连接通知的创建过程揭示了它们由一个指定类型的头部和基于该类型的联合体组成。这形成了一个 134 字节长的结构。在创建通知时,驱动程序在栈上使用临时通知结构。然后动态分配一块内存并用零初始化。但是,它使用 XMM 寄存器将栈上的临时结构复制到分配的内存中。例如,_FLOW_DELETE_NOTIFICATION 只指示正在删除的流句柄,因此连接通知长度为 24 字节。由于联合体结构,分配并复制了 134 字节。换句话说,从栈复制到发送给用户层的缓冲区中多出了 110 字节(并泄漏)。此外,在完成 IRP 时,驱动程序指定大小为 134。以下两张图显示了实用程序显示_delete_连接通知的情况;浅蓝色矩形是正在泄漏的栈地址,对应于实际的流删除回调。
测试漏洞
为了评估这些漏洞是否真的可以触发,我分享了一个小工具和一个 WinDbg Preview 脚本,允许打开设备句柄。该工具可以在这里[6]找到。
按照前面描述的执行脚本后,可以使用以下选项启动工具:
-
inject:将文件中的数据包注入到开放的连接中
-
notify:显示实时连接通知
-
bsod:通过 IP 地址排除漏洞触发整数溢出
-
ipexclu:随机生成 IPv4 地址以排除(仅用于测试目的)
大多数选项都很容易理解,但 inject 和 notify 命令需要一些额外步骤。在这两种情况下,EnableNetworkProtection 选项必须设置为 1 或 2,这可以通过以管理员权限执行以下 PowerShell 命令来实现。
Set-MpPreference -EnableNetworkProtection1
然后可以使用 notify 命令运行该工具。例如,当打开 Edge 浏览器时,应该会显示一长串通知。如果不起作用:终止工具,将网络保护级别更改为 1 或 2,然后重新运行该工具。
需要注意的是,当终止工具时,过滤器会被注销。由于驱动程序并非设计为可被多个进程查询,其清理主要功能(当用户层进程关闭句柄时调用)会通过调用 FwpsCalloutUnregisterById0 来注销回调。
在测试连接内的数据包注入时,需要检索其_流句柄_和过滤引擎回调 ID。第二个可以通过 XML 文件获取,或者像第一个一样通过工具的 notify 命令获取。
例如,让我们设想这样一个场景:调试机器 A 想要向 HTTP 服务器 B 发出请求。在设置 notify 命令后,A 可以开始查询 B。流句柄可以在_flow established_连接通知中获取。应该保持 notify 命令运行,并打开另一个终端,使用带有检索到的参数的 inject 命令。
披露时间线
-
2021 年 5 月 28 日:向 MSRC(微软安全响应中心)报告漏洞。
-
2021 年 6 月 1 日:MSRC 开启案例。
-
2021 年 6 月 21 日:MSRC 确认了发现,但不符合安全更新发布的标准。案例关闭。
结论
深入研究防病毒软件的内部机制并观察它如何使用系统收集信息,从攻击和防御的角度来看都非常有趣。这有助于调整现有机制,并更深入地了解后台实际发生的情况。遗憾的是,由于设备对象上的 DACL 限制,我发现的漏洞无法被触发,但这是一次很好的代码分析练习。
参考资料
n4r1b: https://www.n4r1b.com/posts/
[2]About Windows Filtering Platform: https://docs.microsoft.com/en-us/windows/win32/fwp/about-windows-filtering-platform
[3]WFP 架构: https://docs.microsoft.com/en-us/windows/win32/fwp/images/wfp-architecture.png
[4]TCP 数据包流: https://docs.microsoft.com/en-us/windows/win32/fwp/tcp-packet-flows
[5]AVL 树: https://en.wikipedia.org/wiki/AVL_tree
[6]这里: https://github.com/quarkslab/wdnis_tool/
原文始发于微信公众号(securitainment):Windows Defender 网络检测驱动程序内部导览
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论