Перевод:
Вы должны зарегистрироваться, чтобы увидеть внешние ссылки
Мотивация
Учитывая, что встроенные хуки являются основным методом обнаружения, используемым продуктами EDR, обход их является для меня интересной темой.Что касается обхода встроенных хуков, установленных EDR, уже существует довольно много доступных подходов. Хотя некоторые из ранних подходов очень легко обнаруживаются, существуют и несколько зрелых подходов.
Тем не менее, я считаю, что было бы очень интересно найти новый подход к обходу хуков, надеюсь, это принесет некоторые улучшения или преимущества.
Встроенный хук
Продукты EDR (Endpoint Detection and Response) часто размещают встроенные хуки на NTAPI, которые обычно используются в вредоносном ПО, таких как NtAllocateVirtualMemory, NtUnmapViewOfSection, NtWriteVirtualMemory и других. Это связано с тем, что NTAPI является мостом между пользовательским пространством и пространством ядра.Например, NtAllocateVirtualMemory является версией NTAPI функции VirtualAlloc. Размещая безусловную команду перехода в NTAPI, независимо от того, вызывает ли программа Win32 API или NTAPI, EDR способен проверить вызов и далее определить его намерение.
Следующие скриншоты показывают, как выглядит хуки на NTAPI:
А если на NTAPI нет хука, мы можем заметить очень последовательный шаблон, который показывает, как выглядит заглушка системного вызова:
Хотя EDR имеет несколько уровней обнаружения, встроенный хук является одним из основных.
Аппаратная точка останова
В моей статье "Обход AMSI на Windows 11" (
Вы должны зарегистрироваться, чтобы увидеть внешние ссылки
) я обнаружил, что если R8 равен 0 при вызове AmsiScanBuffer, AMSI можно обойти. Однако я отметил, что использовать этот обход без WinDBG непросто.На самом деле, это возможно, используя аппаратную точку останова. В этой статье (
Вы должны зарегистрироваться, чтобы увидеть внешние ссылки
) объясняется, как использовать аппаратную точку останова для обхода AMSI без патчей. Это достигается путем установки RAX в 0, изменения аргумента результата сканирования AMSI и установки RIP как адреса возврата, когда выполнение передается функции AmsiScanBuffer. В нашем случае, нам просто нужно установить R8 в 0.
Я не буду здесь подробно объяснять основные знания о аппаратной точке останова и VEH, так как вы можете ознакомиться с ними в статье, а общая идея более важна.
Также на форуме есть статья:Уроки - Обход AMSI при помощи хардварных точек останова
Некоторые из существующих подходов
Следующие подходы могут быть использованы для обхода встроенного хука. Учитывая, что уже существует множество статей, подробно описывающих их, давайте кратко рассмотрим некоторые из подходов, которые могут обойти встроенный хук.Прямой системный вызов
Каждая Nt-версия Win32 API, такая как NtAllocateVirtualMemory, требует только 4 инструкции для работы, этот набор инструкций называется заглушкой системного вызова (syscall stub). Единственное различие между различными NTAPI - это значение их номера системного вызова.
Код:
mov r10, rcx
mov rax, [SSN]
syscall
ret
Вот пример реализации этого подхода:
В файле .asm определите заглушку системного вызова для NtAllocateVirtualMemory:
Код:
.code
<...Other Stubs...>
NtAllocateVirtualMemory PROC
mov r10, rcx
mov eax, 18h
syscall
ret
NtAllocateVirtualMemory ENDP
<...Other Stubs...>
end
Внутри файла заголовка или c/cpp файла используйте макрос EXTERN_C для связывания определения функции с ассемблерным кодом заглушки системного вызова, имя должно быть одинаковым.
C:
EXTERN_C NTSTATUS NtAllocateVirtualMemory(
IN HANDLE ProcessHandle,
IN OUT PVOID * BaseAddress,
IN ULONG ZeroBits,
IN OUT PSIZE_T RegionSize,
IN ULONG AllocationType,
IN ULONG Protect);
Таким образом, мы можем напрямую инициировать системный вызов, вызывая определенную функцию. Однако этот подход имеет подозрительные индикаторы компрометации (IOCs):
Жестко закодированная заглушка системного вызова может быть обнаружена, ее шаблон: 4c8bd1 c7c0<DWORD> 0f05c3.
Возможное правило Yara для обнаружения прямого системного вызова выглядит следующим образом:
Код:
rule direct_syscall
{
meta:
description = "Hunt for direct syscall"
strings:
$s1 = {4c 8b d1 48 c7 c0 ?? ?? ?? ?? 0f 05 c3}
$s2 = {4C 8b d1 b8 ?? ?? ?? ?? 0F 05 C3}
condition:
#s1 >=1 or #s2 >=1
}
Хотя обойти этот шаблон тривиально, вставив некоторые инструкции, подобные NOP.
У этого подхода есть недостаток: мы жестко закодируем номер системного вызова в исходном коде. Это плохо работает, когда в целевой организации используется несколько версий операционной системы, потому что SSN (System Service Numbers) различаются в разных версиях ОС.
Набор инструментов Syswhisper решает эту проблему: Syswhisper 1 обнаруживает версию ОС хоста и выбирает правильный SSN. Syswhisper 2 динамически получает SSN во время выполнения. В любом случае, прямой системный вызов используется в этих подходах.
Без пользовательской модификации заглушки системного вызова Syswhisper2 возможное правило Yara для обнаружения выглядит следующим образом:
Код:
rule syswhisper2
{
meta:
description = "Hunt for syswhisper2 generated asm code"
strings:
$s1 = {58 48 89 4C 24 08 48 89 54 24 10 4C 89 44 24 18 4C 89 4C 24 20 48 83 EC 28 8B 0D ?? ?? 00 00 E8 ?? ?? ?? ?? 48 83 C4 28 48 8B 4C 24 08 48 8B 54 24 10 4C 8B 44 24 18 4C 8B 4C 24 20 4C 8B D1 0F 05 C3}
condition:
#s1 >=1
}
Помимо шаблона последовательностей байтов заглушки системного вызова, выполнение инструкции syscall является ненормальным для легитимной программы, т.е. системный вызов должен инициироваться внутри области памяти ntdll.dll.
Например, при вызове Win32 API SleepEx в программе на C, мы можем заметить стек вызовов следующим образом: sleep!main -> kernelbase!SleepEx -> ntdll!NtDelayExecution -> ntoskrnl!KeDelayExecutionThread.
Однако, если мы инициируем системный вызов напрямую, стек вызовов будет следующим: sleep!main -> sleep!NtDelayExecution -> ntoskrnl!KeDelayExecutionThread.
Легко обнаружить, что инструкция системного вызова инициируется в программе, а не в модуле ntdll.
Косвенный системный вызов
Мы обсудили недостатки прямого системного вызова, поэтому мы хотим избежать выполнения системного вызова напрямую. Косвенный системный вызов является улучшением. Шаблон заглушки системного вызова очень похож на шаблон прямого системного вызова, однако, вместо непосредственного выполнения инструкции системного вызова, заглушка косвенного системного вызова использует безусловный переход для передачи выполнения по адресу инструкции системного вызова.
C:
NtAllocateVirtualMemory PROC
mov r10, rcx
mov eax, (ssn of NtAllocateVirtualMemory)
jmp (address of a syscall instruction)
ret
NtAllocateVirtualMemory ENDP
Конечно, нам нужно получить действительный адрес для инструкции системного вызова. Предполагая, что NTAPI не захуклен, мы можем получить номер системного вызова по смещению 0x4 от начала функции, а инструкция системного вызова находится по смещению 0x12.
Однако это приводит нас к проблеме "что было первым: курица или яйцо". Если NTAPI захуклен, то его заглушка системного вызова не будет соответствовать шаблону заглушки системного вызова, и, естественно, мы не сможем успешно извлечь SSN и адрес инструкции системного вызова.
К счастью, учитывая, что системный вызов (syscall) является особым типом инструкции вызова, который не указывает напрямую адрес для перехода, а определяет адрес в пространстве ядра на основе SSN, все, что нам нужно сделать, это указать правильный SSN и соответствующие аргументы функции.
Поэтому нам не обязательно получать адрес инструкции системного вызова в функции NtAllocateVirtualMemory. Мы можем выбрать незахукленный NTAPI, тот, который обычно не используется во вредоносном ПО, например, NtDrawText.
Хотя косвенные системные вызовы улучшили уклонение, продукты безопасности все еще могут обнаружить их на основе некоторых индикаторов компрометации (IOCs):
Если использовать Syswhisper3 без пользовательских модификаций, хотя жестко закодированных байтов заглушки системного вызова меньше, все равно возможно найти шаблон последовательности байтов: 4c8bd1 41ff<DWORD> c3
Возможное правило Yara для обнаружения выглядит следующим образом:
Код:
rule syswhisper3
{
meta:
description = "Hunt for syswhispe3 generated asm code"
strings:
$s1 = {48 89 4c 24 08 48 89 54 24 10 4c 89 44 24 18 4c 89 4c 24 20 48 83 ec 28 b9 ?? ?? ?? ?? e8}
$s2 = {48 83 c4 28 48 8b 4c 24 08 48 8b 54 24 10 4c 8b 44 24 18 4c 8b 4c 24 20 4c 8b d1}
condition:
#s1 >=1 or #s2 >=1
}
Кроме того, хотя стек вызовов выглядит более легитимным, правило обнаружения может основываться на том факте, что адрес возврата находится в функции NtDrawText, в то время как выполненный системный вызов — это ntoskrnl!NtAllocateVirtualMemory.
Перезапись текстового сегмента загруженного NTDLL
EDR перехватывает некоторые NTAPI, перезаписывая код в текстовом сегменте модуля ntdll. Поэтому, чтобы восстановить перехваченные функции, мы можем перезаписать текстовый сегмент загруженного ntdll.Для достижения этого необходимо выполнить несколько шагов:
- Прочитать свежую копию ntdll. Мы можем прочитать её с диска, через Интернет, из директории KnownDLL и т.д.
- Изменить разрешение страницы с RX на RWX, так как текстовый сегмент по умолчанию не доступен для записи. Мы также можем использовать WriteProcessMemory или его NTAPI для перезаписи перехваченного кода.
- Скопировать текстовый сегмент из свежей копии в загруженный модуль.
- Восстановить разрешение страницы.
- EDR может обнаружить, что хук был изменен, проверяя целостность загруженного модуля NTDLL.
- Мы используем перехваченные функции для выполнения вышеуказанных действий, что может вызвать срабатывание предупреждений.
- Область памяти с разрешением RWX является серьезным сигналом тревоги.
Перезапись перехваченных функций
Вместо перезаписи всего текстового сегмента, мы можем выбрать перезапись необходимых функций, как при патчинге AmsiScanBuffer. Хотя это влечет за собой меньше изменений в загруженном модуле ntdll, индикаторы компрометации перезаписи текстового сегмента NTDLL все равно применимы к этому подходу.Существуют и другие подходы к обходу встроенного хука, но я не буду рассматривать их все. Эта статья (
Вы должны зарегистрироваться, чтобы увидеть внешние ссылки
) отлично объясняет общие подходы.MutationGate
MutationGate — это вариант косвенного системного вызова, новый подход к обходу встроенного хука EDR, использующий аппаратную точку останова для перенаправления системного вызова.MutationGate работает путем вызова незахукленного NTAPI и замены SSN незахукленного NTAPI на SSN захукленного NTAPI.
Таким образом, системный вызов перенаправляется на захукленный NTAPI, и встроенный хук можно обойти без загрузки второго модуля ntdll или изменения байтов в пространстве памяти загруженного ntdll.
Репозиторий на GitHub:
Вы должны зарегистрироваться, чтобы увидеть внешние ссылки
.Как мы обсуждали ранее, EDR склонны устанавливать встроенные хуки для различных NTAPI, особенно тех, которые обычно используются в вредоносном ПО, таких как NtAllocateVirtualMemory. В то время как другие NTAPI, которые обычно не используются в вредоносном ПО, такие как NtDrawText, скорее всего, не имеют встроенных хуков. Маловероятно, что EDR установит встроенные хуки для всех NTAPI.
Предположим, что NTAPI NtDrawText не захуклен, в то время как NTAPI NtQueryInformationProcess захуклен.
Шаги следующие:
- Получить адрес NtDrawText. Это можно сделать, используя комбинацию GetModuleHandle и GetProcAddress или их пользовательскую реализацию через обход PEB.
C:
pNTDT = GetFuncByHash(ntdll, 0xA1920265); //NtDrawText hash
pNTDTOffset_8 = (PVOID)((BYTE*)pNTDT + 0x8); //Offset 0x8 from NtDrawText
- Подготовьте аргументы для NtQueryInformationProcess.
- Установите аппаратную точку останова на адресе NtDrawText+0x8. Когда выполнение достигнет этого адреса, SSN NtDrawText будет сохранен в регистре RAX, но системный вызов еще не будет инициирован.
C:
0:000> u 0x00007FFBAD00EB68-8
ntdll!NtDrawText:
00007ffb`ad00eb60 4c8bd1 mov r10,rcx
00007ffb`ad00eb63 b8dd000000 mov eax,0DDh
00007ffb`ad00eb68 f604250803fe7f01 test byte ptr [SharedUserData+0x308 (00000000`7ffe0308)],1
00007ffb`ad00eb70 7503 jne ntdll!NtDrawText+0x15 (00007ffb`ad00eb75)
00007ffb`ad00eb72 0f05 syscall
00007ffb`ad00eb74 c3 ret
00007ffb`ad00eb75 cd2e int 2Eh
00007ffb`ad00eb77 c3 ret
- Получите SSN NtQueryInformationProcess. Внутри обработчика исключений обновите регистр RAX значением SSN NtQueryInformationProcess. То есть, оригинальный SSN был заменен.
Код:
...<SNIP>...
uint32_t GetSSNByHash(PVOID pe, uint32_t Hash)
{
PBYTE pBase = (PBYTE)pe;
PIMAGE_DOS_HEADER pImgDosHdr = (PIMAGE_DOS_HEADER)pBase;
PIMAGE_NT_HEADERS pImgNtHdrs = (PIMAGE_NT_HEADERS)(pBase + pImgDosHdr->e_lfanew);
IMAGE_OPTIONAL_HEADER ImgOptHdr = pImgNtHdrs->OptionalHeader;
DWORD exportdirectory_foa = RvaToFileOffset(pImgNtHdrs, ImgOptHdr.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT].VirtualAddress);
PIMAGE_EXPORT_DIRECTORY pImgExportDir = (PIMAGE_EXPORT_DIRECTORY)(pBase + exportdirectory_foa); //Calculate corresponding offset
PDWORD FunctionNameArray = (PDWORD)(pBase + RvaToFileOffset(pImgNtHdrs, pImgExportDir->AddressOfNames));
PDWORD FunctionAddressArray = (PDWORD)(pBase + RvaToFileOffset(pImgNtHdrs, pImgExportDir->AddressOfFunctions));
PWORD FunctionOrdinalArray = (PWORD)(pBase + RvaToFileOffset(pImgNtHdrs, pImgExportDir->AddressOfNameOrdinals));
for (DWORD i = 0; i < pImgExportDir->NumberOfFunctions; i++)
{
CHAR* pFunctionName = (CHAR*)(pBase + RvaToFileOffset(pImgNtHdrs, FunctionNameArray[i]));
DWORD Function_RVA = FunctionAddressArray[FunctionOrdinalArray[i]];
if (Hash == ROR13Hash(pFunctionName))
{
void *ptr = malloc(10);
if (ptr == NULL) {
perror("malloc failed");
return -1;
}
unsigned char byteAtOffset5 = *((unsigned char*)(pBase + RvaToFileOffset(pImgNtHdrs, Function_RVA)) + 4);
//printf("Syscall number of function %s is: 0x%x\n", pFunctionName,byteAtOffset5); //0x18
free(ptr);
return byteAtOffset5;
}
}
return 0x0;
}
...<SNIP>...
Поскольку мы вызвали NtDrawText, но с аргументами NtQueryInformationProcess, вызов должен был бы завершиться неудачей. Однако, так как мы изменили SSN, системный вызов выполняется успешно.
C:
fnNtQueryInformationProcess pNTQIP = (fnNtQueryInformationProcess)pNTDT;
NTSTATUS status = pNTQIP(pi.hProcess, ProcessBasicInformation, &pbi, sizeof(PROCESS_BASIC_INFORMATION), NULL);
В этом примере, SSN NtDrawText равен 0xdd, SSN NtQueryInformationProcess равен 0x19, адрес NtDrawText равен 0x00007FFBAD00EB60.
Вызов производится по адресу NtDrawText, но с аргументами NtQueryInformationProcess. Поскольку SSN изменен с 0xdd на 0x19, системный вызов выполняется успешно.
Давайте изменим код и снова поэкспериментируем с NtDelayExecution, учитывая, что нам будет легче наблюдать стек вызовов. Как и ожидалось, эти правила Yara, которые мы использовали ранее, не могут обнаружить никакой шаблон последовательности байтов.
Проверьте стек вызовов: системный вызов инициируется из пространства памяти ntdll, что выглядит легитимным с этой точки зрения. Однако KeDelayExecutionThread ожидает NtDelayExecution в качестве соответствующего NTAPI, а не NtDrawText. Эта подсказка может быть использована как правило для обнаружения.
Преимущества и обнаружение
MutationGate имеет свои преимущества, но его также возможно обнаружить. Если у вас есть другие идеи о преимуществах и способах обнаружения, пожалуйста, сообщите мне : )Преимущества
- Не загружается второй модуль ntdll
- Нет изменений в загруженном модуле ntdll
- Отсутствие пользовательской заглушки системного вызова и шаблона последовательности байтов
- Системный вызов инициируется в модуле ntdll, что выглядит легитимно
Возможные способы обнаружения
- Вызов AddVectoredExceptionHandler может выглядеть подозрительно в обычной программе
- Функция в ntoskrnl.exe не соответствует функции в модуле ntdll
- Инициированный системный вызов в безвредном NTAPI не должен ожидать SSN другого NTAPI
Сравнение с другими похожими подходами
HWSyscall (
Вы должны зарегистрироваться, чтобы увидеть внешние ссылки
) и TamperingSyscall (
Вы должны зарегистрироваться, чтобы увидеть внешние ссылки
) оба изобретательно используют аппаратные точки останова для обхода встроенных хуков, и оба этих подхода отличные. Хотя я не читал и не ссылался на эти два проекта во время разработки и выпуска MutationGate (после выпуска MutationGate, друг прислал мне ссылки на эти два проекта), действительно есть некоторые похожие техники или общие идеи. Я внимательно прочитал и исследовал их, и я составил таблицу для сравнения, как показано ниже.
Подход | Вызов | Аргументы | SSN | Системный вызов |
---|---|---|---|---|
MutationGate | Безвредный NTAPI | Аргументы целевого NTAPI | SSN безвредного NTAPI -> SSN целевого NTAPI | В безвредном NTAPI |
HWSyscall | Целевой NTAPI | Аргументы целевого NTAPI | SSN целевого NTAPI после его получения | В ближайшем незахваченном NTAPI |
TamperingSyscall | Целевой NTAPI | Подставные -> Аргументы целевого NTAPI | SSN целевого NTAPI после прохождения проверки EDR | В целевом NTAPI |
Косвенный системный вызов | Пользовательская ASM функция | Аргументы целевого NTAPI | SSN целевого NTAPI после его получения | В любом незахваченном NTAPI |
Благодарности и ссылки
В период после того, как я был вдохновлен, разработал и выпустил MutationGate, следующие ресурсы были очень полезны для меня, и я благодарен их авторам.
Вы должны зарегистрироваться, чтобы увидеть внешние ссылки
Вы должны зарегистрироваться, чтобы увидеть внешние ссылки
Вы должны зарегистрироваться, чтобы увидеть внешние ссылки
Вы должны зарегистрироваться, чтобы увидеть внешние ссылки
Вы должны зарегистрироваться, чтобы увидеть внешние ссылки
Вы должны зарегистрироваться, чтобы увидеть внешние ссылки
Вы должны зарегистрироваться, чтобы увидеть внешние ссылки
Вы должны зарегистрироваться, чтобы увидеть внешние ссылки
Вы должны зарегистрироваться, чтобы увидеть внешние ссылки
Вы должны зарегистрироваться, чтобы увидеть внешние ссылки
Вы должны зарегистрироваться, чтобы увидеть внешние ссылки
Вы должны зарегистрироваться, чтобы увидеть внешние ссылки
Вы должны зарегистрироваться, чтобы увидеть внешние ссылки