星期二补丁- 星期三利用:24 小时内破解 WinSock (afd.sys) 的 Windows 辅助功能驱动程序

原文地址:Patch Tuesday -> Exploit Wednesday: Pwning Windows Ancillary Function Driver for WinSock (afd.sys) in 24 Hours

“星期二补丁,星期三利用” 是一句古老的黑客格言,指的是每月安全补丁公开后的第二天,漏洞就被武器化。随着安全性的提高和漏洞缓解措施变得更加复杂,制作武器化漏洞所需的研究和开发量也随之增加。这与内存损坏漏洞尤其相关。

然而,随着 Windows 11 内核中添加新功能(以及内存不安全的 C 代码),可能会引入成熟的新攻击面。通过研究这个新引入的代码,我们证明了可以轻易武器化的漏洞仍然经常发生。在这篇博文中,我们分析并利用了 Winsock 的 Windows 辅助功能驱动程序 afd.sys 中的一个漏洞,用于 Windows 11 上的本地权限升级 (LPE)。虽然我们之前都没有使用此内核模块的经验,但我们能够在大约一天内诊断、重现该漏洞并将其武器化。您可以在 [此处] (https://github.com/xforcered/Windows_LPE_AFD_CVE-2023-21768)找到漏洞利用代码。

补丁差异和根本原因分析

根据微软安全响应中心(MSRC)发布的CVE-2023-21768的详细信息,该漏洞存在于辅助功能驱动程序(AFD)中,其二进制文件名为 afd.sys。AFD 模块是 Winsock API 的内核入口点。利用这些信息,我们分析了 2022 年 12 月的驱动程序版本,并将其与 2023 年 1 月新发布的版本进行了比较。这些样本可以从 Winbindex 单独获取,无需从 Microsoft 补丁中提取更改的耗时过程。分析的两个版本如下所示。

  • AFD.sys / Windows 11 22H2 / 10.0.22621.608 (December 2022)
  • AFD.sys / Windows 11 22H2 / 10.0.22621.1105 (January 2023)

Ghidra 用于为这两个文件创建二进制导出,以便可以在 BinDiff 中对它们进行比较。匹配功能的概述如下所示。

Figure 2 — Binary comparison of AFD.sys

似乎只有一个函数发生了变化,afd!AfdNotifyRemoveIoCompletion。这大大加快了我们对漏洞的分析速度。然后我们比较了这两个功能。下面的屏幕截图显示了在 Binary Ninja 中查看反编译代码时修补前后代码的变化。

Pre-patch, afd.sys version 10.0.22621.608.

Figure 3 — afd!AfdNotifyRemoveIoCompletion pre-patch

Post-patch, afd.sys version 10.0.22621.1105.

Figure 4 — afd!AfdNotifyRemoveIoCompletion post-patch

上面显示的此更改是对已识别功能的唯一更新。一些快速分析表明,正在根据以下内容执行检查

PreviousMode 。 如果 PreviousMode 为 0 (表明调用源自内核),则将值写入由未知结构中的字段指定的指针。另一方面,如果 PreviousMode 不为零,则 ProbeForWrite函数被调用确保该字段中设置的指针是驻留在用户模式中的有效地址。

补丁前版本的驱动程序中缺少此检查。由于该函数有一个针对 PreviousMode 的特定 switch 语句,因此假设开发人员打算添加此检查但忘记了(我们有时都缺咖啡☕!)。

从这次更新中,我们可以推断攻击者可以通过未知结构的 field_0x18 处的受控值到达此代码路径。如果攻击者能够使用内核地址填充此字段,则可以创建任意内核 Write-Where 原语。此时,尚不清楚正在写入什么值,但任何值都可能用于本地权限升级原语。

函数原型本身包含 PreviousMode 值和指向未知结构的指针,分别作为第一个和第三个参数。

Figure 5 — afd!AfdNotifyRemoveIoCompletion function prototype

逆向工程

我们现在知道漏洞的位置,但不知道如何触发有漏洞的代码路径的执行。在开始进行概念验证 (PoC) 之前,我们将进行一些逆向工程。

首先,交叉引用易受攻击的函数以了解其使用地点和方式。

afd!AfdNotifySock 中对易受攻击的函数进行了一次调用。

我们重复该过程,寻找对 AfdNotifySock 的交叉引用。我们发现没有对该函数的直接调用,但其地址出现在名为 AfdIrpCallDispatch 的函数指针表上方。

Figure 7 — afd!AfdIrpCallDispatch

该表包含 AFD 驱动程序的调度例程。调度例程用于通过调用来处理来自 Win32 应用程序的请求 DeviceIoControl. 每个函数的控制代码可在 AfdIoctlTable 中找到。

然而,上面的指针并不像我们预期的那样位于 AfdIrpCallDispatch 表中。从 Steven Vittitoe 的 Recon 谈话幻灯片中,我们发现 AFD 实际上有两个调度表。第二个是 AfdImmediateCallDispatch 。通过计算该表的开头与存储 AfdNotifySock 的指针之间的距离,我们可以计算 AfdIoctlTable 的索引,该索引显示该函数的控制代码是 0x12127

Figure 8 — afd!AfdIoctlTable

值得注意的是,它是表中最后一个输入/输出控制(IOCTL)代码,表明 AfdNotifySock 很可能是最近添加到 AFD 驱动程序中的新调度函数。

此时,我们有几个选择。我们可以在用户空间对相应的 Winsock API 进行逆向工程,以更好地了解底层内核函数是如何调用的,或者对内核代码进行逆向工程并直接调用它。我们实际上并不知道哪个 Winsock 函数对应于 AfdNotifySock ,因此我们选择了后者。

我们发现 x86matthew 发布的一些代码通过直接调用 AFD 驱动程序来执行套接字操作,放弃了 Winsock 库。从隐秘的角度来看,这很有趣,但就我们的目的而言,它是一个很好的模板,可以创建 TCP 套接字的句柄以向 AFD 驱动程序发出 IOCTL 请求。从那里,我们能够到达目标函数,正如在内核调试时到达 WinDbg 中设置的断点所证明的那样。

Figure 9 — afd!AfdNotifySock breakpoint

现在,回顾一下 DeviceIoControl 的函数原型,通过它我们从用户空间调用 AFD 驱动程序。参数之一 lpInBuffer 是用户模式缓冲区。如上一节所述,该漏洞的发生是因为用户能够在未知数据结构中将未经验证的指针传递给驱动程序。该结构是通过 lpInBuffer 参数直接从我们的用户模式应用程序传入的。它作为第四个参数传递到 AfdNotifySock 中,并作为第三个参数传递到 AfdNotifyRemoveIoCompletion 中。

此时,我们不知道如何填充 lpInBuffer 中的数据(我们将其称为 AFD_NOTIFYSOCK_STRUCT ),以便通过到达 AfdNotifyRemoveIoCompletion 中易受攻击的代码路径所需的检查。我们逆向工程过程的其余部分包括遵循执行流程并检查如何访问易受攻击的代码。

让我们逐一检查一下。

我们遇到的第一个检查是在 AfdNotifySock 的开头:

Figure 10 — afd!AfdNotifySock size check

此检查告诉我们 AFD_NOTIFYSOCK_STRUCT 的大小应等于 0x30 字节,否则函数会失败并显示 STATUS_INFO_LENGTH_MISMATCH

下一个检查验证结构中各个字段中的值:

Figure 11 — afd!AfdNotifySock structure validation

当时我们不知道这些字段对应什么,所以我们传入一个 0x30 字节数组,其中填充了 0x41 字节(AAAAAAAAA…)。

我们遇到的下一个检查是在调用之后 ObReferenceObjectByHandle. 该函数将输入结构的第一个字段作为其第一个参数。

Figure 12 — afd!AfdNotifySock call nt!ObReferenceObjectByHandle

该调用必须返回成功才能继续到正确的代码执行路径,这意味着我们必须将有效的句柄传递给 IoCompletionObject 。没有正式记录的方法可以通过 Win32 API 创建该类型的对象。然而,经过一番搜索,我们发现了一个未记录的 NT 函数 NtCreateIoCompletion 之后,我们到达一个循环,其计数器是结构中的值之一:

Figure 13 — afd!AfdNotifySock loop

该循环检查结构中的一个字段,以验证它包含有效的用户模式指针并将数据复制到其中。每次循环迭代后指针都会递增。我们用有效地址填充了指针,并将计数器设置为 1。从这里,我们最终能够到达存在漏洞的函数 AfdNotifyRemoveIoCompletion

Figure 14 — afd!AfdNotifyRemoveIoCompletion call

进入 AfdNotifyRemoveIoCompletion 后,第一个检查是结构中的另一个字段。它必须是非零的。然后将其乘以 0x20,并与结构体中的另一个字段一起作为指针参数传递到 ProbeForWrite 。从这里,我们可以使用有效的用户模式指针 (pData2) 和字段 dwLen = 1 进一步填充结构(以便传递给 ProbeForWrite 的总大小等于 0x20 ),并且检查通过。

Figure 15 — afd! Afd!AfdNotifyRemoveIoCompletion field check

最后,在到达目标代码之前要通过的最后一个检查是对 IoRemoveCompletion 的调用,它必须返回 0 (STATUS_SUCCESS)。

该函数将阻塞,直到:

  • 完成记录可用于 IoCompletionObject 参数

  • 超时到期,作为函数的参数传入

我们通过结构控制超时值,但简单地将超时设置为 0 不足以让函数返回成功。为了使该函数无错误地返回,必须至少有一个可用的完成记录。经过一番研究,我们发现了未记录的功能 NtSetIoCompletion 它手动增加 IoCompletionObject 上的 I/O 挂起计数器。在我们之前创建的 IoCompletionObject 上调用此函数可确保对 IoRemoveCompletion 的调用返回 STATUS_SUCCESS

Figure 16 — afd!AfdNotifyRemoveIoCompletion check return nt!IoRemoveIoCompletion

触发任意写位置

现在我们可以到达易受攻击的代码,我们可以用任意要写入的地址填充结构中的适当字段。我们写入该地址的值来自一个整数,该整数的指针被传递到对 IoRemoveIoCompletion 的调用中。 IoRemoveIoCompletion 将此整数的值设置为调用 KeRemoveQueueEx 的返回值。

Figure 17 — nt!KeRemoveQueueEx return value

Figure 18 — nt!KeRemoveQueueEx return use

在我们的概念验证中,该写入值始终等于 0x1。我们推测 KeRemoveQueueEx 的返回值是从队列中删除的项目数,但没有进一步调查。此时,我们已经有了所需的原语,并继续完成漏洞利用链。我们后来证实了这个猜测是正确的,并且可以通过对 IoCompletionObject 上的 NtSetIoCompletion 进行额外调用来任意增加写入值。

LPE with IORING

由于能够在任意内核地址写入固定值 (0x1),我们继续将其转换为完整的任意内核读/写。由于此漏洞影响最新版本的 Windows 11(22H2),因此我们选择利用 Windows I/O 环对象损坏来创建我们的原语。Yarden Shafir 撰写了许多关于 Windows I/O 环的优秀文章,并且还开发并披露了我们在漏洞利用链中利用的原语。据我们所知,这是该原语首次被用于公共漏洞利用。 当用户初始化 I/O 环时,会创建两个独立的结构,一个在用户空间,一个在内核空间。 这些结构如下所示。

内核对象映射到 nt!_IORING_OBJECT,如下所示。

Figure 19 — nt!_IORING_OBJECT initialization

请注意,内核对象有两个字段:RegBuffersCountRegBuffers,它们在初始化时被清零。该计数指示有多少 I/O 操作可以在 I/O 环中排队。另一个参数是指向当前排队操作列表的指针。

在用户空间端,调用 kernelbase!CreateIoRing时,如果成功,您将返回一个 I/O 环句柄。该句柄是指向未记录结构 (HIORING) 的指针。我们对这种结构的定义是从 Yarden Shafir 所做的研究中获得的。

1
2
3
4
5
6
7
8
9
10
11
typedef struct _HIORING {
HANDLE handle;
NT_IORING_INFO Info;
ULONG IoRingKernelAcceptedVersion;
PVOID RegBufferArray;
ULONG BufferArraySize;
PVOID Unknown;
ULONG FileHandlesCount;
ULONG SubQueueHead;
ULONG SubQueueTail;
};

如果某个漏洞(例如本博文中提到的漏洞)允许您更新 RegBuffersCountRegBuffers 字段,则可以使用标准 I/O Ring API 来读取和写入内核内存。

正如我们在上面看到的,我们可以利用该漏洞在我们喜欢的任何内核地址写入 0x1。要设置 I/O 环原语,我们只需触发该漏洞两次即可。

Figure 20 — nt!_IORING_OBJECT first time triggering the bug

在第二个触发器中,我们将 RegBuffers 设置为可以在用户空间中分配的地址(例如 0x0000000100000000)。

Figure 21 — nt!_IORING_OBJECT second time triggering the bug

备注: 需要验证两次的原因就是 RegBuffersCountRegBuffers 分别验证是否能通过带有缺陷的驱动写入。

剩下的就是通过在用户空间地址 (0x100000000) 写入伪造的 nt!_IOP_MC_BUFFER_ENTRY 结构的指针来对 I/O 操作进行排队。条目数应等于 RegBuffersCount 。下图中突出显示了此过程。

Figure 22 — Setting up user space for I/O Ring kernel R/W primitive

下面的屏幕截图显示了这样一个 nt!_IOP_MC_BUFFER_ENTRY 。请注意,操作的目标是内核地址 (0xfffff8052831da20),并且在本例中操作的大小为 0x8 字节。从结构中无法判断这是读操作还是写操作。操作的方向取决于使用哪个 API 对 I/O 请求进行排队。利用 kernelbase!BuildIoRingReadFile 导致任意内核写入,kernelbase!BuildIoRingWriteFile 导致任意内核读取。

Figure 23 — Example faked I/O Ring operation

为了执行任意写入,I/O 操作的任务是从文件句柄读取数据并将该数据写入内核地址。

Figure 24 — I/O Ring arbitrary write

相反,为了执行任意读取,I/O 操作的任务是读取内核地址处的数据并将该数据写入文件句柄。

Figure 25 – I/O Ring arbitrary read

Demo

设置原语后,剩下的就是使用一些标准内核后利用技术来泄漏系统(PID 4)等提升进程的令牌并覆盖不同进程的令牌。

Exploiting Windows 11 with Ancillary Function Driver vulnerability CVE-2023-21768

在野利用

在我们的漏洞代码公开后,来自360 Icesword Lab的Xiaoliang Liu(@flame36987044)首次公开披露,他们在今年早些时候发现了利用该漏洞的样本(ITW)。 ITW 样本使用的技术与我们的不同。攻击者使用相应的 Winsock API 函数 ProcessSocketNotifications 触发漏洞,而不是像我们的漏洞利用那样直接调用 afd.sys 驱动程序。

360冰剑实验室官方声明如下:

“360冰剑实验室专注于APT检测与防御。基于我们的0day漏洞雷达系统,今年1月份我们在野外发现了CVE-2023-21768的利用样本,该样本与 @chompie1337@FuzzySec 公布的利用样本不同,它是通过系统机制和漏洞特征进行利用的。该漏洞与 NtSetIoCompletionProcessSocketNotifications 有关, ProcessSocketNotifications 获取调用 NtSetIoCompletion 的次数,因此我们使用它来更改权限计数。”

结论和最终反思

您可能会注意到,在逆向工程的某些部分,我们的分析是肤浅的。有时,仅观察一些相关的状态变化并将程序的某些部分视为黑匣子是有帮助的,以避免陷入不相关的兔子洞。这使我们能够快速扭转漏洞,尽管最大化完成速度不是我们的目标。此外,我们对 afd.sys 中所有报告的漏洞进行了补丁差异审查,这些漏洞被标记为“利用可能性更大”。我们的审查显示,除了两个漏洞之外,所有漏洞都是由于对从用户模式传入的指针验证不当造成的。这表明,了解过去的漏洞(尤其是特定目标内的漏洞)的历史知识,对于发现新漏洞可能会卓有成效。当代码库扩展时,同样的错误很可能会重复。请记住,新的 C 代码 == 新的 bug 😀。正如发现上述漏洞在野外被利用所证明的那样,可以肯定地说,攻击者也在密切监视新的代码库添加。

Windows 内核中缺乏对管理员模式访问保护 (SMAP) 的支持,这给我们提供了丰富的选项来构建新的纯数据利用原语。这些原语在支持 SMAP 的其他操作系统中不可行。例如,考虑 CVE-2021-41073,这是 Linux 的 I/O 环预注册缓冲区实现中的一个漏洞(我们在 Windows 中滥用相同的功能用于 R/W 原语)。此漏洞可以允许覆盖已注册缓冲区的内核指针,但不能用于构造任意 R/W 原语,因为如果该指针被替换为用户指针,并且内核尝试在那里读取或写入,系统将崩溃。

尽管微软尽了最大努力来消除人们喜爱的漏洞利用原语,但肯定会发现新的原语来取代它们。我们能够利用最新版本的 Windows 11 22H2,而不会遇到 HVCI(内存完整性和基于虚拟化的安全性) 等基于虚拟化的安全功能的任何缓解或限制。

引用


星期二补丁- 星期三利用:24 小时内破解 WinSock (afd.sys) 的 Windows 辅助功能驱动程序
https://xxxxnnxxxx.github.io/2023/07/04/CVE-2023-21768/
作者
xxxxnnxxxx
发布于
2023年7月4日
许可协议