[하루한줄] CVE-2025-21298: Windows OLE Double Free 취약점

URL

Target

  • Windows 10 Version 1809 affected from 10.0.17763.0 before 10.0.17763.6775
  • Windows Server 2019 affected from 10.0.17763.0 before 10.0.17763.6775
  • Windows Server 2019 (Server Core installation) affected from 10.0.17763.0 before 10.0.17763.6775
  • Windows Server 2022 affected from 10.0.20348.0 before 10.0.20348.3091
  • Windows 10 Version 21H2 affected from 10.0.19043.0 before 10.0.19044.5371
  • Windows 11 version 22H2 affected from 10.0.22621.0 before 10.0.22621.4751
  • Windows 10 Version 22H2 affected from 10.0.19045.0 before 10.0.19045.5371
  • Windows Server 2025 (Server Core installation) affected from 10.0.26100.0 before 10.0.26100.2894
  • Windows 11 version 22H3 affected from 10.0.22631.0 before 10.0.22631.4751
  • Windows 11 Version 23H2 affected from 10.0.22631.0 before 10.0.22631.4751
  • Windows Server 2022, 23H2 Edition (Server Core installation) affected from 10.0.25398.0 before 10.0.25398.1369
  • Windows 11 Version 24H2 affected from 10.0.26100.0 before 10.0.26100.2894
  • Windows Server 2025 affected from 10.0.26100.0 before 10.0.26100.2894
  • Windows 10 Version 1507 affected from 10.0.10240.0 before 10.0.10240.20890
  • Windows 10 Version 1607 affected from 10.0.14393.0 before 10.0.14393.7699
  • Windows Server 2016 affected from 10.0.14393.0 before 10.0.14393.7699
  • Windows Server 2016 (Server Core installation) affected from 10.0.14393.0 before 10.0.14393.7699
  • Windows Server 2008 Service Pack 2 affected from 6.0.6003.0 before 6.0.6003.23070
  • Windows Server 2008 Service Pack 2 (Server Core installation) affected from 6.0.6003.0 before 6.0.6003.23070
  • Windows Server 2008 Service Pack 2 affected from 6.0.6003.0 before 6.0.6003.23070
  • Windows Server 2008 R2 Service Pack 1 affected from 6.1.7601.0 before 6.1.7601.27520
  • Windows Server 2008 R2 Service Pack 1 (Server Core installation) affected from 6.1.7601.0 before 6.1.7601.27520
  • Windows Server 2012 affected from 6.2.9200.0 before 6.2.9200.25273
  • Windows Server 2012 (Server Core installation) affected from 6.2.9200.0 before 6.2.9200.25273
  • Windows Server 2012 R2 affected from 6.3.9600.0 before 6.3.9600.22371
  • Windows Server 2012 R2 (Server Core installation) affected from 6.3.9600.0 before 6.3.9600.22371

Explain

Windows OLE 에서 Double Free 취약점이 발견되었습니다. MS Word 와 같은 OLE를 사용하는 MS Office 류들에서 취약점을 트리거할 수 있기에 상당히 영향도가 높은 RCE 취약점이라고 보여지네요.

OLE (Object Linking and Embedding) 는 Windows 의 각종 프로그램들이 데이터를 주고받는 기능 중에 하나입니다. 대표적으로 문서 간 데이터 공유를 할 때 사용되는데요. 예를 들어서 이미지 하나가 여러 문서 파일에 삽입되었다고 해봅시다.

이미지가 변경된다면 삽입된 모든 문서들의 이미지를 변경해야해서 상당히 불편할 겁니다. 이 때, 문서 하나에서 이미지를 변경해도 다른 문서들에 다 반영되게끔 하는게 OLE 의 장점 중 하나입니다.

다만 이런 강력한 기능 덕분에 수 년간 많은 취약점들이 이 OLE 기능 통해 발견되곤 하였습니다. 😢

Root Cause

ole32.dllUtOlePresStmToContentsStm 라는 함수에서 취약점이 발생했는데요. 이 함수는 OlePres 스트림 내 데이터를 적절하게 변환해서 CONTENTS 라는 스트림에 삽입하게끔 구현된 코드입니다.

아래 코드는 2025년 1월에 패치된 UtOlePresStmToContentsStm 함수를 디핑한 결과입니다. 코드를 보시면 CONTENTS 스트림용 pstmContents 를 할당하고서 즉시 해제를 하는데요. OlePres 스트림이 세팅되어 있지 않으면 OpenStream 함수가 실패하고 이어서 UtReadOlePresStmHeader 함수도 실패하여 pstmContents 이 값을 가리키는 채로 해제를 한 번 더 하게 됩니다.

__int64 __fastcall UtOlePresStmToContentsStm(IStorage *pstg, wchar_t *puiStatus, __int64 a3, unsigned int *lpszPresStm)
{
  struct IStorageVtbl *lpVtbl; // rax
  int v7; // r14d
+ bool IsEnabled; // al
  IStream *v10; // rcx
  bool v11; // zf
  struct IStorageVtbl *v12; // rax
  int v13; // ebx
  HRESULT v14; // eax
  const wchar_t *v15; // rdx
  IStream *pstmContents; // [rsp+40h] [rbp-19h] BYREF
  IStream *pstmOlePres; // [rsp+48h] [rbp-11h] BYREF
  tagFORMATETC foretc; // [rsp+50h] [rbp-9h] BYREF
  tagHDIBFILEHDR hdfh; // [rsp+70h] [rbp+17h] BYREF

  *lpszPresStm = 0;
  lpVtbl = pstg->lpVtbl;
  pstmContents = 0LL;
  v7 = 1;
 
  // "CONTENTS" 스트림을 생성하고 pstmContents 에 저장
  if ( (lpVtbl->CreateStream)(pstg, L"CONTENTS", 18LL, 0LL, 0, &pstmContents) )
    return 0LL;

  // pstmContents 를 즉시 해제.
  (pstmContents->lpVtbl->Release)(pstmContents);
+ IsEnabled = wil::details::FeatureImpl<__WilFeatureTraits_Feature_3047977275>::__private_IsEnabled(&`wil::Feature<__WilFeatureTraits_Feature_3047977275>::GetImpl'::`2'::impl);
+ v10 = pstmContents;
+ v11 = !IsEnabled;
  v12 = pstg->lpVtbl;
+ if ( !v11 )
+   v10 = 0LL;
+ pstmContents = v10;
  (v12->DestroyElement)(pstg, L"CONTENTS");

  v13 = (pstg->lpVtbl->OpenStream)(pstg, &OlePres, 0LL, 16LL, 0, &pstmOlePres);
  if ( v13 )
  {
    *lpszPresStm |= 1u;
    if ( (pstg->lpVtbl->OpenStream)(pstg, L"CONTENTS", 0LL, 16LL, 0, &pstmContents) )
    {
      *lpszPresStm |= 2u;
    }
    else
    {
      // 이쪽 분기를 타도록 트리거
      (pstmContents->lpVtbl->Release)(pstmContents);
+     wil::details::FeatureImpl<__WilFeatureTraits_Feature_3047977275>::__private_IsEnabled(&`wil::Feature<__WilFeatureTraits_Feature_3047977275>::GetImpl'::`2'::impl);
    }
    return v13;
  }
  foretc.ptd = 0LL;
  v13 = UtReadOlePresStmHeader(pstmOlePres, &foretc, 0LL, 0LL);
  if ( v13 >= 0 )
  {
    v13 = (pstmOlePres->lpVtbl->Read)(pstmOlePres, &hdfh, 16LL);
    if ( v13 >= 0 )
    {
      v13 = OpenOrCreateStream(pstg, L"CONTENTS", &pstmContents);
      if ( v13 < 0 )
      {
        *lpszPresStm |= 2u;
        goto $errRtn_197;
      }
      if ( foretc.dwAspect == 4 )
      {
        *lpszPresStm |= 4u;
        v7 = 0;
        v13 = 0;
        goto $errRtn_197;
      }
      if ( foretc.cfFormat == 8 )
      {
        v14 = UtDIBStmToDIBFileStm(pstmOlePres, hdfh.dwSize, pstmContents);
LABEL_19:
        v13 = v14;
        goto $errRtn_197;
      }
      if ( foretc.cfFormat == 3 )
      {
        v14 = UtMFStmToPlaceableMFStm(pstmOlePres, hdfh.dwSize, hdfh.dwWidth, hdfh.dwHeight, pstmContents);
        goto LABEL_19;
      }
      v13 = -2147221398;
    }
  }
$errRtn_197:
  if ( pstmOlePres )
    (pstmOlePres->lpVtbl->Release)(pstmOlePres);
  // pstmContents 내 값이 있다면 해제 진행.
  if ( pstmContents )
    (pstmContents->lpVtbl->Release)(pstmContents);
  if ( foretc.ptd )
    CoTaskMemFree(foretc.ptd);
  if ( v13 )
  {
    v15 = L"CONTENTS";
    goto LABEL_31;
  }
  if ( v7 )
  {
    v15 = &OlePres;
LABEL_31:
    (pstg->lpVtbl->DestroyElement)(pstg, v15);
  }
  return v13;
}

패치된 라인을 보면 IsEnabled 라는 flag 를 하나 추가해서 pstmContents 의 해제 여부를 판단하고 해제가 되었으면 0으로 초기화 하게끔 패치가 되었습니다.

Reference