Исследование уязвимости ядра Windows с CVE-2024-38063

Анализ CVE-2024-38063: удаленная эксплуатация ядра Windows​


Мы разобрались, как работает ошибка в сетевом стеке Windows, позволяющая удаленно получить максимальные привилегии в системе без каких-либо действий со стороны пользователя. Рассказываем, как локализовали уязвимость, сравнив две версии драйвера, и сформировали сценарий атаки.


Каждый второй вторник месяца компания 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 (и выиграли только через две недели).

714d500ad342e38037a846bbac0ba40b.png

Первоначальный анализ​

Чтобы понять, чем вызвана уязвимость, нужно сравнить файлы драйвера до патча и после. Есть два основных способа:

  1. Сохранить старый файл, после чего обновиться и получить новый. Это самый стабильный способ, но при его использовании желательно, чтобы старый файл не был слишком старым. Иначе есть большая вероятность вместо красивого и удобного сравнения в BinDiff получить огромное количество изменений, большинство из которых не связано с безопасностью.
  2. Использовать прекрасный сайт для проведения всех возможных исследований, связанных с Windows, — Winbindex. Обновленные файлы появляются там с небольшой задержкой, но это не очень критично.
Для наглядности мы решили использовать второй способ: зашли на сайт Winbindex и загрузили две последние версии драйвера для Windows 10 22H2.

f265abefe554cef7f9011772de41746e.png

Здесь 10.0.19041.4780 — версия с устраненной уязвимостью, а предыдущие — с еще не исправленной. Дату публикации файла можно определить, если зайти в дополнительную информацию или просто навести курсор на номер патча.

После загрузки файлов можно провести сравнение с помощью утилиты с открытым исходным кодом BinDiff, например, в виде плагина для IDA Pro. При сравнении двух файлов мы натолкнулись на достаточно редкую ситуацию в анализе Patch Tuesday: единственной функцией, подверженной изменению, была Ipv6pProcessOptions, использующаяся для обработки опций в IPv6-пакетах, например Jumbo Payload или Hop-By-Hop.

348443291d81f13b4bf393be638fcfbd.png

Если посмотреть внутрь функции в декомпилированном коде, то в конце можно обнаружить единственный измененный участок.

В версии от 23 июля:

b691a8f43f521a27d5554f4038d930b1.png

В версии от 13 августа:

4e25629887c18ce76f601edce94288f7.png

Теперь при появлении ошибок в обработке опций IPv6 используется функция IppSendError вместо IppSendErrorList. Различие между ними состоит в том, что IppSendErrorList отправляет ошибки на каждый пакет в цепочке, в то время как IppSendError — только на один. В этом можно убедиться, если посмотреть декомпилированный код функции IppSendErrorList:

963d91a6bc58947977266e97a2a12ef7.png

При дальнейшем анализе кода вокруг измененного участка становится понятна логика: Ipv6pProcessOptions предназначена для обработки только одного пакета (или фрагмента), в то время как IppSendErrorList проходится по всем пакетам в цепочке. Это явная логическая ошибка, которая может привести к чему-то плохому. Впрочем, к чему именно, мы не знали, поэтому продолжили исследование. Но сначала решили разработать чекер, чтобы проверить, что правильно поняли логику.

Действительно, на три пакета с ошибками в опциях IPv6 пришло шесть ответов:

  • три ошибки на первый пакет (Packet1 → Packet2 → Packet3);
  • две ошибки на второй пакет (Packet2 → Packet3);
  • одна ошибка на третий пакет (Packet3).
dc9723e62b4b709630ab289abfdae868.png

Надо заметить, что пакеты с ошибками дойдут до отправителя, только если в системе выключен Windows Firewall. Однако наличие включенного брандмауэра не влияет на то, дойдут ли отправленные пакеты до уязвимой системы, так как их обработка происходит на уровне ядра операционной системы, еще до обработки брандмауэром.

Zero to Hero​

При просмотре декомпилированного кода функции IppSendError можно заметить следующий участок кода:

83c9352d7e116816ca7e1c56f58762f7.png

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

Осознавая, что так быть не должно и в рабочем пакете поле размера не должно быть равно нулю, мы долго не могли понять, где именно применить этот примитив, поэтому обратились к предыдущим исследованиям уязвимостей в сетевом стеке TCP/IP в Windows (1, 2).

Мы наткнулись на статью с анализом похожей уязвимости, связанной с реструктуризацией фрагментов пакета, — CVE-2022-34718. Она находилась в функции Ipv6pReassembleDatagram, использующей недокументированные структуры пакета и реструктуризации (в терминологии большинства исследователей: Packet_t и Reassembly_t).

Хотя в этой функции не используется поле, значение которого мы изменили, в родительской Ipv6pReceiveFragment оно влияет на поля структуры Reassembly_t:

34fc714ff9f0104756f7c44fbe5f405a.png

Здесь мы видим, что из ошибочно измененного ранее значения поля IPv6_HeaderSize происходит вычитание 0x30, так как функция предполагает, что на данном этапе размер никак не может быть меньше 48 байт. Этот участок кода — отличная возможность применить ранее приобретенный примитив, однако сюда еще нужно дойти. Для этого необходимо пройти две проверки, первую из которых можно увидеть на изображении выше: это проверка на то, что фрагмент является первым в цепочке. Это условие хоть и уменьшает возможности эксплуатации, но не сильно: вполне вероятно, нам хватит и одного пакета.

Второе условие — это проверка на то, что сдвиг фрагмента не является нулем, однако из-за обработки в IppSendError (внутри которой вызывается NetioRetreatNetBufferList, чтобы вернуть каретку к началу пакета) фактически это условие проверяет то, что нулю не равен FlowLabel в структуре заголовка IPv6.

Как Windows видит код tcpip.sys

Как Windows видит код tcpip.sys
Как происходит на самом деле

Как происходит на самом деле
Поле этой структуры используется в вышеупомянутой функции Ipv6pReassembleDatagram при копировании данных из буфера.

db4886218ea5600610b7fad329e44b31.png

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

be2a06dace8ac8fe09a7c13950b71303.png

Здесь переменная TotalLength равна сумме размера только что проинициализированного Reassembly_t с нашим размером в underflow (после эксплуатации он всегда равен 0xffd8):

1b88c7070f14bfb65889d944eb25b091.png

Можно подумать, что в этом участке кода произойдет переполнение UInt16, коим является UnfragmentableLength, однако сложение происходит с типом UInt32, из-за чего переполнение не произойдет и TotalLength будет больше чем 0xfff.

Финиш и импакт​

Исследуя другие функции, в которых используется структура Reassembly, мы нашли обработчик, вызывающийся при истечении времени ожидания следующего фрагмента, — Ipv6pReassemblyTimeout. В этой функции также происходит копирование данных из нашего буфера по отрицательному (большому положительному) размеру.

51e61408af6e5f0c9492a298941be36f.png

Чтобы попасть в этот участок кода, нам нужно поставить FlowLimit равным единице (из-за IppSendError драйвер воспринимает это поле как FragOffset), а затем подождать минуту. После этого происходит переполнение кучи в ядре Windows почти полностью контролируемыми нами значениями, ведь наспреить рядом нужные чанки можно достаточно легко. Казалось бы, все отлично?

В удаленной эксплуатации таких уязвимостей всегда есть большая проблема: мы не знаем адресов ядра после рандомизации KASLR. Если бы мы исполнялись на самом атакуемом хосте (что вполне возможно и позволяет поднять привилегии до SYSTEM), то можно было бы использовать все многообразие утечек KASLR в Windows (1, 2). Однако это не наш случай, и приведенный метод использования примитива хоть и дает прекрасные возможности для локального повышения привилегий и удаленного BSOD, но достаточно жесткие ограничения по размеру аллоцируемого участка памяти не позволяют узнать адреса ядра, не допуская его падения.

Здесь есть две оговорки:

  1. Любая дополнительная уязвимость ядра, которая позволит узнать адреса KASLR, в цепочке с представленной уязвимостью ведет к RCE.
  2. Сетевой стек Windows — одна из самых сложных частей ОС. Представленный в статье примитив эксплуатации может оказывать влияние на участки кода в дальнейшей обработке пакета, поэтому, вполне вероятно, существуют и другие (крайне сложные) методы атак, которые будут вести к утечке KASLR и компрометации ядра.

PoC video​

VK.com | VK

Авторы статьи:

Павел Блинников, руководитель группы исследования уязвимостей.

Андрей Чижов, старший специалист по исследованию уязвимостей.

Источник
 
Перенес из темы
 
Назад
Сверху Снизу