一、研究背景
(一)操作系统内核漏洞
操作系统是计算机系统的核心软件,其主要功能在于管理计算机系统中的各种软硬件资源,并为计算机用户和用户程序提供访问这些资源的统一抽象。为达到这一设计目标,操作系统内核通常运行在称为内核态的高特权级执行环境中。一旦操作系统内核被恶意攻击者非法侵入,攻击者便可立即拥有对整个计算机系统的控制权,造成极大危害。因此,针对操作系统内核的漏洞挖掘一直是学术界和工业界的研究重点。
Windows操作系统是桌面计算机领域最广泛应用的操作系统之一,在网络空间中扮演着至关重要的角色。与之对应的,Windows操作系统中存在的安全漏洞也往往具有广泛的影响和严重的潜在危害。攻击者可以利用这些漏洞来进行大规模的网络攻击,给网络安全带来严重的威胁。例如,在2017年流行的WannaCry勒索病毒攻击,就是利用了MS17-010“永恒之蓝”漏洞,其受害者遍布全球多国,造成了巨大的损失。
在Linux操作系统上,以Syzkaller为代表的模糊测试工具可以7x24小时不间断地对内核进行漏洞挖掘,并自动化地生成漏洞报告供维护者查阅。然而,在Windows操作系统上,还没有出现影响力和测试效果能够比拟Syzkaller的开源工具。造成这一差异的原因是多方面的,例如,研究人员可以通过对Linux源码进行静态分析得到供Syzkaller使用的Syzlang模版,其中包含对系统调用接口的完整描述,由此可以驱动模糊测试。但在Windows上,由于内核是闭源的,对其系统调用接口的分析过程更为复杂和困难。
通过分析近年来披露的Windows内核态漏洞,我们发现,这些漏洞大多是由独立的研究人员或安全研究团队发现,并且极大地依赖于专家知识,只在特定的情况下使用了模糊测试等自动化方法来辅助漏洞挖掘。奇安信天工实验室近年来在Hyper-V中发现的漏洞,也同样离不开安全研究员对Hyper-V整体架构和漏洞模式的深入理解。
有鉴于此,笔者也针对近年来披露的Windows内核态漏洞进行了人工的分析和研究,并主要聚焦于竞态条件这一漏洞类型。本文便是对相关研究发现的总结。
(二)竞态条件漏洞
竞态条件是计算机科学中的一个重要概念,它指的是在多线程或多进程环境中,由于不恰当的同步操作或竞争资源的访问而导致的不确定性行为。在现代操作系统中,多线程的程序调度是至关重要的一项任务,它直接影响了计算机系统的性能和资源利用效率。多线程编程允许程序同时执行多个任务,从而提高了系统的响应速度和并发处理能力。然而,为了有效地管理这些线程,操作系统必须具备高效的调度机制,决定哪个线程获得CPU时间片,以确保各个线程能够合理地分享计算资源。这一调度决策是基于一系列算法和策略进行的,涉及到线程的优先级、状态管理、抢占机制以及资源争用解决等多个方面。
这一调度机制也导致多个线程或进程在争夺资源的同时,执行顺序并不确定,因此可能会产生意想不到的结果。为了避免问题,多线程场景下对关键资源的访问需要利用加锁、同步等机制来保证安全。倘若多个线程同时访问共享变量或资源,而没有适当的同步机制来保护这些资源,这可能导致数据损坏、程序崩溃或不一致的结果,从而带来严重的安全和可靠性问题。
图1:典型的竞态条件成因,两个线程无保护地访问同一个内核对象
如图1所示,漏洞CVE-2022-29142的原因正是两个线程可以同时访问同一个内核对象,如果其中一个线程试图关闭该对象的句柄,另一个线程便可能访问已被释放的指针。
本文将会具体介绍竞态条件漏洞带来的潜在风险,并描述复现真实存在于Windows内核中的竞态条件漏洞时的发现。
二、漏洞分析采用的关键技术
在进行针对Windows内核的漏洞分析和研究时,主要采用的关键技术包括:补丁对比分析、二进制逆向分析、竞态条件构建和调试等。
(一)补丁对比及二进制逆向分析
通过在CVE数据库中的检索,发现近年来披露的Windows内核漏洞中不乏用户提权和任意代码执行等高危漏洞。然而,由于微软积极控制漏洞的影响,相关漏洞的公开PoC程序数量相对较少。对于那些没有公开PoC的漏洞,往往只能获得漏洞发现者通过博客或社交媒体透露的少量信息,难以由此开展系统性的漏洞研究。考虑到这些漏洞的修复补丁存在于Windows安全更新中,通过对补丁进行分析,包括进行补丁的解包和二进制对比等,能够有效地从中提取出更新内容,识别受影响的模块,并可以进一步地从中提取关键的补丁点,由此分析补丁所修复的漏洞。
为了能够解析微软的Windows安全补丁,首先需要了解补丁的格式以及获取方法。打包补丁的文件格式包括:.MSU(Microsoft Standalone Update)和.CAB(Cabinet)格式。补丁一般会作为 Windows 更新的一部分自动分发到用户设备上,但也可以直接从微软的更新目录中下载独立的补丁。此前,微软主要提供顺序的更新包,它们必须依次安装到用户的系统中。如今,更新以累积的方式提供,这意味着基本系统版本中的所有必需更新都包含在补丁包中,这也允许用户平滑地升级系统版本。此外,出于节省带宽等考虑,许多更新以增量的方式分发,即:更新包中只包含对特定二进制目标的修补方式,而不包含全部的文件。这也进一步增加了补丁分析的难度。
对于每次安全更新,具体的补丁文件可以从Microsoft Update Catalog上获取。.MSU格式的补丁在使用expand.exe程序解包后,将能够获得.CAB格式的补丁文件,且这些文件按照指令集和二进制差异类型进行命名。对于补丁内容的进一步提取,将依赖于微软提供的msdelta.dll库。该库中提供了ApplyDelta系列函数用于执行Windows系统更新。通过C或Python语言调用相关库函数,即可实现打补丁的过程,获得补丁之后的二进制文件。
图2:漏洞CVE-2023-21537补丁对比
最后,通过BinDiff或Diaphora等二进制对比工具,即可完成对补丁内容的分析,并由此定位到漏洞点。如图2所示,以漏洞CVE-2023-21537为例,通过对比分析补丁前后的函数控制流图,可以发现补丁新增了参数检查的分支(见红色框)。在确定可能的漏洞点后,即可开展人工的逆向分析。
(二)Windows内核调试
在成功确定漏洞点后,下一步是构造PoC程序,并触发漏洞行为,例如使内核出现崩溃,造成蓝屏死机(Blue Screen of Death)。再此基础上,可以进一步构造漏洞利用的方式,例如,利用释放后使用(Use after free)漏洞来覆盖关键的内核数据结构,实现进程提权的效果。
为了触发内核中的漏洞代码的执行,需要构造用户态程序执行系统调用或驱动程序的IoControl调用。这些函数调用往往需要大量的参数,并且,参数需要满足一定的约束条件。在实际的测试中,发现通过人工构造的参数难以一次性通过检查,必须不断进行调试并修改PoC程序。
微软提供了WinDbg程序用于支持Windows内核调试,但由于在内核函数中触发断点等操作会中断整个系统的执行,因而必须在另一台计算机上运行WinDbg,并通过TCP连接至待调试的计算机。在实际的实验中,相关的内核调试借助Hyper-V虚拟机完成。经笔者测试,运行在Hyper-V虚拟机中的Windows系统开启内核调试模式并设置端口和密钥参数后,即可在Host上通过WinDbg程序开启TCP连接进行内核调试。
三、典型漏洞分析
笔者总共分析和研究了10个近年来被披露并分配了CVE编号的Windows内核竞态条件安全漏洞,具体的漏洞编号、内核模块和漏洞类型情况如表1所示。下面将通过案例分析介绍其中的典型漏洞,以及对未来针对此类漏洞进行自动化挖掘的启发。
CVE ID | 内核模块 | 漏洞类型 |
---|---|---|
CVE-2018-7249 | secdrv.sys | UAF |
CVE-2018-8410 | ntoskrnl | Double dereference |
CVE-2018-8611 | ntoskrnl | UAF |
CVE-2020-1015 | UMPS | UAF |
CVE-2021-26868 | win32k | UAF |
CVE-2021-40449 | win32k | UAF |
CVE-2021-41335 | ntoskrnl | OOB |
CVE-2022-29142 | ntoskrnl | UAF |
CVE-2023-21536 | ETW | UAF |
CVE-2023-21537 | mqac.sys | Double fetch |
表1:分析研究的Windows内核竞态条件安全漏洞
(一)CVE-2018-7249漏洞分析
该漏洞存在于Windows的secdrv.sys驱动程序中,是一个UAF类型的漏洞。该漏洞的复现版本为Windows 7 7600。
secdrv.sys驱动程序提供了IoControl调用号为0x0CA002813的接口,其处理函数sub_11A88接收三种不同的输入参数:0x96、0x97和0x98,并根据参数调用不同的派发函数。其中:0x96从分页内存池中分配内存块,进行初始化操作,并将其中一部分复制到用户提供的缓冲区中(此处还存在一个未初始化访问的问题,导致内核态的16比特内存泄漏到用户态)。0x97读取先前由0x96分配的块,并对用户输入缓冲区进行加密。0x98则用于释放由0x96分配的块。
图3:处理函数根据输入参数a2执行不同操作
当调用IoControl类型0x97时,它通过标签找到之前由类型0x96分配的内存块。这里的漏洞在于,0x97使用内存的过程缺少加锁和同步机制。在其操作内存期间,这段内存可能被释放,因此如果恶意攻击者成功赢得竞态条件,将触发释放后使用的问题。进一步地,如果攻击者在IoControl类型0x97的操作期间,使用类型0x98成功释放了块,并重新分配了一个由攻击者控制的新块到完全相同的内存位置,那么这将有可能最终劫持驱动程序的执行控制流,并在内核态执行任意代码。由于0x97中的加密函数在用户提供的缓冲区上执行,这个缓冲区可以非常大,因此加密可能需要很长时间才能执行,从而为IoControl类型0x98提供了完美的时间窗口来达成竞态条件。
该漏洞存在公开的Exp程序,经过简单调试后即可稳定实现漏洞利用,获得SYSTEM权限。
图4:复现漏洞CVE-2018-7249实现提权
该漏洞的触发方式较为简单,且输入参数也没有较为苛刻的约束条件,因而通过自动化漏洞挖掘工具发现该漏洞是可能的。现有的竞态条件漏洞挖掘方案可以自动由顺序执行的IoControl调用序列生成多线程的序列,并探索不同线程交错的情况下其执行过程是否出现异常。不过,这仍需要先通过对驱动程序的分析,推断出相关的IoControl调用号,并指导模糊测试器通过启动多个线程来分别执行0x97和0x98对应的IoControl操作。有关问题仍需进一步研究。
(二)CVE-2021-41335漏洞分析
该漏洞存在于Windows的内核ntoskrnl.exe中。漏洞的原因是两个内核函数:ObpCreateSymbolicLinkName与ObpDeleteSymbolicLinkName可能操作同一个_OBJECT_SYMBOLIC_LINK内核对象,但它们并未正确进行加锁操作,因而存在竞态条件安全漏洞。该漏洞将导致越界访问,并可以被进一步利用,导致内核任意地址写,造成严重的破坏。该漏洞的复现版本为Windows 10 21H1 19043.928。
触发漏洞的调用链是:NtCreateSymbolicLinkObject系统调用会执行ObInsertObjectEx来创建符号链接对象,该函数先调用ObpCreateHandle来为新的符号链接对象创建一个新的句柄,再调用ObpCreateSymbolicLinkName创建符号链接。然而,在ObpCreateHandle执行结束后,用户态程序就已经拥有了指向内核对象的有效句柄。这意味着,在随后ObInsertObjectEx调用ObpCreateSymbolicLinkName时,用户态进程可以开启另一个线程并尝试通过该句柄操作仍在被内核函数使用的新符号链接对象。例如,可以在另一个线程中通过NtClose函数调用关闭该句柄,这显然会导致问题。
为了进一步明确漏洞的成因,通过人工逆向分析比较ObpCreateSymbolicLinkName和ObpDeleteSymbolicLinkName的代码,可以注意到:ObpDeleteSymbolicLinkName将符号链接对象的DosDeviceDriveIndex成员设置为0;而ObpCreateSymbolicLinkName读取符号链接对象的该成员并将其减小1。此外,ObpCreateSymbolicLinkName使用减小后的DosDeviceDriveIndex值作为_DEVICE_MAP结构中的DriveType数组的索引。
图5:符号链接对象的DosDeviceDriveIndex成员被置零
由于加锁操作存在问题,恶意攻击者可以试图使DosDeviceDriveIndex成员已经被置零后,又进行自减操作,由此产生整数下溢。又由于该数值被用于数组索引,这将导致越界访问。
为了复现此漏洞,需要创建两个线程,第一个线程通过NtCreateSymbolicLinkObject不断创建新的符号链接对象,而第二个线程则通过NtClose不断关闭新创建的符号链接对象句柄。通过这样做,很可能出现ObpDeleteSymbolicLinkName在ObpCreateSymbolicLinkName之前被调用的线程交错情况。因此,ObpCreateSymbolicLinkName将以值0xFFFFFFFF作为索引访问DriveType数组,导致在数组边界之外写入任意值,最终引发内核崩溃。具体的漏洞复现情况和调试器读出的寄存器参数如图6、图7所示,可以看到,执行NtClose的线程进行了多于35万次尝试,才触发了导致漏洞的线程交错方式。
图6:运行PoC程序,两个线程分别创建和销毁内核对象
图7:WinDbg捕获蓝屏死机的情况,可见rcx寄存器值存在整数下溢
通过补丁对比分析可以发现,该漏洞的修复方式是由ObpCreateHandle来调用ObpCreateSymbolicLinkName创建符号链接,避免用户态进程提前对句柄进行操作。
(三)CVE-2023-21537漏洞分析
该漏洞于2023年1月披露,并已被微软修复。漏洞并没有公开的PoC程序,漏洞发现者只通过博客文章透露了部分信息。该漏洞的复现版本为Windows 10 21H1 19043.928。
该漏洞存在于消息队列(MSMQ)驱动程序mqac.sys中。消息队列是Windows的可选功能,需要在控制面板中手动启用,并在计算机管理中创建消息队列。微软为消息队列的开发提供了头文件mq.h以及运行时库mqrt.dll,其中包含MQOpenQueue和MQSendMessage等函数。这些函数其实是封装了对驱动程序mqac.sys发起的IoControl调用,通过构造合法的参数,用户态程序也可以直接使用NtDeviceIoControlFile发送IoControl调用,触发此驱动程序的功能。
该漏洞的成因是mqac.sys中的ACSendMessage函数会两次读取一个来自用户的输入参数,第一次该参数用于控制数组长度,第二次则是在释放堆内存时,根据该长度进行释放。然而,这段逻辑并未考虑参数会被用户修改的可能,因而构成一个Double fetch漏洞,可能导致错误的内存被释放。
mqac.sys中负责处理IoControl调用的函数名称为ACDeviceControl,该函数将会解析用户传入的参数,并调用不同的派发函数。通过逆向分析ACDeviceControl函数,发现当IoControl调用号为0x19658107且输出缓冲区的总长度为0x2C0时,它会进一步调用ACSendMessage这一派发函数。ACSendMessage函数首先将用户态缓冲区复制到内核态栈上的缓冲区,之后,将执行核心的业务逻辑,调用 CQueue::PutNewPacket来发送用户请求的数据,完成后再调用 ACFreeDeepCopyQueueFormat进行堆内存的释放。此处便存在漏洞:进行内存释放操作传入的第二个参数直接读取自用户态缓冲区中。如图8所示,参数a4就是指向缓冲区的指针。
图8:漏洞函数重复读取了用户输入
这意味着,如果恶意攻击者设法赢得竞态条件,在ACSendMessage函数进行内存释放前成功地修改了此处的参数值,将其设置为较大的值,那么将导致超出范围地释放任意指针。
经过进一步分析,触发漏洞时消息队列虚拟设备需要处于特定的状态,这需要PoC程序预先执行其它的IoControl调用。同时,用户缓冲区中偏移量584个字节处应当是一个合法的指针,才能通过消息队列驱动程序的参数检查。在满足这些约束条件的情形下,PoC程序启动两个线程,一个用于发送IoControl调用,另一个则修改用户缓冲区中偏移量592个字节处的值为较大值。通过人工构造参数并编写PoC程序发起IoControl请求,可以成功地使内核崩溃,如图9、图10所示。
图9:漏洞导致蓝屏,原因为BAD_POOL_CALLER
图10:WinDbg观察触发漏洞的调用栈,与预期的相同
四、漏洞总结
在本章中,笔者将概括Windows内核竞态条件漏洞的一些模式,并说明对漏洞的分析如何指导模糊测试方法或者开展相关的研究工作。
在研究中观察到,这些竞态条件漏洞的发现的时间范围从2018年至2023年,系统版本跨越Windows 7至Windows 11。其中,消息队列驱动程序中的漏洞,已经存在超过10年的时间才被发现。该驱动程序是Windows的可选功能,默认并未开启,可以推测此前的很长时间内也没有被安全研究人员所注意。事实上,Windows系统中存在大量的老旧代码,它们可能并未经过充分的测试,因此包含潜在的安全漏洞。
发现漏洞所处的模块也具有多样性。除去Windows内核ntoskrnl.exe外,还有相当部分的漏洞存在于Windows的图形子系统和内核驱动程序中。触发这些漏洞的方法也各不相同,包括ntoskrnl系统调用、win32系统调用和IoControl调用等。
根据对驱动程序中所存在的竞态条件漏洞的观察,它们往往可以通过多线程同时发起IoControl调用来触发,但IoControl的参数必须通过严格的检查才能使驱动程序的控制流到达漏洞点。特别地,对CVE-2023-21537漏洞的研究体现出,通过人工方法来触发漏洞需要大量的专家知识,并且构造参数、调试PoC程序的效率较低。这意味着,如果要开展自动化模糊测试,必须通过静态分析或符号执行等方式从驱动程序中提取信息,才能对巨大的输入参数空间进行划分,提高测试的效率。否则,模糊测试可能无法到达更深层的代码逻辑中,效果上也将无法达到人工漏洞挖掘的效果。
此外,研究中还发现,具体漏洞类型主要包括UAF、OOB和Double fetch这三类。其中,UAF漏洞数量较多。对于竞态条件导致的UAF漏洞,往往需要在特定的时间窗口内触发,因此即使执行了漏洞代码,也不一定能够直接产生崩溃。在实际的测试中,PoC程序也往往需要运行约数秒到数十秒的时间才能赢得竞态条件。因此,要自动化地挖掘此类漏洞,必须对内核态的内存访问违例行为进行监测,也即为模糊测试器部署对应的Sanitizer。
由于竞态条件漏洞的出现伴随着线程交错的随机性,其相比于通过顺序的系统调用序列来测试内核更为复杂。目前学术界对内核竞态条件漏洞挖掘的研究往往针对Linux等开源操作系统,而在Windows这一闭源目标上,还没有诞生成熟的竞态条件漏洞挖掘工具。
总而言之,对Windows内核竞态条件漏洞的挖掘仍有很多值得探索的方向,期待未来能在学术界和工业界看到更多的创新成果,希望本文能起到抛砖引玉的效果。