2023年1月,微软发布了一个在野Oday漏洞补丁,位于NT模块的ALPC相关处理中的UAF漏洞,此漏洞可以导致Chrome沙箱穿越,此前曾再Theori的六个漏洞组成的穿越链漏洞演示中,展示了如何从chrome RCE到VMware逃逸,再到HOST机器提权。此漏洞被用于Chrome沙箱穿越。
漏洞原理
此漏洞将在NtAlpcImpersonateClientOfPort->SeCreateClientSecurity调用链条下导致访问异常,本质是由于KThread对象被提前释放导致的UAF。
创建连接端口
NtAlpcCreatePort用于在服务端创建连接端口,实际上在标准的Client-Server模式的ALPC通信中,存在三个端口(2 个在服务器端,1 个在客户端),即一个连接端口,两个通信端口,但实际上内核统一由一个指定的结构_ALPC_PORT结构来处理。
1 | 0: kd> dt _ALPC_PORT |
下面是NtAlpcCreatePort的内核实现,实际上核心代码在AlpcpCreateConnectionPort函数中实现。
首先调用AlpcpCreatePort创建内核对象。
实际上调用ObCreateObjectEx创建AlpcPort对象,共0x1D8大小,如前面所示。
接着调用AlpcpInitializePort来初始化AlpcPort对象结构。
注意开始的初始化都是将ListEntry结构初始化为成员起始地址。例如MainQueue成员就是将其ListEntry结构的Flink和Blink成员都初始化为MainQueue结构地址。
- Main queue: A message has been sent, and the client is processing it.
- Pending queue: A message has been sent and the caller is waiting for a reply, but the reply has not yet been sent.
- Large message queue: A message has been sent, but the caller’s buffer was to small to receive it. The caller gets another chance to allocate a larger buffer and request the message payload again.
- Canceled queue: A message that was sent to the port but has since then been canceled.
- Direct queue: A message that was sent with a direct event attached.
接着返回对象句柄。
客户端连接
NtAlpcConnectPortEx用于与Server端进行连接,我们需要了解两个内部操作。
首先使用AlpcpCreateClientPort创建ClientCommunicationPort,类型依旧是_ALPC_PORT。
其次将连接请求消息放入AlpcPort的MainQueue队首。
处理消息链,首先在AlpcpProcessConnectionRequest中将相关结构放入RequestContext结构,此结构尚未公开,我将其定义为如下所示结构。
1 | typedef struct _REQUEST_CONTEXT |
接着调用AlpcpDispatchConnectionRequest函数,传入RequestContext结构。
继续向后调用AlpcpCompleteDispatchMessage函数。
此函数中,将连接请求的Message消息放入MainQueue队首。
服务端接收连接消息
The name of this function sounds like three things at once - Send, Wait and Receive - and that’s exactly what it is. Server and client use this single function to wait for messages, send messages and receive messages on their ALPC port. This sounds unnecessary complex and I can’t tell you for sure why it was build this way, but here’s my guess on it: Remember that ALPC was created as a fast and internal-only communication facility and the communication channel was build around a single kernel object (the ALPC port). Using this 3-way function allows to do multiple operations, e.g. sending and receiving a message, in a single call and thus saves time and reduces user-kernel-land switches. Additionally, this function acts as a single gate into the message exchange process and therefore allows for easier code change and optimizations (ALPC communication is used in a lot of different OS components ranging from kernel drivers to user GUI applications developed by different internal teams). Lastly ALPC is intended as an internal-only IPC mechanism so Microsoft does not need to design it primarily user or 3rd party developer friendly.
正如引用所述,NtAlpcSendWaitReceivePort进行发送-等待-接受一条龙操作。此外,当仅仅想只发送或者只等待接收消息时,可以通过参数来控制即可。在当前漏洞中,我们无需了解发送逻辑,仅仅只需要知道接收消息的逻辑即可。
可以看到,当传入的SendMessage参数为空时,将仅仅执行通过调用AlpcpReceiveMessage函数来进行等待-接受操作。
AlpcpReceiveMessage首先调用 AlpcpReceiveMessagePort函数去访问各个消息队列,循环查看各个消息队列有无消息,并进行处理。
正常的消息将在MainQueue队列中被处理,因此当正常消息被发送到MainQueue队列时,将被立即处理。
从MainQueue队列拿出消息数据后,再将此消息出队,准备处理下一条。
返回AlpcpReceiveMessage函数后,再将拿到的Message结构(KALPC_MESSAGE类型)中最后一个成员放入用户态缓冲区(_PORT_MESSAGE结构如下所示)。
1 | 2: kd> dt _KALPC_MESSAGE |
服务端允许连接
NtAlpcAcceptConnectPort在收到连接请求后,服务端调用此API用来决定是否允许此客户端连接。
最后一参数表明是否允许客户端连接。
1 | NTSYSCALLAPI |
相似的手法,此函数是对AlpcpAcceptConnectPort的包装。
AlpcpAcceptConnectPort调用AlpcpLookupMessage函数根据ConnectionPort、MessageId和CallbacId取出Message(KALPC_MESSAGE)。
接着对取出的Message内容与传入的用户态结构进行校验操作。
AlpcpValidateConnectionMessage=>AlpcpValidateMessage
接着若不允许连接,释放资源,返回。
若允许连接则创建ServerComm端口,后续用于与客户端通信。
线程池工厂
NtWaitForWorkViaWorkerFactory实际上是windows实现线程池工厂操作函数,具体使用方法不做赘述。
在此poc中,我们仅需了解到此函数是为了在工作线程中向通信端口发送消息,但是按照我们朴素的想法,内核AlpcpSendMessage函数肯定不知只有这一处调用,为什么必须使用此函数来进行消息请求呢?
这里先卖一个关子,这里也是除了UAF本身漏洞之外精彩之处。
而内部正是通过AlpcpSendMessage向通信端口发送消息请求,后续再次被NtAlpcSendWaitReceivePort函数进行获取。
关闭线程句柄以及服务端模拟执行
通过NtAlpcSendWaitReceivePort读取到工作线程发来的请求消息,当工作线程退出后。强制销毁工作线程句柄,但由于内核的Message消息依旧保留了内核线程指针,因此造成UAF。
具体来看,首先调用AlpcpCaptureIdMessage解析参数port_message,此参数是前面通过NtAlpcSendWaitReceivePort接收到的消息数据。主要是拿到MessageId和CallbackId成员;
接着调用AlpcpLookupMessage拿到内核Message结构。
接着调用在服务端模拟客户端身份执行。接着需要进行身份权限检测。
再调用SeCreateClientSecurity函数进行权限检测,但由于传入的WaitingThread指针已经被释放,因此当后续访问此结构成员时,将造成UAF。
漏洞调试
NtAlpcCreatePort
1 | 1: kd> g |
根据上述调试信息可以看到,此时仅仅申请了一个0x1d8大小池内存地址为ffff818d`ae55fa70,并被初始化为0。
1 | 3: kd> g |
此时,经过AlpcpInitializePort函数,开始初始化AlpcPort结构,根据前面的静态分析,此时MainQueue成员应该被初始化为此结构地址,即ffff818d`ae55fb00。
NtAlpcConnectPortEx
NtAlpcConnectPortEx通过以下调用,将在AlpcpCompleteDispatchMessage函数中将消息结构放在MainQueue队列头。
1 | 3: kd> g |
通过windbg提供的!alpc命令,我们也可以看到此时AlpcPort对象的队列上确实存在一个LPC_CONNECTION_REQUEST消息。
此外Connect函数还会创建ClientCommPort。我们具体查看此条消息,可以看到OwnerPort是ALPC_CLIENT_COMMUNICATION_PORT类型,也就是新创建的ClientCommPort。
1 | 2: kd> !alpc /m ffffbe8f723c9c60 |
具体来说,就是ffff818dae0ed6f0,可以看到在CommunicationInfo成员中已经存在了两个Port对象。
NtAlpcSendWaitReceivePort=>NtAlpcAcceptConnectPort
第一次Reveive消息,去MainQueue队列拿消息。
此时我们查看ConnectionPort,发现消息已经被取走。
1 | 2: kd> g |
当Accept最后一参数bool值传入True即Allow时,将创建ServerCommPort,如下所示。
并且,向ClientCommPort发送Reply消息,如下所示:
1 | Breakpoint 2 hit |
CreateThread=>NtWaitForWorkViaWorkerFactory
接下来在线程工厂中,工作现场调用NtWaitForWorkViaWorkerFactory函数,实际上向ConnectionPort发送消息,立即返回。
1 | 3: kd> g |
当我们具体查看此消息时,发现已经绑定了当前工作线程。
1 | 1: kd> !alpc /m ffffbe8f7200bc90 |
此时,我们查看线程内存属性,此时线程句柄尚未关闭。
1 | 1: kd> !pool ffff818dae171080 |
NtAlpcSendWaitReceivePort=>NtClose
接着接受前面的消息
1 | 1: kd> g |
返回用户态,关闭工作线程句柄。
1 | 3: kd> !pool ffff818dae171080 |
原先内存将被释放或者重新使用,如上所示。
触发UAF=>NtAlpcImpersonateClientOfPort
1 | 3: kd> g |
此时,正如静态分析所述,将拿出Message–>WaitingThread成员,将在SeCreateClientSecurity访问,导致UAF。
只有一种触发方式?Yes!
现在需要解开前面卖的关子。前面的静态分析和调试,其实一直没有注意到内核消息构造的过程中,KThread指针和Message结构是如何关联的。
1 | __int64 __fastcall AlpcpSendMessage( |
注意AlpcpSendMessage的第一参数,实际上是我们前面提到的REQUEST_CONTEXT结构,后续继续调用 AlpcpDispatchNewMessage函数时,同样使用REQUEST_CONTEXT结构。
而在内部调用AlpcpCompleteDispatchMessage完成最终的消息投递之前,会判断Flag & 0x20000是否为真,为真才将当前内核线程结构放入Message结构。
因此对于其他的交叉引用,大部分函数的调用是直接制定了Flag,即就是不为0x20000标志。
例如①LpcRequest函数。
②NtRequest函数。
③NtReplyWaitReceivePortEx函数
以及④NtReplyPort函数。
如前所述,均指定了flag的值,因此无法后续造成UAF。
而对于未指定flag值的调用,例如 NtAlpcSendWaitReceivePort函数,我一开始觉得依旧可以造成UAF。
但是更进一步的静态分析以及经过测试代码测试,是无法完成的。原因在于NtAlpcSendWaitReceivePort如下所示的代码中,是不存在只进行Send操作的逻辑的,即只能发送完等待或者返回错误,如下所示。
而有没有这样一种思路,即手动停止线程结束Recv等待,因为我们Send消息并且绑定线程指针的目的已经达到了。
实际上这个思路也是行不通的,根据测试以及静态分析来看,当线程停止等待时,会手动将WaitingThread成员置为空。
因此,到现在谜底解开,这也是为什么avast原始POC使用线程工厂的原因,因为只有这一种方法可以保证只发送消息后立即返回。
演示
在对NT模块开启Verifier校验后,运行poc将造成BSOD。
补丁
2023年1月微软的补丁中,做出了如下修正。
可以看到是对两处函数做出了修正。
NtWaitForWorkViaWorkerFactory
对flag = 0x20000时作出严格判断。
AlpcpCreateClientPort
传参做了改变,通过查看前后逻辑,此处修改似乎是因为以前传入的参数实际并未在内部使用,因此此次更新进行了修正,实际与此漏洞的修补无关。
参考链接
[1]https://msrc.microsoft.com/update-guide/vulnerability/CVE-2023-21674
[2]https://twitter.com/theori_io/status/1764544922005430576
[3]https://ti.dbappsecurity.com.cn/vul/DAS-T104708
[4]https://csandker.io/2022/05/24/Offensive-Windows-IPC-3-ALPC.html
[6]https://github.com/hd3s5aa/CVE-2023-21674
[7]Windows Internals, Part 2, 7th Edition (Andrea Allievi, Alex Ionescu, Mark Russinovich etc.)