Железный бряк. Используем хардверные брейк-пойнты в пентестерских целях

Содержание статьи

  • Обработка исключений
  • Установка hardware breakpoint
  • Обход AMSI
  • Извлечение номеров сисколов
  • Анхукинг
  • Пишем кастомный GetThreadContext()
  • Ставим хуки
  • Выводы

Windows пре­дос­тавля­ет мощ­ные инс­тру­мен­ты для уста­нов­ки точек оста­нова непос­редс­твен­но в памяти. Но зна­ешь ли ты, что с их помощью мож­но ста­вить и сни­мать хуки, а так­же получать сис­колы? В этой статье я в под­робнос­тях рас­ска­жу, как это делать.

Точ­ки оста­нова слу­жат для кон­тро­ля выпол­нения прог­раммы и, конеч­но же, их оста­нов­ки в опре­делен­ный момент. Гло­баль­но сущес­тву­ет два вида брейк‑пой­нтов: software breakpoints и hardware breakpoints.

Software breakpoint — точ­ка оста­нова, которая ста­вит­ся с помощью отладчи­ка или IDE. Что­бы пос­тавить такую точ­ку оста­нова, мож­но, нап­ример, прос­то клик­нуть на нуж­ную стро­ку прог­раммы в Visual Studio.

Ус­танов­ка software breakpoint в Visual Studio

Та­кие точ­ки оста­нова мож­но ста­вить где угод­но и сколь­ко угод­но. Никаких огра­ниче­ний нет.

Ус­танов­ка мно­жес­тва software breakpoint

Hardware breakpoint — уже более слож­ная шту­ка, которую мы сегод­ня и будем изу­чать. Эти бря­ки ста­вят­ся путем запол­нения спе­циаль­ных отла­доч­ных регис­тров про­цес­сора (DR0–DR7). Сог­ласно докумен­тации, Dr0–3 дол­жны хра­нить адрес, по которо­му уста­нов­лен breakpoint, но у меня бряк сра­баты­вал, толь­ко если адрес запол­нялся в DR0.

Пер­вые три регис­тра называ­ются регис­тра­ми с отла­доч­ными адре­сами (Debug Address Registers). Регис­тры с номера­ми 4 и 5 не исполь­зуют­ся и называ­ются зарезер­вирован­ными отла­доч­ными регис­тра­ми (Reserved Debug Registers). DR6 содер­жит раз­личную информа­цию о сра­ботав­шем исклю­чении. Исклю­чение — это событие, воз­ника­ющее, ког­да компь­ютер попыта­ется выпол­нить инс­трук­цию, адрес которой рас­положен в DR0. DR7 содер­жит биты управле­ния отладкой. Если зна­чение рав­но еди­нице, то точ­ка оста­нова дол­жна сра­ботать, если нулю, то не дол­жна.

Hardware breakpoints, как ты понима­ешь, через кра­сивый GUI не ста­вят­ся. Нам пот­ребу­ется вза­имо­дей­ство­вать с регис­тра­ми нап­рямую, исполь­зуя, конеч­но же, наш любимый WinAPI. И само собой, толь­ко хар­двер­ные брей­ки поз­волят хукать, обхо­дить AMSI и получать сис­колы. Соф­твер­ные, к сожале­нию, для это­го не под­ходят.

 

Обработка исключений

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

Как выг­лядит исклю­чение

Лю­бые исклю­чения могут быть обра­бота­ны. Здесь есть два пути — VEH (Vectored Exception Handling) и SEH (Structured Exception Handling). Отдель­но я выделю еще UEH (Unhandled Exception Handling). Нач­нем с SEH. SEH — стан­дар­тный блок __try — __finally, __try — __except.

#include <iostream>#include <Windows.h>int main() { int a = 2 - 2; int b = 3; __try { std::cout << b / a << std::endl; } __except (EXCEPTION_EXECUTE_HANDLER) { std::cout << "EXCEPTION" << std::endl; } return 0;}

Об­работ­ка исклю­чения с помощью SEH

SEH мож­но счи­тать надс­трой­кой над конс­трук­цией try — except из С++. В SEH в блок __except добав­ляют­ся спе­циаль­ные зна­чения, в зависи­мос­ти от которых может менять­ся поведе­ние обра­бот­чика исклю­чений:

  • EXCEPTION_EXECUTE_HANDLER — сис­тема переда­ет управле­ние в обра­бот­чик исклю­чения. То есть будет поведе­ние, как в коде выше;
  • EXCEPTION_CONTINUE_SEARCH — эта конс­трук­ция зас­тавля­ет сис­тему перей­ти к пре­дыду­щему бло­ку try, которо­му соот­ветс­тву­ет блок except, и обра­ботать этот блок. То есть сис­тема игно­риру­ет текущий обра­бот­чик исклю­чений и пыта­ется най­ти обра­бот­чик исклю­чений в охва­тыва­ющем бло­ке (или бло­ках);
  • EXCEPTION_CONTINUE_EXECUTION — обна­ружив такое зна­чение, сис­тема воз­вра­щает­ся к инс­трук­ции, выз­вавшей исклю­чение, и пыта­ется выпол­нить ее сно­ва.

Ни­же — при­мер EXCEPTION_CONTINUE_EXECUTION.

#include <iostream>#include <cstddef>#include <Windows.h>char g_szBuffer[100];LONG Filter(char** ppchBuffer) { if (*ppchBuffer == NULL) { *ppchBuffer = g_szBuffer; return(EXCEPTION_CONTINUE_EXECUTION); } return(EXCEPTION_EXECUTE_HANDLER);}int main() { int x = 0; char* pchBuffer = NULL; __try { *pchBuffer = 'J'; x = 5 / x; } __except (Filter(&pchBuffer)) { MessageBox(NULL, L"An exception occurred", NULL, MB_OK); } MessageBox(NULL, L"Function completed", NULL, MB_OK); return 0;}

При­мер EXCEPTION_CONTINUE_EXECUTION

Прог­раммы могут быть слож­ные, страш­ные, боль­шие, нуж­но пре­дус­матри­вать кор­рек­тный выход из всех бло­ков, изу­чать воз­можные исклю­чения. Вдруг пот­ребу­ется фун­кция уве­дом­ления поль­зовате­ля о сра­ботав­шем исклю­чении? В общем, CEH хорош, но, помимо него, появил­ся и VEH. VEH мож­но счи­тать эда­кой надс­трой­кой над SEH. Работа­ет она, само собой, толь­ко в Windows.

Ес­ли в прог­рамме воз­ника­ет исклю­чение, то пер­выми вызыва­ются имен­но век­торные обра­бот­чики и лишь затем сис­тема нач­нет раз­ворачи­вать стек. С помощью VEH про­га может, нап­ример, зарегис­три­ровать фун­кцию для прос­мотра или обра­бот­ки всех исклю­чений при­ложе­ния. При­чем в прог­рамму мож­но добавить нес­коль­ко VEH-обра­бот­чиков, и они будут выз­ваны в том поряд­ке, в котором были добав­лены. Пер­вый — пер­вым, вто­рой — вто­рым и так далее. CEH пос­ле VEH вызыва­ется толь­ко в том слу­чае, если VEH вер­нул EXCEPTION_CONTINUE_SEARCH.

С помощью VEH мож­но ловить исклю­чения, воз­ника­ющие при хар­двер­ных брей­ках. Добавить обра­бот­чик мож­но с помощью фун­кции AddVectoredExceptionHandler().

PVOID AddVectoredExceptionHandler( ULONG FirstHandler, PVECTORED_EXCEPTION_HANDLER VectoredHandler)

  • FirstHandler — вызывать обра­бот­чик рань­ше всех ранее зарегис­три­рован­ных обра­бот­чиков (зна­чение CALL_FIRST) или пос­ле всех (зна­чение CALL_LAST);
  • VectoredHandler — адрес фун­кции обра­бот­чика. Эта фун­кция дол­жна воз­вра­щать EXCEPTION_CONTINUE_EXECUTION. Обра­бот­чики далее не выпол­няют­ся, обра­бот­ка средс­тва­ми SEH не про­изво­дит­ся, управле­ние переда­ется в ту точ­ку прог­раммы, из которой было выз­вано исклю­чение или EXCEPTION_CONTINUE_SEARCH (выпол­няет­ся сле­дующий век­торный обра­бот­чик, а если таких нет, то раз­ворачи­вает­ся SEH).

За­регис­три­руем обра­бот­чик и про­верим работу VEH. Исклю­чени­ем пока будет стан­дар­тный Null-Pointer Reference. То есть обра­щение к ука­зате­лю, который име­ет зна­чение nullptr.

#include <iostream>#include <windows.h>#include <errhandlingapi.h>LONG WINAPI MyVectoredExceptionHandler(PEXCEPTION_POINTERS exceptionInfo){ std::cout << "Exception occurred!" << std::endl; std::cout << "Exception Code: " << exceptionInfo->ExceptionRecord->ExceptionCode << std::endl; std::cout << "Exception Address: " << exceptionInfo->ExceptionRecord->ExceptionAddress << std::endl; return EXCEPTION_CONTINUE_SEARCH;}int main(){ if (AddVectoredExceptionHandler(1, MyVectoredExceptionHandler) == nullptr) { std::cout << "Failed to add the exception handler!" << std::endl; return 1; } int* p = nullptr; *p = 42; // Исключение возникает тут return 0;}

Об­работ­ка исклю­чения с помощью VEH

Ви­дим, что обра­бот­чик успешно сра­баты­вает и вызыва­ется, затем воз­вра­щает EXCEPTION_CONTINUE_SEARCH. Это, в свою оче­редь, дер­гает SEH, SEH в прог­рамме нет, поэто­му Visual Studio вклю­чает­ся и выда­ет нам исклю­чение. Если будем воз­вра­щать EXCEPTION_CONTINUE_EXECTION, то получим бес­конеч­ный вызов обра­бот­чика, так как каж­дый раз будет сра­баты­вать стро­ка *p = 42.

Бес­конеч­ная обра­бот­ка исклю­чения

Точ­но такое же исклю­чение будет сра­баты­вать и при хар­двер­ных бря­ках.

На­конец, пос­ледний тип обра­бот­чиков — Unhandled Exception Filter. Он ред­ко ког­да исполь­зует­ся, но изна­чаль­но задумы­вал­ся как обра­бот­чик для исклю­чений, которые вооб­ще ник­то не обра­баты­вает. Ни VEH (если отсутс­тву­ет или вер­нул EXCEPTION_CONTINUE_SEARCH), ни CEH (если тоже отсутс­тву­ет или ука­зано EXCEPTION_CONTINUE_SEARCH). Уста­нав­лива­ются такие обра­бот­чики через фун­кцию SetUnhandledExceptionFilter().

LPTOP_LEVEL_EXCEPTION_FILTER SetUnhandledExceptionFilter( [in] LPTOP_LEVEL_EXCEPTION_FILTER lpTopLevelExceptionFilter);

Фун­кция при­нима­ет один‑единс­твен­ный параметр — адрес фун­кции‑обра­бот­чика, которая дол­жна вызывать­ся при воз­никно­вении необ­работан­ного исклю­чения. С помощью UEF так­же ловят­ся исклю­чения, воз­ника­ющие при бря­ках.

Возь­мем прош­лый код и переде­лаем его под UEF.

#include <iostream>#include <windows.h>LONG WINAPI MyUnhandledExceptionHandler(PEXCEPTION_POINTERS exceptionInfo){ std::cout << "Unhandled exception occurred!" << std::endl; std::cout << "Exception Code: " << exceptionInfo->ExceptionRecord->ExceptionCode << std::endl; std::cout << "Exception Address: " << exceptionInfo->ExceptionRecord->ExceptionAddress << std::endl; return EXCEPTION_CONTINUE_SEARCH;}int main(){ if (SetUnhandledExceptionFilter(MyUnhandledExceptionHandler) == nullptr) { std::cout << "Failed to set the unhandled exception filter!" << std::endl; return 1; } int* p = nullptr; *p = 42; return 0;}

Об­рати вни­мание, что если ты запус­тишь этот код в Visual Studio, то она выдаст ошиб­ку до UEF.

Ис­клю­чение от «Сту­дии», а не от UEF

Это свя­зано с тем, что исклю­чение в дан­ном слу­чае обра­баты­вает Visual Studio. Если же файл будет запущен за пре­дела­ми IDE, то мы получим успешный вызов обра­бот­чика.

Вы­зов обра­бот­чика 

Установка hardware breakpoint

Источник: xakep.ru

Ответить

Ваш адрес email не будет опубликован. Обязательные поля помечены *