小漏洞大作为:CVE-2024-30089 是一个细微的内核漏洞,我利用它成功攻破了一台完全更新的Windows 11机器(启用了所有虚拟化安全和硬件安全缓解措施),并在今年的Pwn2Own比赛中首次获胜。
在本文中,我将概述我简单的漏洞挖掘方法:选择一个起点,然后直觉性地跟随路径直到某些东西引起我的注意。这个漏洞很有趣,因为它可以由于逻辑错误被可靠地触发。错误发生在一个特定状态的进程间通信系统中,导致了一个使用后释放。找到这个漏洞需要比较程序在其各种可能状态下的代码路径,我将在文中详细描述这个过程。漏洞的起源及微软的修补方法也同样令人着迷,这些主题也在本文中进行了讨论。
0-Day漏洞挖掘:从哪里开始?
关于漏洞研究,我经常收到的一个问题是如何开始。实际上,选择一个目标并坚持下去可能是研究过程中最难的一步。本文讨论的漏洞位于微软内核流服务(mskssrv.sys)中。查看这篇博客文章以了解该子系统的一般概况。在那篇文章中,我指出了MSKSSRV子系统的一些特点,可能使其成为一个好的攻击面,特别是它的进程间通信(IPC)机制。
MSKSSRV的代码库相当小,我发现的该子系统中的最后一个漏洞也在野外被独立利用为一个0-day。我还听说其他研究人员和公司也在审计这个驱动程序。因为这些,我最初陷入了一个常见的陷阱,以为在这个攻击面上没有更多的漏洞可发现。但因为我在之前的博客文章中建议过,我选择相信自己的直觉,继续寻找。
锁,锁:谁在那里?
一个获得新研究灵感的好方法是了解当前的研究。我读了一篇优秀的博客文章由k0shl撰写,激发了我寻找特定类型漏洞的灵感。在k0shl发现的漏洞中,一个对象的引用计数在没有正确锁定的情况下初始化和递增,创建了一个使用后释放窗口。尽管k0shl的漏洞是一个用户态漏洞,而不是内核中的漏洞,但易受攻击的库的编码风格让我想起了我之前审计MSKSSRV驱动程序的时候。
MS KS Server(MSKSSRV)通过FSStreamReg
和FSContextReg
对象与用户态进程交互。FSStreamReg
和FSContextReg
都是从基础FSRegObject
类派生的。我注意到FSContextReg
没有实现锁定虚表函数,而基础类(FSRegObject
)的实现只是一个*nop
*指令。这意味着实际上没有为FSContextReg
对象实现锁定机制。相反,FSStreamReg
实现了一个利用互斥锁的锁定函数。锁定机制在清理对象时使用,在函数FSRendezvousServer::Close
中:
代码块1:FSRendezvousServer::Close,锁定和解锁FSRegObjects的路径
有两个从FSRegObject
基础类派生的对象,但一个对象类型实现了适当的锁定机制,另一个没有。这对我来说很可疑。为了验证我的理论,我尝试使用对未受保护的FSContextReg
对象的引用来触发未定义行为。然而,由于全局FSRendezvousServer
对象上的锁定保护,该对象持有FSRegObjects
列表的指针,我无法触发任何有趣的行为,尽管FSContextReg
对象没有锁定保护。但我仍然感觉FSRegObject
的引用计数系统有些“不对劲”。只是我还不知道是什么。
MS KS Server中的IPC
正如我在上一篇博客文章中提到的,MSKSSRV的进程间对象共享方面是一个有趣的漏洞途径,所以我决定进一步关注它。子系统的IPC机制如下面的图表所示:
图表1:MS KS Server中的进程间通信
通过CreateFile打开一个MSKSSRV设备的文件句柄,会创建一个对应于该句柄的FILE_OBJECT。使用该句柄,进程可以通过使用DeviceIoControl函数向设备发送一个IOCTL来初始化一个新的流或上下文对象。初始化进程通过lpInBuffer
参数指定哪个远程进程可以注册该对象。远程进程现在可以使用一个新的文件句柄向MSKSSRV设备注册该对象,通过设备IOCTL进行注册。FSRegObject
的指针存储在Irp->CurrentStackLocation->FileObject->FsContext2
中。FSRegObject
对象的相同指针在FsContext2
中存储了两次,一次在用于初始化进程的FILE_OBJECT
中,另一次在用于注册进程的FILE_OBJECT
中。通过这种方式,对FSRegObject
对象的引用可以在进程间共享。例如,使用一个FSStreamReg
对象,多个进程可以访问流帧缓冲区,如图表1所示。FSRegObject
的引用计数初始化为1,然后在初始化完成后再次递增。注册FSRegObject
会再次增加其引用计数,总共每个对象有三个引用。
图表2:初始化和注册FSContextReg对象
发现漏洞
我决定再次查看我之前审计的锁定漏洞。我注意到函数FSRendezvousClose
调用FSRendezvousServer::Close
,在驱动程序的调度清理和关闭功能例程中调用:
代码块2:MS KS服务器驱动程序的DispatchCleanup和DipatchCleanup例程
Dispatch例程处理一种或多种类型的IRP,即打包的I/O请求。在Windows中,当所有文件的句柄引用都被关闭时,该文件的相应文件系统驱动程序会收到一个IRP_MJ_CLEANUP
和一个IRP_MJ_CLOSE
请求,这些请求由驱动程序的DispatchCleanup和DispatchClose函数例程处理。
在MSKSSRV中,如果存储在Irp->CurrentStackLocation->FileObject->FsContext2
中的指针不为NULL
,则在驱动程序的DispatchCleanup和DispatchClose函数例程中调用FSRendezvousClose
。在我之前分析的FSRendezvousServer::Close
中,有一个显著的地方是对调用者进程ID的各种检查。进程上下文很重要,因为所有内核模式代码都在单一的内核地址空间中操作,而这个地址空间是独立于用户模式地址空间的。每个进程都有自己的用户模式内存上下文,内核线程执行的进程上下文决定了如果线程访问用户地址,将访问哪个进程的用户模式地址空间。以下代码检查调用进程是否为初始化或注册进程:
代码块3:FSRendezvousServer::Close,对FSRegObjects的进程检查
特定于进程的信息存储在Irp->CurrentStackLocation->FileObject->FsContext2
在初始化或注册时存储的FSRegObject
中。驱动程序通过检查调用者的进程ID来确定需要释放哪些特定于进程的资源(如EPROCESS
对象、事件对象等)。如果进程是初始化或注册进程,则会对这些特定于进程的资源进行一些额外的清理。
这点引起了我的注意,因为一般来说,所有Dispatch例程都在任意进程上下文中执行,除了某些例外。换句话说,系统选择一个线程来执行Dispatch工作;选择哪个线程是任意的。我发现DispatchCleanup
在关闭最后一个句柄的进程上下文中被调用。因此,在这种情况下,进程ID检查是有意义的。然而,DispatchClose
是在任意进程上下文中被调用的。这意味着,如果FSRendezvousServer::Close
是由于IRP_MJ_CLOSE
请求被调用的,它将是在任意进程上下文中,这就破坏了进程ID检查的目的。这是一个很大的线索,表明这里有问题。
此外,作为Windows操作系统的一个特性,句柄可以与其他进程共享(通过子进程继承或使用DuplicateHandle API函数)。通过共享文件句柄,其他进程也可以与同一个FILE_OBJECT
进行交互。
图3:共享MS KS服务器设备句柄
因此,也有可能在DispatchCleanup
期间的进程上下文既不是初始化进程也不是注册进程。如果这些句柄中的任何一个被复制并共享给另一个进程,并且该进程是最后一个关闭句柄的进程,则DispatchCleanup
将在该外部进程的上下文中被调用(而不是初始化或注册进程)。
图4:外部进程关闭指向FILE_OBJECT的最后一个句柄
我注意到,当关闭一个文件句柄时,FSRendezvousServer::Close
函数可能会被调用两次(一次用于IRP_MJ_CLEANUP
请求,一次用于IRP_MJ_CLOSE
请求,见代码块2)。在该函数内,可能有两次调用FSRegObject::Release
(代码块4),如果引用计数降为0,该对象将被解除引用并释放其内存。这意味着每个句柄最多可以调用四次FSRegObject::Release
。此外,有两个文件句柄,FILE_OBJECT
通过FSContext2
指针指向同一个FSRegObject
。这意味着在同一个对象上最多可以调用八次FSRegObject::Release
,每个句柄四次。如果对象的初始引用计数只有三次,可能会由于过多的解除引用而触发使用后释放。这是我的思路。我知道程序结构可能会阻止达到理论上的最大解除引用次数,但也许还不足够。在这一点上,我觉得自己发现了一些问题,并决定进一步调查。
代码块4:FSRegObject::Release可以从FSRendezvousServer::Close中调用两次
一般来说,对对象的解除引用次数多于引用次数并不是唯一可能导致使用后释放的方式。然而,在这种情况下,我们可以确定,如果一个FSRegObject
已被释放,它的引用计数已降为零。在IRP_MJ_CLEANUP/CLOSE
IRP请求期间,最后一次有效的FSRegObject
访问发生在调用FSRegObject::Release
时。因此,如果可能出现使用后释放,调用FSRegObject::Release
总是会在对象已经被释放之后发生。在调用期间,对象将再次被解除引用。因此,计算解除引用的次数是找到这种特定情况下使用后释放的一种好方法。
剩下的唯一事情就是跟踪程序的可能状态,注意对象何时被释放和访问。我通过在每个可能的情况下,心理模拟程序逻辑在IRP_MJ_CLEANUP/CLOSE
请求期间的行为,从相应的Dispatch函数(代码块2)开始,完成了这项工作。
下面显示的是基于哪个进程关闭句柄的最终引用而产生的状态。每个条目代表如果相应进程关闭最后一个句柄时发生的FSContextReg
对象的解除引用次数。注意:句柄#1(初始化句柄)和句柄#2(注册句柄)之间没有功能差异,因为FileObject->FSContext2
字段在两个由相应句柄表示的FILE_OBJECT
中指向相同的内存。
FSContextReg在每个可能的MSKSSRV IPC状态下的解除引用
成功了!最后一个状态结果是四次解除引用:两次由外部进程完成,两次由初始化(也是注册)进程完成,而最初只有三次引用,这意味着可能出现使用后释放!我还重复了对FSStreamReg
对象的相同操作,但由于代码中的内存泄漏错误,在注册后实际上不可能释放FSStreamReg
对象。
漏洞
在进行上述虚拟机脑力练习时,我发现了这个问题。如果进程是初始化进程或注册进程,适当的清理会发生,并且存储在 Irp->CurrentStackLocation->FileObject->FsContext2
中的指针会被设置为 NULL
。下面的代码块展示了在调用者是初始化进程的情况下发生这种情况的地方:
代码块 5: FSRegObject::CloseInitProcess 将 FSContext 指针设置为 NULL
这意味着在 DispatchClose
(SrvDispatchClose
, 代码块 2) 中处理 IRP_MJ_CLOSE
请求的完成时,FSRendezvousClose
将不会再次被调用。
然而,如果调用进程是外部进程,则不会发生清理,并且 FSRegObject::Release
会在函数结束时被调用一次。由于 FileObject->FsContext2
不为 NULL
,在随后的 IRP_MJ_CLOSE
请求中会再次调用 FSRendezvousServer::Close
,并且会再次调用 FSRegObject::Release
。
现在,如果第二个句柄由同时初始化和注册了 FSContextReg 对象的进程关闭,则该对象将清理其所有存储的进程资源,使其变为空。这会导致在 FSRendezvousServer::Close
(代码块 4) 中调用 FSRegObject::Release
两次。额外的解引用用于处理对象变为空时的额外初始化引用。
这导致在单个 FSContextReg
对象上总共进行四次解引用,表明发生了“使用后释放”问题。
图示 5: CVE-2024-30089 描述
读者可能会想知道,为什么外部进程不能是最后一个关闭两个句柄的进程,因为这似乎也会导致四次解引用。在对象被析构并释放之前,它会从存储在全局 FSRendezvousServer
对象中的列表中解除链接。在 FSRendezvousServer::Close
开始时,检查 FSContext2
中的指针是否是列表的有效成员。在这种情况下,对象会在函数结束时第二次调用 FSRegObject::Release
时被释放。在第四次调用 FSRendezvousServer::Close
时,对象已经从列表中解除链接,成为无效对象,因此无法使用。为了触发“使用后释放”问题,必须发生在 FSRendezvousServer::Close
中检索并验证对象之后。下面的代码片段展示了通过该漏洞获得的“使用后释放”原始路径:
代码块 6: UAF 原始路径
攻击复杂性
在安全更新指南中,该漏洞的 CVSS 分数表示“利用可能性较高”,攻击复杂性为“低”。虽然 Microsoft 没有提供详细的评分解释,但我在修补其他漏洞时注意到了一些模式。该漏洞可能因其源于逻辑错误而被赋予了这个分数,使其可以可靠地触发。通过遵循图示 5 中概述的步骤,攻击者可以持续触发代码块 6 中描述的“使用后释放”场景。然而,这并不意味着实际利用它是简单的。下一部分将详细介绍利用步骤。
回顾
了解漏洞的发生过程对于培养主动的安全开发实践是很重要的。为了确定漏洞的引入方式,我分析了从Winbindex获得的驱动程序的早期版本,并查看了 FSRendezvousServer::Close
函数中的逻辑差异。
在漏洞部分,我提到这个错误的根本原因是当调用进程是外部进程时没有将 Irp->CurrentStackLocation->FileObject->FsContext2
设置为 NULL
。令我惊讶的是,我在早期版本的 mskssrv.sys 中看到了这一行代码:
代码块 7: 早期版本的 FSRendezvousServer::Close,FsContext2 被设置为 NULL
在上面的代码块中,无论前面的进程 ID 检查结果如何,FileObject->FsContext2
都被显式设置为 NULL
。这防止了在随后的 IRP_MJ_CLOSE
请求中再次调用 FSRendezvousServer::Close
,因此无法发生额外的解引用。奇怪的是——为什么这行代码被去掉了?让我们看看函数的一个较晚版本,漏洞首次被引入的地方:
代码块 8: 在功能标志检查中将 FsContext 设置为 NULL
上面的代码块中显示了一个对功能标志 Feature_Servicing_TeamsUsingMediaFoundationCrashes
的检查。功能标志是 Windows 的一个组件,用于切换各种功能和实验,尽管关于它们的公开信息不多。在之前的博客文章中,我们讨论了功能标志如何用于漏洞补丁。功能标志有时用于测试功能在正式采用之前的效果。在这种情况下,如果启用了 Feature_Servicing_TeamsUsingMediaFoundationCrashes
功能,则 FileObject->FsContext2
不会被设置为 NULL
,从而引入了漏洞。观察到 Windows 10 安装默认启用此功能。在 Windows 11 和代码块 1 中,功能标志条件不再存在,指针也没有被设置为 NULL
,使其同样存在漏洞。
由于功能的名称,我研究了Microsoft Teams的视频会议软件。我确认该应用程序可以使用 MSKSSRV 功能在进程之间共享媒体流。可能是流句柄共享导致 Teams 崩溃。一个有趣的进一步研究话题是检查 Teams 如何跨进程共享 MSKSSRV 设备句柄,以及为什么进行适当的指针清理会导致应用程序崩溃。
补丁
本系列的这一部分集中在漏洞本身,包括其补丁。我特别感兴趣于检查此漏洞的补丁,因为我提出的修复似乎在 Microsoft Teams 中引发了崩溃。值得一提的是,在这一点上,我还没有检查实际应用程序如何在实践中使用 MSKSSRV 驱动程序。没有这些背景信息,会使我们在理解系统设计方式时存在盲点。针对这个漏洞的完整补丁可能需要一些基础代码重构,并可能揭示有关 IPC 系统如何运作的更多细节。我还希望从 Microsoft 开发人员那里获得一些安全编码实践的见解。
令我失望的是,导致漏洞的逻辑错误,在 2024 年 6 月的安全更新中并没有直接得到解决。相反,在易受攻击的代码路径之前添加了访问令牌检查。请参见下面的 initialize context IOCTL
代码,由函数 FSRendezvousServer::InitializeContext
处理:
代码块 9:IOCTL 函数开始进行特性标志检查,并检查调用进程是否为帧服务器
上述函数首先检查一个特性是否启用。这可能是与补丁对应的特性标志。如果启用该特性,KsIsCurrentProcessFrameServer
必须返回 TRUE,否则返回 NTSTATUS
值 STATUS_ACCESS_DENIED
。
让我们看看 KsIsCurrentProcessFrameServer
:
代码块 10:KsIsCurrentProcessFrameServer 在调用线程的访问令牌上执行 SID 检查
该函数检查调用线程的令牌是否与两个特定的安全标识符(SID)匹配。这些 SID 对应于组 NT SERVICEFrameServer
中的令牌。如果调用线程的访问令牌中启用了任一 SID,则易受攻击的函数代码可以执行。
看到这些之后,我怀疑可能仍存在一个管理员到内核的漏洞。最终,内存损坏问题根本没有得到解决。我通过对原始漏洞利用进行轻微修改来确认这一点:管理员用户可以启动 FrameServer
服务,打开服务的句柄,并使用该句柄创建漏洞利用进程。我能够在完全修补的系统上获得完整的内核读/写原语。
尽管 Microsoft 并不认为管理员到内核是一个安全边界,但类似的漏洞已经被威胁行为者用来获得内核读/写原语,并用于 EDR 盲目操作和根套件操作。如果你对这种原语可以做什么感兴趣,可以查看我与 FuzzySec 的BlackHat 演讲。
结论与下一步
本文集中于我在 Pwn2Own 竞赛中的漏洞研究部分,涉及找到一个可以利用的 0-day 内核漏洞以进行权限提升。本文概述了这一过程:受到其他研究的启发,未能找到漏洞,选择新的角度,发现可疑之处,然后最终准确定位漏洞所在。现在,漏洞已被识别,并且有一个使用后释放原语,剩下的应该很简单,对吧?微软似乎这么认为,他们将此漏洞评级为“更可能被利用”,攻击复杂性为“低”。他们对吗?我将在下一部分中讨论这一点、利用策略,并揭示系列标题的含义!
参考文献
-
https://securityintelligence.com/x-force/critically-close-to-zero-day-exploiting-microsoft-kernel-streaming-service -
https://msrc.microsoft.com/update-guide/vulnerability/CVE-2024-30089 -
https://googleprojectzero.github.io/0days-in-the-wild//0day-RCAs/2023/CVE-2023-36802.html -
https://whereisk0shl.top/post/isolate-me-from-sandbox-explore-elevation-of-privilege-of-cng-key-isolation -
https://www.osr.com/nt-insider/2017-issue2/handling-cleanup-close-cancel-wdf-driver/ -
https://www.csoonline.com/article/1311082/north-koreas-lazarus-deploys-rootkit-via-applocker-zero-day-flaw.html
原文始发于微信公众号(3072):从k0师傅的博客中 获得 Pwn2Own full exploit的灵感
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论