APC Injection в Windows: механизм выполнения и признаки, которые может увидеть защитник

APC Injection (T1055.004 по MITRE ATT&CK) — техника внедрения кода, при которой произвольная функция ставится в очередь асинхронных вызовов (Asynchronous Procedure Call) чужого потока. Когда поток входит в alertable-состояние, Windows выполняет все накопленные APC-функции в порядке очереди. Ключевая особенность для защитника: вредоносный код выполняется в контексте легитимного процесса без создания нового потока, поэтому классический детект на CreateRemoteThread здесь не срабатывает.

Техника популярна у операторов вредоносного ПО именно из-за этой скрытности: она не порождает заметных артефактов на уровне потоков и позволяет выполнять код до инициализации процесса, обходя hooks средств защиты.

Как устроена очередь APC​


Каждый поток в Windows имеет собственную очередь APC — FIFO-структуру, которую ядро обрабатывает при переходе потока в alertable-состояние. Механизм предназначен для легитимных задач: асинхронный ввод-вывод, уведомления о завершении операций, callback-функции драйверов.

Постановка функции в очередь выполняется через QueueUserAPC:

C:
DWORD QueueUserAPC(
  PAPCFUNC  pfnAPC,   // указатель на функцию для выполнения
  HANDLE    hThread,   // дескриптор целевого потока
  ULONG_PTR dwData     // параметр, передаваемый в функцию
);

Функция не выполняется немедленно. Она ожидает, пока целевой поток не войдёт в alertable-состояние. Если поток уже находится в таком состоянии, APC выполняется сразу. Если поток приостановлен — выполнение откладывается до возобновления.

Alertable-состояние: обязательное условие выполнения​


Не каждая функция ожидания переводит поток в alertable-состояние. Это определяет, какие потоки могут стать мишенью:

ФункцияAlertableУсловие
SleepExДаbAlertable = TRUE
WaitForSingleObjectExДаbAlertable = TRUE
WaitForMultipleObjectsExДаbAlertable = TRUE
MsgWaitForMultipleObjectsExДаФлаг MWMO_ALERTABLE
SignalObjectAndWaitДаbAlertable = TRUE
SleepНетНе поддерживает alertable
WaitForSingleObjectНетНет параметра alertable
WaitForMultipleObjectsНетНет параметра alertable
MsgWaitForMultipleObjectsНетНет флага alertable

Потоки GUI-приложений часто вызывают MsgWaitForMultipleObjectsEx в цикле обработки сообщений, что делает их удобной мишенью. Потоки системных сервисов, использующие WaitForSingleObjectEx или WaitForMultipleObjectsEx с флагом alertable, также подходят для атаки.

Три уровня API для постановки APC​


Помимо документированного QueueUserAPC, существуют нативные системные вызовы, которые делают то же самое, но обходят часть проверок user-mode:

  • QueueUserAPC — WinAPI из kernel32.dll. Внутри вызывает NtQueueApcThread.
  • NtQueueApcThread — нативный вызов из ntdll.dll. Прямой вызов минуя kernel32 усложняет детектирование через user-mode hooks.
  • ZwQueueApcThread — тот же системный вызов через stub в ntdll.dll с переходом в режим ядра. С точки зрения ядра Nt- и Zw-варианты идентичны.

Для защитника: если в ETW-трейсе или телеметрии EDR виден вызов NtQueueApcThread или ZwQueueApcThread из процесса, не являющегося системным компонентом, это подозрительный сигнал. Большинство реальных образцов вредоносного ПО используют именно нативные вызовы, а не документированный QueueUserAPC.

Последовательность атаки​


Классическая цепочка APC Injection включает четыре этапа:

  1. Получение дескриптора целевого потока — через OpenThread для существующего процесса или создание нового.
  2. Выделение памяти и запись полезной нагрузкиVirtualAllocEx + WriteProcessMemory (или нативные аналоги) в адресном пространстве цели.
  3. Постановка APCQueueUserAPC или нативный аналог с указанием адреса полезной нагрузки как функции.
  4. Обеспечение выполнения — если поток приостановлен, вызов ResumeThread; если поток уже в alertable-состоянии, APC выполнится автоматически.

Типичный паттерн подготовки памяти: выделение региона с PAGE_READWRITE, копирование payload, затем смена прав на PAGE_EXECUTE_READWRITE через VirtualProtect. Этот переход прав — один из наиболее надёжных индикаторов подготовки shellcode.

Early Bird Injection​


Атакующий создаёт процесс с флагом CREATE_SUSPENDED, записывает полезную нагрузку до выполнения точки входа, ставит APC в очередь главного потока и возобновляет его через ResumeThread.

Код выполняется до инициализации процесса, что позволяет обойти hooks, которые EDR или антивирус устанавливает при загрузке образа. Для защитника это означает, что стандартные callback-уведомления о создании процесса могут не зафиксировать вредоносную активность, потому что она происходит до полной инициализации.

Типичные цели: svchost.exe, rundll32.exe, explorer.exe — системные процессы, создание которых не вызывает подозрений у пользователя.

Вариант через отладчик​


Вместо CREATE_SUSPENDED процесс создаётся с флагом DEBUG_PROCESS. После записи полезной нагрузки и постановки APC вызывается DebugActiveProcessStop, который прекращает отладку и возобновляет потоки — APC выполняется.

DEBUG_PROCESS — легитимный флаг, используемый отладчиками. Однако сочетание создания процесса с этим флагом, последующего DebugActiveProcessStop в коротком временном окне и отсутствия реального отладочного взаимодействия (чтения регистров, установки breakpoints, обработки исключений) — аномалия, которую можно детектировать.

AtomBombing​


Вариация, использующая глобальную таблицу атомов Windows как канал передачи данных. Вместо WriteProcessMemory атакующий сохраняет данные в атомную таблицу, а затем ставит APC, который читает и выполняет их. Это обходит детект на WriteProcessMemory — один из наиболее отслеживаемых API-вызовов при расследовании process injection Process Injection в Windows: механизм на уровне концепции и артефакты для детектирования.

Признаки для детектирования​


APC Injection не создаёт новый поток, поэтому правила на CreateRemoteThread эту технику не покрывают. Защитнику нужно ориентироваться на комбинацию косвенных признаков.

API-уровень (ETW, EDR-телеметрия):

  • QueueUserAPC, NtQueueApcThread или ZwQueueApcThread вызывается из несистемного процесса.
  • Вызов направлен на поток в другом процессе (cross-process APC).
  • Перед APC наблюдается VirtualAllocEx + WriteProcessMemory (или ZwWriteVirtualMemory) в том же целевом процессе.

Процессы:

  • Процесс создан с CREATE_SUSPENDED или DEBUG_PROCESS и быстро возобновлён.
  • DebugActiveProcessStop вызывается вскоре после CreateProcess с DEBUG_PROCESS без реального отладочного взаимодействия.
  • Системный процесс (svchost.exe, rundll32.exe) создан не системным родителем.

Память:


Потоки:

  • ResumeThread вызывается сразу после постановки APC.
  • Изменение контекста потока в сочетании с APC-вызовом.

Ложные срабатывания и настройка​


Не каждый cross-process APC — атака. Системные компоненты и некоторые легитимные приложения используют APC для асинхронного ввода-вывода. При настройке правил стоит учитывать:

  • svchost.exe и csrss.exe регулярно ставят APC собственным потокам — это нормальное поведение.
  • Cross-process APC из csrss.exe в другие процессы — часть штатной работы подсистемы.
  • Подозрение вызывает APC из пользовательского процесса в системный, особенно в сочетании с выделением памяти и сменой прав.

Реальные примеры из threat intelligence​


APC Injection задокументирована в десятках кампаний. По данным MITRE ATT&CK:

ВредоносAPIЦель
AttorNtQueueApcThreadAPC-очередь целевого процесса
BADHATCH (FIN8)APC-очередьsvchost.exe -k netsvcs
BumblebeeAPC injectionВыполнение команд C2
CarberpZwQueueApcThreadexplorer.exe
IcedIDZwQueueApcThreadУдалённые процессы
PillowmintNtQueueApcThreadsvchost.exe
Saint BotNtQueueApcThread + ZwAlertResumeThreadEhStorAuthn.exe
SardonicQueueUserAPCShellcode
TURNEDUPEarly Birdrundll32.exe
XLoaderNtQueueApcThreadAPC-очередь

Обращает внимание: большинство образцов используют нативные вызовы (NtQueueApcThread, ZwQueueApcThread), а не документированный QueueUserAPC. Saint Bot дополнительно применяет ZwAlertResumeThread — вызов, который одновременно переводит поток в alertable-состояние и возобновляет его, гарантируя выполнение APC без отдельного ResumeThread.

Чек-лист защитника​


  • [ ] Настроить ETW-подписку или EDR-правило на NtQueueApcThread / ZwQueueApcThread / QueueUserAPC с cross-process контекстом.
  • [ ] Отслеживать последовательность VirtualAllocExWriteProcessMemoryQueueUserAPC в рамках одного целевого процесса.
  • [ ] Мониторить создание процессов с флагами CREATE_SUSPENDED и DEBUG_PROCESS, особенно если цель — системный процесс.
  • [ ] Обращать внимание на DebugActiveProcessStop без предшествующего отладочного взаимодействия.
  • [ ] Контролировать переходы прав памяти PAGE_READWRITEPAGE_EXECUTE_READWRITE в процессах, не ожидающих исполняемый код.
  • [ ] Проверять родительские процессы: svchost.exe, созданный не services.exe, — аномалия.
  • [ ] Учитывать, что APC Injection не создаёт новый поток — правила на CreateRemoteThread не покрывают эту технику.
  • [ ] Исключить из алертов штатные cross-process APC от csrss.exe и внутренних APC системных сервисов.

Источники​


 

AtomBombing: статус техники в 2025 году​


Короткий ответ: запатчить нечего, потому что это не уязвимость, а злоупотребление легитимным механизмом Windows. Техника работает и сегодня, но её практическая ценность сильно упала из-за простоты детектирования и ограничений по размеру полезной нагрузки.

Что именно "не запатчено"​


Механизм глобальной таблицы атомов (GlobalAddAtom / GlobalGetAtomName) — штатная часть Windows, используемая для межпроцессного обмена строками. Microsoft не считает это багом, поэтому патча не будет. Однако с момента публикации исследования CyberArk (2019) изменилось другое — уровень детектирования:

  • Современные EDR и AV научились отслеживать паттерн GlobalAddAtomQueueUserAPCGlobalGetAtomName в целевом процессе.
  • Появились сигнатуры на аномальное использование атомов: массовое создание атомов с нечитаемым содержимым, атомы с base64/hex-строками, вызов GlobalGetAtomName из APC-функции.
  • Поведенческие детекторы обращают внимание на связку "атом + APC в чужой процесс" как на индикатор.

Ограничения, которые делают технику менее привлекательной​


  • Размер: GlobalAddAtom принимает строку до 255 байт (для одного атома). Для шеллкода большего размера приходится дробить payload на несколько атомов и конкатенировать их в APC-функции — это добавляет код и артефакты.
  • Кодирование: payload должен быть строкой, поэтому шеллкод обычно кодируют (base64, hex). Это требует дополнительного декодера в целевом процессе.
  • Скорость: работа с атомами медленнее, чем прямая запись в память через WriteProcessMemory.
  • Ограничение на количество атомов: системный лимит (обычно ~16384), но при интенсивном использовании можно упереться в него.

Что изменилось в Windows​


Принципиальных изменений в API атомов не было. Но есть нюансы:

  • В Windows 10/11 GlobalAddAtom и NtAddAtom по-прежнему доступны любому процессу без особых прав.
  • Некоторые EDR ставят user-mode hooks на GlobalAddAtom и GlobalGetAtomName, поэтому для обхода используют нативные вызовы NtAddAtom / NtGetAtomName из ntdll.dll — но это уже не спасает от поведенческого анализа.
  • В новых версиях Windows появились дополнительные телеметрии ETW, которые позволяют видеть cross-process APC независимо от способа передачи данных.

Почему техника всё ещё встречается​


Несмотря на детекты, AtomBombing остаётся в арсенале по нескольким причинам:

  • Обход детектов на WriteProcessMemory: если защита жёстко мониторит запись в чужой процесс, атомы позволяют передать данные без этого вызова.
  • Работает в изолированных средах: если EDR не имеет правил на атомы, техника проходит.
  • Используется в связке с Early Bird: создание процесса с CREATE_SUSPENDED, запись payload через атомы, постановка APC в главный поток — код выполняется до инициализации процесса, что обходит hooks.

Как проверить в лаборатории​


Для воспроизведения и тестирования детектов:

  1. Создай процесс-жертву (например, notepad.exe) с CREATE_SUSPENDED.
  2. Запиши шеллкод (base64) в глобальную таблицу атомов через GlobalAddAtom (или NtAddAtom).
  3. Поставь APC через QueueUserAPC (или NtQueueApcThread) на главный поток, указав функцию-декодер, которая читает атом через GlobalGetAtomName, декодирует и выполняет.
  4. Вызови ResumeThread.

Примерный скелет на C:

C:
// Запись payload в атом
ATOM atom = GlobalAddAtom(base64_payload);

// Постановка APC
QueueUserAPC((PAPCFUNC)decoder_func, hThread, (ULONG_PTR)atom);

// Возобновление потока
ResumeThread(hThread);

В decoder_func нужно вызвать GlobalGetAtomName, декодировать base64 и передать управление на шеллкод.

Признаки для защитника​


  • GlobalAddAtom / NtAddAtom с аргументом, похожим на base64/hex (длинная строка без пробелов).
  • QueueUserAPC / NtQueueApcThread вскоре после GlobalAddAtom в том же процессе.
  • GlobalGetAtomName внутри APC-функции (видно по трассе выполнения).
  • Создание процесса с CREATE_SUSPENDED + последующий ResumeThread без видимой причины.
  • Атомы с именами, содержащими символы, не типичные для легитимных строк (например, +, /, = в base64).

Итог​


AtomBombing не "запатчена", но морально устарела для серьёзных кампаний: слишком много артефактов, легко детектируется, ограничена по размеру. Современные импланты предпочитают прямые syscalls (NtWriteVirtualMemory + NtQueueApcThread) или вообще отказываются от injection в пользу fileless-загрузки через легитимные инструменты (LOLBins). Если цель — обход конкретного EDR, который не мониторит атомы, техника может сработать, но как универсальное решение она неактуальна.
 

Похожие темы

Назад
Верх Низ