Введение
Эта статья является второй частью серии из двух статей о повреждении пула в эпоху кучи сегментов в Windows. Часть 1, которую можно найти здесь, начинает эту серию с использования уязвимости out-of-bounds для обхода kASLR из-за низкой целостности. Связывая эту уязвимость утечки информации с ошибкой, описанной в этом посте, которая представляет собой переполнение пула, ведущее к произвольному примитиву чтения/записи, мы завершим эту серию, указав, почему повреждение пула в эру кучи сегментов стало менее разрушительным, со времен Windows 7.
В связи с недавним выпуском Windows 11, в которой по умолчанию будет включена безопасность на основе виртуализации (VBS) и целостность защищенного кода гипервизора (HVCI), мы отдадим должное методам повреждения записей в таблице страниц для обхода SMEP и DEP в ядре с помощью эксплойта, который будет описан в этом сообщении. Хотя Windows 11 не будет использоваться компаниями в течение некоторого времени, как в случае с развертыванием новых технологий на любом предприятии, исследователям уязвимостей необходимо будет начать отходить от использования искусственно созданных областей исполняемой памяти в ядре для выполнения кода, в пользу атак в стиле data-only или исследовать новые методы обхода VBS и HVCI. Это направление, в котором я надеюсь начать свои исследования в будущем. Скорее всего, это будет последний мой пост, в котором используется повреждение записей в таблице страниц.
Хотя есть гораздо лучшие объяснения внутреннего устройства пула в Windows, такие как эта статья и предстоящий доклад моего коллеги Ярдена Шафира по BlackHat 2021 USA, который можно найти здесь, часть 1 этой серии блогов будет содержать большую часть необходимых знаний, используемых для этого сообщения - так что, хотя есть ресурсы получше, я настоятельно рекомендую вам сначала прочитать часть 1, если вы используете это сообщение в качестве пошагового руководства (что является причиной и объясняет длину моих сообщений)
Анализ Уязвимости
Давайте посмотрим на исходный код
Первая функция в исходном файле -
Взглянув на этот файл заголовка, мы можем увидеть директиву
Фрагмент пула режима ядра, который теперь выделяется на
Это в основном означает, что значение, указанное в
Это означает, что клиент, взаимодействующий с драйвером через
Давайте проверим эту теорию, расширив наш эксплойт из первой части и задействовав уязвимость.
Вызываем уязвимость
Мы воспользуемся предыдущим эксплойтом из Части 1 и доработаем код переполнения пула до конца, после цикла
После того, как это распределение выполнено и фрагмент пула впоследствии освобожден, мы можем видеть, что происходит BSOD с проверкой ошибок, указывающей, что заголовок пула был поврежден.
Это результат нашей уязвимости записи за пределами допустимого диапазона, которая повредила заголовок пула. Когда заголовок пула поврежден и фрагмент впоследствии освобождается, для фрагмента пула, входящего в область видимости, выполняется проверка «целостности», чтобы убедиться, что он имеет допустимый заголовок. Поскольку мы произвольно записали содержимое за блоком пула, выделенным для нашего буфера, отправленным из пользовательского режима, мы впоследствии перезаписали другие блоки пула. Из-за этого, а также из-за того, что каждый фрагмент в kLFH, где находится наше выделение на основе эвристики, упомянутой в Части 1, с добавлением структуры
Точка останова, установленная на адресе
Затем мы можем проверить уязвимый фрагмент, ответственный за переполнение, чтобы определить, соответствует ли тег
После возобновления выполнения снова выполняется проверка ошибок. Это связано с освобождением блока пула с недопустимым заголовком.
Это подтверждает, что запись за пределами допустимого диапазона действительно существует. Теперь, имея в распоряжении обходной путь kASLR, возникает вопрос - как полностью выполнить код режима ядра из пользовательского режима?
Стратегия эксплуатации
Справедливое предупреждение - в этом разделе содержится много анализа кода, чтобы понять, что делает этот драйвер для очистки пула, поэтому имейте это в виду.
Как вы можете вспомнить из Части 1, ключ к эксплуатации пула в эпоху сегментной кучи лежит в поиске объектов, особенно при использовании kLFH, которые имеют тот же размер, что и уязвимый объект, содержат интересный член в объекте, могут быть вызваны из пользовательского режима и размещаются в пуле того же типа, что и уязвимый объект. Напомним, что ранее размер уязвимого объекта составлял 16 байт. Теперь цель состоит в том, чтобы посмотреть на исходный код драйвера, чтобы определить, нет ли полезного объекта, который мы можем выделить, который бы удовлетворял всем указанным выше параметрам. Обратите внимание, что самая сложная часть эксплуатации пула - это поиск стоящих объектов.
К счастью, есть два файла с названиями
Как мы видим, каждое определение функции определяет параметр типа
Давайте исследуем
Глядя на
Похоже, что объекты
Затем вызывается функция
После выделения памяти для
Затем эта вновь выделенная память инициализируется нулем. Ранее объявленный указатель
Затем массив с именем
Прежде чем двигаться дальше - я понимаю, что это большой анализ кода, но позже я добавлю диаграммы и tl;dr, чтобы помочь разобраться во всем этом. А пока давайте продолжим копаться в коде
Вспомним, как создавался прототип функции
Этот объект
Теперь мы знаем, что эта функция выделит фрагмент пула для указателя
Также обратите внимание, что длина имени отладочной печати равна нулю. Это значение было предоставлено нами из пользовательского режима, и, поскольку мы установили буфер в ноль, поэтому длина равна нулю.
Затем мы знаем, что произойдет еще один вызов
Мы видим, что этот фрагмент выделен в том же пуле и сегменте kLFH, что и предыдущий указатель
Отсюда мы можем нажать
Мы можем ясно видеть, что адрес режима ядра указателя
Давайте выполним все еще раз и запишем результат.
Заметили что-нибудь? Каждый раз, когда мы вызываем
Мы знаем, что эта переменная
Давайте посмотрим, как эта функция определяется и выполняется. Заглянув в
Эта функция, которая возвращает целочисленное значение, выполняет цикл for на основе
Tl;dr того, что здесь происходит, находится в функции
Давайте посмотрим на следующий обработчик IOCTL после
Давайте посмотрим, что эта функция будет делать с нашим входным буфером. Вспомните, как в прошлый раз мы могли указать длину, которая использовалась в операции над размером члена
Эта функция начинается с определения нескольких переменных:
Выполняется вызов
Откуда взялось это значение?
Эта функция принимает значение любого указателя, но на практике это используется для указателя на объект
Давайте посмотрим на следующий фрагмент кода из функции
После подтверждения существования
После подтверждения того, что буфер поступает из пользовательского режима, эта функция завершается копированием значения
Давайте проверим эту теорию в WinDbg. Здесь мы должны искать значение, указанное членом
Приведенный выше код должен перезаписать член
Затем мы можем установить точку останова в WinDbg для выполнения динамического анализа.
Затем мы можем установить точку останова на
Это значение
Вспомните ранее, что связанный объект
Это не удается. Код ошибки здесь - доступ запрещен. Почему это? Напомним, что есть последний вызов
Переменная
После повторного выполнения кода и достижения всех точек останова мы видим, что выполнение теперь достигает подпрограммы
После выполнения процедуры memcpy объект
Мы приближаемся к нашей цели! Вы можете видеть, что это в значительной степени неконтролируемый произвольный примитив записи сам по себе. Однако проблема здесь в том, что значение, которое мы можем перезаписать, - это
Прежде чем перейти к этапу эксплуатации, нам нужно изучить еще один обработчик IOCTL, а также обработчик IOCTL для удаления объектов, что не должно занимать много времени.
Последний обработчик IOCTL, который нужно исследовать, - это обработчик IOCTL
Этот обработчик передает предоставленный пользователем буфер типа
Это означает, что если бы мы использовали функцию
В этом случае нам не нужно WinDbg для проверки чего-либо. Мы можем просто установить содержимое нашего члена
Поскольку
Последний обработчик IOCTL - это операция удаления, найденная в
Этот обработчик IOCTL анализирует входной буфер из
Эта функция довольно упрощена - поскольку
Теперь, когда мы знаем все тонкости работы драйвера, мы можем продолжить (обратите внимание, что нам повезло, что у нас есть исходный код в этом случае. Использование дизассемблера требует немного больше времени, чтобы прийти к тем же выводам. мы смогли прийти)
Теперь давайте приступим к эксплуатации (на этот раз по-настоящему)
Мы знаем, что наша ситуация в настоящее время допускает неконтролируемый произвольный примитив чтения/записи. Это связано с тем, что член
Наша стратегия очистки пула, поскольку все эти объекты имеют одинаковый размер и размещены в пуле одного и того же типа (
Давайте проверим значение
Поскольку этот фрагмент составляет 16 байтов и будет частью kLFH, ему предшествует стандартная структура
Зная об этом и зная, что нам предстоит немного поработать, давайте изменим наш текущий эксплойт, чтобы сделать его более логичным. Мы создадим три функции для выполнения grooming:
Пожалуйста, обратитесь к Части 1, чтобы понять, что это делает, но, по сути, этот метод заполнит любые фрагменты в соответствующей корзине kLFH в NonPagedPoolNx и заставит диспетчер памяти (теоретически) предоставить нам новую страницу для работы. Затем мы заполняем эту новую страницу объектами, которые мы контролируем, например объекты
Поскольку у нас есть контролируемое переполнение на основе пула, цель будет состоять в том, чтобы перезаписать любую из структур
Последняя функция, называемая
Первый бит этой функции создает «основной»
После освобождения всех остальных объектов мы заменяем эти освобожденные слоты нашими уязвимыми буферами. Мы также создаем «автономный/основной» объект
На самом деле мы надеемся здесь сделать следующее.
Мы хотим использовать управляемую запись только для перезаписи первого члена этого соседнего объекта
Как показано в функции
Установив точку останова для подпрограммы
Как видно на изображении выше, мы ясно видим, что мы заполнили пул контролируемыми объектами
Затем, после пошагового выполнения до подпрограммы mempcy, мы можем проверить содержимое следующего фрагмента, который находится на
Мы можем проверить, что адрес, который был записан за границу, на самом деле является адресом «основного», автономного объекта
Помните - структура
Теперь мы успешно выполнили запись за пределы диапазона через переполнение пула и повредили член
Давайте посмотрим на поврежденный объект
Мы знаем, что можно установить член
Давайте разберемся с этим. Мы знаем, что в настоящее время у нас есть поврежденный объект с членом
Если мы выполняем операцию «Установить» в данный момент на поврежденном объекте, показанном в команде
В WinDbg мы можем «имитировать» операцию «Set», как показано.
Выполнение операции «Set» на поврежденном объекте фактически установит значение нашего основного объекта на то, что указано пользователю, из-за того, что мы повредили предыдущий случайный адрес из-за уязвимости переполнения пула. На этом этапе выполнение операции «Get» для нашего основного объекта, поскольку он был установлен на значение, указанное пользователем, приведет к разыменованию значения и вернет его нам!
На этом этапе нам нужно определить, какова наша цель. Чтобы полностью обойти kASLR, наша цель заключается в следующем:
Мы можем обновить наш код, чтобы обрисовать это. Как вы помните, мы подготовили пул с 5000 объектами
После этого мы можем затем использовать тот же примитив, о котором говорилось ранее, используя функцию «Set» в HEVD, чтобы установить член
Затем мы можем добавить к нашему эксплойту функцию «press enter to continue», чтобы приостановить выполнение после того, как основной объект будет выведен на экран, а также поврежденный объект, используемый для очистки, который находится в 5000 объектах, используемых для очистки.
Затем мы можем взять адрес
Сравнивая элемент
Теперь, если бы мы использовали функцию «Установить» HEVD и предоставили объект
Как показал наш эксплойт, в этом случае мы используем
Как видите, у нашего основного объекта теперь есть член
// Ограничение на кол-во прикрепляемых файлов не позволяет мне сделать перевод одним постом. Второй кусок будет ниже
Эта статья является второй частью серии из двух статей о повреждении пула в эпоху кучи сегментов в Windows. Часть 1, которую можно найти здесь, начинает эту серию с использования уязвимости out-of-bounds для обхода kASLR из-за низкой целостности. Связывая эту уязвимость утечки информации с ошибкой, описанной в этом посте, которая представляет собой переполнение пула, ведущее к произвольному примитиву чтения/записи, мы завершим эту серию, указав, почему повреждение пула в эру кучи сегментов стало менее разрушительным, со времен Windows 7.
В связи с недавним выпуском Windows 11, в которой по умолчанию будет включена безопасность на основе виртуализации (VBS) и целостность защищенного кода гипервизора (HVCI), мы отдадим должное методам повреждения записей в таблице страниц для обхода SMEP и DEP в ядре с помощью эксплойта, который будет описан в этом сообщении. Хотя Windows 11 не будет использоваться компаниями в течение некоторого времени, как в случае с развертыванием новых технологий на любом предприятии, исследователям уязвимостей необходимо будет начать отходить от использования искусственно созданных областей исполняемой памяти в ядре для выполнения кода, в пользу атак в стиле data-only или исследовать новые методы обхода VBS и HVCI. Это направление, в котором я надеюсь начать свои исследования в будущем. Скорее всего, это будет последний мой пост, в котором используется повреждение записей в таблице страниц.
Хотя есть гораздо лучшие объяснения внутреннего устройства пула в Windows, такие как эта статья и предстоящий доклад моего коллеги Ярдена Шафира по BlackHat 2021 USA, который можно найти здесь, часть 1 этой серии блогов будет содержать большую часть необходимых знаний, используемых для этого сообщения - так что, хотя есть ресурсы получше, я настоятельно рекомендую вам сначала прочитать часть 1, если вы используете это сообщение в качестве пошагового руководства (что является причиной и объясняет длину моих сообщений)
Анализ Уязвимости
Давайте посмотрим на исходный код
BufferOverflowNonPagedPoolNx.c в ветке win10-klfh HEVD, который обнаруживает довольно тривиальную и управляемую уязвимость переполнения буфера на основе пула.
Первая функция в исходном файле -
TriggerBufferOverflowNonPagedPoolNx. Эта функция, которая возвращает значение типа NTSTATUS, имеет прототип для приема буфера UserBuffer и размера Size. TriggerBufferOverflowNonPagedPoolNx вызывает API режима ядра ExAllocatePoolWithTag для выделения фрагмента из пула NonPagedPoolNx размером POOL_BUFFER_SIZE. Откуда такой размер? Взглянув на самое начало BufferOverflowNonPagedPoolNx.c, мы ясно увидим, что BufferOverflowNonPagedPoolNx.h включен.
Взглянув на этот файл заголовка, мы можем увидеть директиву
#define для размера, которая определяется директивой процессора, чтобы сделать эту переменную равной 16 на 64-битной машине Windows, с которой мы проводим тестирование. Теперь мы знаем, что фрагмент пула, который будет выделен из вызова ExAllocatePoolWithTag в TriggerBufferOverfloowNx, составляет 16 байтов.
Фрагмент пула режима ядра, который теперь выделяется на
NonPagedPoolNx, управляется возвращаемым значением ExAllocatePoolWithTag, которым в данном случае является KernelBuffer. Посмотрев немного дальше по коду, мы увидим, что RtlCopyMemory, которая является оболочкой для вызова memcpy, копирует значение UserBuffer в распределение, управляемое KernelBuffer. Размер буфера, копируемого в KernelBuffer, определяется параметром Size. После записи фрагмента на основе кода в BufferOverflowNonPagedPoolNx.c фрагмент пула также впоследствии освобождается.
Это в основном означает, что значение, указанное в
Size и UserBuffer, будет использоваться в операции копирования для копирования памяти в блок пула. Мы знаем, что UserBuffer и Size встроены в определение функции для TriggerBufferOverflowNonPagedPoolNx, но откуда эти значения? Заглянув дальше в BufferOverflowNonPagedPoolNx.c, мы можем увидеть, что эти значения извлекаются из пакета IRP, отправленного этой функции через обработчик IOCTL.
Это означает, что клиент, взаимодействующий с драйвером через
DeviceIoControl, может управлять содержимым и размером буфера, скопированного в блок пула, выделенный на NonPagedPoolNx, который составляет 16 байтов. Уязвимость здесь заключается в том, что мы можем контролировать размер и содержимое памяти, скопированной в блок пула, что означает, что мы можем указать значение больше 16, которое будет записывать в память за пределами выделения, как если бы граничная уязвимость записи, известная в данном случае как «переполнение пула».Давайте проверим эту теорию, расширив наш эксплойт из первой части и задействовав уязвимость.
Вызываем уязвимость
Мы воспользуемся предыдущим эксплойтом из Части 1 и доработаем код переполнения пула до конца, после цикла
for, который выполняет синтаксический анализ для извлечения базового адреса HEVD.sys. Этот код можно увидеть ниже, который отправляет буфер размером 50 байтов в блок пула размером 16 байтов. IOCTL для достижения функции TriggerBufferOverflowNonPagedPool равен 0x0022204b.
После того, как это распределение выполнено и фрагмент пула впоследствии освобожден, мы можем видеть, что происходит BSOD с проверкой ошибок, указывающей, что заголовок пула был поврежден.
Это результат нашей уязвимости записи за пределами допустимого диапазона, которая повредила заголовок пула. Когда заголовок пула поврежден и фрагмент впоследствии освобождается, для фрагмента пула, входящего в область видимости, выполняется проверка «целостности», чтобы убедиться, что он имеет допустимый заголовок. Поскольку мы произвольно записали содержимое за блоком пула, выделенным для нашего буфера, отправленным из пользовательского режима, мы впоследствии перезаписали другие блоки пула. Из-за этого, а также из-за того, что каждый фрагмент в kLFH, где находится наше выделение на основе эвристики, упомянутой в Части 1, с добавлением структуры
_POOL_HEADER, мы впоследствии повредили заголовок каждого последующего фрагмента. Мы можем подтвердить это, установив точку останова при вызове ExAllocatePoolWithTag и включив отладочную печать, чтобы увидеть структуру пула до того, как произойдет освобождение.
Точка останова, установленная на адресе
fffff80d397561de, который является первой точкой останова, установленной на приведенной выше фотографии, является точкой останова при фактическом вызове ExAllocatePoolWithTag. Точка останова, установленная по адресу fffff80d39756336, - это инструкция, которая идет непосредственно перед вызовом ExFreePoolWithTag. Эта точка останова находится в нижней части фотографии выше с помощью точки останова 3. Это необходимо для обеспечения приостановки выполнения перед освобождением блока.
Затем мы можем проверить уязвимый фрагмент, ответственный за переполнение, чтобы определить, соответствует ли тег
_POOL_HEADER фрагменту, что он и делает.
После возобновления выполнения снова выполняется проверка ошибок. Это связано с освобождением блока пула с недопустимым заголовком.
Это подтверждает, что запись за пределами допустимого диапазона действительно существует. Теперь, имея в распоряжении обходной путь kASLR, возникает вопрос - как полностью выполнить код режима ядра из пользовательского режима?
Стратегия эксплуатации
Справедливое предупреждение - в этом разделе содержится много анализа кода, чтобы понять, что делает этот драйвер для очистки пула, поэтому имейте это в виду.
Как вы можете вспомнить из Части 1, ключ к эксплуатации пула в эпоху сегментной кучи лежит в поиске объектов, особенно при использовании kLFH, которые имеют тот же размер, что и уязвимый объект, содержат интересный член в объекте, могут быть вызваны из пользовательского режима и размещаются в пуле того же типа, что и уязвимый объект. Напомним, что ранее размер уязвимого объекта составлял 16 байт. Теперь цель состоит в том, чтобы посмотреть на исходный код драйвера, чтобы определить, нет ли полезного объекта, который мы можем выделить, который бы удовлетворял всем указанным выше параметрам. Обратите внимание, что самая сложная часть эксплуатации пула - это поиск стоящих объектов.
К счастью, есть два файла с названиями
ArbitraryReadWriteHelperNonPagedPoolNx.c и ArbitraryReadWriteHelperNonPagedPoolNx.h, которые нам пригодятся. Судя по названию, эти файлы, похоже, выделяют какой-то объект на NonPagedPoolNx. Опять же, обратите внимание, что на этом этапе в реальном мире нам нужно будет реконструировать драйвер и просмотреть все экземпляры распределения пула, проверить их аргументы во время выполнения и посмотреть, нет ли способа получить полезные объекты на том же самом пуле и kLFH bucket как уязвимый объект для выполнения pool grooming.ArbitraryReadWriteHelperNonPagedPoolNx.h содержит две интересные структуры, показанные ниже, а также несколько определений функций (которые мы коснемся позже - убедитесь, что вы ознакомились с этими структурами и их членами!).
Как мы видим, каждое определение функции определяет параметр типа
PARW_HELPER_OBJECT_IO, который является указателем на объект ARW_HELP_OBJECT_IO, определенный на изображении выше!Давайте исследуем
ArbitraryReadWriteHelpeNonPagedPoolNx.c, чтобы определить, как эти объекты ARW_HELPER_OBJECT_IO создаются и используются в определенных функциях на изображении выше.Глядя на
ArbitraryReadWriteHelperNonPagedPoolNx.c, мы видим, что он содержит несколько обработчиков IOCTL. Это указывает на то, что эти объекты ARW_HELPER_OBJECT_IO будут отправлены клиентом (нами). Давайте посмотрим на первый обработчик IOCTL.
Похоже, что объекты
ARW_HELPER_OBJECT_IO создаются с помощью обработчика IOCTL CreateArbitraryReadWriteHelperObjectNonPagedPoolNxIoctlHandler. Этот обработчик принимает буфер, приводит его к типу ARW_HELP_OBJECT_IO и передает буфер функции CreateArbitraryReadWriteHelperObjectNonPagedPoolNx. Давайте проверим CreateArbitraryReadWriteHelperObjectNonPagedPoolNx
CreateArbitraryReadWriteHelperObjectNonPagedPoolNx сначала объявляет несколько вещей:- Указатель с именем
Name - Переменная с типои
SIZE_T- Length, длина - Переменная
NTSTATUS, для которой установлено значениеSTATUS_SUCCESSдля обработки ошибок. - Целое число FreeIndex, которому присвоено значение STATUS_INVALID_INDEX.
- Указатель типа PARW_HELPER_OBJECT_NON_PAGED_POOL_NX, называемый ARWHelperObject, который является указателем на объект ARW_HELPER_OBJECT_NON_PAGED_POOL_NX, который мы видели ранее определенным в ArbitraryReadWriteHelperNonPagedPoolNx.h.
ARW_HELPER_OBJECT_NON_PAGED_POOL_NX, проверяет входной буфер от клиента, проанализированный обработчиком IOCTL, чтобы убедиться, что он находится в пользовательском режиме, а затем сохраняет длину, указанную элементом Length структуры ARW_HELPER_OBJECT_IO, в ранее объявленном переменная длина. Эта структура ARW_HELPER_OBJECT_IO берется из клиента пользовательского режима, взаимодействующего с драйвером (нами), то есть она предоставляется из вызова DeviceIoControl.Затем вызывается функция
GetFreeIndex, и результат операции сохраняется в ранее объявленной переменной FreeIndex. Если возвращаемое значение этой функции равно STATUS_INVALID_INDEX, функция возвращает статус вызывающей стороне. Если значение не STATUS_INVALID_INDEX, CreateArbitraryReadWriteHelperObjectNonPagedPoolNx затем вызывает ExAllocatePoolWithTag, чтобы выделить память для ранее объявленного указателя PARW_HELPER_OBJECT_NON_PAGED_POOL_NX, который называется ARWHelperOb. Этот объект размещается на NonPagedPoolNx, как показано ниже.
После выделения памяти для
ARWHelperObject функция CreateArbitraryReadWriteHelperObjectNonPagedPoolNx затем выделяет другой фрагмент из NonPagedPoolNx и выделяет эту память для ранее объявленного указателя Name.Затем эта вновь выделенная память инициализируется нулем. Ранее объявленный указатель
ARWHelperObject, который является указателем на ARW_HELPER_OBJECT_NON_PAGED_POOL_OBJECT, затем имеет свой член Name, установленный на ранее объявленное имя указателя, память которого была выделена в предыдущей операции ExAllocatePoolWithTag, а его член Length установлен на локальную переменную Length, который получил длину, отправленную клиентом пользовательского режима в операции IOCTL, через входной буфер типа ARW_HELPER_OBJECT_IO, как показано ниже. По сути, это просто инициализирует значения структуры.
Затем массив с именем
g_ARWHelperOjbectNonPagedPoolNx по индексу, указанному FreeIndex, инициализируется адресом ARWHelperObject. Этот массив фактически является массивом указателей на объекты ARW_HELPER_OBJECT_NON_PAGED_POOL_NX и управляет такими объектами. Это определяется в начале ArbitraryReadWriteHelperNonPagedPoolNx.c, как показано ниже.
Прежде чем двигаться дальше - я понимаю, что это большой анализ кода, но позже я добавлю диаграммы и tl;dr, чтобы помочь разобраться во всем этом. А пока давайте продолжим копаться в коде
Вспомним, как создавался прототип функции
CreateArbitraryReadWriteHelperObjectNonPagedPoolNx:
C:
NTSTATUS
CreateArbitraryReadWriteHelperObjectNonPagedPoolNx(
_In_ PARW_HELPER_OBJECT_IO HelperObjectIo
);
Этот объект
HelperObjectIo имеет тип PARW_HELPER_OBJECT_IO, который предоставляется клиентом пользовательского режима (нами). Эта структура, которая предоставляется нами через DeviceIoControl, имеет член HelperObjectAddress, установленный на адрес ARWHelperObject, ранее выделенный в CreateArbitraryReadWriteHelperObjectNonPagedPoolNx. По сути, это означает, что наша структура пользовательского режима, которая отправляется в режим ядра, имеет один из своих членов, а точнее HelperObjectAddress, установленный на адрес другого объекта режима ядра. Это означает, что он будет возвращен обратно в пользовательский режим. Это конец функции CreateArbitraryReadWriteHelperObjectNonPagedPoolNx! Давайте обновим наш код, чтобы увидеть, как это выглядит динамически. Мы также можем установить точку останова на HEVD!CreateArbitraryReadWriteHelperObjectNonPagedPoolNx в WinDbg. Обратите внимание, что IOCTL для запуска CreateArbitraryReadWriteHelperObjectNonPagedPoolNx равен 0x00222063
Теперь мы знаем, что эта функция выделит фрагмент пула для указателя
ARWHelperObject, который является указателем на ARW_HELPER_OBJECT_NON_PAGED_POOL_NX. Давайте установим точку останова для вызова ExAllocatePoolWIthTag, отвечающего за это, и включим отладочную печать.
Также обратите внимание, что длина имени отладочной печати равна нулю. Это значение было предоставлено нами из пользовательского режима, и, поскольку мы установили буфер в ноль, поэтому длина равна нулю.
FreeIndex также равен нулю. Мы коснемся этого значения позже. После выполнения операции выделения памяти и проверки возвращаемого значения мы можем увидеть знакомый тег пула Hack, который составляет 0x10 байтов (16 байтов) + 0x10 байтов для структуры _POOL_HEADER_, что в сумме составляет 0x20 байтов. Адрес этого ARW_HELPER_OBJECT_NON_PAGED_POOL_NX - 0xffff838b6e6d71b0
Затем мы знаем, что произойдет еще один вызов
ExAllocatePoolWithTag, который выделит память для члена Name класса ARWHelperObject->Name, где ARWHelperObject имеет тип PARW_HELPER_OBJECT_NON_PAGED_POOL_NX. Давайте установим точку останова для этой операции выделения памяти и проверим ее содержимое.
Мы видим, что этот фрагмент выделен в том же пуле и сегменте kLFH, что и предыдущий указатель
ARWHelperObject. Адрес этого фрагмента, который равен 0xffff838b6e6d73d0, в конечном итоге будет установлен как член Name ARWHelperObject, а член Length ARWHelperObject будет установлен на член Length исходного буфера ввода пользовательского режима, который поступает из структуры ARW_HELPER_OBJECT_IO.Отсюда мы можем нажать
g в WinDbg, чтобы возобновить выполнение.
Мы можем ясно видеть, что адрес режима ядра указателя
ARWHelperObject возвращается в пользовательский режим через HelperObjectAddress объекта ARW_HELPER_OBJECT_IO, указанного в параметрах входного и выходного буфера вызова DeviceIoControl.Давайте выполним все еще раз и запишем результат.
Заметили что-нибудь? Каждый раз, когда мы вызываем
CreateArbitraryReadWriteHelperObjectNonPagedPoolNx, на основе анализа выше всегда создается PARW_HELPER_OBJECT_NON_PAGED_POOL_OBJECT. Мы знаем, что существует также созданный массив этих объектов, и созданный объект для каждого заданного вызова функции CreateArbitraryReadWriteHelperObjectNonPagedPoolNx назначается массиву с индексом FreeIndex. После повторного запуска обновленного кода мы видим, что при повторном вызове функции и, следовательно, создании другого объекта, значение FreeIndex было увеличено на единицу. Повторно выполнив все еще раз во второй раз, мы снова увидим, что это так!
Мы знаем, что эта переменная
FreeIndex устанавливается с помощью вызова функции GetFreeIndex, как показано ниже.
C:
Length = HelperObjectIo->Length;
DbgPrint("[+] Name Length: 0x%X\n", Length);
//
// Get a free index
//
FreeIndex = GetFreeIndex();
if (FreeIndex == STATUS_INVALID_INDEX)
{
//
// Failed to get a free index
//
Status = STATUS_INVALID_INDEX;
DbgPrint("[-] Unable to find FreeIndex: 0x%X\n", Status);
return Status;
}
Давайте посмотрим, как эта функция определяется и выполняется. Заглянув в
ArbitraryReadWriteHelperNonPagedPoolNx.c, мы увидим, что функция определена как таковая.
Эта функция, которая возвращает целочисленное значение, выполняет цикл for на основе
MAX_OBJECT_COUNT, чтобы определить, имеет ли массив g_ARWHelperObjectNonPagedPoolNx, который является массивом указателей на ARW_HELPER_OBJECT_NON_PAGED_POOL_NXs значение, назначенное для данного индекса, который начинается с 0. Например, Цикл for сначала проверяет, присвоено ли значение 0-му элементу в массиве g_ARWHelperObjectNonPagedPoolNx. Если он назначен, индекс в массиве увеличивается на единицу. Это продолжается до тех пор, пока цикл for больше не сможет найти значение, присвоенное данному индексу. В этом случае текущее значение, используемое в качестве счетчика, присваивается значению FreeIndex. Это значение затем передается в операцию присваивания, используемую для присвоения ARWHelperObject в области видимости массиву, управляющему всеми такими объектами. Этот цикл повторяется MAX_OBJECT_COUNT раз, что определено в ArbitraryReadWriteHelperNonPagedPoolNx.h как #define MAX_OBJECT_COUNT 65535. Это общее количество объектов, которыми может управлять массив g_ARWHelperObjectNonPagedPoolNxTl;dr того, что здесь происходит, находится в функции
CreateArbitraryReadWriteHelperObjectNonPagedPoolNx:- Создайте объект
PARW_HELPER_OBJECT_NON_PAGED_POOL_OBJECTс именемARWHelperObject - Задайте для члена
NameARWHelperObjectбуфер наNonPagedPoolNx, который имеет значение 0 - Установите для члена
LengthобъектаARWHelperObjectзначение, указанное в буфере ввода, предоставляемом пользователем, черезDeviceIoControl. - Назначьте этот объект массиву, который управляет всеми активными объектами
PARW_HELPER_OBJECT_NON_PAGED_POOL_OBJECT - Вернуть адрес
ARWHelpeObjectв пользовательский режим через выходной буферDeviceIoControl
Давайте посмотрим на следующий обработчик IOCTL после
CreateArbitraryReadWriteHelperObjectNonPagedPoolNx, который является SetArbitraryReadWriteHelperObjecNameNonPagedPoolNxIoctlHandler. Этот обработчик IOCTL примет пользовательский буфер, предоставленный DeviceIoControl, который, как ожидается, будет иметь тип ARW_HELPER_OBJECT_IO. Эта структура затем передается в функцию SetArbitraryReadWriteHelperObjecNameNonPagedPoolNx, которая прототипируется как таковая:
Код:
NTSTATUS
SetArbitraryReadWriteHelperObjecNameNonPagedPoolNx(
_In_ PARW_HELPER_OBJECT_IO HelperObjectIo
)
Давайте посмотрим, что эта функция будет делать с нашим входным буфером. Вспомните, как в прошлый раз мы могли указать длину, которая использовалась в операции над размером члена
Name объекта ARWHELPER_OBJECT_NON_PAGED_POOL_NX ARWHelperObject. Кроме того, мы смогли вернуть адрес этого указателя в пользовательский режим.
Эта функция начинается с определения нескольких переменных:
- Указатель с именем
Name - Указатель с именем
HelperObjectAddress - Целочисленное значение с именем
Index, которому присваивается статусSTATUS_INVALID_INDEX - Код
NTSTATUS
ARW_HELPER_OBJECT_IO, в пользовательском режиме. После подтверждения этого член Name, который является указателем, из этого буфера пользовательского режима сохраняется в указателе Name, ранее объявленном в списке объявленных переменных. Член HelperObjectAddress из буфера пользовательского режима, который после вызова CreateArbitraryReadWriteHelperObjectNonPagedPoolNx, содержит адрес режима ядра PARW_HELPER_OBJECT_NON_PAGED_POOL_OBJECT ARWHelperObject, который извлекается и сохраняется в объекте объявленной функции HelperOb в начале объявленной функции HelperOb.Выполняется вызов
GetIndexFromPointer с адресом HelperObjectAddress в качестве аргумента в этом вызове. Если возвращаемое значение - STATUS_INVALID_INDEX, вызывающей стороне возвращается код NTSTATUS STATUS_INVALID_INDEX. Если функция возвращает что-либо еще, значение индекса выводится на экран.Откуда взялось это значение?
GetIndexFromPointer определяется как таковой.
Эта функция принимает значение любого указателя, но на практике это используется для указателя на объект
ARW_HELPER_OBJECT_NON_PAGED_POOL_NX. Эта функция принимает предоставленный указатель и индексирует массив указателей ARW_HELPER_OBJECT_NON_PAGED_POOL_NX, g_ARWHelperObjectNonPagedPoolNx. Если значение не было присвоено массиву (например, если CreateArbitraryReadWriteHelperObjectNonPagedPoolNx не был вызван, так как это присвоит массиву любой созданный ARW_HELPER_OBJECT_NON_PAGED_POOL_NX или объект был освобожден), возвращается STATUS_INVALID_INDEX. Эта функция в основном гарантирует, что объект ARW_HELPER_OBJECT_NON_PAGED_POOL_NX в области видимости управляется массивом. Если он существует, эта функция возвращает индекс массива, в котором находится данный объект.Давайте посмотрим на следующий фрагмент кода из функции
SetArbitraryReadWriteHelperObjecNameNonPagedPoolNx.
После подтверждения существования
ARW_HELPER_OBJECT_NON_PAGED_POOL_NX выполняется проверка, чтобы убедиться, что указатель Name, который был извлечен из буфера пользовательского режима элемента Name типа PARW_HELPER_OBJECT_IO, находится в пользовательском режиме. Обратите внимание, что g_ARWHelperObjectNonPagedPoolNx[Index] используются в этой ситуации, как еще один способ ссылки на объекте ARW_HELPER_OBJECT_NON_PAGED_POOL_NX в области видимости, так как все g_ARWHelperObjectNonPagedPoolNx находится в конце дня является массивом, типа PARW_HELPER_OBJECT_NON_PAGED_POOL_NX, которая управляет всеми активными указателями ARW_HELPER_OBJECT_NON_PAGED_POOL_NX.После подтверждения того, что буфер поступает из пользовательского режима, эта функция завершается копированием значения
Name, которое является значением, предоставленным нами через DeviceIoControl и объект ARW_HELPER_OBJECT_IO, в член Name ранее созданного ARW_HELPER_OBJECT_NON_PAGED_POOL_NX через CreateArbitraryReadWriteHelperObagedДавайте проверим эту теорию в WinDbg. Здесь мы должны искать значение, указанное членом
Name нашего предоставленного пользователем ARW_HELPER_OBJECT_IO, которое должно быть записано в член Name объекта ARW_HELPER_OBJECT_NON_PAGED_POOL_NX, созданного в предыдущем вызове CreateArbitraryReadWriteHelperObjectNonPagedPoolNx. Наш обновленный код выглядит следующим образом.
Приведенный выше код должен перезаписать член
Name ранее созданного объекта ARW_HELPER_OBJECT_NON_PAGED_POOL_NX из функции CreateArbitraryReadWriteHelperObjectNonPagedPoolNx. Обратите внимание, что IOCTL для функции SetArbitraryReadWriteHelperObjecNameNonPagedPoolNx равен 0x00222067.Затем мы можем установить точку останова в WinDbg для выполнения динамического анализа.
Затем мы можем установить точку останова на
ProbeForRead, которая будет принимать первый аргумент, который является нашим пользовательским ARW_HELPER_OBJECT_IO, и проверять, находится ли он в пользовательском режиме. Мы можем проанализировать этот адрес памяти в WinDbg, который будет в RCX, когда вызов функции происходит из-за соглашения о вызовах __fastcall, и увидеть, что это не только буфер пользовательского режима, но и объект, из которого мы намеревались отправить пользовательский режим для функции SetArbitraryReadWriteHelperObjecNameNonPagedPoolNx.
Это значение
HelperObjectAddress является адресом ранее созданного/связанного объекта ARW_HELPER_OBJECT_NON_PAGED_POOL_NX. Мы также можем проверить это в WinDbg.Вспомните ранее, что связанный объект
ARW_HELPER_OBJECT_NON_PAGED_POOL_NX имеет член Length, взятый из Length, отправленного из нашей структуры ARW_HELPER_OBJECT_IO пользовательского режима. Член Name в ARW_HELPER_OBJECT_NON_PAGED_POOL_NX также инициализируется нулевым значением в соответствии с вызовом RtlFillMemory из подпрограммы CreateArbitraryReadWriteHelperObjectNonPagedPoolNx, который инициализирует буфер Name с помощью значения 0 (напомним, что член Name буфера для ARW_HELPER_OBJECT_NON_PAGED_POOL_NX фактически был выделен с помощью ExAllocatePoolWithTag структуры ARW_HELPER_OBJECT_IO в нашем вызове DeviceIoControl).ARW_HELPER_OBJECT_NON_PAGED_POOL_NX.Name - это член, который должен быть перезаписан содержимым объекта ARW_HELPER_OBJECT_IO, который мы отправили из пользовательского режима, который в настоящее время установлен на 0x4141414141414141. Зная это, давайте установим точку останова в подпрограмме RtlCopyMemory, которая будет отображаться как memcpy в HEVD через WinDbg.
Это не удается. Код ошибки здесь - доступ запрещен. Почему это? Напомним, что есть последний вызов
ProbeForRead непосредственно перед вызовом memcpy
C:
ProbeForRead(
Name,
g_ARWHelperObjectNonPagedPoolNx[Index]->Length,
(ULONG)__alignof(UCHAR)
);
Переменная
Name здесь извлекается из буфера пользовательского режима ARW_HELPER_OBJECT_IO. Поскольку мы предоставили значение 0x4141414141414141, это технически недействительный адрес, и вызов ProbeForRead не сможет найти этот адрес. Вместо этого давайте создадим указатель пользовательского режима и воспользуемся им!
После повторного выполнения кода и достижения всех точек останова мы видим, что выполнение теперь достигает подпрограммы
memcpy.
После выполнения процедуры memcpy объект
ARW_HELPER_OBJECT_NON_PAGED_POOL_NX, созданный из функции CreateArbitraryReadWriteHelperObjectNonPagedPoolNx, теперь указывает на значение, указанное нашим буфером пользовательского режима, 0x4141414141414141.
Мы приближаемся к нашей цели! Вы можете видеть, что это в значительной степени неконтролируемый произвольный примитив записи сам по себе. Однако проблема здесь в том, что значение, которое мы можем перезаписать, - это
ARW_HELPER_OBJECT_NON_PAGED_POOL_NX. Name - это указатель, который выделяется в ядре через ExAllocatePoolWithTag. Поскольку мы не можем напрямую управлять адресом, хранящимся в этом элементе, мы ограничены только перезаписью того, что нам предоставляет ядро. Наша цель - использовать уязвимость переполнения пула, чтобы преодолеть это (в будущем).Прежде чем перейти к этапу эксплуатации, нам нужно изучить еще один обработчик IOCTL, а также обработчик IOCTL для удаления объектов, что не должно занимать много времени.
Последний обработчик IOCTL, который нужно исследовать, - это обработчик IOCTL
GetArbitraryReadWriteHelperObjecNameNonPagedPoolNxIoctlHandler.
Этот обработчик передает предоставленный пользователем буфер типа
ARW_HELPER_OBJECT_IO в GetArbitraryReadWriteHelperObjecNameNonPagedPoolNx. Эта функция идентична функции SetArbitraryReadWriteHelperObjecNameNonPagedPoolNx в том, что она копирует один член Name в другой член Name, но в обратном порядке. Как видно ниже, член Name, используемый в аргументе назначения для вызова RtlCopyMemory, на этот раз взят из буфера, предоставленного пользователем.
Это означает, что если бы мы использовали функцию
SetArbitraryReadWriteHelperObjecNameNonPagedPoolNx перезаписать элемент Name объекта ARW_HELPER_OBJECT_NON_PAGED_POOL_NX из функции CreateArbitraryReadWriteHelperObjectNonPagedPoolNx тогда мы могли бы использовать GetArbitraryReadWriteHelperObjecNameNonPagedPoolNx, чтобы получить имя элемента объекта ARW_HELPER_OBJECT_NON_PAGED_POOL_NX и пузырек его по итогам обратно в пользовательском режиме. Давайте изменим наш код, чтобы обрисовать это. Код IOCTL для доступа к функции GetArbitraryReadWriteHelperObjecNameNonPagedPoolNx - 0x0022206B.
В этом случае нам не нужно WinDbg для проверки чего-либо. Мы можем просто установить содержимое нашего члена
ARW_HELPER_OBJECT_IO.Name как нежелательное в качестве POC, что после вызова IOCL для достижения GetArbitraryReadWriteHelperObjecNameNonPagedPoolNx этот элемент будет перезаписан содержимым связанного/ранее созданного объекта ARW_HELPER_OBJECT_NON_PAGED_POOL_NX, который будет 0x4141414141414141
Поскольку
tempBuffer назначен для ARW_HELPER_OBJECT_IO.Name, это технически значение, которое наследует содержимое ARW_HELPER_OBJECT_NON_PAGED_POOL_NX.Name в операции memcpy от функции GetArbitraryReadWriteHelperObjecNameNonPagedPoolNx. Как мы видим, мы можем успешно получить содержимое связанного объекта ARW_HELPER_OBJECT_NON_PAGED_POOL_NX.Name. Опять же, проблема в том, что мы не можем выбрать, на что указывает ARW_HELPER_OBJECT_NON_PAGED_POOL_NX.Name, поскольку это определяется драйвером. Вскоре мы воспользуемся уязвимостью переполнения пула, чтобы преодолеть это ограничение.Последний обработчик IOCTL - это операция удаления, найденная в
DeleteArbitraryReadWriteHelperObjecNonPagedPoolNxIoctlHandler.
Этот обработчик IOCTL анализирует входной буфер из
DeviceIoControl как структуру ARW_HELPER_OBJECT_IO. Затем этот буфер передается в функцию DeleteArbitraryReadWriteHelperObjecNonPagedPoolNx.
Эта функция довольно упрощена - поскольку
HelperObjectAddress указывает на связанный объект ARW_HELPER_OBJECT_NON_PAGED_POOL_NX, этот член используется в вызове ExAllocateFreePoolWithTag для освобождения объекта ARW_HELPER_OBJECT_NON_PAGED_POOL_NX. Кроме того, освобождается член ARW_HELPER_OBJECT_NON_PAGED_POOL_NX.Name, который также выделяется ExAllocatePoolWithTag.Теперь, когда мы знаем все тонкости работы драйвера, мы можем продолжить (обратите внимание, что нам повезло, что у нас есть исходный код в этом случае. Использование дизассемблера требует немного больше времени, чтобы прийти к тем же выводам. мы смогли прийти)
Теперь давайте приступим к эксплуатации (на этот раз по-настоящему)
Мы знаем, что наша ситуация в настоящее время допускает неконтролируемый произвольный примитив чтения/записи. Это связано с тем, что член
ARW_HELPER_OBJECT_NON_PAGED_POOL_NX.Name в настоящее время установлен на адрес распределения пула через ExAllocatePoolWithTag. При переполнении нашего пула мы попытаемся перезаписать этот адрес на значимый адрес. Это позволит нам повредить управляемый адрес, что позволит нам получить произвольный примитив чтения/записи.Наша стратегия очистки пула, поскольку все эти объекты имеют одинаковый размер и размещены в пуле одного и того же типа (
NonPagedPoolNx), будет следующей:- «Заполнить дыры» в текущей странице, обслуживающей выделение размера 0x20
- Подготовьте пул до следующего макета:
VULNERABLE_OBJECT | ARW_HELPER_OBJECT_NON_PAGED_POOL_NX | VULNERABLE_OBJECT | ARW_HELPER_OBJECT_NON_PAGED_POOL_NX | VULNERABLE_OBJECT | ARW_HELPER_OBJECT_NON_PAGED_POOL_NX - Используйте примитив чтения/записи, чтобы записать наш шелл-код, по одному QWORD за раз, в
KUSER_SHARED_DATA + 0x800и переверните бит no-eXecute, чтобы обойти DEP в режиме ядра.
_POOL_HEADER? Здесь все для нас проходит полный круг. Напомним из части 1, что kLFH по-прежнему использует унаследованные структуры _POOL_HEADER для обработки и хранения метаданных для блоков пула. Это означает, что кодирование не происходит, и можно жестко закодировать заголовок в эксплойте, чтобы при переполнении пула мы могли убедиться, что при перезаписи заголовка он перезаписывается тем же содержимым, что и раньше.Давайте проверим значение
_POOL_HEADER объекта ARW_HELPER_OBJECT_NON_PAGED_POOL_NX, которое мы бы переполнили
Поскольку этот фрагмент составляет 16 байтов и будет частью kLFH, ему предшествует стандартная структура
_POOL_HEADER. Поскольку это так и кодирования нет, мы можем просто жестко закодировать значение _POOL_HEADER (напомним, что _POOL_HEADER будет на 0x10 байтов перед значением, возвращаемым ExAllocatePoolWithTag). Это означает, что мы можем жестко закодировать значение 0x6b63614802020000 в нашем эксплойте, чтобы во время переполнения в следующий фрагмент, который должен был быть в одном из этих ARW_HELPER_OBJECT_NON_PAGED_POOL_NX объектов, которые мы ранее распыляли, первые 0x10 байтов, которые были переполнены в этом фрагменте, который будет ARW_HELPER_OBJECT_NON_PAGED_POOL_NX _POOL_HEADER, будет сохранен и сохранен как действительный, минуя более раннюю проблему, показанную при возникновении недопустимого заголовкаЗная об этом и зная, что нам предстоит немного поработать, давайте изменим наш текущий эксплойт, чтобы сделать его более логичным. Мы создадим три функции для выполнения grooming:
- fillHoles()
- groomPool()
- pokeHoles()
fillHoles()
groomPool()
pokeHoles()
Пожалуйста, обратитесь к Части 1, чтобы понять, что это делает, но, по сути, этот метод заполнит любые фрагменты в соответствующей корзине kLFH в NonPagedPoolNx и заставит диспетчер памяти (теоретически) предоставить нам новую страницу для работы. Затем мы заполняем эту новую страницу объектами, которые мы контролируем, например объекты
ARW_HELPER_OBJECT_NON_PAGED_POOL_NXПоскольку у нас есть контролируемое переполнение на основе пула, цель будет состоять в том, чтобы перезаписать любую из структур
ARW_HELPER_OBJECT_NON_PAGED_POOL_NX «уязвимым фрагментом», который копирует память в выделение без проверки границ. Поскольку уязвимый фрагмент и фрагменты ARW_HELPER_OBJECT_NON_PAGED_POOL_NX имеют одинаковый размер, теоретически они оба окажутся смежными друг с другом, поскольку они окажутся в одном ведре kLFH.Последняя функция, называемая
readwritePrimitive(), содержит большую часть кода эксплойта.Первый бит этой функции создает «основной»
ARW_HELPER_OBJECT_NON_PAGED_POOL_NX через объект ARW_HELPER_OBJECT_IO и выполняет заполнение блоков пула, заполняет новую страницу объектами, которые мы контролируем, а затем освобождает все остальные из этих объектов.
После освобождения всех остальных объектов мы заменяем эти освобожденные слоты нашими уязвимыми буферами. Мы также создаем «автономный/основной» объект
ARW_HELPER_OBJECT_NON_PAGED_POOL_NX. Также обратите внимание, что заголовок пула имеет размер 16 байтов, то есть это 2 QWORDS, отсюда «Padding».
На самом деле мы надеемся здесь сделать следующее.
Мы хотим использовать управляемую запись только для перезаписи первого члена этого соседнего объекта
ARW_HELPER_OBJECT_NON_PAGED_POOL_NX, Name. Это связано с тем, что у нас есть дополнительные примитивы для управления и возврата этих значений члена Name, как показано в этом сообщении в блоге. Однако проблема, с которой мы столкнулись до сих пор, заключается в том, что адрес члена Name объекта ARW_HELPER_OBJECT_NON_PAGED_POOL_NX полностью контролируется драйвером и не может быть изменен нами, если мы не используем уязвимость (например, переполнение пула)Как показано в функции
readwritePrimitive(), цель здесь будет состоять в том, чтобы фактически повредить соседние блоки с адресом «основного» объекта ARW_HELPER_OBJECT_NON_PAGED_POOL_NX, которым мы будем управлять через ARW_HELPER_OBJECT_IO.HelperObjectAddress. Мы хотели бы повредить соседний объект ARW_HELPER_OBJECT_NON_PAGED_POOL_NX точным переполнением, чтобы испортить значение Name адресом нашего «основного» объекта. В настоящее время это значение установлено на 0x9090909090909090. Как только мы докажем, что это возможно, мы можем пойти дальше, чтобы получить в конечном итоге примитив чтения/записиУстановив точку останова для подпрограммы
TriggerBufferOverflowNonPagedPoolNx в HEVD.sys и установив дополнительную точку останова для подпрограммы memcpy, которая выполняет переполнение пула, мы можем исследовать содержимое пула.
Как видно на изображении выше, мы ясно видим, что мы заполнили пул контролируемыми объектами
ARW_HELPER_OBJECT_NON_PAGED_POOL_NX, а также «текущим» фрагментом, который относится к уязвимому фрагменту, используемому при переполнении пула. Все эти фрагменты начинаются с тега Hack.Затем, после пошагового выполнения до подпрограммы mempcy, мы можем проверить содержимое следующего фрагмента, который находится на
0x10 байтов после значения в RCX, которое используется в месте назначения для операции копирования памяти. Помните - наша цель - перезаписать соседние блоки пула. Пошаговое выполнение операции, чтобы четко увидеть, что мы испортили следующий фрагмент пула, который имеет тип ARW_HELPER_OBJECT_NON_PAGED_POOL_NX
Мы можем проверить, что адрес, который был записан за границу, на самом деле является адресом «основного», автономного объекта
ARW_HELPER_OBJECT_NON_PAGED_POOL_NX, который мы создали.
Помните - структура
_POOL_HEADER имеет длину 0x10 байт. Это делает общий размер каждого фрагмента пула в этом ведре kLFH 0x20 байтов. Поскольку мы хотим переполнить соседние блоки, нам нужно сохранить заголовок пула. Поскольку мы находимся в kLFH, мы можем просто жестко закодировать заголовок пула, как мы доказали, чтобы удовлетворить пул и избежать каких-либо сбоев, которые могут возникнуть в результате неправильного фрагмента пула. Кроме того, мы можем испортить первые 0x10 байтов значения в RCX, которое является адресом назначения в операции копирования памяти, потому что в «уязвимом» фрагменте пула (который используется в операции копирования) есть 0x20 байтов. Первые 0x10 байтов - это заголовок, а вторая половина нас фактически не волнует, так как мы беспокоимся о повреждении соседнего фрагмента. Из-за этого мы можем установить первые 0x10 байтов нашей копии, которая записывает за пределы, на 0x10, чтобы гарантировать, что байты, которые копируются за пределы, являются байтами, которые составляют заголовок пула следующего фрагмента.Теперь мы успешно выполнили запись за пределы диапазона через переполнение пула и повредили член
Name соседнего объекта ARW_HELPER_OBJECT_NON_PAGED_POOL_NX, который динамически выделяется в пуле раньше и имеет адрес, который мы не контролируем, если мы не используем уязвимость например, запись за границу, с адресом, который мы контролируем, который является адресом объекта, созданного ранееArbitrary Read Примитив
Хотя в настоящее время это может быть не совсем очевидным, наша стратегия использования уязвимостей вращается вокруг нашей способности использовать переполнение пула для записи за пределы. Напомним, что возможности «Установить» и «Получить» в драйвере позволяют нам читать и писать в память, но не в контролируемых местах. Расположение контролируется блоком пула, выделенным для членаName объекта ARW_HELPER_OBJECT_NON_PAGED_POOL_NX.Давайте посмотрим на поврежденный объект
ARW_HELPER_OBJECT_NON_PAGED_POOL_NX. Поврежденный объект - один из множества распыленных объектов. Мы успешно заменили элемент Name этого объекта адресом «основного» или автономного объекта ARE_HELPER_OBJECT_NON_PAGED_POOL_NX.
Мы знаем, что можно установить член
Name структуры ARW_HELPER_OBJECT_NON_PAGED_POOL_NX через функцию SetArbitraryReadWriteHelperObjecNameNonPagedPoolNx через вызов IOCTL. Поскольку теперь мы можем контролировать значение Name в поврежденном объекте, давайте посмотрим, сможем ли мы злоупотребить этим с помощью произвольного примитива чтения.Давайте разберемся с этим. Мы знаем, что в настоящее время у нас есть поврежденный объект с членом
Name, которому присвоено значение другого объекта. Для краткости можно вспомнить это из предыдущего изображения.
Если мы выполняем операцию «Установить» в данный момент на поврежденном объекте, показанном в команде
dt, и в настоящее время для его члена Name установлено значение 0xffffa00ca378c210, он выполнит эту операцию для члена Name. Однако мы знаем, что член Name на самом деле в настоящее время установлен на значение «основного» объекта через запись за границу! Это означает, что выполнение операции «Установить» на поврежденном объекте будет фактически принимать адрес основного объекта, поскольку он установлен в члене Name, разыменовать его и записать указанное нами содержимое. Это приведет к тому, что наш основной объект будет указывать на то, что мы укажем, вместо значения ffffa00ca378c3b0, которое в настоящее время отображается в содержимом памяти, отображаемом dq в WinDbg. Как это превращается в произвольный примитив чтения? Поскольку наш «основной» объект будет указывать на любой адрес, который мы укажем, операция «Get», если она выполняется с «основным» объектом, затем разыменует этот указанный нами адрес и вернет значение!В WinDbg мы можем «имитировать» операцию «Set», как показано.
Выполнение операции «Set» на поврежденном объекте фактически установит значение нашего основного объекта на то, что указано пользователю, из-за того, что мы повредили предыдущий случайный адрес из-за уязвимости переполнения пула. На этом этапе выполнение операции «Get» для нашего основного объекта, поскольку он был установлен на значение, указанное пользователем, приведет к разыменованию значения и вернет его нам!
На этом этапе нам нужно определить, какова наша цель. Чтобы полностью обойти kASLR, наша цель заключается в следующем:
- Используйте базовый адрес
HEVD.sysиз исходного эксплойта в первой части, чтобы указать смещение для таблицы адресов импорта. - Предоставьте запись IAT, которая указывает на
ntoskrnl.exeдля произвольного чтения эксплойта (таким образом получая указатель наntoskrnl.exe) - Рассчитайте расстояние от указателя до ядра, чтобы получить базу
Мы можем обновить наш код, чтобы обрисовать это. Как вы помните, мы подготовили пул с 5000 объектами
ARW_HELPER_OBJECT_NON_PAGED_POOL_NX. Однако мы не обрызгали бассейн 5000 «уязвимыми» объектами. Поскольку мы подготовили пул, мы знаем, что наш уязвимый объект, который мы можем произвольно записывать в прошлое, в конечном итоге окажется рядом с одним из объектов, используемых для очистки. Поскольку мы запускаем переполнение только один раз, и поскольку мы уже установили значения Name для всех объектов, используемых для ухода, значение 0x9090909090909090, мы можем просто использовать операцию «Get» для просмотра каждого члена Name используемых объектов. для ухода за шерстью. Если один из объектов не содержит NOP, это указывает на то, что переполнение пула, описанное ранее, чтобы повредить значение имени ARW_HELPER_OBJECT_NON_PAGED_POOL_NX, прошло успешно.
После этого мы можем затем использовать тот же примитив, о котором говорилось ранее, используя функцию «Set» в HEVD, чтобы установить член
Name целевого поврежденного объекта, что фактически «обманет» программу, чтобы перезаписать член Name поврежденного объекта. , который на самом деле является адресом «автономного»/основного ARW_HELPER_OBJECT_NON_PAGED_POOL_NX. Перезапись приведет к разыменованию автономного объекта, что позволит использовать произвольный примитив чтения, поскольку у нас есть возможность позже использовать функцию «Get» для основного объекта.
Затем мы можем добавить к нашему эксплойту функцию «press enter to continue», чтобы приостановить выполнение после того, как основной объект будет выведен на экран, а также поврежденный объект, используемый для очистки, который находится в 5000 объектах, используемых для очистки.
Затем мы можем взять адрес
0xffff8e03c8d5c2b0, который является поврежденным объектом, и проверить его в WinDbg. Если все идет хорошо, этот адрес должен содержать адрес «основного» объекта.
Сравнивая элемент
Name с предыдущим снимком экрана, на котором используется эксплойт с оператором «press enter to continue», мы видим, что повреждение пула было успешным и что член Name одного из 5000 объектов, используемых для очистки, был перезаписан!Теперь, если бы мы использовали функцию «Установить» HEVD и предоставили объект
ARW_HELPER_OBJECT_NON_PAGED_POOL, который был поврежден и также использовался для очистки, по адресу 0xffff8e03c8d5c2b0, HEVD использовал бы значение, хранящееся в Name, разыменовав его и перезаписав его. Это связано с тем, что HEVD ожидает одного из распределений пула, ранее показанных для указателей Name, которые мы не контролируем. Поскольку мы предоставили другой адрес, HEVD фактически выполнит перезапись, но на этот раз он перезапишет предоставленный нами указатель, который является другим ARW_HELPER_OBJECT_NON_PAGED_POOL. Поскольку первый член одного из этих объектов имеет имя члена, произойдет то, что HEVD фактически запишет все, что мы передаем члену Name нашего основного объекта! Давайте посмотрим на это в WinDbg.Как показал наш эксплойт, в этом случае мы используем
HEVD + 0x2038. Это значение нужно записать в наш основной объект
Как видите, у нашего основного объекта теперь есть член
Name, указывающий на HEVD + 0x2038, который является указателем на ядро! После запуска полного эксплойта мы теперь получили базовый адрес HEVD из предыдущего эксплойта, а теперь и базу ядра посредством произвольного чтения посредством переполнения пула - все из-за низкой целостности!// Ограничение на кол-во прикрепляемых файлов не позволяет мне сделать перевод одним постом. Второй кусок будет ниже
Последнее редактирование: