
CVE-2024-38063 - 通过 IPv6 远程利用内核
Во всём файле драйвера было сделано ровно одно изменение, которое, как оказалось, и было ошибкой.
Обзор bindiff tcpip.sys до и после установки патча.
Во всём драйвере была изменена только одна функция. Обычно я мог потратить целый день, разбирая 20 и более изменений функций, чтобы понять, на какую из них стоит смотреть, но не в этот раз.

Ipv6pProcessOptions() до установки патча.
Ipv6pProcessOptions() После установки патча.
Была изменена не только одна функция, но и всего одна строка кода.
Функция с чрезвычайно длинным именем Feature_2660322619__private_IsEnabledDeviceUsage_3() иногда добавляется Microsoft для обеспечения частичного отката патчей. Этот вызов проверяет наличие глобального флага или настройки реестра, при установке которых функция возвращает false, в результате чего выполняется оригинальный код вместо исправленной версии.
Причина, по которой Microsoft делает это, заключается в том, что патчи безопасности иногда непреднамеренно ломают что-то, поэтому эта настройка позволяет администратору отменить патч для отдельной уязвимости без удаления всего ежемесячного набора обновлений и существенного ослабления безопасности системы.
Учитывая это, становится ясно, что весь этот патч просто заменяет вызов IppSendErrorList() на IppSendError(), что даёт нам подсказку: проблема связана с каким-то списком. Самый простой дифф патча за всю историю (или я так думал).
Обратный инжиниринг патча для нахождения изменённого кода — это лишь половина задачи (или в данном случае менее 0,1%). Оставшаяся часть процесса включает достаточный обратный инжиниринг кодовой базы, чтобы понять, что вообще происходит, выяснить, какой тип уязвимости был исправлен, как сформировать запрос для достижения целевого кода, и какое состояние приводит к эксплуатабельной ситуации.
Первая часть достаточно проста. Изменение находится в Ipv6pProcessOptions(), что говорит нам о том, что это IPv6 и связано с обработкой опций. Быстрый запрос к RFC даёт точное определение, что такое опция IPv6 и где её можно найти.
Структура заголовка опций назначения из Wikipedia.
Ок, отлично. Похоже, то, что мы ищем, — это заголовок опций назначения, который находится сразу после основного заголовка 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 вызывается для элемента списка, отличного от первого.
Вот тут всё перестало быть очевидным и стало чрезвычайно сложным, хотя я думаю, что большая часть этого была вызвана тем, что одна из моих двух доступных клеток мозга была занята борьбой с тяжёлой формой ковида. Я потратил пару дней на попытки понять части кода, засыпал, а затем забывал, что же я выяснил. Весь процесс потребовал более недели обратного инжиниринга частей tcpip.sys, чтобы разобраться, что происходит. Но пост в блоге Акселя был чрезвычайно полезен.
Изучив функции и структуры, которые Аксель восстановил, а также то, куда они передаются, становится ясно, что единственный аргумент, передаваемый в Ipv6pProcessOptions(), — это та же структура packet_t, описанная в статье. По сути, указатель, передаваемый в Ipv6pProcessOptions и итерируемый IppSendErrorList, представляет собой связный список пакетов.
Итак, я установил точку останова на Ipv6pProcessOptions() и проверил список.

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