Skip to content
KitploitKITPLOIT
ИнструментыБлог
Отправить
ИнструментыБлог
Отправить

Инструменты для хакинга, пентеста и кибербезопасности — ваш арсенал защиты!

Kitploit — это каталог инструментов для хакинга, кибербезопасности и пентестинга. Находите последние обновления проектов для поиска уязвимостей, анализа систем, автоматизации тестирования и усиления вашей безопасности.

··Ленты·Контакты·Конфиденциальность·© 2026 Kitploit

Каталог инструментов

Категории

Все категории
Loading categories
CVE-2024-38063- — Remotely Exploiting The Kernel Via IPv6 | Kitploit
Инструменты/GitHubGitHub/adminpentester/cve-2024-38063-
Memory ForensicsVulnerability AnalysisExploitationReverse EngineeringLearning & EducationBinary Exploitation
GitHubadminpentester/cve-2024-38063-

CVE-2024-38063-

Remotely Exploiting The Kernel Via IPv6

Репозиторий
211 год назадЕщё не проверено

Популярное

Смотреть все →

Откройте для себя самые используемые инструменты нашего сообщества.

Изучить все инструменты

Просмотрите нашу коллекцию инструментов

Смотреть все инструменты →
Поделиться

CVE-2024-38063-

Удаленная эксплуатация ядра через 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 overview of tcpip.sys before and after installing the patch.

Обзор bindiff для tcpip.sys до и после установки патча.

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

Ipv6pProcessOptions() before the patch.

Ipv6pProcessOptions() до патча.

Ipv6pProcessOptions() after the patch.

Ipv6pProcessOptions() после патча.

Была изменена не только одна функция, но и одна строка кода.

Функция с чрезвычайно длинным названием Feature_2660322619__private_IsEnabledDeviceUsage_3() иногда добавляется Microsoft для обеспечения частичного отката патчей. Этот вызов проверяет наличие глобального флага или параметра реестра, который, если установлен, заставит функцию вернуть false, в результате чего будет выполнен исходный код вместо исправленной версии.

Причина, по которой Microsoft это делает, заключается в том, что патчи безопасности иногда непреднамеренно что-то ломают, поэтому этот параметр позволяет администратору откатить одну уязвимость, не удаляя весь ежемесячный накопительный пакет обновлений и не ослабляя кардинально безопасность системы.

Учитывая это, становится ясно, что этот патч просто заменяет вызов IppSendErrorList() на IppSendError(), что дает нам подсказку, что проблема связана с каким-то списком. Самый простой дифф патча за всю историю (или, по крайней мере, я так думал). Уязвимости опциональны, эксплуатация обязательна

Реверс-инжиниринг патча для поиска измененного кода — это только половина задачи (или в данном случае менее 0.1%). Остальная часть процесса состоит из реверс-инжиниринга достаточного объема кода, чтобы понять, что вообще происходит, выяснить, какая уязвимость была исправлена, как сформировать запрос для достижения целевого кода и какое состояние приводит к эксплуатируемому условию.

Первая часть достаточно проста. Изменение находится в Ipv6pProcessOptions(), что говорит нам о том, что это IPv6 и включает обработку опций. Быстрый запрос к RFC точно говорит нам, что такое опция IPv6 и где ее можно найти.

The destination options header layout from Wikipedia.

Макет заголовка опций назначения из Википедии.

Окей, круто. То, что мы ищем, похоже, является заголовком опций назначения, который находится сразу после основного заголовка IPv6. Давайте воспользуемся библиотекой Python scapy, чтобы сформировать тестовый IPv6-пакет.

Примечание: Для смягчения DDoS-атак с использованием поддельных IP-адресов Windows ограничивает возможность создания необработанных IP-пакетов. По этой причине я решил использовать Linux для разработки своего proof-of-concept. Хотя Linux позволяет пользователям создавать и отправлять необработанные пакеты второго и третьего уровней, для этого требуется, чтобы Python-скрипт запускался от root.

root@kitploit:~
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 байт).

root@kitploit:~
options_header = IPv6ExtHdrDestOpt(options=[Jumbo(jumboplen=0x1337)])

Итак, что же на самом деле делает IppSendErrorList()? Код довольно прост.

The entire IppSendErrorList function.

Вся функция IppSendErrorList.

Код перебирает связанный список и вызывает IppSendError() для каждого элемента в списке. Опять же, звезды сошлись, и до сих пор все было легко. Если IppSendErrorList просто вызывает IppSendError для каждого элемента в списке, а патч заменяет вызов IppSendErrorList на IppSendError, то проблема возникает, когда IppSendError вызывается для элемента списка, отличного от первого.

Итак, что это за список и как его создать? Он составляет список, он его проверяет... 52 567 раз

Здесь все перестало быть очевидным и стало аномально сложным, хотя я думаю, что большая часть этого была связана с тем, что одна из двух моих доступных клеток мозга была занята борьбой с тяжелой инфекцией COVID. Я потерял пару дней, пытаясь понять части кода, засыпая, а затем забывая то, что выяснил. Весь процесс потребовал более недели реверс-инжиниринга частей tcpip.sys, чтобы разобраться, что происходит. Но пост в блоге Акселя был чрезвычайно полезен.

Посмотрев на функции и структуры, которые реверс-инжинирил Аксель, и на то, каким другим функциям они передаются, становится ясно, что единственный аргумент, передаваемый в Ipv6pProcessOptions(), — это та же структура packet_t, определенная в статье. По сути, указатель, передаваемый в Ipv6pProcessOptions и перебираемый IppSendErrorList, представляет собой связанный список пакетов.

Итак, я установил точку останова на Ipv6pProcessOptions() и проверил список.

The list->Next entry is NULL.

Запись list->Next равна NULL.

Каждый раз, когда срабатывала моя точка останова, список содержал только один пакет. Я потратил гораздо больше времени, чем мне хотелось бы признать, пытаясь понять, почему и как сделать так, чтобы мой список действительно был списком. Моей первой мыслью была фрагментация IPv6: IPv6 позволяет отправителям разбивать большие пакеты на отдельные меньшие пакеты, которые имело бы смысл хранить вместе в списке.

После обширного реверс-инжиниринга я подтвердил свои предположения, хотя список фрагментов не относится к тому, с которым мы имеем дело здесь.

На самом деле я нашел ответ совершенно случайно. Иногда список заполнялся, но причина была неясна. После долгих хождений по кругу я понял, что когда срабатывает моя точка останова в ядре, она приостанавливает все ядро, в результате чего сетевой адаптер накапливает пакеты. Когда ядро возобновляет работу, эти пакеты передаются вниз по стеку в tcpip.sys в виде аккуратного списка. Это происходило только в том случае, если пакеты были отправлены во время паузы ядра, но не были обработаны до того, как сработала следующая точка останова.

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

Теперь, когда мы знаем, что пакеты объединяются в списки при высокой пропускной способности, становится ясно, какой вариант самый простой. Наш PoC DoS, по иронии судьбы, должен будет использовать DoS для запуска условия DoS. Если мы зальем систему потоками IPv6-пакетов, мы должны быть в состоянии получить хороший большой список, передаваемый в IppSendErrorList().

Сначала, сколько бы пакетов я ни отправлял, я мог получить список размером n > 1, только если приостанавливал ядро. Но... поскольку мы используем Python (мучительно медленно) в виртуальной машине (в два раза мучительно медленно), нам, вероятно, придется подкрутить некоторые настройки. Чтобы противодействовать эффекту «матрешки» из виртуальных машин в моей атакующей системе, я решил просто перенастроить целевую виртуальную машину на использование только одного ядра ЦП.

Nice! The packet list is now a list containing lots of entries!

Отлично! Список пакетов теперь состоит из множества записей!

Итак, оказывается, виртуальная машина внутри виртуальной машины — не лучший вариант для DoS, кто бы мог подумать? Но мы заставили это работать в итоге. Теперь нам просто нужно выяснить, что делает IppSendError() и в чем заключается проблема. Больше реверса... снова... навсегда...

После некоторого обширного реверс-инжиниринга стало гораздо понятнее, что делает IppSendError. В обычных обстоятельствах он просто отключает пакет, устанавливая net_buffer_list->Status в 0xC000021B (STATUS_DATA_NOT_ACCEPTED). Затем он отправляет ICMP-сообщение об ошибке, содержащее информацию о ошибочном пакете, обратно отправителю.

Two relevant parts of IppSendError.

Две соответствующие части IppSendError.

Моим первым делом было проверить, есть ли в tcpip.sys какие-либо функции, которые игнорируют значение net_buffer_list->Status. Это привело бы к тому, что драйвер обрабатывал бы пакеты в неопределенных или неожиданных состояниях, что, возможно, привело бы к условию эксплуатации.

The main loop responsible for processing packets.

Основной цикл, отвечающий за обработку пакетов.

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

После еще большего количества реверс-инжиниринга я нашел следующий код в самом низу IppSendError.

A code path in IppSendError which sets the packet_size to zero.

Путь кода в IppSendError, который устанавливает packet_size в ноль.

Когда IppSendErrorList, а следовательно, и IppSendError, вызывается с аргументом always_send_icmp, установленным в true, похоже, он пытается отправить ICMP-сообщение об ошибке для каждого пакета в списке.

Затем, по причинам, известным, вероятно, только богу, он достигает блока кода, где поле packet->packet_size устанавливается в ноль.

Чтобы установить always_send_icmp в true, все, что нам нужно сделать, это вызвать определенную ошибку при обработке заголовка опций, установив значение «Option Type» в любое число больше 0x80.

root@kitploit:~
def build_malicious_option(next_header, header_length, option_type, option_length):
    dest_options_header = 60
	
    options_header = struct.pack('BBBB', next_header, header_length, option_type, option_length) + b'1337'
    return Ether(dst=mac_addr) / IPv6(dst=ip_addr, nh=dest_options_header) / raw(options_header)

packet = build_malicious_option(next_header=59, header_length=0, option_type=0x81, option_length=0)	
sendp(packet)

Но не должно ли установка packet_size в ноль сломать парсер?

A snippet from the main loop responsible for processing packets.

Фрагмент основного цикла, отвечающего за обработку пакетов.

Обработчик пакетов просто вызывает функцию VTable на основе значения packet->next_header, которое остается неизменным с момента его установки во время предварительного разбора. Это позволяет продолжить обработку пакета и даже дает нам контроль над тем, какая обработка будет выполняться.

Поскольку значение packet->next_header берется из поля «Next Header» IPv6-пакета, мы можем установить его в любое допустимое значение заголовка IPv6, и цикл вызовет соответствующий парсер. Это дает нам большую потенциальную поверхность атаки.

The IPv6 packet format.

Формат IPv6-пакета.

Все, что осталось сделать, — это найти доступную часть парсера IPv6, которая делает что-то глупое с полем packet_size. Возвращение к фрагментации

Первое место, которое я решил проверить, — это парсер фрагментов IPv6, потому что именно там находилась старая уязвимость CVE-2021-24086, так что это казалось хорошим местом для поиска более странного кода.

Eh… it's so close, but also so far.

Эх... так близко, но так далеко.

Здесь у нас действительно есть уязвимость, но это не RCE.

По сути, на большинстве процессоров регистры являются циклическими. Если увеличивать регистр после его максимального возможного значения, он возвращается к нулю. Аналогично, если уменьшать его ниже минимального возможного значения, он переходит к максимальному возможному значению. Это называется целочисленным переполнением и целочисленным опустошением соответственно. Это поведение немного отличается для знаковых целых чисел, но мы не имеем с ними дела здесь.

Первая строка, fragment_size = LOWORD(packet->packet_size) - 0x30, состоит из следующего ASM-кода:

The ASM code calculating the fragment size.

ASM-код, вычисляющий размер фрагмента.

AX — это младшие 16 бит регистра EAX. Хотя регистр EAX является 32-битным, AX работает так, как если бы это был собственный 16-битный регистр, поэтому любые переполнения или опустошения ограничиваются AX и не влияют на остальную часть регистра EAX. Это невероятно удобно, потому что опустошение регистра EAX привело бы к значению 4 миллиарда, что привело бы к попытке выделить 4 ГБ памяти, которая, скорее всего, завершилась бы неудачей.

Поскольку значение packet->packet_size равно нулю, этот код устанавливает ax в ноль, а затем вычитает из него 0x30.

В нормальных условиях заголовок пакета составляет 0x30 байт, поэтому packet_size - 0x30 — это размер данных фрагмента.

В нашем случае packet->packet_size равен 0, поэтому даже вычитание 1 из него заставит регистр перейти к максимально возможному 16-битному целочисленному значению (0xFFFF). Поскольку мы вычитаем 0x30, значение AX опустошится и станет MAX_VALUE - 0x2F, или 0xFFD0, что равно 65 488.

К сожалению, поскольку одно и то же вычисление используется как для выделения памяти, так и для копирования данных, мы не получаем переполнение буфера. Я полагаю, что RtlCopyMdlToBuffer() также выполняет проверку границ исходного буфера, так что мы даже не получаем чтение за пределами границ. Однако мы не уходим с пустыми руками.

Поскольку ExAllocatePoolWithTagPriority() не обнуляет выделенную память, а RtlCopyMdlToBuffer() копирует только фактическое количество доступных данных, мы получаем около 65 КБ неинициализированной памяти ядра. Поскольку адреса памяти перерабатываются после освобождения, буфер, скорее всего, будет заполнен тем, что ранее хранилось по этому адресу до перераспределения. Если мы сможем использовать фрагментацию для создания пакета, который будет отправлен обратно нам, например, ICMP-запроса Echo, мы потенциально могли бы утечь случайную память ядра, что приведет к обходу ASLR.

Вдобавок ко всему, код также устанавливает reassembly->fragment_size в опустошенное 16-битное целое (65 488), так что теперь у нас есть две отдельные переменные, которые мы потенциально можем использовать для вызова переполнения буфера. Побежден, но не сломлен

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

Решение (или по крайней мере одно из них) — это Ipv6pReassemblyTimeout(). Хотя мы не можем вызвать переполнение при начальной обработке фрагмента, мы, видимо, можем сделать это во время очистки.

Фрагменты IPv6 будут оставаться в памяти до тех пор, пока не произойдет одно из трех условий:

  1. Мы достаточно сильно испортим фрагментацию, и система скажет нам остановиться.
  2. Мы отправим фрагмент с полем «More», установленным в 0, что указывает на то, что это последний фрагмент, и система начнет сборку.
  3. Мы не отправим последний фрагмент до истечения тайм-аута (60 секунд), и система отбросит фрагменты.

Ipv6pReassemblyTimeout() вызывается при условии 3, поэтому давайте рассмотрим, как это можно использовать.

This is exactly what we need!

Это именно то, что нам нужно!

Ранее нашей проблемой было то, что код использовал одно и то же вычисление как для выделения памяти, так и для операции копирования. Этот код, с другой стороны, этого не делает. Давайте более детально рассмотрим ASM, чтобы увидеть, как это можно использовать.

The assembly code responsible for calculating the allocation size.

Код на ассемблере, отвечающий за вычисление размера выделения.

Как вы можете видеть здесь, первая часть вычисления (fragment_list->net_buffer_length + reassembly->packet_length + 8) выполняется с использованием 16-битного регистра DX.

Если вы помните ранее, мы опустошили reassembly->packet_length до 0xFFD0. Таким образом, регистр DX после добавления 8 байт становится 0xFFD8. Если fragment_list->net_buffer_length больше 0x27 (39 байт), DX переполнится и сбросится до нуля.

fragment_list->net_buffer_length должен быть около 0x38 байт, поэтому это приведет к переполнению регистра DX до 8. После добавления 0x28 байт мы получим выделение памяти всего на 48 байт.

Поскольку последующие вызовы memmove() просто используют неизмененное значение reassembly->packet_length для размера, это приведет к копированию 65 488 байт из reassembly->payload в 30-байтовый буфер. Приятным бонусом является то, что большая часть копируемых данных поступает из полезной нагрузки фрагмента, которую мы контролируем, и это могут быть произвольные данные любого формата, так что мы получаем хорошее, довольно контролируемое переполнение буфера в пуле ядра.

Чтобы иметь шанс вызвать уязвимость, нам нужно, чтобы один или несколько фрагментированных пакетов находились после пакета с неверной опцией в связанном списке в момент вызова IppSendErrorList. Однако, судя по моим тестам, это не гарантирует эксплуатацию. Я полагаю, что также должны быть выполнены некоторые другие условия. Я подозреваю, но не подтвердил, что код синхронизации в IppSendError означает, что нам также нужно выиграть состояние гонки.

Скачать инструмент