Анализ уязвимости Use-After-Free в ole32.dll (CVE-2025-21298)

Введение

В январе 2025 года Microsoft представила обновление безопасности, исправляющее несколько уязвимостей, в том числе CVE-2025-21298Windows 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) — это класс уязвимостей, при которых программа обращается к уже освобожденной области памяти, что может быть использовано злоумышленниками для выполнения произвольного кода.

Рассмотрим ситуацию:

Ситуация, располагающая к UAF
Рис. 1. Ситуация, располагающая к UAF.

После выполнения free(A) диапазон памяти может быть выделен заново, при необходимости. И если переменная А будет использоваться в дальнейшем при работе программы, например в проверке условий или вызове методов объектов, находящихся в выделенной памяти, то возникает уязвимость UAF. Наибольшую опасность она представляет, если злоумышленник контролирует содержимое в переменной С (например, память выделена для имени пользователя), — в таком случае он может повлиять на ход работы программы и даже полностью перехватить управление.

Proof of Concept

Вернемся к нашей уязвимости: в течение недели после выхода исправления, на GitHub появился репозиторий с PoC для CVE-2025-21298 и дополнительной информацией к нему. Исследователь провел diffing версий, результаты которого также выложены в открытый доступ, описана причина возникновения уязвимости и представлен memory corruption PoC.

Memory corruption PoC
Рис. 2. Memory corruption PoC.

Технические подробности

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

Исправленная UtOlePresStmToContentsStm
Рис. 3. Исправленная UtOlePresStmToContentsStm.
Запись значения 0 в pstmContents
Рис. 4. Запись значения «0» в pstmContents.

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

Изначально создается поток — объект класса IStream, адрес которого записывается в переменную pstmContents. В нее записывается указатель на кучу, в которой хранится содержимое объекта.

Вызов CreateStream
Рис. 5. Вызов CreateStream.

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

Содержимое буфера до вызова CreateStream
Рис. 6. Содержимое буфера до вызова CreateStream.
Содержимое буфера после вызова CreateStream
Рис. 7. Содержимое буфера после вызова CreateStream.

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

Вызов метода Release
Рис. 8. Вызов метода Release.
Содержимое буфера после вызова Release
Рис. 9. Содержимое буфера после вызова Release.

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

Вызов OpenStream
Рис. 10. Вызов OpenStream.

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

Содержимое pstmContents и pstmOlePres
Рис. 11. Содержимое pstmContents и pstmOlePres.

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

Вызов UtReadOlePresStmHeader
Рис. 12. Вызов UtReadOlePresStmHeader.

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

Завершающий блок UtOlePresStmToContentsStm
Рис. 13. Завершающий блок UtOlePresStmToContentsStm.

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

Ошибка получения адреса
Рис. 14. Ошибка получения адреса.

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

Значение, приводящее к ошибке
Рис. 15. Значение, приводящее к ошибке.

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

В программе реализована поддержка всех значений, кроме значения 2:

Возврат из UtReadOlePresStmHeader
Рис. 16. Возврат из UtReadOlePresStmHeader.

В этой функции существуют и другие ситуации, которые приводят к возврату ненулевого значения, однако это никак не будет влиять на поведение целевой функции UtOlePresStmToContentsStm.

Итак, подведем итог нашего обзора работы уязвимой функции при эксплуатации уязвимости:

  1. Создается IStream, указатель на объект записывается в pstmContents.
  2. Метод Release освобождает память, но указатель по прежнему хранится в pstmContents.
  3. Создается IStream на освобожденном месте, указатель записывается в pstmOlePres.
  4. pstmOlePres и pstmContents указывают на одну и ту же ячейку памяти.
  5. UtReadOlePresStmHeader возвращает не нулевое значение (ошибку).
  6. Метод 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 лишь подтверждает этот вывод.

Paraniod Security Обход KASLR в Windows: Атака на механизм предварительной выборки 6 мая
Обход KASLR в Windows: Атака на механизм предварительной выборки
Paraniod Security Анализ обновлений Microsoft Patch Tuesday – Апрель 2025 8 апреля
Анализ обновлений Microsoft Patch Tuesday – Апрель 2025
Paraniod Security Уязвимости манипуляции MSR: полный контроль? 1 февраля
Уязвимости манипуляции MSR: полный контроль?