본문 바로가기

Research/Operating System

Windows Ancillary Function Driver for WinSock Elevation of Privilege Vulnerability (CVE-2023-21768)

Introduction

2023년 1월 10일, Patch Tuesday에 MSRC로부터 Ancillary Function Driver(AFD.sys)에 존재하는 EoP 취약점이 공개되었고, IBM X-Froce의 @chompie@b33f는 일명 Patch Tuesday, Exploit Wednesday로 일컫는, 취약점의 patch가 공개된 다음날 취약점을 weaponizing 하는 연구 결과를 공개하였다.

 

해당 연구에서는 임의의 주소에 0x1을 쓰는 취약점을 Yarden Sharif가 공개한 IoRing Primitive를 활용, Full AAR/AAW로 확장하여 EoP를 달성하였다. chompie와 b33f의 연구에서 0x1을 쓰는 것을 넘어 더 큰 값을 쓸 수 있는 가능성을 제시하였지만, 더 발전시키지는 않았다.

이후, 360 lceSword Lab의 공식 성명으로부터 in-the-wild에서 사용된 sample에서 해당 추측에 대한 사실 여부를 확인하였다.

 

본 글에서는 앞선 연구에서 더 나아가, 임의의 주소에 0x1을 쓰는 것을 넘어 임의의 값을 증가시키는 primitive로 발전시켰고 이를 활용해 Token privileges를 조작하여 EoP를 달성하였다.

Credits

Affected Versions

  • AFD.sys (10.0.22621.317 ~ 10.0.22621.608)

Root Cause Analysis

Patch Diffing

v10.0.22621.608 VS v10.0.22621.1105

Similarity는 거의 유사하며, AfdNotifyRemoveIoCompletion 함수에서 차이점을 확인할 수 있다.

 

Patched version인 10.22621.1105에는 PreviousMode(KPROCESSOR_MODE)를 나타내는 a1의 값을 확인하고, user-mode(0x1)로부터 호출된 경우 ProbeForWrite 함수를 호출하여 전달받은 주소값이 user address space의 범위인지 확인하는 점을 미루어 보았을 때, 사용자가 제공한 주소값에 대한 부적절한 검증이 취약점의 원인이었음을 확인할 수 있다.

Vulenerability Overview

__int64 __fastcall AfdNotifyRemoveIoCompletion(char a1, __int64 a2, __int64 a3)

...

LABEL_27:
    v8 = IoRemoveIoCompletion(v25, Pool2, v4, (unsigned int)v6, &v20, a1, v13, 0);
    if ( !v8 )
    {
      if ( v19 )
      {
        for ( i = 0; i < v20; ++i )
        {
          v15 = &Pool2[32 * i];
          v16 = (_DWORD *)(*(_QWORD *)(a3 + 16) + 16i64 * i);
          *v16 = *(_DWORD *)v15;
          v16[1] = *((_DWORD *)v15 + 2);
          v16[3] = *((_DWORD *)v15 + 6);
          v16[2] = *((_DWORD *)v15 + 4);
        }
      }
      **(_DWORD **)(a3 + 0x18) = v20;  // here
      goto LABEL_33;
    }

}

...

AfdNotifyRemoveIoCompletion 함수에서 세 번째 인자인 a3(UserInputBuffer)의 0x18 offset에 해당하는 field에 저장되어 있는 주소에 v20의 값을 쓴다.

 

이때 v20의 값은 IoRemoveIoCompletion 함수에 의해 설정된다.

Triggering the vulnerability

AfdNotifyRemoveIoCompletion 함수는 AfdNotifySock 함수를 통해 호출된다.

__int64 __fastcall AfdNotifySock(
        _QWORD *FileObject,
        __int64 IoControlCode,
        KPROCESSOR_MODE PreviousMode,
        struct_InputBufferPtr *InputBuffer,
        int InputBufferLength,
        __int64 OutputBuffer,
        int OutputBufferLength)
{

...

  InputBufferPtr = InputBuffer;
  memset(&v29, 0, sizeof(v29));
  IoCompletionPortObject = 0i64;
  v25 = 0i64;
  v26 = 0i64;
  if ( InputBufferLength != 0x30 || OutputBufferLength )// [1]
  {
    v10 = 0xC0000004;
    goto LABEL_45;
  }
  if ( OutputBuffer )                           // [2]
    goto LABEL_5;
  if ( PreviousMode )                           // [3]
  {
    if ( (unsigned __int64)InputBuffer >= MmUserProbeAddress )
      InputBufferPtr = (struct_InputBufferPtr *)MmUserProbeAddress;
    v29 = *InputBufferPtr;
    InputBufferPtr = &v29;
  }
  if ( !InputBufferPtr->registrationCount )     // [4]
    goto LABEL_5;
  if ( InputBufferPtr->completionCount )        // [5]
  {
    if ( !InputBufferPtr->receivedEntryCount || !InputBufferPtr->completionPortEntries )// [5-1]
      goto LABEL_5;
  }
  else if ( InputBufferPtr->completionPortEntries || InputBufferPtr->timeoutMs )// [5-2]
  {
LABEL_5:
    v10 = 0xC000000D;
    goto LABEL_45;
  }

...

AfdNotifySock 함수에서는 다음과 같은 검사가 수행된다.

[1] InputBufferLength0x30인지 확인, OutputBufferLength가 0인지 확인

[2] OutputBufferNULL ptr인지 확인

[3] 만약 user-mode에서 호출이 됐다면, InputBuffer가 user address space에 속하는지 확인

[4] InputBufferPtr->registrationCount0이 아닌지 확인

[5] InputBufferPtr->completionCount1 이상일 때

[5-1] InputBufferPtr->receiveEntryCount가 NULL ptr이 아닌지 확인,

InputBufferPtr->completionPortEntries가 NULL ptr이 아닌지 확인

[5-2] InputBufferPtr->completionCount0일 때,

InputBufferPtr->completionPortEntriesNULL ptr인지 확인,

InputBufferPtr->completionCountNULL ptr인지 확인

__int64 __fastcall AfdNotifySock(
        _QWORD *FileObject,
        __int64 IoControlCode,
        KPROCESSOR_MODE PreviousMode,
        struct_InputBufferPtr *InputBuffer,
        int InputBufferLength,
        __int64 OutputBuffer,
        int OutputBufferLength)
{

...

ioCompletionPortHandle = InputBufferPtr->ioCompletionPortHandle;
  Object = 0i64;
  v10 = ObReferenceObjectByHandle(ioCompletionPortHandle, 2u, IoCompletionObjectType, PreviousMode, &Object, 0i64);
  IoCompletionPortObject = Object;
  if ( v10 >= 0 )
  {
    v12 = IoIs32bitProcess(0i64);
    count = 0;
    ValidAddress = (unsigned __int64 *)MmUserProbeAddress;
    while ( count < InputBufferPtr->registrationCount )
    {

      ...

      v20 = FileObject;
      if ( count )
        v20 = 0i64;
      result = AfdNotifyProcessRegistration(PreviousMode, IoCompletionPortObject, (__int64)registrationInfo, v20);
      if ( PreviousMode )
      {
        v22 = InputBufferPtr->registrationInfos;
        ValidAddress = (unsigned __int64 *)MmUserProbeAddress;
        if ( v12 )
          v23 = (_DWORD *)(16 * v15 + v22 + 12);
        else
          v23 = (_DWORD *)(v22 + 24 * v15 + 20);
        if ( (unsigned __int64)v23 >= MmUserProbeAddress )
          v23 = (_DWORD *)MmUserProbeAddress;
        *v23 = result;
      }
      else
      {
        *(_DWORD *)(InputBufferPtr->registrationInfos + 24 * v15 + 20) = result; // registrationResult
        ValidAddress = (unsigned __int64 *)MmUserProbeAddress;
      }
      ++count;
    }
    v10 = AfdNotifyRemoveIoCompletion(PreviousMode, (__int64)IoCompletionPortObject, InputBufferPtr);  // here
  }
LABEL_45:
  if ( IoCompletionPortObject )
    ObfDereferenceObject(IoCompletionPortObject);
  return (unsigned int)v10;

AfdNotifySock 함수의 실제 동작 routine으로써, AfdNotifyProcessRegistration 함수를 호출하여 completion port에 알림을 등록하고 최종적으로 AfdNotifyRemoveIoCompletion 함수를 호출한다.

 

이때 구성되는 인자는 PreviousMode, IoCompletionPortObject 그리고 InputBufferPtr이다.

그럼 이제, AfdNotifySock 함수는 어떻게 호출할 수 있는지 확인해 보자.

AfdNotifySock 함수는 call 형태의 참조는 존재하지 않고, dispatch table 형식으로 참조되고 있는 것을 확인할 수 있다.

.rdata:00000001C004F3B0 ; __int64 AfdImmediateCallDispatch[]
.rdata:00000001C004F3B0 AfdImmediateCallDispatch dq 0           ; DATA XREF: AfdFastIoDeviceControl+F26↑r
.rdata:00000001C004F3B0                                         ; AfdDispatchImmediateIrp+17↑o
.rdata:00000001C004F3B8                 align 80h
.rdata:00000001C004F400                 dq offset AfdPartialDisconnect
.rdata:00000001C004F408                 align 10h
.rdata:00000001C004F410                 dq offset AfdQueryReceiveInformation
.rdata:00000001C004F418                 dq offset AfdQueryHandles
.rdata:00000001C004F420                 dq offset AfdSetInformation
.rdata:00000001C004F428                 dq offset AfdGetRemoteAddress
.rdata:00000001C004F430                 dq offset AfdGetContext
.rdata:00000001C004F438                 dq offset AfdSetContext
.rdata:00000001C004F440                 dq offset AfdSetConnectData
...
.rdata:00000001C004F5F6                 db    0
.rdata:00000001C004F5F7                 db    0
.rdata:00000001C004F5F8                 dq offset AfdNotifySock    // here

AfdImmediateCallDispatch라는 table에 AfdNotifySock함수가 속해있는 것을 확인할 수 있다.

AfdImmediateCallDispatch를 참조하는 곳은 AfdFastIoDeviceControl 함수이다.

char __fastcall AfdFastIoDeviceControl(
        _FILE_OBJECT *FileObject,
        __int64 Wait,
        unsigned int *UserInputBuffer,
        unsigned int UserInputBufferLength,
        unsigned __int64 UserOutputBuffer,
        int UserOutputBufferLength,
        unsigned int IoControlCode,
        __int64 IoStatusBlock,
        __int64 DeviceObject)
{

...

LABEL_204:
    IoctlIndex = (IoControlCode >> 2) & 0x3FF;
    if ( IoctlIndex < 0x4A && AfdIoctlTable[IoctlIndex] == IoControlCode )
    {
    v66 = (__int64 (__fastcall *)(__int64, _QWORD, _QWORD, unsigned int *, unsigned int, unsigned __int64, int, __int64))AfdImmediateCallDispatch[IoctlIndex];
    if ( v66 )
    {
        *(_DWORD *)IoStatusBlock_1 = v66(
                                        (__int64)FileObject,
                                        IoControlCode,
                                        PreviousMode,
                                        UserInputBuffer,
                                        UserInputBufferLength,
                                        UserOutputBuffer_1,
                                        UserOutputBufferLength,
                                        IoStatusBlock_1 + 8);
        LOBYTE(v12) = 1;
    }

...

AfdFastIoDeviceControl 함수에서는 AfdIoctlTable에서 IoControlCode를 확인하여 AfdImmediateCallDispatch table에서 IoctlIndex에 해당하는 routine을 dispatch하여 호출한다.

.rdata:00000001C0050A00 ; int AfdIoctlTable[76]
.rdata:00000001C0050A00 AfdIoctlTable   dd     12003h,    12007h,    1200Bh,    1200Ch,    12010h,    12017h,    1201Bh,    1201Fh
.rdata:00000001C0050A00                                         ; DATA XREF: AfdFastIoDeviceControl+F18↑r
.rdata:00000001C0050A00                                         ; AfdDispatchDeviceControl+4F↑r
.rdata:00000001C0050A00                 dd     12023h,    12024h,    1202Bh,    1202Fh,    12033h,    12037h,    1203Bh,    1203Fh
.rdata:00000001C0050A00                 dd     12043h,    12047h,    1204Bh,    1204Fh,    12053h,    12057h,    1205Bh,    1205Fh
.rdata:00000001C0050A00                 dd     12063h,    12067h,    1206Bh,    1206Fh,    12073h,    12077h,    1207Bh,    1207Fh
.rdata:00000001C0050A00                 dd     12083h,    12087h,    1208Bh,    1208Ch,    12090h,    12094h,    12098h,    1209Fh
.rdata:00000001C0050A00                 dd     120A0h,    120A7h,    120ABh,    120ACh,    120B3h,    120B4h,    120BBh,    120BFh
.rdata:00000001C0050A00                 dd     120C3h,    120C7h,    120CBh,    120CFh,    120D3h,    120D7h,    120DBh,    120DFh
.rdata:00000001C0050A00                 dd     120E2h,    120E7h,    120EBh,    120EFh,    120F3h,    120F7h,    120FBh,    120FFh
.rdata:00000001C0050A00                 dd     12103h,    12107h,    1210Bh,    1210Ch,    12113h,    12117h,    1211Bh,    1211Fh
.rdata:00000001C0050A00                 dd     12123h,    12127h,    2 dup(0)

AfdIoctlTable에서 AfdNotifySock 함수의 IoControlCode index를 구해보면 다음과 같다.

AfdNotifySock (0x4F5F8) - AfdImmediateCallDispatch (0x4F3B0)

= 0x248 / 8

= 0x49(73)

즉, AfdIoctlTable의 73번째 element인 0x12127AfdNotifySock 함수의 IoControlCode index이다.

.data:00000001C0059160 AfdFastIoDispatch db 0E0h               ; DATA XREF: DriverEntry+607↓o
...
.data:00000001C0059170                 dq offset AfdFastIoRead
.data:00000001C0059178                 dq offset AfdFastIoWrite
...
.data:00000001C00591A0                 dq offset AfdSanFastUnlockAll
.data:00000001C00591A8                 align 10h
.data:00000001C00591B0                 dq offset AfdFastIoDeviceControl

AfdFastIoDeviceControl 함수는 AfdFastIoDispatch table의 element다.

AfdFastIoDispatch table은 DriverEntry에서 참조된다.

NTSTATUS __stdcall DriverEntry(_DRIVER_OBJECT *DriverObject, PUNICODE_STRING RegistryPath)
{
    ...

    if ( AfdInitializeGroup() )
    {
        memset64(DriverObject->MajorFunction, (unsigned __int64)AfdDispatch, 0x1Cui64);
        DriverObject->MajorFunction[14] = (PDRIVER_DISPATCH)AfdDispatchDeviceControl;
        DriverObject->MajorFunction[15] = (PDRIVER_DISPATCH)AfdWskDispatchInternalDeviceControl;
        DriverObject->MajorFunction[23] = (PDRIVER_DISPATCH)AfdEtwDispatch;
        DriverObject->FastIoDispatch = (PFAST_IO_DISPATCH)&AfdFastIoDispatch;
        DriverObject->DriverUnload = (PDRIVER_UNLOAD)AfdUnload;

    ...

Driver를 초기화하는 DriverEntry 함수에서 DriverObject->FastIoDispatchAfdFastIoDispatch table을 등록하며, 이는 곧 Fast I/O의 dispatch table로써 사용되는 것을 확인할 수 있다.

 

이로써, IoControlCode(0x12127)와 AFD.sys의 Endpoint인 SOCKET handle과 함께 NtDeviceIoControlFile 함수를 호출하면 취약한 함수에 도달할 수 있다는 사실을 확인했다.

Revisit vulnerable site

__int64 __fastcall AfdNotifyRemoveIoCompletion(
        char PreviousMode,
        __int64 IoCompletionPortObject,
        struct_InputBufferPtr *UserInputBuffer)
{
...

LABEL_27:
    v8 = IoRemoveIoCompletion(
           IoCompletionPortObjectPtr,
           Pool2,
           EntryArray,
           (unsigned int)completionCount_1,
           &removedIoCompletionObjectCount,
           PreviousMode,
           v13,
           0);
    if ( !v8 )
    {
      if ( v19 )
      {
        for ( i = 0; i < removedIoCompletionObjectCount; ++i )
        {
          v15 = &Pool2[32 * i];
          v16 = (_DWORD *)(UserInputBuffer->completionPortEntries + 16i64 * i);
          *v16 = *(_DWORD *)v15;
          v16[1] = *((_DWORD *)v15 + 2);
          v16[3] = *((_DWORD *)v15 + 6);
          v16[2] = *((_DWORD *)v15 + 4);
        }
      }
      *(_DWORD *)UserInputBuffer->receivedEntryCount = removedIoCompletionObjectCount;
      goto LABEL_33;
    }
  }
LABEL_34:
  if ( Pool2 && Pool2 != v27 && Pool2 != (char *)UserInputBuffer->completionPortEntries )
    ExFreePoolWithTag(Pool2, 0x4E646641u);
  if ( EntryArray && EntryArray != v26 )
    ExFreePoolWithTag(EntryArray, 0x4E646641u);
  return (unsigned int)v8;
}

Reverse enginnering을 진행해 보니 이제 취약점이 존재하는 부분의 routine을 명확히 이해할 수 있다.

결론은 UserInputBuffer를 통해 제공된 receivedEntryCountIoRemoveIoCompletion함수를 통해 반환된 removedIoCompletionObjectCount의 값을 할당하는데,

receivedEntryCount가 user address space 범위의 주소가 맞는지 확인하지 않는 것이 취약점의 root cause라고 할 수 있다.

 

그럼 이제, 임의의 주소에 원하는 값을 증가시키기 위해 IoRemoveIoCompletion 함수를 살펴보자.

Make a primitive that Arbitrary Address Increment

__int64 __fastcall IoRemoveIoCompletion(
        struct _KQUEUE *IoCompletionPortObject,
        __int64 a2,
        PLIST_ENTRY *EntryArray,
        ULONG Count,
        ULONG *removedIoCompletionObjectCount,
        KPROCESSOR_MODE a6,
        LARGE_INTEGER *Timeout,
        BOOLEAN a8)
{
...

  v8 = EntryArray;
  v37 = EntryArray;
  v9 = a2;
  Object = IoCompletionPortObject;
  v40 = IoCompletionPortObject;
  v41 = a2;
  v42 = EntryArray;
  removedIoCompletionObjectCountPtr = removedIoCompletionObjectCount;
  v36 = 0i64;
  v10 = KeRemoveQueueEx(IoCompletionPortObject, a6, a8, Timeout, EntryArray, Count);

  ...

  LABEL_37:
  *removedIoCompletionObjectCountPtr = v10;
  return v29;
}

UserInputBuffer에서 제공한 receiveEntryCountntoskrnl.exeIoRemoveIoCompletion 함수에서 반환된 값이 사용되는데, 이 값은 KeRemoveQueueEx 함수의 인자로 제공된 IoCompletionPortObject queue에서 완료된 작업을 제거한다.

 

따라서, IoCompletionPortObject queue에 대기 중인 I/O completion의 개수가 Count만큼 제거되므로 queue에 원하는 개수의 I/O completion을 추가하고 Count를 설정하면 일정 한도 내의 값을 쓸 수 있게 된다.

NTSTATUS __fastcall NtSetIoCompletion(void *hIoCompletionPort, __int64 a2, __int64 a3, int a4, __int64 a5)
{
  NTSTATUS result; // eax
  int v9; // ebx
  PVOID Object; // [rsp+40h] [rbp-18h] BYREF

  Object = 0i64;
  result = ObReferenceObjectByHandle(
             hIoCompletionPort,
             2u,
             IoCompletionObjectType,
             KeGetCurrentThread()->PreviousMode,
             &Object,
             0i64);
  if ( result >= 0 )
  {
    v9 = IoSetIoCompletionEx2((__int64)Object, a2, a3, a4, a5, 1u, 0i64);
    ObfDereferenceObject(Object);
    return v9;
  }
  return result;
}

NtSetIoCompletionIoCompletionPortHandle로부터 object를 참조하여 IoSetIoCompletionEx2 함수를 실행함으로써 I/O completion을 추가한다.

Exploitation

Enable GOD mode via manipulating Token.Privileges

copy
//0x498 bytes (sizeof)
struct _TOKEN
{
    struct _TOKEN_SOURCE TokenSource;                                       //0x0
    struct _LUID TokenId;                                                   //0x10
    struct _LUID AuthenticationId;                                          //0x18
    struct _LUID ParentTokenId;                                             //0x20
    union _LARGE_INTEGER ExpirationTime;                                    //0x28
    struct _ERESOURCE* TokenLock;                                           //0x30
    struct _LUID ModifiedId;                                                //0x38
    struct _SEP_TOKEN_PRIVILEGES Privileges;                                //0x40
    struct _SEP_AUDIT_POLICY AuditPolicy;                                   //0x58
    ULONG SessionId;                                                        //0x78
    ULONG UserAndGroupCount;                                                //0x7c
    ULONG RestrictedSidCount;
    ...

Windows에서는 Token을 통해 user를 인증하는데, Token에는 특수한 권한을 지정하는 Privileges라는 field가 존재한다.

//0x18 bytes (sizeof)
struct _SEP_TOKEN_PRIVILEGES
{
    ULONGLONG Present;                                                      //0x0
    ULONGLONG Enabled;                                                      //0x8
    ULONGLONG EnabledByDefault;                                             //0x10
};

Privileges field는 _SEP_TOKEN_PRIVILEGES라는 structure로 관리되며 각 bit가 설정됨에 따라 특정 권한을 사용할 수 있게 된다.

 

여러 권한 중 20번째 bit는 SeDebugPrivileges 권한이며, 다른 process의 memory를 조정할 수 있는 권한이다.

이는 곧, DLL Injection 등을 통하여 NT AUTHORITY\SYSTEM으로 실행되는 process에 code injection이 가능할 수 있게 된다.

Exploitation strategy

  1. Arbitrary Address Increment Primitive 생성
    a. NtCreateIoCompletion 함수를 호출하여 IoCompletion object 생성 및 handle 얻어오기
    b. NtSetIoCompletion 함수를 호출하여 queue에 I/O comletion 추가하기 (증가를 원하는 만큼 호출)
    c. NtCreateFile 함수를 호출하여 AFD Endpoint (SOCKET) 생성
    d. 취약점을 trigger하기 위한 InputBuffer 구성 (completionCount는 I/O completion queue에 추가한 count로 설정)
    e. NtDeviceIoControlFile 함수를 0x12127 Ioctl code와 함께 호출하여 취약점 trigger
  2. [1]의 primitive를 사용하여 target process의 Token.Privileges.PresentEnabled의 20번째 bit를 설정하여 SeDebugPrivilege 활성화
  3. NT AUTHORITY\SYSTEM으로 실행되는 process에 DLL Injection or Code Injection
  4. NT AUTHORITY\SYSTEM으로 권한 상승

Exploit code

Exploit.c

DEMO

References