Анализ критической уязвимости Windows CVE‑2024‑38063
Каждый второй вторник месяца компания Microsoft выпускает Patch Tuesday — обновление для ОС Windows, в котором устраняет критические уязвимости. В обновлении от 13 августа 2024 года была исправлена критическая уязвимость в сетевом стеке, позволяющая получить удаленный доступ с максимальными привилегиями при возможности сетевого взаимодействия по протоколу IPv6.
Ей был присвоен идентификатор CVE‑2024‑38063. По сути, она являлась zero‑click Windows TCP/IP RCE. Согласно информации на официальной странице в Microsoft Security Bulletin, реализация уязвимости связана с Integer Underflow в одной из функций драйвера tcpip.sys
, отвечающего за обработку пакетов IPv6. Ввиду высокой критичности этой ошибки сотрудники нашей группы исследования уязвимостей приступили к игре в Patch Tuesday — Exploit Wednesday (и выиграли только через две недели).
Чтобы понять, чем вызвана уязвимость, нужно сравнить файлы драйвера до патча и после. Есть два основных способа:
- Сохранить старый файл, после чего обновиться и получить новый. Это самый стабильный способ, но при его использовании желательно, чтобы старый файл не был слишком старым. Иначе есть большая вероятность вместо красивого и удобного сравнения в BinDiff получить огромное количество изменений, большинство из которых не связано с безопасностью.
- Использовать прекрасный сайт для проведения всех возможных исследований, связанных с Windows, — Winbindex. Обновленные файлы появляются там с небольшой задержкой, но это не очень критично.
Для наглядности мы решили использовать второй способ: зашли на сайт Winbindex и загрузили две последние версии драйвера для Windows 10 22H2.
Здесь 10.0.19041.4780 — версия с устраненной уязвимостью, а предыдущие — с еще не исправленной. Дату публикации файла можно определить, если зайти в дополнительную информацию или просто навести курсор на номер патча.
После загрузки файлов можно провести сравнение с помощью утилиты с открытым исходным кодом BinDiff, например, в виде плагина для IDA Pro. При сравнении двух файлов мы натолкнулись на достаточно редкую ситуацию в анализе Patch Tuesday: единственной функцией, подверженной изменению, была Ipv6pProcessOptions
, использующаяся для обработки опций в IPv6-пакетах, например Jumbo Payload или Hop‑By‑Hop.
Если посмотреть внутрь функции в декомпилированном коде, то в конце можно обнаружить единственный измененный участок.
В версии от 23 июля:
В версии от 13 августа:
Теперь при появлении ошибок в обработке опций IPv6 используется функция IppSendError
вместо IppSendErrorList
. Различие между ними состоит в том, что IppSendErrorList
отправляет ошибки на каждый пакет в цепочке, в то время как IppSendError
— только на один. В этом можно убедиться, если посмотреть декомпилированный код функции IppSendErrorList
:
При дальнейшем анализе кода вокруг измененного участка становится понятна логика: Ipv6pProcessOptions
предназначена для обработки только одного пакета (или фрагмента), в то время как IppSendErrorList
проходится по всем пакетам в цепочке. Это явная логическая ошибка, которая может привести к чему‑то плохому. Впрочем, к чему именно, мы не знали, поэтому продолжили исследование. Но сначала решили разработать чекер, чтобы проверить, что правильно поняли логику.
Действительно, на три пакета с ошибками в опциях IPv6 пришло шесть ответов:
- три ошибки на первый пакет (Packet1 → Packet2 → Packet3);
- две ошибки на второй пакет (Packet2 → Packet3);
- одна ошибка на третий пакет (Packet3).
Надо заметить, что пакеты с ошибками дойдут до отправителя, только если в системе выключен Windows Firewall. Однако наличие включенного брандмауэра не влияет на то, дойдут ли отправленные пакеты до уязвимой системы, так как их обработка происходит на уровне ядра операционной системы, еще до обработки брандмауэром.
При просмотре декомпилированного кода функции IppSendError
можно заметить следующий участок кода:
Такое поведение является корректным, так как при вызове этой функции заканчивается обработка текущего пакета и отправляется сообщение об ошибке. Однако по причине того, что в уязвимой версии драйвера этот участок кода выполняется для каждого пакета из цепочки, в том числе еще не обработанных, появляется вероятность использования поля IPv6_HeaderSize
при обработке следующих пакетов. Фактически это поле равно размеру заголовка IPv6, включая все вложенные заголовки опций.
Осознавая, что так быть не должно и в рабочем пакете поле размера не должно быть равно нулю, мы долго не могли понять, где именно применить этот примитив, поэтому обратились к предыдущим исследованиям уязвимостей в сетевом стеке TCP/IP в Windows (1, 2).
Мы наткнулись на статью с анализом похожей уязвимости, связанной с реструктуризацией фрагментов пакета, — CVE‑2022‑34718. Она находилась в функции Ipv6pReassembleDatagram
, использующей недокументированные структуры пакета и реструктуризации (в терминологии большинства исследователей: Packet_t
и Reassembly_t
).
Хотя в этой функции не используется поле, значение которого мы изменили, в родительской Ipv6pReceiveFragment
оно влияет на поля структуры Reassembly_t
:
Здесь мы видим, что из ошибочно измененного ранее значения поля IPv6_HeaderSize
происходит вычитание 0×30
, так как функция предполагает, что на данном этапе размер никак не может быть меньше 48 байт. Этот участок кода — отличная возможность применить ранее приобретенный примитив, однако сюда еще нужно дойти. Для этого необходимо пройти две проверки, первую из которых можно увидеть на изображении выше: это проверка на то, что фрагмент является первым в цепочке. Это условие хоть и уменьшает возможности эксплуатации, но не сильно: вполне вероятно, нам хватит и одного пакета.
Второе условие — это проверка на то, что сдвиг фрагмента не является нулем, однако из‑за обработки в IppSendError
(внутри которой вызывается NetioRetreatNetBufferList
, чтобы вернуть каретку к началу пакета) фактически это условие проверяет то, что нулю не равен FlowLabel
в структуре заголовка IPv6.
tcpip.sys
Поле этой структуры используется в вышеупомянутой функции Ipv6pReassembleDatagram
при копировании данных из буфера.
В этот момент мы уже были уверены, что полностью поняли уязвимость, однако ядро не доходит до выполнения нужного кода из‑за следующей проверки в начале функции:
Здесь переменная TotalLength
равна сумме размера только что проинициализированного Reassembly_t
с нашим размером в underflow
(после эксплуатации он всегда равен 0xffd8
):
Можно подумать, что в этом участке кода произойдет переполнение UInt16
, коим является UnfragmentableLength
, однако сложение происходит с типом UInt32
, из-за чего переполнение не произойдет и TotalLength
будет больше чем 0xfff
.
Исследуя другие функции, в которых используется структура Reassembly
, мы нашли обработчик, вызывающийся при истечении времени ожидания следующего фрагмента, — Ipv6pReassemblyTimeout
. В этой функции также происходит копирование данных из нашего буфера по отрицательному (большому положительному) размеру.
Чтобы попасть в этот участок кода, нам нужно поставить FlowLimit
равным единице (из‑за IppSendError
драйвер воспринимает это поле как FragOffset
), а затем подождать минуту. После этого происходит переполнение кучи в ядре Windows почти полностью контролируемыми нами значениями, ведь наспреить рядом нужные чанки можно достаточно легко. Казалось бы, все отлично?
В удаленной эксплуатации таких уязвимостей всегда есть большая проблема: мы не знаем адресов ядра после рандомизации KASLR. Если бы мы исполнялись на самом атакуемом хосте (что вполне возможно и позволяет поднять привилегии до SYSTEM), то можно было бы использовать все многообразие утечек KASLR в Windows (1, 2). Однако это не наш случай, и приведенный метод использования примитива хоть и дает прекрасные возможности для локального повышения привилегий и удаленного BSOD, но достаточно жесткие ограничения по размеру аллоцируемого участка памяти не позволяют узнать адреса ядра, не допуская его падения.
Здесь есть две оговорки:
- Любая дополнительная уязвимость ядра, которая позволит узнать адреса KASLR, в цепочке с представленной уязвимостью ведет к RCE.
- Сетевой стек Windows — одна из самых сложных частей ОС. Представленный в статье примитив эксплуатации может оказывать влияние на участки кода в дальнейшей обработке пакета, поэтому, вполне вероятно, существуют и другие (крайне сложные) методы атак, которые будут вести к утечке KASLR и компрометации ядра.
Несмотря на сложность эксплуатации данной уязвимости с точки зрения удаленного выполнения вредоносного кода, рекомендуем отключать IPv6 на хостах где он на самом деле не используется. BI.ZONE EDR в рамках рекомендаций безопасности выявляет включенные компоненты IPv6 на сетевых интерфейсах хоста и сообщает о мисконфигурации команде мониторинга. Это правило доступно как в коробочной версии BI.ZONE EDR, так и в сервисе BI.ZONE TDR.