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

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

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

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

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

Категории

Все категории
Loading categories
CVE-2024-38063 — CVE-2024-38063 - 通过 IPv6 远程利用内核 | Kitploit
Инструменты/GitHubGitHub/faizan-khanx/cve-2024-38063
Криминалистика памятиАнализ уязвимостейЭксплуатацияОбратная инженерияСетевая безопасностьСтатьи и ИсследованияОбучение и ОбразованиеЭксплуатация Бинарных Файлов
GitHubfaizan-khanx/cve-2024-38063

CVE-2024-38063

CVE-2024-38063 - 通过 IPv6 远程利用内核

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

Популярное

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

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

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

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

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

CVE-2024-38063 - Удаленная эксплуатация ядра через IPv6

  • С момента выхода последнего патча Windows 13 августа я глубоко погрузился в дебри tcpip.sys (драйвера ядра, отвечающего за обработку пакетов TCP/IP). Уязвимость с оценкой CVSS 9.8 в самой доступной части ядра Windows — от такого я просто не мог отказаться. Я никогда серьезно не изучал IPv6 (и драйверы, отвечающие за его разбор), поэтому знал, что попытка обратного инжиниринга этой уязвимости будет крайне сложной, но хорошим опытом обучения.

Самый легкий анализ патча в истории

  • Обычно даже обратный инжиниринг патча, чтобы выяснить, какое изменение кода соответствует уязвимости, может занять дни или даже недели, но в этом случае всё было мгновенно. Это было настолько просто, что несколько человек в социальных сетях сказали мне, что я ошибаюсь и что ошибка находится в другом месте. Прислушался ли я к ним и затем потратил целый день на реверс-инжиниринг неправильного драйвера? Мы, возможно, никогда не узнаем.

Во всём файле драйвера было сделано ровно одно изменение, которое, как оказалось, и было ошибкой. image Обзор bindiff tcpip.sys до и после установки патча.

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

Ipv6pProcessOptions() до установки патча.

image Ipv6pProcessOptions() После установки патча.

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

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

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

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

Уязвимости необязательны, эксплуатация обязательна

  • Обратный инжиниринг патча для нахождения изменённого кода — это лишь половина задачи (или в данном случае менее 0,1%). Оставшаяся часть процесса включает достаточный обратный инжиниринг кодовой базы, чтобы понять, что вообще происходит, выяснить, какой тип уязвимости был исправлен, как сформировать запрос для достижения целевого кода, и какое состояние приводит к эксплуатабельной ситуации.

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

image Структура заголовка опций назначения из 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 байт).

options_header = IPv6ExtHdrDestOpt(options=[Jumbo(jumboplen=0x1337)])

  • Итак, что же на самом деле делает IppSendErrorList()? Ну, код довольно прост. image Вся функция IppSendErrorList.

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

Он составляет список, он проверяет его 52 567 раз

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

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

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

image

Поле list->Next равно NULL.

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

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

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

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

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

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

image

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

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

Ещё больше реверс-инжиниринга снова и навсегда.

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

    image Две релевантные части IppSendError.

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

image

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

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

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

image

Путь кода в 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 в ноль не сломает парсер?

image

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

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

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

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

  • Всё, что осталось сделать, — найти достижимую часть парсера IPv6, которая делает что-то глупое с полем packet_size.

Назад к фрагментации

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

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

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

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

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

    image

    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.

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

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

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

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

image

Именно то, что нам нужно!

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

image

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

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

Социальные сети

Faizan's GitHub stats

instagram twitter linkedin github

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

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

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