Привет, Хабр! Меня зовут Ноэль, и я являюсь специалистом по информационной безопасности в компании HEX.TEAM.

В нашей предыдущей статье https://habr.com/ru/companies/hex_team/articles/988008/ мы рассказали, как забытый PostgreSQL привел по цепочке уязвимостей к компрометации всей сети. Мы подробно рассказали о логике атаки, но опустили технические детали работы механизмов защиты на конечном хосте, которые мы обошли для загрузки вредоносного файла.

Данная статья посвящена более глубокому анализу EDR и AMSI, а также общему представлению концепции обходов данных механизмов безопасности. Мы разберем, как работает низкоуровневая модификация памяти и .NET Reflection, а также почему XOR-шифрование с многобайтовым ключом все еще актуально в 2026 году.

Дисклеймер: Все действия выполнялись в рамках согласованного контракта на пентест. Материал носит образовательный характер.

Зачем нужен AMSI и как он работает

Популярность и повсеместное использование скриптовых языков программирования привело к значительному росту безфайловых атак. Эти атаки характеризуются отсутствием сохранения вредоносных файлов на диске и представляют собой серьезный вызов для традиционных антивирусных решений, основанных на сигнатурном анализе файлов. В ответ на эту угрозу, корпорация Microsoft разработала и внедрила Antimalware Scan Interface (AMSI)  - открытый программный интерфейс, предназначенный для расширения возможностей антивирусных решений по обнаружению вредоносного кода непосредственно в памяти, до его исполнения.

AMSI представляет собой API, который позволяет приложениям и службам передавать скрипты, командные строки или другие буферы памяти для сканирования установленному в системе антивирусу. Этот процесс происходит до того, как код будет выполнен интерпретатором. Таким образом, AMSI лишает злоумышленников основного преимущества обфускации кода: даже если код сильно запутан или зашифрован, он должен быть деобфусцирован и расшифрован в памяти перед выполнением. Именно в этот момент AMSI перехватывает код для анализа.

Упрощенная схема работы AMSI
Упрощенная схема работы AMSI

Обобщенная схема работы AMSI

Принцип работы AMSI выглядит следующим образом:

  1. Приложение, такое как powershell.exe или скриптовый движок, инициализирует сессию AMSI, загружая библиотеку amsi.dll, вызывая функции AmsiInitialize и AmsiOpenSession;

  2. Перед выполнением потенциально опасного содержимого приложение вызывает ключевые функции AmsiScanString или AmsiScanBuffer, передавая буфер с кодом провайдеру AMSI;

  3. Зарегистрированный антивирусный провайдер получает этот буфер и анализирует его с помощью своих сигнатурных баз, эвристических анализаторов и поведенческих моделей;

  4. провайдер AMSI возвращает приложению результат сканирования (AMSI_RESULT_CLEAN, AMSI_RESULT_NOT_DETECTED, AMSI_RESULT_DETECTED). В случае AMSI_RESULT_DETECTED выполнение кода может быть заблокировано.

Основные механизмы обхода AMSI

Существует несколько способов обойти AMSI, и в ходе исследования стоит попробовать несколько разных, ведь в системе на AMSI могут быть установлены специфичные патчи, не говоря уже о блокировке сессии при попытки переписать определенные значения в памяти. Основные способы представлены ниже.

Модификация памяти

Модификация памяти является очень популярным из-за своей простоты, хоть и устаревшим, способом обхода AMSI. Суть способа заключается в непосредственном изменении исполняемого кода функций AMSI в оперативной памяти процесса, чтобы он всегда возвращал положительный результат сканирования или вовсе не выполнял проверку. Но есть один нюанс. Установленные EDR могут проверять целостность библиотеки, и в обнаружения несоответствий отправлять алерты в системы мониторинга, и по итогу активная сессия терминала будет закрыта. Поэтому я этим способом практически не пользуюсь.

Технические детали следующие. Злоумышленник ищет в адресном пространстве текущего процесса загруженные библиотеки, такие как amsi.dll или clr.dll в случае PowerShell. Затем с помощью функций Win32 API, таких как GetProcAddress, VirtualProtect и WriteProcessMemory, он находит точку входа ключевой функции AMSI, например, AmsiScanBuffer или AmsiScanString, и изменяет несколько первых байтов этой функции. Стандартный патч заключается в замене оригинальных инструкций на инструкции, которые принудительно возвращают значение AMSI_RESULT_CLEAN или ошибку, которая заставит вызывающую сторону проигнорировать AMSI.

Например, типичный патч может заменить начало функции на:

MOV EAX, 0×80070005 — Загрузить константу ошибки в регистр EAX

RET — Вернуться из функции

Общий ход реализации можно описать следующим образом:

1) динамически создается сборка, содержащая объявления необходимых функций Win32 API;

2) затем с помощью LoadLibraryA загружается amsi.dll;

3) GetProcAddress используется для получения адреса функции AmsiScanBuffer или AmsiScanString;

4) VirtualProtect изменяет права доступа к памяти по адресу этой функции, делая ее записываемой;

5) WriteProcessMemory перезаписывает байты пролога функции.

Пример выполнения кода в терминале
Пример выполнения кода в терминале
$Win32 = Add Type  MemberDefinition @'
[DllImport(“kernel32.dll”)]
public static extern IntPtr LoadLibrary(string name);
[DllImport(“kernel32.dll”)]
public static extern IntPtr GetProcAddress(IntPtr hModule, string procName);
[DllImport(“kernel32.dll”)]
public static extern bool VirtualProtect(IntPtr lpAddress, UIntPtr dwSize, uint flNewProtect, out uint lpflOldProtect);
'@  Name “Win32”  Namespace “Native”  PassThru
$hAmsi = $Win32::LoadLibrary(“amsi.dll”)
if ($hAmsi  eq [IntPtr]::Zero) { return }
$pAmsiScanBuffer = $Win32::GetProcAddress($hAmsi, “AmsiScanBuffer”)
if ($pAmsiScanBuffer  eq [IntPtr]::Zero) { return }
$patch = [Byte[]] @(0xB8, 0×57, 0×00, 0×07, 0×80, 0xC3)
$oldProtect = 0
$Win32::VirtualProtect($pAmsiScanBuffer, [UIntPtr]::new($patch.Length), 0×40, [ref]$oldProtect) | Out Null
[System.Runtime.InteropServices.Marshal]::Copy($patch, 0, $pAmsiScanBuffer, $patch.Length)

Также стоит помнить, что в проектах могут не одобрить такие агрессивные методы, по причине крайней заметности метода для продвинутых EDR. В общем случае операция Add‑Type оставляет на диске системы артефакты, да и в целом метод модификации памяти в современных системах может не работать, поэтому злоумышленники прибегают к способу “Отражения”.

Использование отражения (Reflection)

Если лезть в память опасно, то можно использовать встроенный в .NET способ переопределения приватных полей классов.

Техника использования отражения (Reflection) в .NET Framework и PowerShell является более «высокоуровневым» подходом по сравнению с прямым патчингом памяти. Она позволяет коду исследовать и модифицировать свою собственную структуру, а также вызывать приватные методы и поля во время выполнения.

�� PowerShell контекст AMSI управляется через внутренние классы и статические поля. Нашей главной целью при использовании Reflection становится класс System.Management.Automation.AmsiUtils. Он содержит поля amsiContext и amsiSession, отвечающие за связь с антивирусом. Если мы сможем перезаписать их значения, то эффективно «сломаем» или деактивируем AMSI для текущей сессии PowerShell.

Установка этих полей в $null или некорректное значение приводит к тому, что PowerShell не может найти или корректно инициализировать AMSI‑провайдера, в результате чего сканирование не происходит.

Один из самых известных используемых методов использует Reflection для обнуления поля amsiSession:

[Ref].Assembly.GetType(“System.Management.Automation.AmsiUtils”).GetField(“amsiSession”,“NonPublic,Static”).SetValue($null, $null)

Последствия этого действия заключаются в том, что при следующей попытке PowerShell использовать amsiSession он обнаружит, что ссылка является недействительной, что приведет к сбою инициализации AMSI для всех последующих команд в этой сессии. Этот метод прост в реализации, но его эффективность может зависеть от версии PowerShell, так как Microsoft периодически меняет внутреннюю структуру классов, чтобы усложнить такие обходы (в данный момент в современных системах при проблемах с сессиями AMSI происходит принудительное закрытие процесса).

Перехват функций (Hooking) и иные методы

Помимо прямых манипуляций с памятью и Reflection, существует ряд других техник, которые атакующие используют для обхода AMSI, например, методы перехвата функций и логические атаки.

Вместо того, чтобы патчить саму функцию AMSI, эта техника изменяет поток выполнения программы таким образом, чтобы вызовы к AmsiScanBuffer или AmsiScanString перенаправлялись на контролируемую злоумышленником функцию. Эта функция‑перехватчик (hook) затем может просто вернуть AMSI_RESULT_CLEAN, не вызывая оригинальную функцию, или выполнить какую‑либо другую логику. Это может быть реализовано несколькими способами:

1) Hardware Breakpoints: используя отладочные регистры процессора (DR0-DR3), можно установить точку остановки на начало выполнения функции AMSI. При срабатывании исключения перехватчик управления захватит выполнение функции AMSI и вернет значение и указатель инструкции RIP, что имитирует успешное завершение функции. Данный способ не требует замены байтов в секции .text библиотеки amsi.dll, что делает этот метод незаметным для проверок целостности памяти.

2) Принудительное вызывание ошибок: некоторые методы обхода фокусируются на создании условий, при которых AMSI не может корректно инициализироваться или его контекст сбрасывается. Например, намеренное создание ошибок в процессе инициализации AMSI может привести к его бездействию. Это может быть достигнуто через манипуляции с глобальными переменными или состоянием, на которые AMSI полагается.

3) Обфускация и фрагментация: хотя AMSI предназначен для сканирования кода после деобфускации, статические сигнатуры все еще могут быть обойдены, если вредоносная строка или шаблон разбиты на множество частей. Вместо использования прямой строки, такой как «Invoke‑Mimikatz», атакующий может представить ее как «Invoke‑» + «Mimi» + «katz». Интерпретатор PowerShell соберет эту строку только в момент выполнения, но, если провайдер AMSI не имеет достаточно сложного эвристического анализа, он может не обнаружить сигнатуру до того, как код будет выполнен. Данный вариант может использоваться с другими способами обхода AMSI. Например, с помощью отражения и фрагментации запроса данная команда позволяет отключить AMSI, перезаписав ссылку на выполнения scanContent на свою функцию, возвращающую 1.

Пример выполнения кода в терминале
Пример выполнения кода в терминале

Таким образом, можно создать тестовую функцию со статическим методом для возвращения необходимого значения 1, чтобы методы AMSI при подмене нашего значения думали, что все чисто и безопасно, затем с помощью метода reflection можно воспользоваться вызовом скрытых методов AmsiUtils. Для этой цели используем минимальное фрагментирование, чтобы нам не заблокировали попытку получения данных о процессе. Вывод будет храниться в переменной $o. Затем мы сохраняем данные о нашем созданном методе в переменную $t. Затем с помощью Marshall:Copy мы копируем адрес нашего созданного метода, «перетирая» адреса настоящей функции AMSI. Смещение [long]8 — стандартное смещение на 8 байт для 64-разрядной архитектуры, используемое для наведения на непосредственно исполняемый машинный код функции.

class TrollAMSI{static [int] M([string]$c, [string]$s){return 1}}
$o=[Ref].Assembly.GetType('System.Ma'+'nag'+'eme'+'nt.Autom'+'ation.A'+'ms'+'iU'+'ti'+'ls').GetMethods('N'+'onPu'+'blic,st'+'at'+'ic') | Where‑Object Name ‑eq ScanContent
$t = [TrollAMSI].GetMethods() | Where‑Object Name ‑eq 'M'
[System.Runtime.InteropServices.Marshal]::Copy(@([System.Runtime.InteropServices.Marshal]::ReadIntPtr([long]$t.MethodHandle.Value + [long]8)),0, [long]$o.MethodHandle.Value + [long]8,1)

По опыту проведения внутренних пентестов скажу, что встречаются очень разные системы. Порой случается, что стандартные методы обхода не работают, но при этом есть большое количество известных надстроек к скриптам, поняв принципы которых, получится достигнуть результатов. В данной главе были описаны общие вектора для обхода AMSI, а в качестве методичке вы можете использовать готовые пейлоады с https://github.com/S3cur3Th1sSh1t/Amsi-Bypass-Powershell .

Часть 2: Продвинутые техники уклонения от EDR

Успешный обход AMSI является критически важным, но лишь первым шагом в цепочке компрометации. Основную проблему представляют системы EDR (Endpoint Detection and Response), которые работают на более глубоком уровне. EDR непрерывно анализируют поток данных и поведение запущенных процессов. Чтобы обойти данный механизм защиты, необходимо не только применить методы маскировки содержимого нагрузки, но и сам факт ее передачи.

Обфускация пайплайна доставки

Первая проблема возникает на моменте доставки вредоносного пейлоада. Прямая загрузка почти гарантировано вызовет алерт в системах сетевого мониторинга и EDR. Классическим и самым распространенным вариантом является использование Base64, но данный способ не всегда работает. Многие EDR используют анализ скриптов на энтропию (степень хаотичности данных) и анализ по паттернам. Огромная Base64-строка обладает повышенной энтропией и выглядит неестественно для легитимного скрипта, при этом глубина кодирования для успешного обхода может достигать четырех кодирований подряд, что значительно увеличит энтропию. На одном проекте мы смогли загрузить файл только с тремя вложенными кодировками.

В таких случаях можно использовать довольно простую надстройку, которая поможет обойти описанные выше ограничения: XOR. При применении исключающего ИЛИ можно также без труда разрушить сигнатуры при этом не изменяя энтропию, однако проблема использования однобайтового XOR заключается в том, что исполняемые файлы содержат внутри себя большое число последовательностей нулевых байтов разной длины. При шифровании однобайтовым ключом EDR сможет заметить большие блоки повторяющихся байтов, так еще и без труда извлечет ключ, определив самый часто встречающийся байт в файле.

Для решения данной проблемы стоит использовать многобайтовый ключ или шифр Виженера в контексте XOR. Такая реализация размоет паттерны и усложнит обнаружение.

Вот пример шифрования:

Пример выполнения шифрования в терминале
Пример выполнения шифрования в терминале
$OriginalPayload = [System.Text.Encoding]::UTF8.GetBytes(«Тестовая нагрузка, которая имитирует бинарный файл»)
$Key = [System.Text.Encoding]::UTF8.GetBytes(“MySuperSecretMultiByteKey123”)
$EncryptedBytes = New‑Object Byte[] $OriginalPayload.Length
for ($i = 0; $i ‑lt $OriginalPayload.Length; $i++) {
  $EncryptedBytes[$i] = $OriginalPayload[$i] ‑bxor $Key[$i% $Key.Length]
}
$Base64Payload = [Convert]::ToBase64String($EncryptedBytes)
Write‑Host $Base64Payload

Для  доставки пейлоада на атакуемую систему расшифруем по той же логике:

Пример выполнения дешифровки в терминале
Пример выполнения дешифровки в терминале

Пример выполнения расшифрования в терминале

$Blob = “base64XorString” 
$Key = [System.Text.Encoding]::UTF8.GetBytes(“MySuperSecretMultiByteKey123”)
$EncryptedBytes = [Convert]::FromBase64String($Blob)
$DecodedBytes = New‑Object Byte[] $EncryptedBytes.Length
for ($i = 0; $i ‑lt $EncryptedBytes.Length; $i++) {
  $DecodedBytes[$i] = $EncryptedBytes[$i] ‑bxor $Key[$i% $Key.Length]
}
$ResultString = [System.Text.Encoding]::UTF8.GetString($DecodedBytes)
Write‑Host $ResultString

Беcфайловая загрузка в память (In‑Memory Execution)

Вот мы и дошли до самого главной подтемы статьи. Данный способ мы применяем чаще всего, ведь если файл хоть как-то “коснется” диска, то он точно останется в логах или стриггерит антивирус или EDR. Современные EDR для защиты от подобных видов атак используют различные анализаторы памяти и событийную телеметрию для поиска аномалий.

Как было обговорено выше, PowerShell, является оболочкой над .NET Runtime и, как следствие, предоставляет легальный механизм для загрузки скомпилированных сборок из массива байтов:

System.Reflection.Assembly]::Load()

Данный метод принимает массив байтов, представляющий собой скомпилированную .NET сборку. Эта сборка может быть выполнена немедленно без необходимости сохранения ее на диск.

В общем случае, вызов этого метода достаточен для обхода не очень безопасно настроенных систем, однако современные EDR научились сканировать поток событий, генерируемых через систему ETW (Event Tracking for Windows). Проще говоря, когда .NET код запускается, провайдер Microsoft‑Windows‑DotNETRuntime может отправлять события, содержащие информацию о загрузке сборки, включая ее имя и флаги. EDR может проанализировать данное событие и выявить отсутствие источника на диске, что может быть классифицировано как аномалия. Данное ограничение тоже может быть обойдено разными способами, сходными с обходом AMSI, например, с помощью патчинга функции EtwEventWrite, чтобы она не отправляла данные ядру, а лишь возвращала 0.

Вот обобщенный пример загрузки сборок в память:

$RubeusAssembly = [System.Reflection.Assembly]::Load([Convert]::FromBase64String(“aa...”))
[Rubeus.Program]::Main(“dump /user:administrator”.Split())

Как видим, происходит загрузка закодированного в base64 исполняемого файла с последующим вызовом функции main.

Process Hollowing

Если же выполнение кода в процессе powershell.exe вызвало подозрения у EDR, что привело к принудительному завершению сессии, то стоит попробовать технику Process Hollowing, но стоит быть предельно осторожными и не изменять процессы, необходимые для стабильной работы системы. В некоторых случаях EDR может прервать любую попытку подмены памяти, что завершит процесс. Поэтому будьте внимательны и не кладите сервера заказчикам.

Данная техника представляет собой инъекцию кода, при которой злоумышленник создает новый экземпляр легитимного процесса в состоянии «заморозки», затем полностью подменяет его виртуальное адресное пространство своим вредоносным кодом и «размораживает». В диспетчере задач и для базовых инструментов мониторинга процесс вообще не вызовет подозрений.

Изначально злоумышленник создает процесс, вызывая CreateProcess с флагом CREATE_SUSPENDED, который не дает процессу выполняться, пока не будет вызвана функция ResumeThread. Затем необходимо освободить память, занятую легитимным кодом, чего можно добиться с помощью NtUnmapViewOfSection(hProcess, BaseAddress), сам базовый адрес может быть определен через чтение PEB ImageBaseAddress. Очищение памяти может быть очень громким для EDR систем, потому что не часто легитимные процессы вызывают NtUnmapViewOfSection для самих себя. Затем, с помощью VirtualAllocEx происходит выделение новой памяти, содержащей полезную нагрузку. Заключительным этапом является обновление контекста потока путем модификации регистров с последующим запуском процесса.

Данная атака довольно затруднительна. После ее публикации EDR научились не просто смотреть на имя запущенного процесса, но и проверять соответствие содержимого памяти процесса с приложенным файлом на диске, а также проверять тип памяти, хранимый в структуре VAD (Virtual Address Descriptor): у легитимных процессов тип памяти MEM_IMAGE, после изменения память изменяет тип на MEM_PRIVATE.

Анализ методов обхода защитных систем будет непрерывно происходить и в будущем, и мы не можем исключить, что новый способ обхода не будет основан на уже известных методах. Также отмечу, что еще недавно для загрузки вредоносного файла достаточно было обфусцировать код и изменить сигнатуры, однако в настоящее время успех а��аки лежит через путь крайне низкоуровневых векторов. Очень часто в проектах компании мы сталкивались с различными надстройками к защитным механизмам и именно различные методики обходов позволяли продвинуться дальше при проведении пентестов и, как следствие, повышенной защиты заказчиков от взломов в будущем.

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

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

Кстати, за новостями HEX.TEAM теперь можно следить в тг-канале.

Спасибо за уделенное время и удачи применить полученные знания на практике!