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
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] InputBufferLength
가 0x30
인지 확인, OutputBufferLength
가 0인지 확인
[2] OutputBuffer
가 NULL
ptr인지 확인
[3] 만약 user-mode에서 호출이 됐다면, InputBuffer
가 user address space에 속하는지 확인
[4] InputBufferPtr->registrationCount
가 0
이 아닌지 확인
[5] InputBufferPtr->completionCount
가 1
이상일 때
[5-1] InputBufferPtr->receiveEntryCount
가 NULL ptr이 아닌지 확인,
InputBufferPtr->completionPortEntries
가 NULL ptr이 아닌지 확인
[5-2] InputBufferPtr->completionCount
가 0
일 때,
InputBufferPtr->completionPortEntries
가 NULL
ptr인지 확인,
InputBufferPtr->completionCount
가 NULL
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인 0x12127
이 AfdNotifySock
함수의 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->FastIoDispatch
에 AfdFastIoDispatch
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
를 통해 제공된 receivedEntryCount
에 IoRemoveIoCompletion
함수를 통해 반환된 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
에서 제공한 receiveEntryCount
는 ntoskrnl.exe
의 IoRemoveIoCompletion
함수에서 반환된 값이 사용되는데, 이 값은 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;
}
NtSetIoCompletion
은 IoCompletionPortHandle
로부터 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
- 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 - [1]의 primitive를 사용하여 target process의
Token.Privileges.Present
와Enabled
의 20번째 bit를 설정하여SeDebugPrivilege
활성화 NT AUTHORITY\SYSTEM
으로 실행되는 process에 DLL Injection or Code InjectionNT AUTHORITY\SYSTEM
으로 권한 상승
Exploit code
DEMO
References
- https://securityintelligence.com/posts/patch-tuesday-exploit-wednesday-pwning-windows-ancillary-function-driver-winsock/
- https://github.com/chompie1337/Windows_LPE_AFD_CVE-2023-21768
- https://twitter.com/flame36987044/status/1633659037761036290?s=20
- https://media.blackhat.com/bh-us-12/Briefings/Cerrudo/BH_US_12_Cerrudo_Windows_Kernel_WP.pdf