• [ Регистрация ]Открытая и бесплатная
  • Tg admin@ALPHV_Admin (обязательно подтверждение в ЛС форума)

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

stihl

Moderator
Регистрация
09.02.2012
Сообщения
1,178
Розыгрыши
0
Реакции
510
Deposit
0.228 BTC
stihl не предоставил(а) никакой дополнительной информации.
Windows предоставляет мощные инструменты для установки точек останова непосредственно в памяти. Но знаешь ли ты, что с их помощью можно ставить и снимать хуки, а также получать сисколы? В этой статье я в подробностях расскажу, как это делать.
Точки останова служат для контроля выполнения программы и, конечно же, их остановки в определенный момент. Глобально существует два вида брейк‑пойнтов: software breakpoints и hardware breakpoints.

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


Установка software breakpoint в Visual Studio
Установка software breakpoint в Visual Studio
Такие точки останова можно ставить где угодно и сколько угодно. Никаких ограничений нет.

Установка множества software breakpoint
Установка множества 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

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
Пример EXCEPTION_CONTINUE_EXECUTION

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

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

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

Код:
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
Обработка исключения с помощью VEH
Видим, что обработчик успешно срабатывает и вызывается, затем возвращает EXCEPTION_CONTINUE_SEARCH. Это, в свою очередь, дергает SEH, SEH в программе нет, поэтому Visual Studio включается и выдает нам исключение. Если будем возвращать EXCEPTION_CONTINUE_EXECTION, то получим бесконечный вызов обработчика, так как каждый раз будет срабатывать строка *p = 42.

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

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

Код:
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
Исключение от «Студии», а не от UEF
Это связано с тем, что исключение в данном случае обрабатывает Visual Studio. Если же файл будет запущен за пределами IDE, то мы получим успешный вызов обработчика.

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

Установка hardware breakpoint​

Установить HWBP проще простого — достаточно лишь занести в нужный регистр адрес. Для большей абстракции я написал функцию SetHWBP(), куда нужно передать адрес, по которому следует установить точку останова, булево значение (TRUE — установить, FALSE — снять), а также номер регистра. Согласно документации, адрес может быть указан в Dr0, Dr1 и так далее, но у меня почему‑то работало только с Dr0.

Код:
// address — адрес, по которому ставить функцию
// setBP — FALSE — снять бряк, TRUE — установить
// regnumer — номер регистра, который инициализировать адресом

VOID SetHWBP(LPVOID address, BOOL setBP, int regnumber) {
 // Здесь передаем 0
 CONTEXT context = { 0 };
 context.ContextFlags = CONTEXT_DEBUG_REGISTERS;
 GetThreadContext(GetCurrentThread(), &context);
 // Почему-то бряк, если адрес записывать в регистры, отличные от Dr0, не срабатывает
 std::string registerNames[] = { "Dr0", "Dr1", "Dr2", "Dr3" };
 if (setBP) {
  DWORD64* registers[] = { &context.Dr0, &context.Dr1, &context.Dr2, &context.Dr3 };
  if (regnumber >= 0 && regnumber < 4) {
   *registers[regnumber] = (DWORD64)address;
   std::cout << "Writing Address to " << registerNames[regnumber] << std::endl;
  }
  else {
   std::wcout << L"Invalid Registry Number" << std::endl;
   exit(-1);
  }
  // Установка бита 0 в DR7 для активации DR0
  context.Dr7 |= 1;
  // Установка битов 16–17 в DR7 для типа точки останова (Execute)
  context.Dr7 |= (0b00 << 16);
  // Установка битов 18–19 в DR7 для длины точки останова (1 byte)
  context.Dr7 |= (0b00 << 18);
 }
 else {
  context.Dr0 = 0;
  context.Dr1 = 0;
  context.Dr2 = 0;
  context.Dr3 = 0;
  context.Dr7 &= ~(1 << 0);
 }

 context.ContextFlags = CONTEXT_DEBUG_REGISTERS;
 SetThreadContext(GetCurrentThread(), &context);
}

Для получения значения регистров текущего потока используется функция Для просмотра ссылки Войди или Зарегистрируйся, а для установки измененных значений используется Для просмотра ссылки Войди или Зарегистрируйся.

Причем, если мы хотим обрабатывать только исключения, сработавшие из‑за HWBP, в нашей функции‑обработчике следует предусмотреть проверку на наличие в структуре EXCEPTION_POINTERS элемента ExceptionCode, равного STATUS_SINGLE_STEP. Это значение свидетельствует о том, что возникло событие, когда одна инструкция завершается и следующая инструкция готова к выполнению.

Код:
LONG WINAPI Handler(PEXCEPTION_POINTERS exceptionInfo)
{
 if (exceptionInfo->ExceptionRecord->ExceptionCode == STATUS_SINGLE_STEP) {
  std::cout << "Unhandled exception occurred!" << std::endl;

   // Проверка, что реально наш бряк сработал
  if (exceptionInfo->ContextRecord->Dr0 == exceptionInfo->ContextRecord->Rip || exceptionInfo->ContextRecord->Dr1 == exceptionInfo->ContextRecord->Rip || exceptionInfo->ContextRecord->Dr2 == exceptionInfo->ContextRecord->Rip || exceptionInfo->ContextRecord->Dr3 == exceptionInfo->ContextRecord->Rip) {
   std::cout << "[-] Breakpoint triggered 0x" << std::hex << exceptionInfo->ExceptionRecord->ExceptionAddress << std::endl;
   std::cout << "[!] Exception Code 0x" << std::hex << exceptionInfo->ExceptionRecord->ExceptionCode << std::endl;
   std::cout << "[!] RIP 0x" << std::hex << exceptionInfo->ContextRecord->Rip << std::endl;
   std::cout << "[!] RAX 0x" << std::hex << exceptionInfo->ContextRecord->Rax << std::endl;
   std::cout << "[!] RCX 0x" << std::hex << exceptionInfo->ContextRecord->Rcx << std::endl;
   std::cout << "[!] RDX 0x" << std::hex << exceptionInfo->ContextRecord->Rdx << std::endl;
  }

  exceptionInfo->ContextRecord->EFlags |= (1 << 16);
  return EXCEPTION_CONTINUE_EXECUTION;

 }
 // Вернем EXCEPTION_CONTINUE_SEARCH, чтобы передать исключение дальше
 return EXCEPTION_CONTINUE_SEARCH;
 }
Использовать в своей программе этот код проще простого. Вот пример.

#include <iostream>
#include <windows.h>

LONG WINAPI Handler(PEXCEPTION_POINTERS exceptionInfo)
{
 if (exceptionInfo->ExceptionRecord->ExceptionCode == STATUS_SINGLE_STEP) {
  std::cout << "Unhandled exception occurred!" << std::endl;

  // Проверка того, что наш бряк реально сработал
  if (exceptionInfo->ContextRecord->Dr0 == exceptionInfo->ContextRecord->Rip || exceptionInfo->ContextRecord->Dr1 == exceptionInfo->ContextRecord->Rip || exceptionInfo->ContextRecord->Dr2 == exceptionInfo->ContextRecord->Rip || exceptionInfo->ContextRecord->Dr3 == exceptionInfo->ContextRecord->Rip) {
   std::cout << "[-] Breakppint triggered " << std::hex << exceptionInfo->ExceptionRecord->ExceptionAddress << std::endl;
   std::cout << "[!] Exception Code " << std::hex << exceptionInfo->ExceptionRecord->ExceptionCode << std::endl;
   std::cout << "[!] RIP " << std::hex << exceptionInfo->ContextRecord->Rip << std::endl;
   std::cout << "[!] RAX " << std::hex << exceptionInfo->ContextRecord->Rax << std::endl;
   std::cout << "[!] RCX " << std::hex << exceptionInfo->ContextRecord->Rcx << std::endl;
   std::cout << "[!] RDX " << std::hex << exceptionInfo->ContextRecord->Rdx << std::endl;
   std::cout << "[!] R8 " << std::hex << exceptionInfo->ContextRecord->R8 << std::endl;
   std::cout << "[!] R9 " << std::hex << exceptionInfo->ContextRecord->R9 << std::endl;
   std::cout << "[!] RSP " << std::hex << exceptionInfo->ContextRecord->Rsp << std::endl;
   std::cout << "[!] Dr0 " << std::hex << exceptionInfo->ContextRecord->Dr0 << std::endl;
  }

  exceptionInfo->ContextRecord->EFlags |= (1 << 16);
  return EXCEPTION_CONTINUE_EXECUTION;

 }
 // Вернем EXCEPTION_CONTINUE_SEARCH, чтобы передать исключение дальше
 return EXCEPTION_CONTINUE_SEARCH;
}

VOID SetHWBP(LPVOID address, BOOL setBP, int regnumber) {
 CONTEXT context = { 0 };
 context.ContextFlags = CONTEXT_DEBUG_REGISTERS;
 GetThreadContext(GetCurrentThread(), &context);
 // Почему-то бряк, если адрес записывать в регистры, отличные от Dr0, не срабатывает
 std::string registerNames[] = { "Dr0", "Dr1", "Dr2", "Dr3" };
 if (setBP) {
  DWORD64* registers[] = { &context.Dr0, &context.Dr1, &context.Dr2, &context.Dr3 };
  if (regnumber >= 0 && regnumber < 4) {
   *registers[regnumber] = (DWORD64)address;
   std::cout << "Writing Address to " << registerNames[regnumber] << std::endl;
  }
  else {
   std::wcout << L"Invalid Registry Number" << std::endl;
   exit(-1);
  }
  // Установка бита 1 в DR7 для активации DR0
  context.Dr7 |= 1;
  // Установка битов 16–17 в DR7 для типа точки останова (Execute)
  context.Dr7 |= (0b00 << 16);
  // Установка битов 18–19 в DR7 для длины точки останова (1 byte)
  context.Dr7 |= (0b00 << 18);
 }
 else {
  context.Dr0 = 0;
  context.Dr1 = 0;
  context.Dr2 = 0;
  context.Dr3 = 0;
  context.Dr7 &= ~(1 << 0);
 }

 context.ContextFlags = CONTEXT_DEBUG_REGISTERS;
 SetThreadContext(GetCurrentThread(), &context);
}
int main()
{

 if (AddVectoredExceptionHandler(1, Handler) == nullptr)
 {
  std::cout << "Failed to set the vectored exception filter!" << std::endl;
  return 1;
 }
 // Адрес функции printf
 void* targetAddress = (void*)printf;
 SetHWBP(targetAddress, TRUE, 0);

 // Генерация сигнала точки останова
 printf("Hello, world!");

 return 0;
}

Здесь был установлен HWBP по адресу функции printf(). Как только система дошла до вызова этой функции, сработало исключение, вызвался обработчик исключений, вывел содержимое регистров, а затем вернул управление на функцию, что привело к появлению в консоли Hello, world!.

Сработавший HWBP
Сработавший HWBP
Теперь мы научились ставить бряки. Как же их использовать для пентестерских целей?

Обход AMSI​

У AMSI есть функция AmsiScanBuffer(), которая служит для сканирования буфера на предмет наличия зловредов. Ничто нам не мешает поставить HWBP на эту функцию, перехватить поток управления и заставить функцию вернуть AMSI_RESULT_CLEAN. Этот метод уже давно известен, и Для просмотра ссылки Войди или Зарегистрируйся лежит на GitHub.

Причем, если нам не подходит реализация на C++, есть на C#, проект называется Для просмотра ссылки Войди или Зарегистрируйся. Он патчит AMSI и ETW, используя хардверные брейк‑пойнты. Для этого вынесен отдельный метод EnableBreakpoint().

Отдельный метод
Отдельный метод
Обрати внимание, что в зависимости от архитектуры используются разные методы. Если у нас х86, то адрес следует конвертировать с помощью метода <addr>.ToInt32(), а когда в х64, то <addr>.ToInt64().

Функция EnableBreakpoint для х86
Функция EnableBreakpoint для х86
Функция EnableBreakpoint для х64
Функция EnableBreakpoint для х64

Извлечение номеров сисколов​

Сисколы позволяют напрямую обратиться к ядру операционной системы, что поможет избежать хуков в user mode. Подробно сисколы я рассматривал в материале «Для просмотра ссылки Войди или Зарегистрируйся». Сейчас же предлагаю обратить внимание на проект Для просмотра ссылки Войди или Зарегистрируйся. Этот проект использует UEF для извлечения номеров сисколов. Сначала просто ставится функция‑обработчик.

SetUnhandledExceptionFilter( OneShotHardwareBreakpointHandler );
Следом идет поиск адреса Nt* функции и установка бряка на адрес syscall этой функции. Для этого точка останова ставится непосредственно на саму инструкцию. Адрес инструкции находится стандартным сканированием памяти на паттерны (а именно на последовательность байтов 0f05).

Код:
LPVOID FindSyscallAddress( LPVOID function )
{
 BYTE stub[] = { 0x0F, 0x05 };
 for( unsigned int i = 0; i < (unsigned int)25; i++ )
 {
  if( memcmp( (LPVOID)((DWORD_PTR)function + i), stub, 2 ) == 0 ) {
   return (LPVOID)((DWORD_PTR)function + i);
  }
 }
 return NULL;
}

Когда адрес найден, на него устанавливается хардверный брейк‑пойнт с помощью следующей функции:

Код:
VOID SetOneshotHardwareBreakpoint( LPVOID address )
{
 CONTEXT context = { 0 };
 context.ContextFlags = CONTEXT_DEBUG_REGISTERS;
 GetThreadContext( GetCurrentThread(), &context );

 context.Dr0 = (DWORD64)address;
 context.Dr6 = 0;
 context.Dr7 = (context.Dr7 & ~(((1 << 2) - 1) << 16)) | (0 << 16);
 context.Dr7 = (context.Dr7 & ~(((1 << 2) - 1) << 18)) | (0 << 18);
 context.Dr7 = (context.Dr7 & ~(((1 << 1) - 1) << 0)) | (1 << 0);

 context.ContextFlags = CONTEXT_DEBUG_REGISTERS;
 SetThreadContext( GetCurrentThread(), &context );

 return;
}

Например, если мы хотим засисколить функцию NtMapViewOfSection(), то сначала генерируем нужный код с помощью файла gen.py.

python gen.py NtMapViewOfSection
Это приведет к созданию трех файлов: TamperingSyscalls.cpp, TamperingSyscalls.h и main.cpp. Включаем файлы в проект, после чего подключаем заголовочный.

#include "TamperingSyscalls.h"
И вызываем нужные функции, просто добавляя p к имени.

pNtMapViewOfSection( section, NtCurrentProcess(), &addr, 0, 0, NULL, &size, 1, 0, PAGE_READONLY );
Внутри сгенерированной функции идет получение адреса NtMapViewOfSection() из ntdll.dll.

Код:
NTSTATUS pNtMapViewOfSection( HANDLE SectionHandle, HANDLE ProcessHandle, PVOID BaseAddress, ULONG ZeroBits, SIZE_T CommitSize, PLARGE_INTEGER SectionOffset, PSIZE_T ViewSize, DWORD InheritDisposition, ULONG AllocationType, ULONG Win32Protect ) {
 LPVOID FunctionAddress;
 NTSTATUS status;
 hash( NtMapViewOfSection );
 FunctionAddress = GetProcAddrExH( hashNtMapViewOfSection, hashNTDLL ); typeNtMapViewOfSection fNtMapViewOfSection;

 pNtMapViewOfSectionArgs.SectionHandle = SectionHandle;
 pNtMapViewOfSectionArgs.ProcessHandle = ProcessHandle;
 pNtMapViewOfSectionArgs.BaseAddress = BaseAddress;
 pNtMapViewOfSectionArgs.ZeroBits = ZeroBits;
 pNtMapViewOfSectionArgs.CommitSize = CommitSize;
 pNtMapViewOfSectionArgs.SectionOffset = SectionOffset;
 pNtMapViewOfSectionArgs.ViewSize = ViewSize;
 pNtMapViewOfSectionArgs.InheritDisposition = InheritDisposition;
 pNtMapViewOfSectionArgs.AllocationType = AllocationType;
 pNtMapViewOfSectionArgs.Win32Protect = Win32Protect;
 fNtMapViewOfSection = (typeNtMapViewOfSection)FunctionAddress;

 EnumState = NTMAPVIEWOFSECTION_ENUM;

 SetOneshotHardwareBreakpoint( FindSyscallAddress( FunctionAddress ) );
 status = fNtMapViewOfSection( NULL, NULL, NULL, NULL, pNtMapViewOfSectionArgs.CommitSize, pNtMapViewOfSectionArgs.SectionOffset, pNtMapViewOfSectionArgs.ViewSize, pNtMapViewOfSectionArgs.InheritDisposition, pNtMapViewOfSectionArgs.AllocationType, pNtMapViewOfSectionArgs.Win32Protect );
 return status;
}

Для получения адреса используется техника API Hashing, которую я демонстрировал в статье «Для просмотра ссылки Войди или Зарегистрируйся». Хеш считается с помощью одноименного макроса.

#define hash( VAL ) constexpr auto CONCAT( hash, VAL ) = HASHALGO( TOKENIZE( VAL ) );
Далее инициализируются все элементы структуры NtMapViewOfSectionArgs:

Код:
typedef struct {
 HANDLE      SectionHandle;
 HANDLE      ProcessHandle;
 PVOID       BaseAddress;
 ULONG       ZeroBits;
 SIZE_T      CommitSize;
 PLARGE_INTEGER    SectionOffset;
 PSIZE_T     ViewSize;
 DWORD  InheritDisposition;
 ULONG       AllocationType;
 ULONG       Win32Protect;
} NtMapViewOfSectionArgs;

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

Затем устанавливается специальное значение EnumState, благодаря которому отладчик сможет понять, бряк на какую функцию сработал. Наконец, происходит непосредственно вызов Nt-функции, адрес которой был получен ранее.

Код:
status = fNtMapViewOfSection( NULL, NULL, NULL, NULL, pNtMapViewOfSectionArgs.CommitSize, pNtMapViewOfSectionArgs.SectionOffset, pNtMapViewOfSectionArgs.ViewSize, pNtMapViewOfSectionArgs.InheritDisposition, pNtMapViewOfSectionArgs.AllocationType, pNtMapViewOfSectionArgs.Win32Protect );

Обрати внимание, что первыми четырьмя параметрами мы передаем NULL. Именно эти параметры, скорее всего, будет анализировать антивирус до выполнения инструкции syscall. Но мы на их место передаем NULL, что сбивает антивирус с толку: он не может принять решение, легитимный это вызов или нет, в результате чего пропускает вызов. Фактически таким образом мы обошли потенциальный хук.

Зачем нужны NULL-параметры
Зачем нужны NULL-параметры
Восстановление параметров происходит как раз таки в функции‑обработчике HWBP.

Код:
LONG WINAPI OneShotHardwareBreakpointHandler( PEXCEPTION_POINTERS ExceptionInfo )
{
 if( ExceptionInfo->ExceptionRecord->ExceptionCode == STATUS_SINGLE_STEP )
 {
  if( ExceptionInfo->ContextRecord->Dr7 & 1 ) {
   // If the ExceptionInfo->ContextRecord->Rip == ExceptionInfo->ContextRecord->Dr0
   // Then we are at the one shot breakpoint address
   // ExceptionInfo->ContextRecord->Rax should hold the syscall number
   PRINT( "Syscall : 0x%x\n", ExceptionInfo->ContextRecord->Rax );
   if( ExceptionInfo->ContextRecord->Rip == ExceptionInfo->ContextRecord->Dr0 ) {
 ExceptionInfo->ContextRecord->Dr0 = 0;

 // You need to fix your arguments in the right registers and stack here
 switch( EnumState ) {
  // RCX moved into R10!!! Kudos to @anthonyprintup for catching this
 case NTMAPVIEWOFSECTION_ENUM:
  ExceptionInfo->ContextRecord->R10 =
   (DWORD_PTR)((NtMapViewOfSectionArgs*)(StateArray[EnumState].arguments))->SectionHandle;

  ExceptionInfo->ContextRecord->Rdx =
   (DWORD_PTR)((NtMapViewOfSectionArgs*)(StateArray[EnumState].arguments))->ProcessHandle;

  ExceptionInfo->ContextRecord->R8 =
   (DWORD_PTR)((NtMapViewOfSectionArgs*)(StateArray[EnumState].arguments))->BaseAddress;

  ExceptionInfo->ContextRecord->R9 =
   (DWORD_PTR)((NtMapViewOfSectionArgs*)(StateArray[EnumState].arguments))->ZeroBits;

  break;

 case NTUNMAPVIEWOFSECTION_ENUM:
  ExceptionInfo->ContextRecord->R10 =
   (DWORD_PTR)((NtUnmapViewOfSectionArgs*)(StateArray[EnumState].arguments))->ProcessHandle;

  ExceptionInfo->ContextRecord->Rdx =
   (DWORD_PTR)((NtUnmapViewOfSectionArgs*)(StateArray[EnumState].arguments))->BaseAddress;

  break;

 case NTOPENSECTION_ENUM:
  ExceptionInfo->ContextRecord->R10 =
   (DWORD_PTR)((NtOpenSectionArgs*)(StateArray[EnumState].arguments))->SectionHandle;

  ExceptionInfo->ContextRecord->Rdx =
   (DWORD_PTR)((NtOpenSectionArgs*)(StateArray[EnumState].arguments))->DesiredAccess;

  ExceptionInfo->ContextRecord->R8 =
   (DWORD_PTR)((NtOpenSectionArgs*)(StateArray[EnumState].arguments))->ObjectAttributes;

  break;

  // You have messed up by not providing the indexed state
 default:
  ExceptionInfo->ContextRecord->Rip += 1; // Just so we don’t hang
  break;
 }
 return EXCEPTION_CONTINUE_EXECUTION;
   }
  }
 }
 return EXCEPTION_CONTINUE_SEARCH;
}

Сначала эта функция проверяет, что исключение действительно хардверное. Для этого происходит сравнение со значением STATUS_SINGLE_STEP, об этой проверке я рассказывал выше.

Далее система проверяет, что хардверный брейк‑пойнт был включен (должно быть значение 1 у Dp7). Следующим шагом из регистра Rax извлекается номер сискола, который был туда занесен. После чего проверяем, что адрес следующей выполняемой инструкции (syscall) действительно лежит в Dp0, то есть установленный на нее бряк действительно сработал. После чего HWBP снимается и начинается проверка значения EnumState, которое инициализировали ранее. Это нужно для корректной инициализации всех параметров функции. Если все прошло успешно, срабатывает бряк, а за ним — EXCEPTION_CONTINUE_EXECUTION, что приводит к выполнению сискола с нужными нам параметрами. Фактически мы «пробрасываем параметры» в обход EDR.

Похожая техника применяется и в Для просмотра ссылки Войди или Зарегистрируйся, есть даже отдельный Для просмотра ссылки Войди или Зарегистрируйся.

Анхукинг​

Хардверные бряки — очень полезная штука! Их можно использовать и для снятия хуков в юзермоде. Для этого нужно предварительно создать дочерний процесс, став для него дебаггером. После этого установить HWBP на функцию LdrLoadDll(). Эта функция вызывается при загрузке DLL в процесс, на ее основе я написал собственную LoadLibrary() — подробнее в статье «Для просмотра ссылки Войди или Зарегистрируйся».

Далее остается лишь проверять имена загружаемых DLL, разрешая подгрузку только ntdll.dll. Убедившись, что отлаживаемый процесс загрузил ntdll.dll, мы копируем ее содержимое в свой собственный процесс и перезаписываем ntdll.dll нашего текущего процесса, что и приводит к анхукингу. Техника называется BlindSide, существует готовый Для просмотра ссылки Войди или Зарегистрируйся.

Алгоритм снятия хуков
Алгоритм снятия хуков

Пишем кастомный GetThreadContext()​

Мне кажется, мы недостаточно хорошо прошлись по параметрам, которые прилетают в функцию‑обработчик. Что ж, исправляюсь. Функция‑обработчик принимает структуру Для просмотра ссылки Войди или Зарегистрируйся, содержащую информацию о значении всех регистров потока. Но проблема в том, что регистры х64 отличаются от регистров х86, поэтому есть также структура Для просмотра ссылки Войди или Зарегистрируйся, где хранятся значения регистров у программ для x86.

Мы можем использовать эти данные для получения структуры CONTEXT, чтобы избежать подозрительной функции GetThreadContext(). Вот пример кода.

Код:
#include <iostream>
#include <Windows.h>

#ifdef _WIN64
void ShowContext(CONTEXT ctx) {
 std::cout << "Context values:" << std::endl;
 std::cout << "P1Home: " << ctx.P1Home << std::endl;
 std::cout << "P2Home: " << ctx.P2Home << std::endl;
 std::cout << "P3Home: " << ctx.P3Home << std::endl;
 std::cout << "P4Home: " << ctx.P4Home << std::endl;
 std::cout << "P5Home: " << ctx.P5Home << std::endl;
 std::cout << "P6Home: " << ctx.P6Home << std::endl;
 std::cout << "ContextFlags: " << ctx.ContextFlags << std::endl;
 std::cout << "MxCsr: " << ctx.MxCsr << std::endl;
 std::cout << "SegCs: " << ctx.SegCs << std::endl;
 std::cout << "SegDs: " << ctx.SegDs << std::endl;
 std::cout << "SegEs: " << ctx.SegEs << std::endl;
 std::cout << "SegFs: " << ctx.SegFs << std::endl;
 std::cout << "SegGs: " << ctx.SegGs << std::endl;
 std::cout << "SegSs: " << ctx.SegSs << std::endl;
 std::cout << "EFlags: " << ctx.EFlags << std::endl;
 std::cout << "Dr0: " << ctx.Dr0 << std::endl;
 std::cout << "Dr1: " << ctx.Dr1 << std::endl;
 std::cout << "Dr2: " << ctx.Dr2 << std::endl;
 std::cout << "Dr3: " << ctx.Dr3 << std::endl;
 std::cout << "Dr6: " << ctx.Dr6 << std::endl;
 std::cout << "Dr7: " << ctx.Dr7 << std::endl;
 std::cout << "Rax: " << ctx.Rax << std::endl;
 std::cout << "Rcx: " << ctx.Rcx << std::endl;
 std::cout << "Rdx: " << ctx.Rdx << std::endl;
 std::cout << "Rbx: " << ctx.Rbx << std::endl;
 std::cout << "Rsp: " << ctx.Rsp << std::endl;
 std::cout << "Rbp: " << ctx.Rbp << std::endl;
 std::cout << "Rsi: " << ctx.Rsi << std::endl;
 std::cout << "Rdi: " << ctx.Rdi << std::endl;
 std::cout << "R8: " << ctx.R8 << std::endl;
 std::cout << "R9: " << ctx.R9 << std::endl;
 std::cout << "R10: " << ctx.R10 << std::endl;
 std::cout << "R11: " << ctx.R11 << std::endl;
 std::cout << "R12: " << ctx.R12 << std::endl;
 std::cout << "R13: " << ctx.R13 << std::endl;
 std::cout << "R14: " << ctx.R14 << std::endl;
 std::cout << "R15: " << ctx.R15 << std::endl;
 std::cout << "Rip: " << ctx.Rip << std::endl;
}
#endif
void ShowContext32(CONTEXT ctx) {
 std::cout << "Context values:" << std::endl;
 std::cout << "ContextFlags: " << ctx.ContextFlags << std::endl;
 std::cout << "Dr0: " << ctx.Dr0 << std::endl;
 std::cout << "Dr1: " << ctx.Dr1 << std::endl;
 std::cout << "Dr2: " << ctx.Dr2 << std::endl;
 std::cout << "Dr3: " << ctx.Dr3 << std::endl;
 std::cout << "Dr6: " << ctx.Dr6 << std::endl;
 std::cout << "Dr7: " << ctx.Dr7 << std::endl;
 std::cout << "SegGs: " << ctx.SegGs << std::endl;
 std::cout << "SegFs: " << ctx.SegFs << std::endl;
 std::cout << "SegEs: " << ctx.SegEs << std::endl;
 std::cout << "SegDs: " << ctx.SegDs << std::endl;
 std::cout << "Edi: " << ctx.Edi << std::endl;
 std::cout << "Esi: " << ctx.Esi << std::endl;
 std::cout << "Ebx: " << ctx.Ebx << std::endl;
 std::cout << "Edx: " << ctx.Edx << std::endl;
 std::cout << "Ecx: " << ctx.Ecx << std::endl;
 std::cout << "Eax: " << ctx.Eax << std::endl;
 std::cout << "Ebp: " << ctx.Ebp << std::endl;
 std::cout << "Eip: " << ctx.Eip << std::endl;
 std::cout << "SegCs: " << ctx.SegCs << std::endl;
 std::cout << "EFlags: " << ctx.EFlags << std::endl;
 std::cout << "Esp: " << ctx.Esp << std::endl;
 std::cout << "SegSs: " << ctx.SegSs << std::endl;
}

LONG WINAPI Handler(PEXCEPTION_POINTERS ExceptionInfo) {
 std::cout << "[+] GetThreadContext Result:" << std::endl;
 static int value = 0;
 value += 1;
#ifdef _WIN64
 PCONTEXT ctx = ExceptionInfo->ContextRecord;
 ShowContext(*ctx);
#else
 PCONTEXT ctx = ExceptionInfo->ContextRecord;
 ShowContext32(*ctx);
#endif

 if (value == 2) {
  value = 0;
  return EXCEPTION_CONTINUE_SEARCH;
 }
 else {
  return EXCEPTION_CONTINUE_EXECUTION;
 }
}

void CustomGetThreadContext() {
 AddVectoredExceptionHandler(1, Handler);

 try {
  throw "exception";
 }
 catch (...) {
  RemoveVectoredExceptionHandler(Handler);
  return;
 }
}

int main() {
 CustomGetThreadContext();
 return 0;
}

Ставим хуки​

Наконец, самое сочное — установка хуков. Алгоритм не отличается ровным счетом ничем. Умельцы давно реализовали в виде милой DLL-библиотеки, мне остается лишь приложить Для просмотра ссылки Войди или Зарегистрируйся.

Выводы​

Использование аппаратных точек останова в пентестерских целях — очень необычное и смелое решение, которое встречается редко. Самое главное — грамотно обрабатывать исключения, которые ты сам себе делаешь. Ведь если что‑то забудешь обработать, то твоя программа упадет и придется все запускать заново.
 
Activity
So far there's no one here
Сверху Снизу