РАЗБОР
Статья
Обход AMSI, ETW, userland hooks и kernel callbacks: практическое руководство для offensive-разработчика в 2026 году
Режим чтения
[ обложка статьи ]
За последние два года детект со стороны EDR изменился радикально. То, что работало в 2022-м против Defender, сейчас ловится на стадии загрузки даже бесплатными решениями. Но ядро проблемы осталось тем же: offensive-разработчику нужно понимать не отдельные трюки, а всю цепочку детекта - от архитектуры имплантов до финального обхода EDR (полное руководство по разработке red team инструментов я собрал здесь) - от 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для загрузки скриптов
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 маппится на технику Disable Windows Event Logging (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);
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, контролирующих, какие события логируются - Отключение глобальных системных логгеров
Что реально детектится в 2026
EDR с компонентом в ядре ловит userland-патчингntdll.dll через:- Периодическое сравнение in-memory кода ntdll с образом на диске
- Kernel callbacks на изменение защиты памяти
- ETW Threat Intelligence провайдер, который живёт в ядре и на userland-патчи ему плевать
Userland hooks bypass: unhooking и прямые системные вызовы
EDR ставят хуки вntdll.dll, перехватывая NtAllocateVirtualMemory, NtWriteVirtualMemory, NtCreateThreadEx и десятки других функций. Это покрывает технику Process Injection (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;
}
}
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
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
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-механизмов |
Техника Reflective Code Loading (T1620, Defense Evasion) специально нацелена на обход
PsSetLoadImageNotifyRoutine - загрузка DLL без вызова LdrLoadDll не генерирует уведомления от этого callback.Что реально можно сделать из userland
Без драйвера убрать kernel callback нельзя. Но можно минимизировать генерируемую телеметрию:- Использовать thread pool (
TpAllocWork/TpPostWork) вместоCreateThreadдля выполнения кода - обходит часть эвристик на создание потоков - Выполнять шелл-код через callback-функции легитимных API (
EnumWindows,CertEnumSystemStoreи аналоги) - адрес возврата в стеке выглядит легитимнее - Избегать
PAGE_EXECUTE_READWRITEпри выделении памяти - выделять какRW, записывать, менять наRX
Kernel-level evasion: BYOVD и DKOM
Для red team-операций, где в целевой среде стоит EDR уровня CrowdStrike или SentinelOne, единственный надёжный способ разобраться с kernel callbacks - загрузка собственного драйвера через BYOVD. После получения выполнения в ядре можно:- Снять callbacks EDR через модификацию массива
PspCreateProcessNotifyRoutine- каждый элемент содержитEX_FAST_REFуказатель наEX_CALLBACK_ROUTINE_BLOCK; для доступа к callback нужно замаскировать младшие 4 бита (ref count) и получить адрес структуры. Для нейтрализации можно заменить полеFunctionна указатель на пустую функцию (ret-stub) - простое обнуление создаёт race condition. Более надёжный вариант - полное удаление записи из массива с обновлениемPspCreateProcessNotifyRoutineCount. Оба варианта требуют тщательного тестирования: некорректная модификация - и привет, BSOD - Закрыть хэндлы EDR к защищённым процессам
- Модифицировать ETW-структуры в ядре (описано выше)
Собираем всё вместе: порядок операций для импланта
Практическая последовательность действий, которая минимизирует детект на каждом этапе:Ключевой принцип: каждый байпасс - это дополнительный IoC. Не применяйте обход, если он не нужен для конкретного этапа. Шелл-код лоадер не нуждается в AMSI-байпассе. BOF-модули не нуждаются в отдельном unhooking ntdll, если BOF реализует indirect syscalls через встроенные syscall stubs (InlineWhispers, SysWhispers3 BOF). Стандартные BOF, использующие
BeaconGetProcAddress для резолва Nt-функций, проходят через захукленную ntdll - и вот тут уже нужно думать.Что реально работает против конкретных EDR
Я намеренно не даю матрицу «техника X работает против продукта Y» - она устареет через месяц после публикации. Вместо этого - методология проверки:- Разверните целевой EDR в лабе с полной телеметрией (не в режиме «только алерты»)
- Выполняйте каждую технику изолированно, анализируя, какие события генерируются
- Используйте Process Hacker и x64dbg для инспекции хуков: загрузите ntdll из процесса и сравните пролог каждой Nt-функции с оригиналом на диске
- Проверьте, использует ли EDR kernel minifilter - через
fltMC.exe filters - Проверьте kernel callbacks через WinDbg:
!callback,dx @$cursession.Processes.Where(p => p.Name == "targetEDR.exe")
Заключение
Обход 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, который ловится на первой стадии.
AI-выжимка
сгенерировано ИИ
Тезисы статьи скоро
Статья читается полностью. Тезисы со ссылками на разделы появятся позже.
Содержание
Codeby Academy
Практика и мастерство
От основ до продвинутого — программы для практиков Codeby.
Перейти к курсу →
Поиск в обсуждении
Карта ветки
Продолжить чтение
Следующий разбор
Как обойти SOC при пентесте: stealth-сканирование
Ещё по теме
- Статья
- Статья
Комментарии
1