자기주도학습/CVE 공부

CVE-2024-38080 Windows Hyper-V 권한 상승 취약성

EYEN 2024. 10. 10. 14:33

#영향받는 시스템

더보기
  • Windows 11 21H2
    • < 10.0.22000.3079
  • Windows 11 22H2
    • < 10.0.22621.3880
  • Windows 11 23H2
    • < 10.0.22631.3880

 
diffing을 해서 취약점이 있는 부분을 특정.

diffing에 성공해서 VidExoBrokerIoctlReceive에서 취약점이 발생하였음을 알 수 있다. 

#용어 정리

Non paged Pool: 가상화된 메모리를 사용하지 않고 항상 실제 메모리를 사용하는 메모리 풀
ioctl: input output control
하드웨어의제어와 상태 정보를 얻기 위해 제공되는 함수/ read(), write()를 이용해서 데이터를 읽고 쓰는 등의 기능은 가능하지만 하드웨어를 제어하거나 상태 정보를 확인하려면 ioctl을 이용해야한다.  
SPI 통신 속도를 설정하는 등의 작업은 read, write만으로는 할 수 없으며 ioctl을 이용해야한다.
 
 

#취약점 존재 코드 

__int64 __fastcall VidExoBrokerIoctlReceive(
        __int64 VidExoObj,
        struct _LIST_ENTRY *a2,
        BrokerIrpDataHeader *Dest,
        unsigned int OutputLen,
        unsigned int *a5)
{

 
 

ReceivedIRP = (_IRP *)VidExoBrokerpFindAndDequeueSendIrpForFileObject(VidExoObj, a2);//[1] Sent by VidExoBrokerIoctlSend
  v9 = ReceivedIRP;
  if ( !ReceivedIRP )
  {
    ...
  }
  Source = (BrokerIrpDataHeader *)ReceivedIRP->AssociatedIrp.SystemBuffer;
  if ( OutputLen < 0x10 )
  {
    VidExoBrokerpQueueSendIrp(VidExoObj, (__int64)ReceivedIRP);
    v10 = -1073741306;
    VidTraceErrorInternal0("VidExoBrokerIoctlReceive", "onecore\\vm\\vid\\sys\\driver\\videxobroker.c", 198);
    goto LABEL_16;
  }
  Dest->HeaderSize = 16;
  Dest->NumHandles = Source->NumHandles;
  v12 = 8 * Source->NumHandles + 16;//[2] Integer Overflow
  Dest->DataOffset = v12;
  Dest->DataLen = Source->DataLen;
  v13 = Source->DataLen + v12;//[3] Integer Overflow
  if ( OutputLen < v13 )//[4] Bypass length check
  {
    ...
  }

 
VidExoBrokerpFindAndDequeueSendIrpForFileObject(VidExoObj, a2);//[1] Sent by VidExoBrokerIoctlSend
VidExoBrokerIoctlSend 이 보낸 걸 처리(find dequeue sendirp)한 return값을 ReceivedIRP라고 함. 이 값은 공격자가 조작할 수 있는 값임(왜 인지 모르겠음)
그 IRP를 AssociatedIrp.SystemBuffer Source에 그 irp의 구조체(brokerirpdataheader) 포인터output이 0x10보다 작으면 에러

RequestorProcess = IoGetRequestorProcess(ReceivedIRP);
  v26 = RequestorProcess;
  CurrentProcess = PsGetCurrentProcess(v16, v15, v17, v18);
  v27 = CurrentProcess;
  TargetHandle = (HANDLE *)((char *)Dest + (unsigned int)Dest->HeaderSize);
  SourceHandle = (HANDLE *)((char *)Source + (unsigned int)Source->HeaderSize);
  v25 = SourceHandle;
  Idx = 0i64;
  while ( 1 )
  {
    if ( (unsigned int)Idx >= Source->NumHandles )
    {
      memmove(
        (char *)Dest + (unsigned int)Dest->DataOffset,
        (char *)Source + (unsigned int)Source->DataOffset,
        (unsigned int)Source->DataLen);//[5] Non paged pool BOF
      v10 = 0;
      *a5 = v13;
      goto LABEL_15;
    }

 
memmove를 유심히 볼 것 같긴하다. memmove: src를 dest에 count바이트 복사. 그리고 dest 포인터 리턴
idx>= source->numhandles 
source, 그 구조체의 numhandles가 idx보다 작으면 'dest+dest->dataoffset' 에 'src+src->datadffest'를 'source->datalen'만큼
이때 source->datalen
 
 
 
v12에서 numhandles가 0x1fffffffe가 넘어서 *8+16 했을 때 integer overflow가 나서 v12는 매우 작은 값이 됨 datalen에 v12를 더한게 v13

source->datalen은 엄청 작은 값인데 NumHandle의 값이 0x1ffffffe 이상이면 integer overflow로 

이 조건문 bypass가능.
검증할 때 쓰이는 v12와 v13은 integer overflow를 일으켜 크기 검증 조건문을 우회하면서, 실제로 넣는 양은 엄청 크게해서 bof를 일으킬 수 있음 

poc 일부

 
 
 
 
왜 저 Source->Numhandles, 즉 Source 구조체, 즉 ReceiveIRP->associatedIRP.systembuffer가 조작 가능한 값임? 
 (_IRP *)VidExoBrokerpFindAndDequeueSendIrpForFileObject(VidExoObj, a2);이게 ReceiveIRP인데

 
 

 
 
vidExoBrokerIoctlReceive는 VidExoBrokerIoctlSend가 IRP객체를 보냈을 때 받는 역할을 하는 함수라고 한다. 
VidExoBrokerIoctlSend함수를 사용자가 보낼 수도 있어서 조작할 수 있다고 하는것일거다.
그러면 vid.sys로 데이터를 전송할 수 있는 드라이버의 인터페이스를 찾아야한다. 
일반 사용자 프로그램으로는 접근할 수 없고 vmwp.exe에서만 쓰여서 루트계정으로 접근해야된다고  한다. vmwp.exe에서 vid.sys는 파티션 관리 메모리 생성 등을 할 때처럼 가상머신을 관리하는 용도로 쓰이는 거라고 한다.  

출처: https://msrc.microsoft.com/blog/2019/09/attacking-the-vm-worker-process/

 
그럼 vmwp.exe에서 어떻게 vid.sys로 데이터를 보내는가?는 이제부터 분석~.. receiver가 어디서 받아오는지 타고타고 
아 vid가 video가 아니고 Virtualization Infrastructure Driver 였음... 와!
아까 봤듯이 vidExoBrokerIoctlReceive, send는 videxoIoControlPartition에서 호출이됨.
videxoIoControlPartition은 videxoiocontrolpreprocess에서 호출이됨
videxoiocontrolpreprocess는 VidDeviceAdd나 VidExopDeviceSetupInternal에서 호출이됨
타고타고 맨 위에 FxDriverEntry가 있다. 
내가 궁금한건 가상머신의 저 vmwp.exe에 어떤 파일을 삽입하거나 설정파일에 대한 입력을 통해 vid.sys내에서 VidExoBrokerIoctlSend를 호출하게 할 수 있는지.
 
그걸 알려면 VidExoBrokerIoctlSend가 어떤 경우에 호출이 되는건지, 확실히 파티션에 관련된 것 같은데 파티션에서 입출력 제어를 어떻게 수행하는건지, vmwp.exe에 파일 혹은 코드 삽입이 가능한지 설정파일을 변조하거나 해서..? 
 
상위레벨의 드라이버에서 하위레벨의 드라이버로 IO요청을 할 때 IRP I/O requestpacket이 사용된다 
 
https://github.com/gerhart01/Hyper-V-Internals/blob/master/Hyper-V%20components.pdf

Hyper-V-Internals/Hyper-V components.pdf at master · gerhart01/Hyper-V-Internals

Internals information about Hyper-V. Contribute to gerhart01/Hyper-V-Internals development by creating an account on GitHub.

github.com

 
https://github.com/gerhart01/Hyper-V-Internals/blob/master/vid_dll_evolution/vid.dll%20evolution%20-%20Windows%2011.png

Hyper-V-Internals/vid_dll_evolution/vid.dll evolution - Windows 11.png at master · gerhart01/Hyper-V-Internals

Internals information about Hyper-V. Contribute to gerhart01/Hyper-V-Internals development by creating an account on GitHub.

github.com

 
아니 원데이분석글도 지금 이해하는데 오래 걸리는데
원데이분석글 분석글....푸하학~~
 
지금 모르는 게 너무 많아. 뭘 모르냐면
videxo의 핸들을 얻을 수가 있대. 이 문장 자체가 이해가 안됨 
onecore\vm\vid\sys\driver\videxobroker.c 처럼

그 videxo가 뭔데.. 버추얼라이제이션 인프라스트럭쳐 드라이버 엑소. 외부. 외부와 연결.
디바이스의 드라이버의 핸들을 얻을 수가 있다고 
그걸 어디서 확인한건데
 
아니 videxo 핸들러가 있으면 videxobrokerioctlsend 호출할 수 있다는 건 어떻게 안건데
아아악

일단은 타고타고타고 올라가면서 그냥 슥슥 보다가 videxopregkeynotificationhandler에서 Device\VidExo라는 문자열을 발견함 근데 내가 해석글을 안 읽었으면 보고도 몰랐을 듯. 
1. \Device\VidExo를 찾습니다.
2. VidExopRegKeyNotificationHandler()란
 (1) VidRegistryOpenParametersKey(&v5)  :레지스트리 키를 열어서
 (2) VidRegistryQueryUINT32WithDefault(v5, L"ExoDeviceEnabled", 0LL) : "ExoDeviceEnabled" 값을 가져옴
 (3) VidRegistryQueryUINT32WithDefault(v5, L"ExoDeviceEnabledClient", 0LL) : "ExoDeviceEnabledClient" 값을 가져옴
 (4) (2),(3) 중 하나라도 참이면 내부장치설정함수인 videxopdevicesetupinternal() 호출 여기서 vid exo 에 대한 장치를 설정
 
3. VidExopDeviceSetupInternal() 란 
  (1) *(_OWORD *)L"\\Device\\VidExo" : "Device\VidExo" 문자열을 qword로 저장
  (2) v18 = (*(__int64 (__fastcall **)(__int64, _QWORD))(WdfFunctions_01015 + 312))( // 드라이버 객체 가져오기 WdfDriverGlobals, *(_QWORD *)(v15 + 184)); : 드라이버 객체 가져오기
  (3) v21 = (*(__int64 (__fastcall **)(__int64, __int64, __int64 *))(WdfFunctions_01015 + 200))( // 장치 객체 생성 WdfDriverGlobals, v18, v23); : 장치 객체 생성
  (4) v17 = (*(__int64 (__fastcall **)(__int64, __int64, __int64 *))(WdfFunctions_01015 + 536))( // 드라이버 등록 WdfDriverGlobals, v21, v24); : 장치 객체 생성 시 드라이버 등록 
   (5) 드라이버 등록 성공시 파일 생성( VidExopFileCreate ) 닫기( VidExopFileClose ) 정리( VidExopFileCleanup ) 함수 설정, I/O제어 처리기인 v17( v17 = (*(__int64 (__fastcall **)(__int64, __int64, void *, __int64, _QWORD, _DWORD))(WdfFunctions_01015 + 584))( WdfDriverGlobals, v21, &VidExopIoControlPreProcess, v19, 0LL, 0); )을 생성
  (6) v17은 &VidExopIoControlPreProcess 를 포함
  (7) 성공시 위의 VidExopRegKeyNotificationHandler() 예외처리없이 잘 돌아가게 됨
 
4. VidExopIoControlPreProcess()란 I/O제어 요청 처리중 조건 검사
   (1) v15가 oxE가 아닌지 : v15가 vidExo obj를 뜻함 exo 타입인지 ptrn 타입인지 근데 왜인지 모름!
   (2) 아니면 v15가 ntrp가 아닌지
   (3) ntrp이고 v15 + 48, 22464 값을 검증한 다음 vidExoIoControlPartition( VidExoIoControlPartition(v15, a2, v10, *(unsigned int **)v22, v7, Dst, v8, v4, (unsigned int *)&v21); else ) 호출
   (4) v15가 요청을 보낸 프로세스를 나타낸 값이라고 함 프로세스 검증 후 I/O제어함수 호출되겠지
 
5. vidExoIoControlPartition() 란
    (1) case를 a8값에 따라 엄청 여러개 나눠둠 
    (2) case가 0x221288일 때 VidExoBrokerIoctlSend
    (3) case가 0x22128C일 때 VidExoBrokerIoctlReceive
 
=> 결론은 \vid\exo 드라이버에 대한 핸들러를 만들고 제어하면서 저 send랑 receive를 실행하게 될 수도 있다 이말임 
 
send가 어떤 구조체를 보내는지, 그 구조체의 어느 변수에서 오버플로우를 일으킬 수 있는지. 
send에서는 에러가 안 나면 VidExoBrokerQueueSendIrp를 실행하는데 얘가 receive에서도 쓰임 

__int64 __fastcall VidExoBrokerpQueueSendIrp(__int64 a1, __int64 a2)
{
  __int64 v2; // rbp
  char v5; // al
  _QWORD *v6; // rdx
  char v7; // di
  char v8; // r9
  _QWORD *v9; // rax
  __int64 result; // rax

  v2 = a1 + 24064;
  *(_QWORD *)(a2 + 120) = a1;
  v5 = KeAcquireSpinLockRaiseToDpc(a1 + 24064);
  v7 = 0;
  _InterlockedExchange64((volatile __int64 *)(a2 + 104), (__int64)VidExoBrokerpIrpCancelRoutine);
  v8 = v5;
  if ( *(_BYTE *)(a2 + 68) && _InterlockedExchange64((volatile __int64 *)(a2 + 104), 0LL) )
  {
    v7 = 1;
  }
  else
  {
    v6 = *(_QWORD **)(a1 + 24080);
    v9 = (_QWORD *)(a2 + 168);
    if ( *v6 != a1 + 24072 )
      __fastfail(3u);
    *v9 = a1 + 24072;
    *(_QWORD *)(a2 + 176) = v6;
    *v6 = v9;
    *(_QWORD *)(a1 + 24080) = v9;
  }
  LOBYTE(v6) = v8;
  result = KeReleaseSpinLock(v2, v6);
  if ( v7 )
  {
    *(_DWORD *)(a2 + 48) = -1073741536;
    return IofCompleteRequest(a2, 0LL);
  }
  return result;
}

( a1+24064, KeAcquireSpinLockRaiseToDpc(a1 + 24064), *(_QWORD **)(a1 + 24080), 0, v5 or 1, (_QWORD *)(a2 + 168), KeReleaseSpinLock(v2, v6))
 
 
 

# Proof of Concept

__int64 __fastcall VidExoIoControlPartition(
        struct _KDPC *VidExoObj,
        _IRP *irp,
        struct _LIST_ENTRY *FileObj,
        unsigned int *InputBuffer,
        unsigned int InputLen,
        unsigned int **OutputBuffer,
        unsigned int OutputLen,
        unsigned int IoControlCode,
        unsigned int *a9)
{

 
 
 

# 참고자료

https://hackyboiz.github.io/2024/09/01/pwndorei/hyperv-1dayclass_CVE-2024-38080/#google_vignette

hackyboiz

hack & life

hackyboiz.github.io