深入分析CVE-2021-1732漏洞
简介
2021年2月,Dbappsecurity公司在野外发现了一个利用Windows10 x64系统上的一个零日漏洞的恶意软件样本。
该漏洞(CVE-2021-1732)是一个win32k窗口对象类型混淆导致的OOB(越界)写入漏洞,可用于在Windows内核中实现任意内存读写原语,以实现本地权限提升。攻击者在利用内存损坏型漏洞时,通常需要借助于读、写和执行原语,以绕过诸如Windows 10等操作系统上的DEP、ASLR和CFG等安全措施。不过,纯数据攻击只需要读写原语,因为它并不谋求在内存中执行恶意代码,而是操纵操作系统使用的数据结构以实现提权。
内核漏洞通常是最复杂的一类攻击,因为它们直接与Windows内核进行交互。这种攻击一旦得逞,攻击者就能获得系统的高级权限,从而提高整个攻击链的威力。就本文来说,该漏洞是一个本地权限提升(LPE)漏洞,影响的是64位Windows10 1909版本。最初发现的样本是在2020年5月编译的,并于2020年12月报告给微软。通过搜索引擎,我们找到了研究人员在2021年3月公开的exploit。实际上,这个exploit的公开,可能会被攻击者所利用。虽然我们没有发现明确的证据表明这个POC已经被恶意使用,但我们确实发现一些变体已经上传到VirusTotal网站进行测试。
在这篇文章中,我们将对这个漏洞进行深入的分析,以确定相应的检测和保护措施。该漏洞利用GetMenuBarInfo API使用了一个新的win32k任意内核内存读取原语,这是一种新颖的攻击手段,据我们所知,该原语以前从未公开过。
深入分析CVE-2021-1732漏洞
漏洞CVE-2021-1732的利用过程可以分为六个阶段,最终目标是将相应进程的权限升级到System级别,具体如下图所示:
图1 CVE-2021-1732漏洞利用的过程
在深入讨论细节之前,我们必须提供用于CVE-2021-1732漏洞的win32k攻击原语的背景知识。
Win32K背景知识
Win32k是微软Windows子系统的图形(GUI)组件;通常情况下,为了提高性能,大多数组件都存在于内核中。该组件被用于Windows操作系统桌面的图形打印。但是,由于win32k架构的原因,win32k的内核组件仍然需要能够通过用户模式回调函数对用户模式下的代码进行调用,以方便窗口的创建和管理。
对于内核中的用户模式回调函数,早在2008年和2010年,研究人员就已经对其进行了深入的考察,而Mandt则在2011年进行了非常全面的总结。win32k内核函数(如xxxCreateWindowEx)将通过用户进程PEBKernelCallBackTable来生成回调函数(如xxxClientAllocWindowClassExtraBytes)。
当用户模式回调函数完成后,将会执行NtCallbackReturn,并将预期的返回参数传递回内核。由于这些回调函数的无状态特性,安全研究人员已经发现了许多与对象锁定机制有关的漏洞:这些机制可能会导致UAF漏洞。
实际上,Win32k一直都是Windows内核中爆出漏洞最多的组件之一,其数量占了2010年至2018年间所有漏洞的63%,这是因为相对于ntdll系统库而言,Win32k的系统库攻击面要大的多。Win32k漏洞通常会利用称为tagWND数据结构的桌面对象,然后借助于内核读/写原语转化为纯数据攻击。
纯数据攻击(data-only attacks)过程通常分为两步:
- 发现漏洞。
- 使用对象字段(如tagwnd.cbwndextra)上的特定OS API来利用现有或新的读/写原语。
tagWND数据结构具有两个字段,这使得它成为内核内存中读/写的主要目标;这两个字段分别是tagwnd.cbwndextra和tagWND.ExtraBytes。当使用CreateWindowEx创建窗口时,可以在注册窗口类时通过WNDCLASSEXA结构体中的cbWndExtra字段直接在内存中的tagWND对象之后请求额外的内存字节。
其中,额外字节数是由cbWndExtra字段控制的,分配的额外内存地址保存在ExtraBytes字段中。读/写原语创建过程如下所示:
- 找到一个用于对内存中名为WND0的tagWND对象执行写操作的漏洞(如UAF)。
- 在内存中先前损坏的WND0附近分配另一个名为WND1的tagWND对象。
- 将wnd0.cbwndextra覆盖为一个非常大的值,如0xFFFFFFF。
- 在WND0上调用一个API,比如SetWindowLongPtr,以越界写入WND1中的相关字段。
目前,已经发现多种利用Win32k内核的用户模式回调函数的漏洞,如CVE-2014-4113、CVE-2015-0057、MS15-061、CVE-2016-7255和CVE-2019-0808漏洞,它们都可以利用Windows内核的tagWND读/写功能来实现提权。
Win32k攻击原语
我们在攻击者使用的CVE-2021-1732漏洞中观察到了多个攻击原语;并且,值得一提的是,其中一些是新出现的,以前从未在野外见过。
在Windows RS4之前,攻击者可以轻松通过多种技术来泄漏tagWND内核地址,例如调用HMValidateHandle将tagWND对象从内核空间复制到用户空间桌面堆。Windows10的最新版本已经针对这些攻击技术进行了安全加固。
但是,借助于spmenu内核地址泄漏技术和相关的tagWND桌面堆偏移量,只要找到可以覆盖tagWND.CBWNDExtra字段的安全漏洞,就有可能实现内核级别的读/写原语,而无需泄漏实际的tagWND内核地址。我们发现的exploit中采用的spmenu技术在这里和这里都使用过,但我们不知道GetMenuBarInfo API以前是否在win32k漏洞利用中曾经用过。
下图显示了CVE-2021-1732中使用的攻击原语。
图2 CVE-2021-1732攻击原语
当前Windows操作系统中的缓解措施
实际上,微软OSR团队、Mandt、GoogleProject Zero、Schenk和Dabah已经开展了大量工作,如通过添加新的和改进原有的缓解措施来提高win32k的安全性,以抵御EoP攻击。这些缓解措施包括:
- 类型隔离(使用相同类型的tagWND对象)。
- Win32k过滤(仅限于Edge浏览器,而不是整个进程,但自从这项研究开展以来,win32k API过滤能力有了显著改进,如在win32k.sys中增加了_stub_UserSetWindowLong、_stub_UserSetWindowLongPtr和_stub_UserGetMenuBarInfo)。
- 隔离内核空间桌面堆并删除用户空间桌面堆中的内核地址(可以使用本文后面描述的用户和内核空间桌面堆中的相对偏移)。
- 删除win32k驱动程序中的数据类型符号(旨在混淆而不是缓解)。
如果恶意程序利用了CVE-2021-1732漏洞的话,上述缓解措施都无法提供保护。然而,这些并不影响谷歌浏览器和微软Edge,因为前者不允许调用win32k(Windows 8和更高版本),而后者则会过滤win32k调用。
漏洞的触发与补丁分析
当使用CreateWindowEx API创建窗口时,Windows操作系统将创建一个tagWND对象。如上所述,我们可以通过一个参数创建该窗口;并且,该参数将使用CBWNDExtra分配额外的内存。
在窗口创建过程中(CreateWindowEx API),将触发一个名为xxxClientAllocWindowClassExtraBytes的回调函数,以根据tagwnd.cbwndExtra(偏移量为0xC8)的值的大小,在用户模式下的桌面堆中为tagWND.ExtraBytes(偏移量为0x128)分配内存空间(关于WND1,请参见下面的图3和图4)。
图3 WND1的内核模式tagWND
图4 WND1的用户模式tagWND
该内存的位置将存储为指向桌面堆的用户模式内存指针,并放置在tagWND.ExtraBytes处。然后,可以使用NtUserConsoleControl将普通窗口转换为控制台窗口,该窗口将把位于tagWND.ExtraBytes的用户模式指针转换为指向内核桌面堆的偏移量值(请参见下面的图5中的WND0)。在xxxClientAllocWindowClassExtraBytes回调窗口期间,攻击者可以利用tagWND.ExtraBytes(窗口类型混淆)中的值的变化实现OOB写入。
图5 WND0用户模式tagWND
图6 触发win32kfull!xxxCreateWindowEx中的类型混淆漏洞
就像图6展示的那样,触发该漏洞需要以下步骤:
- 获取一个指向user32.dll中的HMValidateHandle内联函数的指针。
- 钩住PEB KernelCallBack表中xxxClientAllocWindowClassExtraBytes函数。
- 使用CreateWindowEx API创建多个窗口(我们只使用创建的前两个窗口:WND0和WND1),以便在相邻的内存中创建两个窗口。
- 在WND0和WND1上调用HMValidateHandle,这将把它们的对象从内核空间的桌面堆复制到用户空间的桌面堆。在tagwnd+0x8处,将一个偏移量存储到桌面堆中;这个偏移量对于用户和内核空间的桌面堆来说,都是一样的。这里有的话,漏洞利用代码就可以通过这些偏移量值计算内核空间的桌面堆中WND0和WND1之间的相对距离,以供稍后进行越界读写时使用。根据下面的表1可知,通过使用这些偏移量,就不必泄漏实际的WND0和WND1内核地址了,因为可以相对于这些偏移量进行读写操作(用户和内核空间的桌面堆具有相同的偏移量)。
表1 用户和内核空间的桌面堆具有相同的偏移量
5. 然后通过调用NtUserConsoleControl将WND0.ExtraBytes从用户空间桌面堆指针转换为内核空间桌面堆中的一个偏移量,从而将WND0转换为一个控制台窗口。这样的话,后面WND0就可以向WND1执行OOB写操作了。
6. 使用CreateWindowExAPI创建恶意窗口WND_Malicious
在窗口创建过程中,回调函数xxxClientAllocWindowClassExtraBytes被执行,以请求用户模式为WND_Malicious.cbWndExtra分配内存,并将用户空间的桌面堆指针传回内核函数win32kfull!xxxCreateWindowEx。
xxxClientAllocWindowClassExtraBytes现在已经被钩住了,在返回win32kfull!xxxCreateWindowEx之前,我们可以执行下列任务:
调用NtUserConsoleControl将WND_Malicious转换为一个控制台窗口,以便将其WND_Malicious.cbWndExtra从一个用户空间桌面堆指针转换为内核空间桌面堆内的一个偏移量。
最后调用NtCallbackReturn,完成回调并返回单个值给xxxClientAllocWindowClassExtraBytes。这里没有像xxxClientAllocWindowClassExtraBytes所期望的那样把用户空间桌面堆指针传回给内核,而是把WND0+0x08的值传回给了内核空间桌面堆的偏移量,具体如下图7所示。现在,我们只要在WND_Malicious上调用SetWindowLongW,就能向WND0写东西了。
图7 WND_Malicious
补丁分析
这个漏洞的根源在于:win32kfull!xxxCreateWindowEx在启动xxxClientAllocWindowClassExtraBytes和获得NtCallbackReturn的响应的过程中,并没有检查窗口类型是否发生了变化。
当我们在上面的钩子函数中用WND_Malicious调用NtUserConsoleControl时,xxxConsoleControl会检查tagWND+0xE8标志是否已经被设置为0x800,该值代表一个控制台窗口,如下图所示。由于WND_Malicious是作为一个普通窗口创建的,而xxxConsoleControl在内核空间桌面堆的某个偏移处分配内存,然后释放WND_Malicious.ExtraBytes(0ffset0x128)处的用户空间桌面堆指针。然后,它会把这个刚分配的内存的偏移量放在内核堆中的WND_Malicious.ExtraBytes(偏移量为0x128)中,并把tagWND+0xE8标志设置为0x800,以指出这是一个控制台窗口。
当我们执行NtCallbackReturn时,回调函数返回后,xxxCreateWindowEx并不会检查窗口类型是否已发生变化,而是直接将WND0+0x08写入WND_Malicious.ExtraBytes中,具体如图9所示。虽然RedirectFieldpExtraBytes会检查WND_Malicious.ExtraBytes的初始值,但为时已晚,因为WND0+0x08已写入WND_Malicious.ExtraBytes(偏移量为0x128)。
图9 win32kfull!xxxCreateWindowEx(易受攻击的版本)
修复后的win32kfull.sys已经更新了xxxCreateWindowEx,以便在将返回的值从用户模式写入tagWND.ExtraBytes(偏移量为0x128)之前检查ExtraBytes的初始值。
图10 Win32kfull!xxxCreateWinDowex(修复后的版本)
如同图11所示,在正常的窗口创建过程中,ExtraBytes会在xxxCreateWindowEx中初始化为零。
图11 正常窗口的创建过程中,tagWND.ExtraBytes的初始化
如图12所示,在控制台窗口创建期间,tagWND.ExtraBytes将被初始化为xxxConsoleControl内核空间桌面堆中的新偏移量值。RedirectFieldpExtraBytes只是检查这个初始化的值,以确定窗口类型是否已经改变。此外,微软还增加了遥测功能,用于检测补丁版本中窗口类型标志的变化。
图12 控制台窗口的tagWND.ExtraBytes初始化
tagWND OOB写原语
存在于xxxCreateWindowEx API中的这个漏洞,允许攻击者将WND_Malicious.ExtraBytes字段设置为内核空间桌面堆中WND0偏移量值。现在,只要针对wnd_valisable调用SetWindowLongW,它都将写入wnd0。通过提供值为0xC8的偏移量,该函数将覆盖wnd0.cbwndextra字段,使其值为0xFFFFFFF,具体如下面的图13和图14所示。
这意味,着它可以越过自己的tagWND结构体和内核内存中的ExtraBytes对WND1中的各个字段执行写操作。此外,WND0.extrabytes也被一个相对于其自身的偏移量所覆盖,因此,针对WND0的SetWindowLongPtrA的调用将写入到内核桌面堆中相对于WND0起始位置的偏移量处。
图13 从WND_Malicious到WND0的越界写操作
图14 利用WND_Malicious的OOB写入原语,用0xFFFFFFF覆盖WND0cbWndExtra
内核地址泄漏
既然WND0.cbwndextra字段已经设置为非常大的值(0xFFFFFFF),那么在WND0上调用SetWindowLongPtrA时,它将写入内核内存中相邻的WND1中,如下图15所示。通过写入WND1中的特定字段,我们可以实现内核地址内存泄漏,具体如下所示:
- 将值0x4000000000000000000写入WND1类型的字段中,以临时将其更改为子窗口,详见下面的图15和16。
- 在WND0上调用值为-12(GWLP_ID)的SetWindowLongPtrAAPI,这样就可以用一个伪造的spmenu数据结构体覆盖WND1的spmenu字段(类型为tagMENU),因为我们已经根据如图15和17所示那样将其更改为子窗口。
- 根据SetWindowLongPtrA API的说明文档,返回值将为我们提供覆盖偏移量处的原始值,即作为内核内存地址的spmenu数据结构指针。因此,我们现在泄漏了一个指向内核内存中的spmenu(type tagMENU)数据结构的指针,并将wnd1.spmenu中的指针替换为用户空间桌面堆中伪造的spmenu数据结构,具体如下图17所示。
图15 从WND0到WND1的越界写入操作,以泄漏内核地址
图16 写入0x400000000000000000前后的WND1类型字段
图17 spmenu内核内存地址指针泄漏,随后被指向伪造的spmenu数据结构的用户模式地址所替换
内核任意读取原语
使用之前泄露的spmenu数据结构内核指针,我们就可以通过该数据结构的布局和GetMenuBarInfo API逻辑将其转换为任意内核内存读取原语,具体见下面的图18、19和20。
图18 使用伪造的spmenu和GetMenuBarInfo进行内核任意读取操作
图19 用户空间桌面堆中的伪造spmenu数据结构,原始spmenu泄漏的内核指针位于精心设计的位置,以允许使用GetMenuBarInfo API进行任意读取
图20 WinDbg命令显示了spmneu数据结构中的位置,它由xxGetMenuBarInfo指定
从下面图21和22中的xxxGetMenuBarInfo函数中可以看到,通过将泄漏的内核地址写入伪造的spmenu数据结构中的正确位置,我们就可以在调用GetMenubarInfo时实现任意的内核内存读取原语。
图21 win32kfull!xxxGetMenuBarInfo
图22 GetMenuBarInfo数据结构按正常的spmenu和伪造的spmenu填充的返回值(泄漏内核地址)
内核级任意写入原语
现在,我们可以通过调用WND0上的SetWindowLongPtrA将我们的目标地址写入WND1.ExtraBytes字段,这样就能轻松实现一个任意的内核写入原语,它将相对于我们指定的偏移量向WND1写入OOB,具体如下图23所示。
就这里来说,偏移量是0x128,也就是ExtraBytes。然后,直接在WND1上调用SetWindowLongPtrA,就能在WND1.ExtraBytes字段中指定的地址处写入一个指定的值。之所以可以实现任意写入,是因为WND1是一个正常的窗口(没有像WND0和WND_Malicious那样被转换为一个控制台窗口),所以,我们可以向WND1.ExtraBytes中的任何地址处执行写操作。
图23 内核级任意写入原语:向任意地址写入任意内容
纯数据攻击
攻击者可以将内核任意读写原语可以组合起来执行纯数据攻击,以便用PID 4覆盖恶意进程EPROCESS令牌,从而将权限提升为System级别。
之前泄露的原始spmenu内核地址有一个指向WND1的指针,其偏移量为0x50,如图24和25所示。通过GetMenuBarInfo,多次读取伪造的spmenu数据结构,并将其用作WND1内核地址,我们最终就可以读取PID 4 System EPROCESS令牌。
图24 通过伪造的spmenu与GetMenuBarInfo任意读取操作获得PID4令牌
图25 原始的spmenu,WND1内核地址指针偏移量为0x50
通过将目标地址(恶意进程EPROCESS令牌的地址)写入wnd1.extrabytes,随后调用SetWindowLongPtrA,就会把值(PID4,即System EPROCESS令牌)写入该地址,详情请参见下面的图26和27。
图26 交换EPROCESS令牌
图27 用EPROCESS令牌的地址覆盖wnd1.extrabytes
一旦完成提权,该exploit就会恢复覆盖的数据结构值,以防止BSOD(蓝屏)。
小结
在本文中,我们对CVE-2021-1732漏洞进行了深入的分析,这是Windows10上的一个本地特权升级漏洞。众所周知,Windows内核数据攻击是很难防御的,因为一旦发现这种漏洞,它们就可以通过特定的API使用合法和可信的代码来操纵内核内存中的数据结构。
通过Microsoft针对读/写原语所做的大量工作,虽然win32k组件的安全性得到了强化,但由于其攻击面庞大(系统调用和回调函数)以及缺乏进程级别的win32k过滤机制,仍然存在许多的攻击机会。如果能在Windows10中看到系统级别的win32k过滤策略功能,这种局面可能会有较大的改观。
补丁始终是安全漏洞的最佳解决方案,但在无法打补丁的情况下,还需要强有力的防御策略,如威胁狩猎,以检测各种攻击活动所使用的漏洞/exploit的变体。
原文地址:https://www.mcafee.com/blogs/enterprise/mcafee-enterprise-atr/technical-analysis-of-cve-2021-1732/
最新评论