Мы разобрались, как работает ошибка в сетевом стеке 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 (и выиграли только через две недели).
Первоначальный анализ
Чтобы понять, чем вызвана уязвимость, нужно сравнить файлы драйвера до патча и после. Есть два основных способа:
Сохранить старый файл, после чего обновиться и получить новый. Это самый стабильный способ, но при его использовании желательно, чтобы старый файл не был слишком старым. Иначе есть большая вероятность вместо красивого и удобного сравнения в 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. Однако наличие включенного брандмауэра не влияет на то, дойдут ли отправленные пакеты до уязвимой системы, так как их обработка происходит на уровне ядра операционной системы, еще до обработки брандмауэром.
Zero to Hero
При просмотре декомпилированного кода функции IppSendError
можно заметить следующий участок кода:
Такое поведение является корректным, так как при вызове этой функции заканчивается обработка текущего пакета и отправляется сообщение об ошибке. Однако по причине того, что в уязвимой версии драйвера этот участок кода выполняется для каждого пакета из цепочки, в том числе еще не обработанных, появляется вероятность использования поля IPv6_HeaderSize
при обработке следующих пакетов. Фактически это поле равно размеру заголовка IPv6, включая все вложенные заголовки опций.
Осознавая, что так быть не должно и в рабочем пакете поле размера не должно быть равно нулю, мы долго не могли понять, где именно применить этот примитив, поэтому обратились к предыдущим исследованиям уязвимостей в сетевом стеке TCP/IP в Windows (1, 2).
Мы наткнулись на статью с анализом похожей уязвимости, связанной с реструктуризацией фрагментов пакета, — CVE-2022-34718. Она находилась в функции Ipv6pReassembleDatagram
, использующей недокументированные структуры пакета и реструктуризации (в терминологии большинства исследователей: Packet_t
и Reassembly_t
).
Хотя в этой функции не используется поле, значение которого мы изменили, в родительской Ipv6pReceiveFragment
оно влияет на поля структуры Reassembly_t
:
Здесь мы видим, что из ошибочно измененного ранее значения поля IPv6_HeaderSize
происходит вычитание 0x30
, так как функция предполагает, что на данном этапе размер никак не может быть меньше 48 байт. Этот участок кода — отличная возможность применить ранее приобретенный примитив, однако сюда еще нужно дойти. Для этого необходимо пройти две проверки, первую из которых можно увидеть на изображении выше: это проверка на то, что фрагмент является первым в цепочке. Это условие хоть и уменьшает возможности эксплуатации, но не сильно: вполне вероятно, нам хватит и одного пакета.
Второе условие — это проверка на то, что сдвиг фрагмента не является нулем, однако из-за обработки в IppSendError
(внутри которой вызывается NetioRetreatNetBufferList
, чтобы вернуть каретку к началу пакета) фактически это условие проверяет то, что нулю не равен FlowLabel
в структуре заголовка IPv6.
Поле этой структуры используется в вышеупомянутой функции 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 и компрометации ядра.
PoC video
https://vk.com/video-187584378_456239240?list=ln-zarPewf2vrk1X1XbVZ
Авторы статьи:
Павел Блинников, руководитель группы исследования уязвимостей.
Андрей Чижов, старший специалист по исследованию уязвимостей.
Комментарии (7)
NickGorbarov
28.08.2024 18:36Интересная статья, к сожалению редко пишут с детальным разбором... Супер! Ждем еще по другим CVE!
jackchickadee
что если фаерволл выключен и остановлен, но ни одного маршрута IPv6 в таблице роутинга нет,
ни один процесс не слушает IPv6 и вообще сделано вот так:
/bin/regtool remove "/HKLM/SYSTEM/CurrentControlSet/services/Dhcp/Parametersv6"
/bin/regtool -d set '/HKLM/SYSTEM/CurrentControlSet/Services/Tcpip6/Parameters/DisabledComponents' 4294967295
/bin/regtool -d set '/HKLM/SYSTEM/CurrentControlSet/Services/Tcpip6/Parameters/Start' 4
/bin/regtool -d set '/HKLM/SYSTEM/CurrentControlSet/Services/Wanarpv6/Parameters/Start' 4
/bin/regtool -d set '/HKLM/SYSTEM/CurrentControlSet/Services/WANARP/Parameters/Start' 4
NI="$COMSPEC/../netsh.exe interface"
$NI IPV6 set global randomizeidentifier=disabled
$NI IPV6 set privacy state=disable
$NI ipv6 6to4 set state state=disabled
$NI ipv6 isatap set state state=disabled
$NI ipv6 set teredo disable
$NI ipv6 set global dhcpmediasense=disabled
$NI teredo set state disable
$NI 6to4 set state disabled
$NI 6to4 set state disabled undoonstop=disabled
$NI isatap set state disabled
$COMSPEC/../sc.exe stop stop Tcpip6
$COMSPEC/../sc.exe stop stop Wanarpv6
$COMSPEC/../sc.exe config Tcpip6 start= disabled
$COMSPEC/../sc.exe config Wanarpv6 start= disabled
здесь не понял.
как это сочетается с предыдущим тезисом ?
работающий Windows Firewall с полностью заблокированным IPv6 защищает от описанной уязвимости или нет ?
liquiddeath13
Насколько я понял - не защищает
В шинде полно таких дырок..
4uneral
Привет!
В случае, если интерфейс IPv6 полностью отключен, уязвимость неэксплуатабельна.
Что касается файрвола, то он блокирует только исходящие от машины запросы, то есть атакующий не сможет получить ответ ICMP, представленный в статье. Но это никаким образом не влияет на эксплуатабельность: обработка пакетов файрволом происходит после уязвимого кода. Соответственно, провести атаку можно и на машину с включенным файрволом.
jackchickadee
я до сих пор не уверен что мои фокусы полностью
отключают стек IPv6 (код закрыт и все такое).
не могли бы вы проверить это на своем готовом стенде и отписать что получится ?
я специально написал "Windows Firewall с полностью заблокированным IPv6".
полностью значит "и на вход и на выход".
dartraiden
Такая конфигурация системы не рекомендуется производителем (в документации Microsoft явным образом не рекомендует отключать поддержку IPv6):
jackchickadee
конкретно этот производитель может идти куда-нибудь вместе со своими рекомендациями.
у него негативная репутация была до всяких санкций и останется еще надолго.