[하루한줄] CVE-2025-21298: Windows OLE Double Free 취약점
URL
- https://github.com/ynwarcs/CVE-2025-21298
- https://msrc.microsoft.com/update-guide/vulnerability/CVE-2025-21298
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.dll
의 UtOlePresStmToContentsStm
라는 함수에서 취약점이 발생했는데요. 이 함수는 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
본 글은 CC BY-SA 4.0 라이선스로 배포됩니다. 공유 또는 변경 시 반드시 출처를 남겨주시기 바랍니다.