
Удаленная эксплуатация ядра через IPv6
Удаленная эксплуатация ядра через IPv6
CVE-2024-38063 - Удаленная эксплуатация ядра через IPv6 Marcus Hutchins
С момента выхода последнего патча Windows 13 августа я глубоко погрузился в дебри tcpip.sys (драйвера ядра, отвечающего за обработку пакетов TCP/IP). Уязвимость с оценкой CVSS 9.8 в самой доступной части ядра Windows была тем, от чего я просто не мог отказаться. Я никогда раньше не сталкивался с IPv6 (или драйверами, отвечающими за его разбор), поэтому понимал, что попытка реверс-инжиниринга этой уязвимости будет чрезвычайно сложной, но хорошим опытом обучения.
По большей части tcpip.sys практически не документирован. Мне удалось найти пару разборов эксплуатации для более старых ошибок: здесь, здесь и здесь, но не более того. Когда лучший результат поиска на английском в Google оказывается на китайском, я сразу понимаю, что я не в своей тарелке и меня ждут тяжелые времена, но учиться нужно. Несмотря на то, что Google Translate справляется посредственно, этот пост дал невероятно детальное понимание того, как работает фрагментация IPv6, и дал мне хороший старт.
Позже, гугля некоторые имена функций, я наткнулся на еще один анализ той же уязвимости 2021 года, написанный Axel Souchet (он же 0vercl0k), который углубился во внутренности tcpip.sys еще больше и дал мне достаточно информации, чтобы определить несколько недокументированных структур. Самый простой анализ патча за всю историю
Обычно даже реверс-инжиниринг патча, чтобы понять, какое изменение кода соответствует уязвимости, может занять дни или даже недели, но в этом случае это было мгновенно. Это было настолько легко, что несколько человек в социальных сетях сказали мне, что я ошибаюсь, и что ошибка находится в другом месте. Прислушался ли я к ним и в итоге потратил целый день на реверс не того драйвера? Мы, возможно, никогда не узнаем.
Во всем файле драйвера было ровно одно изменение, которое, как оказалось, и было ошибкой.

Обзор bindiff для tcpip.sys до и после установки патча.
Во всем драйвере была изменена только одна функция. Обычно я мог потратить целый день, разбирая 20+ различных изменений функций, просто чтобы понять, на какую из них я должен смотреть, но не в этот раз.

Ipv6pProcessOptions() до патча.

Ipv6pProcessOptions() после патча.
Была изменена не только одна функция, но и одна строка кода.
Функция с чрезвычайно длинным названием Feature_2660322619__private_IsEnabledDeviceUsage_3() иногда добавляется Microsoft для обеспечения частичного отката патчей. Этот вызов проверяет наличие глобального флага или параметра реестра, который, если установлен, заставит функцию вернуть false, в результате чего будет выполнен исходный код вместо исправленной версии.
Причина, по которой Microsoft это делает, заключается в том, что патчи безопасности иногда непреднамеренно что-то ломают, поэтому этот параметр позволяет администратору откатить одну уязвимость, не удаляя весь ежемесячный накопительный пакет обновлений и не ослабляя кардинально безопасность системы.
Учитывая это, становится ясно, что этот патч просто заменяет вызов IppSendErrorList() на IppSendError(), что дает нам подсказку, что проблема связана с каким-то списком. Самый простой дифф патча за всю историю (или, по крайней мере, я так думал).
Уязвимости опциональны, эксплуатация обязательна
Реверс-инжиниринг патча для поиска измененного кода — это только половина задачи (или в данном случае менее 0.1%). Остальная часть процесса состоит из реверс-инжиниринга достаточного объема кода, чтобы понять, что вообще происходит, выяснить, какая уязвимость была исправлена, как сформировать запрос для достижения целевого кода и какое состояние приводит к эксплуатируемому условию.
Первая часть достаточно проста. Изменение находится в Ipv6pProcessOptions(), что говорит нам о том, что это IPv6 и включает обработку опций. Быстрый запрос к RFC точно говорит нам, что такое опция IPv6 и где ее можно найти.

Макет заголовка опций назначения из Википедии.
Окей, круто. То, что мы ищем, похоже, является заголовком опций назначения, который находится сразу после основного заголовка IPv6. Давайте воспользуемся библиотекой Python scapy, чтобы сформировать тестовый IPv6-пакет.
Примечание: Для смягчения DDoS-атак с использованием поддельных IP-адресов Windows ограничивает возможность создания необработанных IP-пакетов. По этой причине я решил использовать Linux для разработки своего proof-of-concept. Хотя Linux позволяет пользователям создавать и отправлять необработанные пакеты второго и третьего уровней, для этого требуется, чтобы Python-скрипт запускался от root.
import sys
import struct
from scapy.all import *
def send_ipv6_option_packet(dest_ip):
ethernet_header = Ether()
ip_header = IPv6(dst=dest_ip)
options_header = IPv6ExtHdrDestOpt()
sendp(ethernet_header / ip_header / options_header)
if len(sys.argv) < 2:
print('Use: python3 script.py <target_ipv6_address>')
exit(-1)
send_ipv6_option_packet(sys.argv[1])
Установив точку останова на tcpip!Ipv6pProcessOptions, а затем запустив скрипт, стало ясно, что все, что нужно для достижения уязвимой функции, — это отправить IPv6-пакет с пустой структурой опций. Затем я попробовал добавить в структуру несколько недопустимых опций, чтобы увидеть, смогу ли я добраться до вызова IppSendErrorList().
Краткий обзор кода показал, что почти любое недопустимое форматирование опций может вызвать вызов IppSendErrorList. Поэтому я решил использовать опцию Jumbo Packet с недопустимой длиной (менее 65535 байт).
options_header = IPv6ExtHdrDestOpt(options=[Jumbo(jumboplen=0x1337)])
Итак, что же на самом деле делает IppSendErrorList()? Код довольно прост.

Вся функция IppSendErrorList.
Код перебирает связанный список и вызывает IppSendError() для каждого элемента в списке. Опять же, звезды сошлись, и до сих пор все было легко. Если IppSendErrorList просто вызывает IppSendError для каждого элемента в списке, а патч заменяет вызов IppSendErrorList на IppSendError, то проблема возникает, когда IppSendError вызывается для элемента списка, отличного от первого.
Итак, что это за список и как его создать? Он составляет список, он его проверяет... 52 567 раз
Здесь все перестало быть очевидным и стало аномально сложным, хотя я думаю, что большая часть этого была связана с тем, что одна из двух моих доступных клеток мозга была занята борьбой с тяжелой инфекцией COVID. Я потерял пару дней, пытаясь понять части кода, засыпая, а затем забывая то, что выяснил. Весь процесс потребовал более недели реверс-инжиниринга частей tcpip.sys, чтобы разобраться, что происходит. Но пост в блоге Акселя был чрезвычайно полезен.
Посмотрев на функции и структуры, которые реверс-инжинирил Аксель, и на то, каким другим функциям они передаются, становится ясно, что единственный аргумент, передаваемый в Ipv6pProcessOptions(), — это та же структура packet_t, определенная в статье. По сути, указатель, передаваемый в Ipv6pProcessOptions и перебираемый IppSendErrorList, представляет собой связанный список пакетов.