
CVE-2025-21298 の概念実証と詳細
これは CVE-2025-21298 - Windows OLE リモートコード実行の脆弱性 (CVSS 9.8) の概念実証です。これは メモリ破壊の PoC であり、エクスプロイトではありません。
完全なパッチ差分は ghidriff によるものです: リンク
脆弱性は ole32.dll!UtOlePresStmToContentsStm にあります。この関数の目的は、OLE ストレージ内の "OlePres" ストリームのデータを適切な形式のデータに変換し、同じストレージ内の "CONTENTS" ストリームに挿入することです。関数は、ストレージオブジェクトへの IStorage ポインタと、あまり重要ではない3つの引数を受け取ります。
以下は、2025年1月のパッチとの差分を含む関数の実装です:
__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;
// Create a "CONTENTS" stream in the storage and store it into pstmContents
if ( (lpVtbl->CreateStream)(pstg, L"CONTENTS", 18LL, 0LL, 0, &pstmContents) )
return 0LL;
// Immediately release pstmContents, we're not going to be using it right now
(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);// 2nd option to fail -> no OlePres stream
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);
// Release pstmContents if it still exists, we need to clean up
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;
}
問題は pstmContents 変数にあります。最初に、この変数は関数の先頭で作成される "CONTENTS" ストリームオブジェクトへのポインタを格納するために使われます。ストリームは作成後すぐに破棄され、pstmContents に格納されたポインタは解放されます (coml2.dll!ExposedStream::~ExposedStream で解放されます)。しかし、変数には解放済みのポインタがまだ残っています。関数のさらに下の方で、この変数は "CONTENTS" ストリームへのポインタを再び格納するために再利用される場合があります。そのため、関数の末尾には、変数にポインタが格納されている場合に備えて解放するクリーンアップコードがあります。このコードは、UtReadOlePresStmHeader が失敗する可能性を考慮していません。失敗した場合、pstmContents は解放済みのポインタを指したままになり、クリーンアップコードに到達して、そのポインタが再度解放されます。これにより、二重解放 (double-free) が発生します。
パッチの差分にあるように、Microsoft は、最初に含まれていたポインタが解放された後に pstmContents をゼロに設定することでこの問題を修正しました。
リポジトリには、この脆弱性を再現する RTF ファイルがあります。私は MS Word でファイルを開いてテストしましたが、RTF データを解析する他のアプリケーション (例: Outlook) でもテストできます。OLE オブジェクトを埋め込む他の形式を介した悪用も可能かもしれませんが、試していません。
ビデオ:
詳細は後日公開します。