[하루한줄] CVE-2024-38144 : Windows 11 ksthunk.sys의 Integer Overflow로 인한 LPE 취약점

URL

Target

  • Windows 11
    • windows_10_1507 Up to (excluding) 10.0.10240.20751
    • windows_10_1607 Up to (excluding) 10.0.14393.7259
    • windows_10_1809 Up to (excluding) 10.0.17763.6189
    • windows_10_21h2 Up to (excluding) 10.0.19044.4780
    • windows_10_22h2 Up to (excluding) 10.0.19045.4780
    • windows_11_21h2 Up to (excluding) 10.0.22000.3147
    • windows_11_22h2 Up to (excluding) 10.0.22621.4037
    • windows_11_23h2 Up to (excluding) 10.0.22631.4037
    • windows_11_24h2 Up to (excluding) 10.0.26100.1457
    • windows_server_2012 Up to (excluding) 6.2.9200.25031
    • windows_server_2016 Up to (excluding) 10.0.14393.7259
    • windows_server_2019 Up to (excluding) 10.0.17763.6189
    • windows_server_2022 Up to (excluding) 10.0.20348.2655
    • windows_server_2022_23h2 Up to (excluding) 10.0.25398.1085

Explain

ksthunk.sys는 커널 스트리밍 서비스의 WOW handler로 32비트 프로세스가 적절하게 동작하도록 하는 핸들러들이 포함되어 있습니다. 취약점이 발생한 CKSAutomationThunk::ThunkEnableEventIrp 함술는 IOCTL 코드 0x2F0007로 호출 가능합니다.
CKSAutomationThunk::ThunkEnableEventIrp 함수는 32비트 프로세스 환경에서 커널 스트리밍 서비스와 상호 작용할 때, 사용자 영역에서 전달된 버퍼를 커널 영역으로 복사하고 적절히 처리하는 로직을 포함하고 있습니다.

__int64 __fastcall CKSAutomationThunk::ThunkEnableEventIrp(__int64 a1, PIRP a2, __int64 a3, int *a4)
{
  ...
 
  inbuflen = CurrentStackLocation->Parameters.DeviceIoControl.InputBufferLength;
  outbuflen = CurrentStackLocation->Parameters.DeviceIoControl.OutputBufferLength;
  // [1]. Align the length of output buffer
  outlen_adjust = (outbuflen + 0x17) & 0xFFFFFFF8;
  if ( a2->AssociatedIrp.MasterIrp )
    return 1i64;
  if ( (unsigned int)inbuflen < 0x18 )
    ExRaiseStatus(-1073741306);
  ProbeForRead(CurrentStackLocation->Parameters.DeviceIoControl.Type3InputBuffer, inbuflen, 1u);
  if ( (*((_DWORD *)CurrentStackLocation->Parameters.DeviceIoControl.Type3InputBuffer + 5) & 0xEFFFFFFF) == 1
    || (*((_DWORD *)CurrentStackLocation->Parameters.DeviceIoControl.Type3InputBuffer + 5) & 0xEFFFFFFF) == 2
    || (*((_DWORD *)CurrentStackLocation->Parameters.DeviceIoControl.Type3InputBuffer + 5) & 0xEFFFFFFF) == 4 )
  {
    // [2]. Validate the Length
    if ( (unsigned int)outbuflen < 0x10 )
      ExRaiseStatus(-1073741306);
    if ( outlen_adjust < (int)outbuflen + 16 || outlen_adjust + (unsigned int)inbuflen < outlen_adjust )
      ExRaiseStatus(-1073741306);
    // [3]. Allocate the buffer to store the data
    a2->AssociatedIrp.MasterIrp = (struct _IRP *)ExAllocatePool2(
                                                   0x61i64,
                                                   outlen_adjust + (unsigned int)inbuflen,
                                                   1886409547i64);
    a2->Flags |= 0x30u;
    ProbeForRead(a2->UserBuffer, outbuflen, 1u); // [*]
    data = (__int64)a2->AssociatedIrp.MasterIrp;
    ...
    // [4]. Copy the Data
    if ( (unsigned int)outbuflen > 0x10 )
      memmove((void *)(data + 0x20), (char *)a2->UserBuffer + 16, outbuflen - 16);
    memmove(
      (char *)a2->AssociatedIrp.MasterIrp + outlen_adjust,
      CurrentStackLocation->Parameters.FileSystemControl.Type3InputBuffer,
      inbuflen);
    ...
}

32비트 프로세스 호출을 처리하기 위해, 전달 받은 outbuflen 값을 정렬((outbuflen + 0x17) & 0xFFFFFFF8)한 뒤, 그 값을 바탕으로 커널 힙에 버퍼를 할당합니다. 문제는 이 과정에서 정수 오버플로우 검증이 없다는 점입니다. 예를 들어, 매우 큰 outbuflen 값을 전달하면 (outbuflen + 0x17) 연산 시 오버플로우가 발생하고, 그 결과 outlen_adjust 값이 실제보다 훨씬 작게 계산될 수 있습니다.

VOID ProbeForRead(ULONG_PTR Address, SIZE_T Length, ULONG Alignment) {
  if ((Length) != 0) {
    if ( (Address & (Alignment - 1)) != 0) {
      ExRaiseDatatypeMisalignment();
    }
    if ( (Address + Length - 1) < Address || (Address + Length - 1) > MM_USER_PROBE_ADDRESS) {
      ExRaiseAccessViolation(); 
    }
  }
}

ProbeForRead 함수는 유효한 메모리 주소 범위 여부를 확인하지만 실제 매핑 여부를 검증하지 않습니다. 이 점을 이용하면, 할당하지 않은 메모리 영역을 참조하는 방식으로 ProbeForRead를 통과할 수 있습니다.

ProbeForRead가 실제 매핑 여부를 검증하지 않는 점을 활용해, 비정상적으로 큰 outbuflen 값을 그대로 통과시키고, 복사 과정 중간에 unmapped 메모리에 접근하도록 유도하면 됩니다. 이렇게 사용자 메모리 폴트 예외를 발생시키면 복사 과정이 원하는 지점에서 멈추며, 결국 공격자는 커널 힙 영역을 제한적으로 오염시키거나 특정 객체를 덮어쓸 수 있게 됩니다.

이후 커널이 작게 할당된 힙 버퍼에 많은 양의 데이터를 복사합니다. memmove로 복사되는 데이터 크기는 outbuflen을 기준으로 하지만, 할당된 버퍼는 outlen_adjust 값에 의존하기 때문에 힙 오버플로우가 발생하게 됩니다. 복사 과정에서 커널이 실제 매핑되지 않은 영역에 접근하면 사용자 메모리 폴트 예외가 발생하여 복사가 중단됩니다. 이를 통해 공격자는 복사 과정을 원하는 시점에서 멈추게 하여 의도한 범위까지만 힙을 덮어쓰는 방식의 공격을 시도할 수 있습니다.