Разбираем уязвимость в службе очереди сообщений Windows

Введение

В конце апреля 2023 года исследователи опубликовали PoC для уязвимости CVE-2023-21769 в сервисе очереди сообщений MSMQ (https://github.com/omair2084/msmq_re).

Это одна из трех уязвимостей, обнаруженных в MSMQ и запатченных в апрельском обновлении ОС Windows:
https://msrc.microsoft.com/update-guide/vulnerability/CVE-2023-21554,
https://msrc.microsoft.com/update-guide/vulnerability/CVE-2023-21769,
https://msrc.microsoft.com/update-guide/vulnerability/CVE-2023-28302.

Опубликованный PoC реализует уязвимость отказа в обслуживании сервиса MSMQ (две другие уязвимости – BSoD и RCE).

Для того, чтобы более качественно детектировать все возможные варианты ее использования, а не только приведенный PoC, необходимо детально проанализировать эту уязвимость. В данной заметке мы на простом примере проведем идентификацию патча и сравнительный анализ бинарного кода, познакомимся с проприетарным протоколом MQQB и проанализируем данную уязвимость.

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

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

Рисунок 1 – Активируемые сетевые сервисы

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


Рисунок 2 – Трассировка вызовов при возникновении ошибки

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

Скачаем патч с сайта MSRC https://msrc.microsoft.com/update-guide/vulnerability/CVE-2023-21554 (или каким-либо другим способом) и найдем в нем библиотеку MQQM.dll, запустим анализ в дизассемблере IDA Pro. Стоит отметить, что Microsoft поставляет отладочные символы для данной библиотеки, поэтому нужно дать IDA Pro возможность скачать их, это очень облегчит анализ.

Сравнительный анализ бинарного кода

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

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

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

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


Рисунок 4 – CFG пропатченной / старой версии CQmPacket

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

Рисунок 5 – Фрагмент пропатченной / старой версии CQmPacket

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

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

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

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

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

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

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


Рисунок 7 – Формат и содержимое BaseHeader

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


Рисунок 8 – Формат и содержимое UserHeader

С подробным описанием структуры можно ознакомиться по ссылке (https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-mqmq/05...). Первые 32 байта – это 2 поля UUID отправителя и получателя. Далее идут поля времени получения и отправления (0xFFFFFF и 0x4C494F52) и ID сообщения (0x3805). И наконец, наиболее важное для нас поле UserHeader располагается по смещению 0x28 (0x38 от начала пакета), это битовая структура флагов пользовательского заголовка (flags), которая равняется 0x201726D0 в PoC. По этим флагам сервис определяет, какие секции содержатся в пакете.

Полный формат флагов представлен по этой же ссылке, а на следующем рисунке представлены эти флаги для пакета с PoC:


Рисунок 9 – Формат и содержимое UserHeader.flags

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


Рисунок 10 – Обработка секции Soap

Обнаруживается интересная схема: проверяется текущий указатель смещения в пакете (nextSection), сохраняется как указатель на секцию, затем из пакета извлекается смещение для следующей секции (239) и опять проверяется на нахождение этой секции в пределах пакета, а новая секция снова сохраняется в структуре QmPacket. И, наконец, перед выходом из блока пересчитывается nextSection, снова беря смещение из текущей секции (245), но без его проверки. Также заметим, что выражение 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.

Если продолжить анализировать QmPacket::QmPacket, то можно прийти к следующему упрощенному представлению данной процедуры:

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

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

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


Рисунок 12 – Фрагмент карты адресного пространства процесса MQSVC.exe

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

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

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

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

  • 17 мая 2023: https://www.zoemurmure.top/posts/cve_2023_21554/ – статья на китайском от исследователя zoemurmure, в которой также приведен анализ PoC. Однако в ней делается предположение о возможности перезаписи не только по положительным смещениям от пакета, но и по отрицательным, что не является верным.
  • 1 июня 2023: https://www.richardosgood.com/posts/diving-into-queuejumper/ – статья на английском от исследователя Rick Osgood. Он копнул глубже и сумел осуществить считывание памяти с помощью этих уязвимостей.
  • 24 июля 2023: https://www.fortinet.com/blog/threat-research/microsoft-message-queuing-... – статья на английском от специалистов Fortinet. В ней описаны некоторые подробности реализации фаззера MSMQ и заявлено о возможности удаленного выполнения кода с помощью изменения заголовка CompoundMessage.

Заключение

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

  • IDA Pro Book либо The Ghidra Book – в зависимости от предпочитаемого вами основного инструмента анализа (PDF-файлы легко можно найти в Google);
  • Денис Юричев – «Reverse Engineering для начинающих» – еще одна классическая книга, очень полезная для начинающих и доступная на русском языке;
  • https://github.com/VulnerabilityResearchCentre/patch-diffing-in-the-dark... – туториал по патч-диффингу от Vulnerability Research Centre.

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

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