Статья Обход AMSI, ETW, userland hooks и kernel callbacks: практическое руководство для offensive-разработчика в 2026 году

Обход AMSI и EDR в Windows — экран отладчика x64dbg с дизассемблированным кодом ntdll и hardware breakpoints


За последние два года детект со стороны EDR изменился радикально. То, что работало в 2022-м против Defender, сейчас ловится на стадии загрузки даже бесплатными решениями. Но ядро проблемы осталось тем же: offensive-разработчику нужно понимать не отдельные трюки, а всю цепочку детекта - от AMSI-сканирования скриптов до kernel callbacks, которые фиксируют создание процесса ещё до того, как ваш имплант получит первую инструкцию.

Здесь я разберу четыре уровня защиты Windows и покажу конкретные техники обхода, которые работают на момент написания - с кодом, объяснением, почему именно эти байты патчатся, и честным указанием, где каждый подход ломается под серьёзным EDR.

Когда обход AMSI действительно необходим​

Скажу прямо: если вы пишете шелл-код лоадер на C/C++ и выполняете позиционно-независимый код через callback-функции или APC - AMSI вам не нужен. AMSI сканирует управляемый код и скриптовые языки: PowerShell, VBScript, JavaScript, VBA-макросы и .NET-сборки, загружаемые через Assembly.Load(). Fabian Mosch из r-tec в своём анализе пишет то же самое: добавление AMSI-байпасса в шелл-код лоадер лишь увеличивает количество индикаторов компрометации и шансы спалиться.

Обход AMSI нужен, когда вы:
  • Загружаете .NET-сборку в память через Assembly.Load() из C2-фреймворка
  • Выполняете PowerShell-скрипты через Invoke-Expression или Add-Type
  • Работаете с VBA-макросами в Office-документах
  • Используете mshta.exe, cscript.exe или wscript.exe для загрузки скриптов
Загрузка .NET-сборок и обфускация скриптов мапятся на технику (T1027, Defense Evasion) по классификации MITRE ATT&CK, использование mshta.exe - на Mshta (T1218.005, Defense Evasion) и Ingress Tool Transfer (T1105, Command and Control), использование cscript.exe и wscript.exe - на Command and Scripting Interpreter (T1059, Execution), а сам обход защитных механизмов - на Disable or Modify Tools (T1562.001, Defense Evasion).

Обход AMSI в PowerShell и .NET: что работает в 2026​

Патчинг AmsiScanBuffer - жив или мёртв​

Классический патч AmsiScanBuffer - запись 0xC3 (ret) или последовательности, возвращающей AMSI_RESULT_CLEAN - по-прежнему работает. Но дьявол в деталях. Проблема не в самом патче, а в том, как вы до него добираетесь.

В 2025 году EDR уровня CrowdStrike и SentinelOne ставят userland hooks на VirtualProtect и WriteProcessMemory. Любая попытка поменять права доступа к страницам памяти amsi.dll или записать данные в её адресное пространство через стандартные API немедленно генерирует телеметрию. Вот что происходит при классическом подходе:
C:
// Классический патч AmsiScanBuffer - ЛОВИТСЯ большинством EDR в 2025
void* amsiAddr = GetProcAddress(GetModuleHandleA("amsi.dll"), "AmsiScanBuffer");
DWORD oldProtect;
VirtualProtect(amsiAddr, 6, PAGE_EXECUTE_READWRITE, &oldProtect); // <-- хук EDR срабатывает здесь
memcpy(amsiAddr, "\xb8\x57\x00\x07\x80\xc3", 6); // mov eax, 0x80070057; ret
Байты \xb8\x57\x00\x07\x80\xc3 - это mov eax, HRESULT(E_INVALIDARG) + ret. Функция возвращает ошибку, а дальше - как повезёт. Поведение PowerShell 5.1 при ошибке HRESULT от AmsiScanBuffer зависит от конкретного билда Windows: в одних сборках сканирование пропускается (fail-open), в других - выполнение блокируется (fail-closed). Microsoft это нигде не документировала (удивительно, правда?), так что проверяйте в целевой среде. В .NET CLR (для Assembly.Load) поведение аналогично, а вот Office VBA-хост может тупо заблокировать выполнение при ошибке.

Более надёжный подход - патч, возвращающий S_OK и записывающий AMSI_RESULT_CLEAN в выходной параметр amsiResult. Но VirtualProtect на адрес внутри amsi.dll - красный флаг, который видят все серьёзные EDR.

Альтернатива: hardware breakpoints через NtContinue​

🔓 Эксклюзивный контент для зарегистрированных пользователей.

ETW patching techniques: ослепление телеметрии​

ETW (Event Tracing for Windows) - главный канал телеметрии для EDR. Через него летят данные о выделении памяти, манипуляциях с потоками, APC-вызовах и куче всего остального. Подавление ETW маппится на технику (T1562.002, Defense Evasion).

Патчинг EtwEventWrite в ntdll​

Классика - патч EtwEventWrite в ntdll.dll, чтобы функция возвращала 0 без реальной отправки событий:
C:
// Патч EtwEventWrite - концептуальный пример
void* etwAddr = GetProcAddress(GetModuleHandleA("ntdll.dll"), "EtwEventWrite");
DWORD oldProtect;
VirtualProtect(etwAddr, 1, PAGE_EXECUTE_READWRITE, &oldProtect);
*(BYTE*)etwAddr = 0xC3; // ret - функция ничего не делает
VirtualProtect(etwAddr, 1, oldProtect, &oldProtect);
Один байт - и userland-ETW ослеп. Но в 2026-м у подхода серьёзные ограничения. Как описывает fluxsec в анализе ETW-детекции в ядре, Remcos RAT использовал именно эту технику - и был пойман EDR, работающим на уровне ядра, через периодическое сканирование целостности памяти ntdll.dll. То есть EDR тупо сравнивает in-memory версию ntdll с тем, что лежит на диске, и видит ваш 0xC3.

ETW Kernel Dispatch Table​

По данным того же исследования fluxsec, более продвинутые атаки целятся в ETW Kernel Dispatch Table - внутреннюю структуру ядра, через которую маршрутизируются ETW-события. Руткит Lazarus (FudModule) использовал DKOM (Direct Kernel Object Manipulation) для модификации этих структур. Конкретно:
  • Обнуление IsEnabled-флага ETW GUID Entry для конкретных провайдеров
  • Модификация масок ETW_REG_ENTRY, контролирующих, какие события логируются
  • Отключение глобальных системных логгеров
Каждая из этих техник работает на уровне ядра и требует загруженного драйвера или эксплойта admin-to-kernel. Для red team-операций это обычно BYOVD (Bring Your Own Vulnerable Driver).

Что реально детектится в 2026​

EDR с компонентом в ядре ловит userland-патчинг ntdll.dll через:
  • Периодическое сравнение in-memory кода ntdll с образом на диске
  • Kernel callbacks на изменение защиты памяти
  • ETW Threat Intelligence провайдер, который живёт в ядре и на userland-патчи ему плевать
Kernel-level DKOM - задача посложнее. По данным fluxsec, периодическая проверка целостности kernel-структур EDR позволяет обнаружить изменения, но есть окно между модификацией и проверкой. Если атакующий успевает восстановить состояние до следующей проверки (аналогично обходу Kernel Patch Protection), обнаружение становится вопросом вероятности и удачи.

Userland hooks bypass: unhooking и прямые системные вызовы​

EDR ставят хуки в ntdll.dll, перехватывая NtAllocateVirtualMemory, NtWriteVirtualMemory, NtCreateThreadEx и десятки других функций. Это покрывает технику (T1055, Defense Evasion / Privilege Escalation) - любая инъекция в чужой процесс проходит через эти API. Обход реализуется через Native API (T1106, Execution).

Unhooking через чтение ntdll с диска​

Классический подход: загружаем чистую копию ntdll.dll с диска и перезаписываем .text-секцию текущей (захукленной) копии:
C:
// Unhooking ntdll - чтение чистой копии с диска
HANDLE hFile = CreateFileA("C:\\Windows\\System32\\ntdll.dll",
    GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, 0, NULL);
HANDLE hMapping = CreateFileMapping(hFile, NULL, PAGE_READONLY, 0, 0, NULL);
LPVOID cleanNtdll = MapViewOfFile(hMapping, FILE_MAP_READ, 0, 0, 0);

// Находим .text секцию в чистой копии
PIMAGE_DOS_HEADER dosHeader = (PIMAGE_DOS_HEADER)cleanNtdll;
PIMAGE_NT_HEADERS ntHeaders = (PIMAGE_NT_HEADERS)((BYTE*)cleanNtdll + dosHeader->e_lfanew);
PIMAGE_SECTION_HEADER section = IMAGE_FIRST_SECTION(ntHeaders);

for (int i = 0; i < ntHeaders->FileHeader.NumberOfSections; i++) {
    if (!strcmp((char*)section[i].Name, ".text")) {
        LPVOID hookedText = (LPVOID)((BYTE*)GetModuleHandleA("ntdll.dll")
            + section[i].VirtualAddress);
        DWORD oldProtect;
        VirtualProtect(hookedText, section[i].Misc.VirtualSize,
            PAGE_EXECUTE_READWRITE, &oldProtect);
        memcpy(hookedText, (BYTE*)cleanNtdll + section[i].PointerToRawData,
            section[i].Misc.VirtualSize);
        VirtualProtect(hookedText, section[i].Misc.VirtualSize,
            oldProtect, &oldProtect);
        break;
    }
}
Проблема: само чтение ntdll с диска и вызов VirtualProtect на .text-секцию - детектируемые паттерны. CrowdStrike Falcon ловит это через мини-фильтр файловой системы и kernel callback на изменение защиты памяти. Лично я на одном проекте видел, как Falcon генерировал алерт ещё до завершения memcpy.

Direct syscalls: SysWhispers и его эволюция​

Прямые системные вызовы обходят userland hooks, потому что не проходят через захукленные функции в ntdll. Вместо вызова NtAllocateVirtualMemory в ntdll, вы вручную помещаете номер syscall в EAX и выполняете syscall:
Код:
; Прямой syscall NtAllocateVirtualMemory (пример для демонстрации концепции)
; SSN (System Service Number) меняется между версиями Windows
mov r10, rcx
mov eax, 0x18          ; SSN 0x18 - NtAllocateVirtualMemory на Windows 10 20H2+ и Windows 11
                       ; (проверяйте для конкретного билда). SSN менялись между major-версиями
                       ; и могут отличаться на insider-билдах.
                       ; В production-коде ВСЕГДА резолвите SSN динамически
                       ; (SysWhispers3, HellsGate, Halo's Gate)
syscall
ret
Проблема прямых syscalls в 2026 году: ETW Threat Intelligence провайдер работает в ядре. Он видит сам факт вызова NtAllocateVirtualMemory с флагами PAGE_EXECUTE_READWRITE - независимо от того, прошёл вызов через ntdll или напрямую. Плюс EDR проверяют, откуда пришёл syscall: если адрес возврата не внутри ntdll - это аномалия, и вас уже разглядывают.

Indirect syscalls​

Эволюция direct syscalls - indirect syscalls. Вместо выполнения инструкции syscall из своего кода, вы прыгаете на syscall; ret гаджет внутри легитимной ntdll. Адрес возврата выглядит «правильно»:
C:
// Indirect syscall - концептуальная схема
// 1. Находим адрес инструкции 'syscall' внутри ntdll
//    (сканируем .text секцию на байты 0x0F 0x05 0xC3)
BYTE* ntdllBase = (BYTE*)GetModuleHandleA("ntdll.dll");
// ... поиск паттерна 0x0F, 0x05, 0xC3 в .text секции ...

// 2. Помещаем правильный SSN в EAX
// 3. Прыгаем на найденный адрес syscall в ntdll
// Результат: стек вызовов выглядит легитимно для EDR
Это усложняет детект по call stack, но kernel-level мониторинг никуда не девается.

Kernel callbacks: что видит EDR до вашего первого байта​

Kernel callbacks - финальный рубеж, и обойти его из userland без эксплойта или загрузки своего драйвера невозможно. Точка. Ключевые callbacks:

CallbackЧто отслеживаетКак используют EDR
PsSetCreateProcessNotifyRoutineСоздание и завершение процессовДетект подозрительных parent-child цепочек
PsSetCreateThreadNotifyRoutineСоздание потоковДетект remote thread injection
PsSetLoadImageNotifyRoutineЗагрузка образов (DLL/EXE)Детект reflective DLL injection
ObRegisterCallbacksОперации с хэндламиЗащита процесса EDR от открытия
CmRegisterCallbackОперации с реестромДетект persistence-механизмов

Техника (T1620, Defense Evasion) специально нацелена на обход PsSetLoadImageNotifyRoutine - загрузка DLL без вызова LdrLoadDll не генерирует уведомления от этого callback.

Что реально можно сделать из userland​

Без драйвера убрать kernel callback нельзя. Но можно минимизировать генерируемую телеметрию:
  • Использовать thread pool (TpAllocWork / TpPostWork) вместо CreateThread для выполнения кода - обходит часть эвристик на создание потоков
  • Выполнять шелл-код через callback-функции легитимных API (EnumWindows, CertEnumSystemStore и аналоги) - адрес возврата в стеке выглядит легитимнее
  • Избегать PAGE_EXECUTE_READWRITE при выделении памяти - выделять как RW, записывать, менять на RX
Третий пункт кажется банальным, но я до сих пор вижу публичные лоадеры, которые аллоцируют RWX одним вызовом. В 2026 году это примерно как кричать «я здесь!» в тихой комнате.

Kernel-level evasion: BYOVD и DKOM​

Для red team-операций, где в целевой среде стоит EDR уровня CrowdStrike или SentinelOne, единственный надёжный способ разобраться с kernel callbacks - загрузка собственного драйвера через BYOVD. После получения выполнения в ядре можно:
  1. Снять callbacks EDR через модификацию массива PspCreateProcessNotifyRoutine - каждый элемент содержит EX_FAST_REF указатель на EX_CALLBACK_ROUTINE_BLOCK; для доступа к callback нужно замаскировать младшие 4 бита (ref count) и получить адрес структуры. Для нейтрализации можно заменить поле Function на указатель на пустую функцию (ret-stub) - простое обнуление создаёт race condition. Более надёжный вариант - полное удаление записи из массива с обновлением PspCreateProcessNotifyRoutineCount. Оба варианта требуют тщательного тестирования: некорректная модификация - и привет, BSOD
  2. Закрыть хэндлы EDR к защищённым процессам
  3. Модифицировать ETW-структуры в ядре (описано выше)
По данным исследования Lazarus FudModule (описанного fluxsec), руткит использовал admin-to-kernel zero-day для получения kernel execution и последующей DKOM-манипуляции ETW-структур. Лазарусы, конечно, ребята серьёзные - но сам подход вполне воспроизводим через BYOVD.

Собираем всё вместе: порядок операций для импланта​

Практическая последовательность действий, которая минимизирует детект на каждом этапе:
🔓 Эксклюзивный контент для зарегистрированных пользователей.

Ключевой принцип: каждый байпасс - это дополнительный IoC. Не применяйте обход, если он не нужен для конкретного этапа. Шелл-код лоадер не нуждается в AMSI-байпассе. BOF-модули не нуждаются в отдельном unhooking ntdll, если BOF реализует indirect syscalls через встроенные syscall stubs (InlineWhispers, SysWhispers3 BOF). Стандартные BOF, использующие BeaconGetProcAddress для резолва Nt-функций, проходят через захукленную ntdll - и вот тут уже нужно думать.

Что реально работает против конкретных EDR​

Я намеренно не даю матрицу «техника X работает против продукта Y» - она устареет через месяц после публикации. Вместо этого - методология проверки:
  1. Разверните целевой EDR в лабе с полной телеметрией (не в режиме «только алерты»)
  2. Выполняйте каждую технику изолированно, анализируя, какие события генерируются
  3. Используйте Process Hacker и x64dbg для инспекции хуков: загрузите ntdll из процесса и сравните пролог каждой Nt-функции с оригиналом на диске
  4. Проверьте, использует ли EDR kernel minifilter - через fltMC.exe filters
  5. Проверьте kernel callbacks через WinDbg: !callback, dx @$cursession.Processes.Where(p => p.Name == "targetEDR.exe")
Детект эволюционирует постоянно. По наблюдениям fluxsec, даже периодическая проверка целостности kernel-структур может быть обойдена при достаточно быстром восстановлении состояния. Это гонка вооружений, и единственный надёжный подход - тестировать конкретную технику против конкретной версии конкретного EDR. На заборе написано «universal bypass» - но вы-то знаете, что на заборе много чего написано.

Заключение​

Обход AMSI, ETW, userland hooks и kernel callbacks - не набор разрозненных трюков, а связанная система, где каждый уровень дополняет предыдущий. В 2026 году ни одна отдельная техника не гарантирует обход EDR корпоративного класса. Патч AmsiScanBuffer бесполезен, если вы не разобрались с userland hooks на VirtualProtect. Unhooking ntdll бессмысленен, если kernel ETW TI логирует ваши syscalls. Direct syscalls не спасут, если EDR проверяет call stack.

Разверните лабу с целевым EDR и прогоните каждую технику из этой статьи изолированно - посмотрите, что генерирует телеметрию, а что проходит тихо. Именно понимание всей вертикали (от PowerShell-скриптов до kernel callbacks и ETW dispatch tables) отличает рабочий имплант от очередного PoC, который ловится на первой стадии.
 
Мы в соцсетях:

Взломай свой первый сервер и прокачай скилл — Начни игру на HackerLab

🚀 Первый раз на Codeby?
Гайд для новичков: что делать в первые 15 минут, ключевые разделы, правила
Начать здесь →
🔴 Свежие CVE, 0-day и инциденты
То, о чём ChatGPT ещё не знает — обсуждаем в реальном времени
Threat Intel →

HackerLab