Малварь как искусство MutationGate - Новый подход работы с сисколами


X-Shar

:)
Администрация
Регистрация
03.06.2012
Сообщения
6 125
Репутация
8 243
1716538434687.png


Перевод:

Мотивация​

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

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

Встроенный хук​

Продукты EDR (Endpoint Detection and Response) часто размещают встроенные хуки на NTAPI, которые обычно используются в вредоносном ПО, таких как NtAllocateVirtualMemory, NtUnmapViewOfSection, NtWriteVirtualMemory и других. Это связано с тем, что NTAPI является мостом между пользовательским пространством и пространством ядра.

1716538568671.png


Например, NtAllocateVirtualMemory является версией NTAPI функции VirtualAlloc. Размещая безусловную команду перехода в NTAPI, независимо от того, вызывает ли программа Win32 API или NTAPI, EDR способен проверить вызов и далее определить его намерение.

1716538623737.png


Следующие скриншоты показывают, как выглядит хуки на NTAPI:

1716538708030.png


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

1716538846460.png


Хотя EDR имеет несколько уровней обнаружения, встроенный хук является одним из основных.

Аппаратная точка останова​

В моей статье "Обход AMSI на Windows 11" ( ) я обнаружил, что если R8 равен 0 при вызове AmsiScanBuffer, AMSI можно обойти. Однако я отметил, что использовать этот обход без WinDBG непросто.

1716538962408.png


На самом деле, это возможно, используя аппаратную точку останова. В этой статье ( ) объясняется, как использовать аппаратную точку останова для обхода 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

1716539172493.png


1716539191221.png


Вот пример реализации этого подхода:

В файле .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
}

1716539501674.png


Хотя обойти этот шаблон тривиально, вставив некоторые инструкции, подобные 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
}

1716539669524.png


Помимо шаблона последовательностей байтов заглушки системного вызова, выполнение инструкции syscall является ненормальным для легитимной программы, т.е. системный вызов должен инициироваться внутри области памяти ntdll.dll.

Например, при вызове Win32 API SleepEx в программе на C, мы можем заметить стек вызовов следующим образом: sleep!main -> kernelbase!SleepEx -> ntdll!NtDelayExecution -> ntoskrnl!KeDelayExecutionThread.

1716539743951.png


Однако, если мы инициируем системный вызов напрямую, стек вызовов будет следующим: sleep!main -> sleep!NtDelayExecution -> ntoskrnl!KeDelayExecutionThread.

1716539784035.png


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

1716539846415.png


Косвенный системный вызов​

Мы обсудили недостатки прямого системного вызова, поэтому мы хотим избежать выполнения системного вызова напрямую. Косвенный системный вызов является улучшением. Шаблон заглушки системного вызова очень похож на шаблон прямого системного вызова, однако, вместо непосредственного выполнения инструкции системного вызова, заглушка косвенного системного вызова использует безусловный переход для передачи выполнения по адресу инструкции системного вызова.

C:
NtAllocateVirtualMemory PROC
    mov r10, rcx
    mov eax, (ssn of NtAllocateVirtualMemory)
    jmp (address of a syscall instruction)
    ret
NtAllocateVirtualMemory ENDP

Конечно, нам нужно получить действительный адрес для инструкции системного вызова. Предполагая, что NTAPI не захуклен, мы можем получить номер системного вызова по смещению 0x4 от начала функции, а инструкция системного вызова находится по смещению 0x12.

1716540066802.png


Однако это приводит нас к проблеме "что было первым: курица или яйцо". Если NTAPI захуклен, то его заглушка системного вызова не будет соответствовать шаблону заглушки системного вызова, и, естественно, мы не сможем успешно извлечь SSN и адрес инструкции системного вызова.

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

Поэтому нам не обязательно получать адрес инструкции системного вызова в функции NtAllocateVirtualMemory. Мы можем выбрать незахукленный NTAPI, тот, который обычно не используется во вредоносном ПО, например, NtDrawText.

1716540159771.png


Хотя косвенные системные вызовы улучшили уклонение, продукты безопасности все еще могут обнаружить их на основе некоторых индикаторов компрометации (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
}

1716540502335.png


Кроме того, хотя стек вызовов выглядит более легитимным, правило обнаружения может основываться на том факте, что адрес возврата находится в функции NtDrawText, в то время как выполненный системный вызов — это ntoskrnl!NtAllocateVirtualMemory.

Перезапись текстового сегмента загруженного NTDLL​

EDR перехватывает некоторые NTAPI, перезаписывая код в текстовом сегменте модуля ntdll. Поэтому, чтобы восстановить перехваченные функции, мы можем перезаписать текстовый сегмент загруженного ntdll.
Для достижения этого необходимо выполнить несколько шагов:
  1. Прочитать свежую копию ntdll. Мы можем прочитать её с диска, через Интернет, из директории KnownDLL и т.д.
  2. Изменить разрешение страницы с RX на RWX, так как текстовый сегмент по умолчанию не доступен для записи. Мы также можем использовать WriteProcessMemory или его NTAPI для перезаписи перехваченного кода.
  3. Скопировать текстовый сегмент из свежей копии в загруженный модуль.
  4. Восстановить разрешение страницы.
Однако, у этого подхода есть несколько индикаторов компрометации (IOCs):
  • 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, системный вызов выполняется успешно.

1716541268691.png


Давайте изменим код и снова поэкспериментируем с NtDelayExecution, учитывая, что нам будет легче наблюдать стек вызовов. Как и ожидалось, эти правила Yara, которые мы использовали ранее, не могут обнаружить никакой шаблон последовательности байтов.

1716541311740.png


Проверьте стек вызовов: системный вызов инициируется из пространства памяти ntdll, что выглядит легитимным с этой точки зрения. Однако KeDelayExecutionThread ожидает NtDelayExecution в качестве соответствующего NTAPI, а не NtDrawText. Эта подсказка может быть использована как правило для обнаружения.

1716541381052.png


Преимущества и обнаружение​

MutationGate имеет свои преимущества, но его также возможно обнаружить. Если у вас есть другие идеи о преимуществах и способах обнаружения, пожалуйста, сообщите мне : )

Преимущества​

  • Не загружается второй модуль ntdll
  • Нет изменений в загруженном модуле ntdll
  • Отсутствие пользовательской заглушки системного вызова и шаблона последовательности байтов
  • Системный вызов инициируется в модуле ntdll, что выглядит легитимно

Возможные способы обнаружения​

  • Вызов AddVectoredExceptionHandler может выглядеть подозрительно в обычной программе
  • Функция в ntoskrnl.exe не соответствует функции в модуле ntdll
  • Инициированный системный вызов в безвредном NTAPI не должен ожидать SSN другого NTAPI

Сравнение с другими похожими подходами​

HWSyscall ( ) и TamperingSyscall ( ) оба изобретательно используют аппаратные точки останова для обхода встроенных хуков, и оба этих подхода отличные.

Хотя я не читал и не ссылался на эти два проекта во время разработки и выпуска MutationGate (после выпуска MutationGate, друг прислал мне ссылки на эти два проекта), действительно есть некоторые похожие техники или общие идеи. Я внимательно прочитал и исследовал их, и я составил таблицу для сравнения, как показано ниже.

ПодходВызовАргументыSSNСистемный вызов
MutationGateБезвредный NTAPIАргументы целевого NTAPISSN безвредного NTAPI -> SSN целевого NTAPIВ безвредном NTAPI
HWSyscallЦелевой NTAPIАргументы целевого NTAPISSN целевого NTAPI после его полученияВ ближайшем незахваченном NTAPI
TamperingSyscallЦелевой NTAPIПодставные -> Аргументы целевого NTAPISSN целевого NTAPI после прохождения проверки EDRВ целевом NTAPI
Косвенный системный вызовПользовательская ASM функцияАргументы целевого NTAPISSN целевого NTAPI после его полученияВ любом незахваченном NTAPI

Благодарности и ссылки​

В период после того, как я был вдохновлен, разработал и выпустил MutationGate, следующие ресурсы были очень полезны для меня, и я благодарен их авторам.

























 
Верх Низ