Анализ уязвимости Use-After-Free в ole32.dll (CVE-2025-21298)
Введение
В январе 2025 года Microsoft представила обновление безопасности, исправляющее несколько уязвимостей, в том числе CVE-2025-21298 — Windows OLE Remote Code Execution Vulnerability. Эта уязвимость была оценена в 9.8 балла по шкале CVSS, а максимальное влияние оценено как удаленное выполнение кода (Remote Code Execution — RCE).
В течение нескольких недель был опубликован Proof of Concept (PoC) на GitHub, а большим количеством компаний, занимающихся вопросами безопасности, были выпущены статьи, посвященные этой уязвимости. Однако с этого момента прошло уже несколько месяцев, и по состоянию на конец марта 2025 года работающий эксплоит не был опубликован (за исключением PoC), а эксплуатации этой уязвимости в «дикой природе» (in the wild, ITW) не обнаружено.
Это наталкивает на мысль: а действительно ли эта уязвимость так страшна, как ее описали изначально? В этой статье мы подробно разберем CVE-2025-21298, представленный на GitHub PoC, и постараемся ответить на вопрос: «Возможно ли добиться RCE, при эксплуатации этой уязвимости?»
Описание уязвимости
CVE-2025-21298 — это уязвимость класса CWE-416: Use After Free, не требующая взаимодействия с пользователем (zero-click vulnerability), в технологии Windows Object Linking and Embedding (OLE). Данная технология используется в большом количестве инструментов Windows — от офисных документов до почты. Для эксплуатации этой уязвимости злоумышленнику необходимо сформировать вредоносный rtf-файл и отправить его жертве.
Уязвимость Use-After-Free (UAF) — это класс уязвимостей, при которых программа обращается к уже освобожденной области памяти, что может быть использовано злоумышленниками для выполнения произвольного кода.
Рассмотрим ситуацию:

После выполнения free(A) диапазон памяти может быть выделен заново, при необходимости. И если переменная А будет использоваться в дальнейшем при работе программы, например в проверке условий или вызове методов объектов, находящихся в выделенной памяти, то возникает уязвимость UAF. Наибольшую опасность она представляет, если злоумышленник контролирует содержимое в переменной С (например, память выделена для имени пользователя), — в таком случае он может повлиять на ход работы программы и даже полностью перехватить управление.
Proof of Concept
Вернемся к нашей уязвимости: в течение недели после выхода исправления, на GitHub появился репозиторий с PoC для CVE-2025-21298 и дополнительной информацией к нему. Исследователь провел diffing версий, результаты которого также выложены в открытый доступ, описана причина возникновения уязвимости и представлен memory corruption PoC.

Технические подробности
Итак, уязвимость оказалась в модуле ole32.dll в функции UtOlePresStmToContentsStm. При исправлении уязвимости в эту функцию были добавлены всего несколько строк, основная цель которых очистить содержимое pstmContents после выполнения метода Release:


Исходя из этого, мы можем предположить, что проблема была в значении, которое хранилось в этой переменной. А теперь давайте разберемся с тем, как эта функция вела себя до исправления. Для этого воспользуемся windbg и запустим winword.exe под отладкой с rtf-документом из PoC.
Изначально создается поток — объект класса IStream, адрес которого записывается в переменную pstmContents. В нее записывается указатель на кучу, в которой хранится содержимое объекта.

При неуспешном выполнении CreateStream функция завершает свою работу. В моем случае в pstmContents записан адрес 0x17560530


Если поток успешно создан, то сразу выполняется метод Release для этого объекта, освобождая память.


Далее открывается поток — объект класса IStream, адрес которого записывается в переменную pstmOlePres.

Если после вызова метода OpenStream мы посмотрим на содержимое переменных pstmContents и pstmOlePres, то увидим, что они содержат указатель на один и тот же адрес.

Далее, при успешном открытии потока, вызывается функция UtReadOlePresStmHeader, которая обрабатывает заголовок потока.

В случае ошибки выполнение переходит на завершающий блок функции, который очищает память выделенную под pstmContents и pstmOlePres.

Так как обе эти переменные содержат указатель на один и тот же адрес в памяти, (на один и тот же объект), то после вызова Release для pstmOlePres, программа, пытаясь получить адрес метода Release для pstmContents, пытается прочитать значение по адресу, который остался мусором после освобождения объекта.

PoC в данном случае необходим для выполнения двух условий: во-первых, привести выполнение программы в UtOlePresStmToContentsStm, и во-вторых, вызвать ошибку в UtReadOlePresStmHeader. Мы не будем останавливаться на первом условии, так как для этого придется разобрать полностью структуру rtf документа и не только, что не входит в рамки этой статьи, а остановимся на втором условии — ошибке в UtReadOlePresStmHeader. Единственное о чем стоит упомянуть — это то, что rtf документ состоит из тегов и их содержимого, а тег objdata содержит байты, записанные в 16-ричной форме. Проблемное значение, из-за которого возникает ошибка записано вот в этих двух байтах:

Они определяют формат STM (я не могу со 100% вероятностью быть уверен, но наиболее подходящее определение, которое я нашел — software transactional memory).
В программе реализована поддержка всех значений, кроме значения 2:

В этой функции существуют и другие ситуации, которые приводят к возврату ненулевого значения, однако это никак не будет влиять на поведение целевой функции UtOlePresStmToContentsStm.
Итак, подведем итог нашего обзора работы уязвимой функции при эксплуатации уязвимости:
- Создается IStream, указатель на объект записывается в pstmContents.
- Метод Release освобождает память, но указатель по прежнему хранится в pstmContents.
- Создается IStream на освобожденном месте, указатель записывается в pstmOlePres.
- pstmOlePres и pstmContents указывают на одну и ту же ячейку памяти.
- UtReadOlePresStmHeader возвращает не нулевое значение (ошибку).
- Метод Release освобождает память pstmOlePres.
Происходит попытка разыменования указателя в освобожденной памяти (вычисление адреса метода Release для дальнейшего вызова).
Теперь для перехвата управления нам необходимо между двумя вызовами методов Release записать в освобожденное место контролируемое нами значение (для Type Confusion), однако, проблема в том, что оба эти метода выполняются последовательно, без каких-либо возможных вариантов записать свое значение. Эта ситуация приводит к невозможности перехвата управления в данном конкретном случае. В этом и заключается главная проблема CVE-2025-21298: два критических вызова Release в блоке очистки (Рис. 13) выполняются строго последовательно, один за другим, без каких-либо промежуточных вызовов или операций, которые дали бы атакующему «окно» для манипуляций с памятью.
Заключение
Таким образом, анализ PoC и кода ole32.dll показывает, что CVE-2025-21298 действительно является Use-After-Free уязвимостью, которая может привести к аварийному завершению (DoS) при открытии специально сформированного RTF-файла.
Однако, из-за специфики кода в функции UtOlePresStmToContentsStm, а именно — непосредственного следования второго вызова Release сразу за первым, окно для эксплуатации и подмены данных в освобожденной памяти отсутствует. Это делает достижение RCE с помощью данного конкретного пути крайне маловероятным.
Вероятно, высокий рейтинг CVSS был присвоен исходя из потенциальной опасности UAF в критическом компоненте OLE с zero-click вектором атаки, но без учета конкретных сложностей эксплуатации именно этого случая. Отсутствие эксплойтов ITW лишь подтверждает этот вывод.