Введение

В конце апреля 2023 был опубликован PoC для уязвимости CVE-2023-21769 в сервисе очереди сообщений MSMQ. Это одна из трёх уязвимостей, обнаруженных в MSMQ и запатченных в апрельском обновлении ОС Windows (1, 2, 3). Опубликованный PoC реализует уязвимость отказа в обслуживании сервиса MSMQ. Две другие уязвимости – удалённый BSoD и RCE, названная Queue Jumper. По информации от MSRC с помощью этих уязвимостей можно было взять под контроль практически все версии ОС Windows, на которых доступен и активен MSMQ. Серьёзно, не правда ли?

Поскольку протокол MSMQ бинарный, сходу идентифицировать в чём заключается данная уязвимость не получилось, и пришлось прибегнуть к методам обратной разработки.

Поэтому в данной заметке мы на простом примере проведём идентификацию патча и сравнительный анализ бинарного кода, немного познакомимся с проприетарным протоколом MQQB, и проанализируем уязвимость, поехали!

Идентификация патча

MSMQ является одной из технологий Windows для распределённого взаимодействия приложений, по умолчанию доступной на всех современных ОС данного семейства. Более подробно о нём можно узнать на Вики и в документации MSDN, здесь лишь отметим, что MSMQ реализует один из способов удалённого взаимодействия между приложениями с использованием сервера очередей, который получает и хранит отдельные сообщения или целые транзакции, и пересылает их приложению-адресату, когда появляется такая возможность.

Активируем этот сервис в опциях нашей Windows 10 (желательно на виртуальной машине), то в TCPView можно убедиться, что указанный в PoC TCP-порт 1801 прослушивается сервисом mqsvc.exe:

Прослушиваемые порты MSMQ
Прослушиваемые порты MSMQ

Подключимся к нему отладчиком WinDBG и запустим обнаруженный ранее PoC. Возникает исключение Access Violation из-за попытки записи по недоступному адресу:

Стек вызовов при возникновении ошибки
Стек вызовов при возникновении ошибки

По трассировке стека видно, что ошибка возникает в потоке, исполняющем библиотеку MQQM.dll, в конструкторе класса QmPacket, при записи 0x0C в запрещенную область памяти. Возможно, что уязвимость содержится именно в этой процедуре.

Скачаем патч с сайта MSRC или каким-либо другим способом, и найдем в нем библиотеку MQQM.dll, запустим анализ в дизассемблере IDA Pro. Стоит отметить, что Microsoft поставляет отладочные символы для данной библиотеки, поэтому нужно дать IDA Pro возможность скачать их, это очень облегчит анализ.

Сравнительный анализ патча

Воспользуемся Bindiff или Diaphora поиска патча в библиотеке. Diaphora это активно развивающийся проект, и в данном случае был использован именно он, однако архитектурно Diaphora не очень подходит для больших файлов, так как экспортирует данные IDA Pro в БД mysql, а затем сравнивает их с другой экспортированной таким же образом БД, и всё это на 100% Python. BinDiff сравнивает напрямую IDB-файлы и является нативным приложением, поэтому работает значительно быстрее, но для исследуемой библиотеки размером 1.2 МБ это не так критично.

Как правило, при анализе патча нас интересуют процедуры, которые слегка подкорректированы (из эмпирического предположения, что фикс не меняет всю логику работы программы). Сравним две версии программы:

Результаты сравнения MQQM.dll с помощью Diaphora
Результаты сравнения MQQM.dll с помощью Diaphora

Во вкладке Partial matches Diaphora показывает, что подмеченный нами ранее конструктор CQmPacket , в котором происходит краш при использовании PoC, как раз удовлетворяет данному критерию, коэффициент похожести между двумя его версиями - 0.720. Помимо этого небольшим изменениям подверглись ряд методов *::SectionIsValid. Что же изменилось в конструкторе CQmPacket? Отметим, что функция довольно объёмная, изменения в ней размазаны по всему телу:

CFG пропатченной / старой версии CQmPacket()
CFG пропатченной / старой версии CQmPacket()

Если приглядеться к добавленным/изменённым блокам, можно заметить использование новой процедуры GetNextSectionPtrSafe и методов вида *::GetNextSectionSafe вместо методов *::GetNextSection:

Фрагмент пропатченной / старой весрии CQmPacket()
Фрагмент пропатченной / старой весрии CQmPacket()

Сразу сделаем предположение, что ошибка в старой версии связана с недостаточными проверками арифметики указателей при разборе секций пакета. Так как у нас есть PoC – мы и так можем понять, где конкретно происходит ошибка, но если бы у нас не было PoC, то из предыдущего предположения, мы бы пытались попасть в эти запатченные ветки функции.

В PoC указано, что в обновленной версии при попытке эксплуатации уязвимости выводится строка Next section is behind packet end. Именно эта строка выводится в проверках GetNextSectionPtrSafe, то есть ошибка триггерится в ней, вот её декомпилированный и аннотированный код:

Декомпилированный код процедуры GetNextSectionPtrSafe
Декомпилированный код процедуры GetNextSectionPtrSafe

Данная процедура используется для того, чтобы безопасно сместить указатель на величины, указанные во втором и третьем аргументах. При этом осуществляется проверка, что смещения и полученный указатель не переполнились (в процедуре SafeAddPointersEx) и не вышли за пределы конца пакета (именно этот путь триггерится в PoC). Осталось только понять, что это за секции, и как контролировать их в пакете. А значит пришло время немного изучить данный бинарный протокол.

Анализ обработчика протокола

Уязвимый сетевой сервис работает по протоколу MQQB. Документация на него является общедоступной, поэтому разобрать отдельный пакет не так уж сложно.

Пакет MQQB отправляемый клиентом может состоять из множества секций, описанных в документации. Обработка таких сложных и вариативных структур всегда подвержена ошибкам, и недостаточные проверки формата данных в программах, написанных на C/C++ ­- лакомый кусочек для исследователей. По умолчанию пакет начинается с 16-байтовой структуры BaseHeader. Вот её поля на примере пакета из PoC:

Формат и содержимое BaseHeader
Формат и содержимое BaseHeader

Поле версии должно равняться 0x10, а сигнатура – 0x4C494F52 (LIOR), иначе пакет будет отброшен сервисом. В поле size размещается полный размер пакета. После структуры BaseHeader в пакете располагается структура UserHeader. Вот её неполный формат в контексте нашего пакета:

Формат и содержимое UserHeader
Формат и содержимое UserHeader

С подробным описанием структуры можно ознакомиться по ссылке. Первые 32 байта – это 2 поля UUID отправителя и получателя. Далее идут поля времени получения и отправления (0xFFFFFF и 0x4C494F52) и ID сообщения (0x3805). И наконец, наиболее важное для нас поле располагается по смещению 0x28 (0x38 от начала пакета), это битовая структура флагов пользовательского заголовка (UserHeaderFlags), которая равняется 0x201726D0 в PoC. По этим флагам сервис определяет, какие секции содержатся в пакете. Полный формат флагов представлен в той же ссылке, а на следующем рисунке представлены эти флаги для пакета с PoC:

Формат и содержимое UserHeader.flags
Формат и содержимое UserHeader.flags

Как видно из рисунка, многие среди прочих в заголовке установлены биты msg_prop_hdr и soap_hdr, значит данные секции содержатся в пакете. Попробуем понять, как они обрабатываются в программе. Что же происходит, когда программа понимает, что внутри есть эти секции? Поищем доступ к этому флагу в процедуре (1<<28 == 0x10000000), и найдём следующий блок кода:

Обработка секции Soap
Обработка секции Soap

Обнаруживается интересная схема: проверяется текущий указатель смещения в пакете nextSection (строки 234, 236), сохраняется как указатель на секцию, затем из пакета извлекается смещение для следующей секции (строка 239) и опять проверяется на нахождение этой секции в пределах пакета, и новая секция снова сохраняется в структуре СQmPacket. И наконец, перед выходом из блока, пересчитывается nextSection, снова используя смещение из данных пакета (245), и после этого nextSection не проверяется. Также заметим, что выражение 2*size + 0xB скорее всего означает, что в секции должны содержаться size двухбайтовых объектов (вероятно, символов Unicode-строки), и заголовок секции длиной 0xB.

Проверим наши догадки, найдём информацию об этой секции в документации. Действительно, размеры представлены в виде размеров Unicode строк, только это не две Soap-секции, а заголовок Soap-секции и её тело. Вот что пишет Microsoft по поводу поля смещения в header и body:

A 32-bit unsigned integer that specifies the length of the Header/Body field. This field MUST be set to the number of elements in the Unicode Header/Body field, including the terminating null character. This field has a valid range between 0x00000000 and the size limit imposed by the value of BaseHeader.PacketSize.

И как мы уже увидели в коде, если для Header результирующий указатель проверяется, то для Body это этого не происходит. Возможно, оно проверяется где-то далее по коду, ведь мы уже видели в начале блока проверку nextSection.

А вот и нет! Если секция Soap была последней при обработке, то управление перейдет к последнему блоку кода, который инициализирует конец пакета какими-то данными:

Последний блок функции
Последний блок функции

Именно на этой строчке кода происходит падение в PoC. Тут-то и сходятся звёзды - недоверенные данные гибкого формата на месте дополняются во время обработки, и всё это реализовано на небезопасной арифметике указателей в сервисе, имеющем доступ к драйверу ядра.

Таким образом мы пришли к следующему упрощенному представлению CQmPacket::CQmPacket:

Упрощенная блок-схема процедуры CQmPacket
Упрощенная блок-схема процедуры CQmPacket

Процедура состоит из последовательности условных переходов на блоки обработки каждой секции пакета. И действительно, если после Soap-секции будет обработка какой-либо другой секции, то указатель nextSection будет проверен и программа сообщит об ошибке в обработке пакета. А если нет, то управление перейдёт к последнему блоку (отмечен красным на диаграмме), и программа начнёт работать с непроверенным указателем. Именно таким образом при обработке пакета из PoC nextSection увеличивается далеко за пределы границ пакета и происходит запись по недоступным адресам. Поэтому корректный обработчик пакета должен проверять указатель nextSection после обработки каждой секции, и в процессе этой обработки.

Также отметим, что в случае современных ОС семейства Windows мы имеем дело с 64-битным адресным пространством, а смещения в пакетах – 32-битные, т.е. потенциально возможно сместить указатель записи только на 2*2^(32) + 11 байт вперёд. Это является уязвимостью, однако возможность её использования для чего-либо опасного зависит от многих факторов.

В данном случае, буфер в котором находятся наши данные всегда предшествует высоким Usermode-адресам процесса:

Фрагмент карты адресного пространства процесса MQSVC.exe
Фрагмент карты адресного пространства процесса MQSVC.exe

На рисунке выделена область памяти 0x0000025AB95E0000, в которой находится наш пакет (и другие пакеты, полученные процессом). Эта область является отображением в память временного файла C:\Windows\System32\msmq\storage\p000000d.mq для хранения пакетов очереди. Однако следующие доступные адреса находятся в 0x00007DF4********, и до них никак не добраться из нашего буфера имея лишь 4-байтовое смещение.

Для дальнейшей проработки данного вектора необходимо проверить возможность перехода данных пакета в другие области памяти, то есть осуществить Taint-анализ. За аллокацию объектов пакетов очереди сообщений отвечает драйвер MQAC.SYS, и, вероятно, уязвимость, вызывающая BSoD связана именно с ним.

Но на этом этапе исследования мы уже получили достаточно сведений для защиты сети от подобных атак, поэтому тут поставим точку (с запятой), и сформулируем результаты анализа.

Сопутствующие работы

На момент написания статьи никаких общедоступных исследований об уязвимостях Queue Jumper не было. Но впоследствии появился ряд работ в данном направлении. Приведу их список в хронологическом порядке, чтобы вы могли узнать ещё больше об MSMQ.

17 мая 2023: статья на китайском от исследователя zoemurmure, в которой также приведён анализ данного PoC. Однако в ней делается предположение о возможности перезаписи не только по положительным смещениям от пакета, но и по отрицательным, что не является верным.

1 июня 2023: статья на английском от исследователя Rick Osgood. Он копнул глубже, и сумел осуществить считывание памяти с помощью этих уязвимостей.

24 июля 2023: статья на английском от специалистов Fortinet. В ней описаны некоторые подробности реализации фаззера MSMQ и заявлено о возможности удалённого выполнения кода с помощью изменения заголовка CompoundMessage.

2 августа 2023: статья на английском на ресурсе securityingellignence.com от исследователя Valentina Palmiotti и др. Исследователи рассматривают перспективы эксплуатации на уровнях приложений и драйверов ядра, и приходят к предварительному заключению, что реализовать такое будет проблематично. Также в статье приведена идея удалённого детектирования уязвимых серверов без нарушения работы сервиса MSMQ.

Заключение

В результате анализа данной уязвимости мы ознакомились с базовыми приёмами Patch diffing’а (сравнительного анализа патчей) на уровне двоичного кода. По предыдущим публикациям заметно, что для некоторых читателей тематика обратной разработки представляет повышенный интерес, поэтому приведу небольшой список литературы, полезной в рамках настоящей статьи.

  • IDA Pro Book либо же The Ghidra Book – в зависимости от предпочитаемого вами основного инструмента анализа (PDF-ки легко гуглятся).

  • Денис Юричев – “Reverse Engineering для начинающих”. Ещё одна классическая книга, очень полезная для начинающих и доступная на русском языке.

  • Patch Diffing in the Dark – туториал по патч диффингу от Vulnerability Research Centre.

Как следует из нашей и остальных работ по теме MSMQ, защититься от сетевых атак с использованием уязвимостей из набора Queue Jumper можно запретив трафик по 1801 порту, либо проверяя каждый пакет MSMQ на корректность, то есть проверяя, что указатель nextSection не выйдет за границы пакета при его обработке.

Учитывая гибкую структуру пакета, просто идентифицировать протокол и проверить значение по какому-либо смещению не выйдет – для обнаружения атаки нужен полноценный парсер MQQB-пакетов. Так как документация на формат пакетов протокола и декомпилированный код разбора пакета доступны, разработка такого парсера возможна без особых усилий по реверс-инжинирингу.

Ввиду того, что уязвимый сервис доступен для активации практически во всех актуальных версиях Windows, пользователям, как и всегда, рекомендуется не пропускать обновления безопасности ОС.

Локально проверить наличие сервиса можно с помощью команды sc query "msmq", а также через GUI "Служб", и проверкой прослушиваемого сервисом порта (1801) через TCPView или powershell: Get-Process -Id (Get-NetTCPConnection -LocalPort 1801).OwningProcess. При обнаружении активного сервиса возможна проверка уязвимости с помощью PoC, упомянутого в начале статьи. Если в результате использования PoC происходит падение сервиса, рекомендуется отключить сервис / ограничить к нему доступ и приступать к обновлению ОС.

Понравился материал? Пишите свои замечания и вопросы в комментариях!


Наши ресурсы

 

Комментарии (2)


  1. virusaga
    22.08.2023 07:38

    По дефолту сервис включен и на каких версиях Windows? только Win10? или более старые версии тоже уязвимы?


    1. yamano Автор
      22.08.2023 07:38

      Спасибо за вопрос! Сервис доступен на всех версиях Windows, даже куда более ранних чем Win10. Данные уязвимости, однако (судя по сайту MSRC) затрагивают Win10, win11, и почти все редакции Windows-серверов
      По дефолту он не должен быть включен, однако устанавливаемые 3rd-party приложения могут на него опираться и включать при установке