Process Injection в Windows: механизм на уровне концепции и артефакты для детектирования

Process Injection — техника выполнения произвольного кода в адресном пространстве другого живого процесса. В Windows она эксплуатирует легитимные API операционной системы, что делает её одновременно мощным инструментом атаки и сложной целью для детектирования: выполнение маскируется под деятельность доверенного процесса.

Концептуальная модель​


Каждый процесс в Windows обладает собственным виртуальным адресным пространством. Операционная система предоставляет набор API, позволяющих одному процессу взаимодействовать с памятью другого — при наличии соответствующих прав. Именно эти API становятся основой инъекции.

Обобщённая последовательность действий при классической DLL-инъекции:

  1. Найти целевой процесс и получить его дескриптор с достаточными правами.
  2. Выделить область памяти в адресном пространстве цели.
  3. Записать в эту область полезные данные (например, путь к DLL).
  4. Создать поток в целевом процессе, который выполнит записанный код.

После завершения этих шагов код выполняется в контексте чужого процесса, наследуя его привилегии, сетевые ресурсы и репутацию.

Классическая последовательность API-вызовов​


Ниже разобран наиболее распространённый вариант — DLL Injection через LoadLibraryW. Понимание этой цепочки необходимо для построения правил детектирования.

Перечисление процессов​


Для поиска цели используется CreateToolhelp32Snapshot с флагом TH32CS_SNAPPROCESS, который создаёт снимок всех выполняющихся процессов. Далее через Process32First и Process32Next происходит итерация по снимку. Каждый процесс описывается структурой PROCESSENTRY32, содержащей идентификатор процесса (th32ProcessID), имя исполняемого файла (szExeFile) и другие поля.

C:
PROCESSENTRY32 Proc = { .dwSize = sizeof(PROCESSENTRY32) };
HANDLE hSnapShot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, NULL);
Process32First(hSnapShot, &Proc);
// итерация через Process32Next...

Получение дескриптора целевого процесса​


Когда нужный процесс найден по имени, вызывается OpenProcess с флагом PROCESS_ALL_ACCESS. Этот флаг запрашивает максимальные права на процесс, включая запись в память и создание потоков.

C:
HANDLE hProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, Proc.th32ProcessID);

Сам по себе вызов OpenProcess с PROCESS_ALL_ACCESS к чужому процессу — уже подозрительный сигнал для EDR.

Выделение памяти и запись данных​


В адресном пространстве целевого процесса выделяется область памяти через VirtualAllocEx с флагами MEM_COMMIT | MEM_RESERVE и защитой PAGE_READWRITE. Затем через WriteProcessMemory в эту область записывается путь к DLL, которую нужно загрузить.

C:
LPVOID pAddress = VirtualAllocEx(hProcess, NULL, dwSizeToWrite,
                                 MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
WriteProcessMemory(hProcess, pAddress, DllName, dwSizeToWrite,
                   &lpNumberOfBytesWritten);

Создание удалённого потока​


Финальный шаг — CreateRemoteThread. Точкой входа потока указывается адрес функции LoadLibraryW (полученный через GetProcAddress из kernel32.dll), а аргументом — адрес выделенной памяти с путём к DLL.

C:
LPVOID pLoadLibraryW = GetProcAddress(GetModuleHandle(L"kernel32.dll"),
                                      "LoadLibraryW");
HANDLE hThread = CreateRemoteThread(hProcess, NULL, NULL,
                                    pLoadLibraryW, pAddress, NULL, NULL);

После этого целевой процесс загружает указанную DLL, и её код начинает выполняться в его контексте.

Зачем атакующие используют инъекцию​


MITRE ATT&CK классифицирует Process Injection как технику T1055. Основные причины применения:

  • Обход защитных механизмов. Код выполняется внутри легитимного процесса, что затрудняет обнаружение сигнатурными и поведенческими средствами.
  • Доступ к ресурсам процесса. Внедрённый код получает доступ к памяти, сетевым соединениям и дескрипторам целевого процесса.
  • Повышение привилегий. Если целевой процесс работает с повышенными правами (например, системный сервис), внедрённый код наследует эти привилегии.
  • Сегментация модулей. Продвинутые образцы выполняют множественные инъекции, распределяя компоненты по разным процессам и используя именованные каналы или другие механизмы IPC для связи между ними.

Типичные целевые процессы в реальных атаках​


Анализ задокументированных кампаний показывает устойчивые паттерны выбора целей:

| Целевой процесс | Примеры использования |
|---|---|
| svchost.exe | BlackEnergy (Sandworm, атака на энергосистему Украины 2015), BlackByte, ABK, Avenger, BBK, Clambling, Pandora |
| explorer.exe | APT38, Backdoor.Oldrea, Kimsuky (Win7Elevate), Mis-Type, QakBot |
| iexplore.exe | Sandworm (через svchost → iexplore для C2), APT41 (TIDYELF → WINTERLOVE), Egregor, Smoke Loader |
| Браузеры (Chrome, Firefox, Edge) | AppleJeus/VEILEDSIGNAL (атака на цепочку поставок 3CX) — инъекция C2-модуля в первый найденный экземпляр браузера |
| wuauclt.exe | ANDROMEDA, BlackByte, NOOPLDR |
| notepad.exe | ROKRAT (через VirtualAlloc + WriteProcessMemory + CreateRemoteThread), NETWIRE |
| lsass.exe | JPIN — загрузка модуля в процесс аутентификации |
| regsvcs.exe, msbuild.exe, installutil.exe | TA2541 — инъекция в .NET-процессы |
| wermgr.exe | TrickBot (через Native API функции Nt*), QakBot |

Выбор цели не случаен: атакующие предпочитают процессы, которые редко вызывают подозрение у аналитиков, имеют сетевой доступ или работают с высокими привилегиями.

Артефакты для детектирования​


Детектирование строится на корреляции нескольких событий, каждое из которых по отдельности может быть легитимным.

Последовательность API-вызовов​


Ключевой индикатор — цепочка вызовов в рамках одного процесса, направленная на другой процесс:

  1. VirtualAllocEx — выделение памяти в удалённом процессе.
  2. WriteProcessMemory — запись данных в эту память.
  3. CreateRemoteThread — создание потока с точкой входа в выделенной области.

EDR-решения отслеживают эту последовательность как единый паттерн. Вызов CreateRemoteThread сам по себе встречается в легитимном ПО (например, отладчики), но в сочетании с VirtualAllocEx и WriteProcessMemory он становится высокоспецифичным индикатором.

Подозрительные параметры вызовов​


  • OpenProcess с PROCESS_ALL_ACCESS к процессу, не являющемуся дочерним.
  • VirtualAllocEx с защитой PAGE_READWRITE в чужом процессе — особенно если выделенная область затем используется как аргумент для CreateRemoteThread.
  • CreateRemoteThread, где адрес точки входа указывает на только что выделенную память, а не на код загруженного модуля.

Аномальная загрузка DLL​


Если в процессе появляется DLL, загруженная из нестандартного пути (временные каталоги, AppData, сетевые шары), это повод для проверки. Особенно подозрительно, когда DLL загружается в процесс, который обычно не использует динамические библиотеки из пользовательских каталогов.

Поведенческие признаки​


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

Специфичные варианты API​


Некоторые реализации используют альтернативные API: RtlCreateUserThread вместо CreateRemoteThread (задокументировано для BADHATCH), VirtualAllocExNuma вместо VirtualAllocEx (Bazar), Native API функции Nt* вместо Win32-обёрток (TrickBot). Правила детектирования должны покрывать и эти варианты.

Меры снижения риска​


  • Attack Surface Reduction (ASR). На Windows 10 правила ASR способны блокировать инъекцию кода из офисных приложений. Это не универсальная защита, но она закрывает один из распространённых векторов начального доступа.
  • Ограничение прав процессов. Минимизация привилегий, с которыми работают пользовательские приложения, снижает вероятность успешного вызова OpenProcess с PROCESS_ALL_ACCESS.
  • Мониторинг и корреляция. Ни один отдельный артефакт не даёт надёжного детектирования. Эффективная защита строится на корреляции событий: последовательность API-вызовов, аномальные загрузки DLL, сетевое поведение процесса после инъекции.

Чек-лист для аналитика​


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

  • [ ] Есть ли в логах последовательность VirtualAllocExWriteProcessMemoryCreateRemoteThread (или их Native API аналоги)?
  • [ ] Какой процесс является источником вызовов и какой — целью? Связаны ли они родительско-дочерним отношением?
  • [ ] Загружена ли в целевой процесс DLL из нестандартного расположения?
  • [ ] Появилась ли у целевого процесса новая сетевая активность после момента инъекции?
  • [ ] Использовались ли альтернативные API (RtlCreateUserThread, VirtualAllocExNuma, Nt*-функции)?
  • [ ] Наблюдаются ли множественные инъекции из одного источника в разные процессы?

Источники​


 
Думаю тема до конца не расскрыта, не сказано как боевые вирусы обходят детекты таких инжектов, ведь это легко обнаружить.
 

Обход детектирования Process Injection: техники и артефакты​


Классическая цепочка VirtualAllocEx → WriteProcessMemory → CreateRemoteThread действительно хорошо детектируется. Современные образцы используют альтернативные пути, которые обходят конкретные сигнатуры, но оставляют другие артефакты.

────────────────────

1. Прямые системные вызовы (Direct Syscalls)​


Проблема: EDR ставит хуки на userland-обёртки (ntdll.dll!NtAllocateVirtualMemory, NtWriteVirtualMemory, NtCreateThreadEx). Вызов через kernel32!VirtualAllocEx проходит через хук и логируется.

Обход: Малварь вызывает системные вызовы напрямую, минуя ntdll.dll:

Код:
; Пример прямого syscall для NtAllocateVirtualMemory (x64)
mov r10, rcx          ; первый аргумент
mov eax, 0x18         ; syscall number для NtAllocateVirtualMemory (Win10 22H2)
syscall
ret

Номера syscall меняются между сборками Windows, поэтому образцы либо:
  • хардкодят номер под целевую версию;
  • парсят ntdll.dll на диске (не в памяти, где хуки) для извлечения номера;
  • используют техники типа Hell's Gate / Halo's Gate / Tartarus Gate для динамического разрешения номеров.

Остающиеся артефакты:
  • syscall из региона, не являющегося ntdll.dll — аномалия для EDR с kernel-коллбэками;
  • ETW-провайдер Microsoft-Windows-Kernel-Audit-API-Calls фиксирует syscall на уровне ядра независимо от userland-хуков;
  • Callback PsSetCreateThreadNotifyRoutine в ядре видит создание потока независимо от способа вызова.

────────────────────

2. APC Injection (Asynchronous Procedure Call)​


Механизм: Вместо создания нового потока малварь ставит APC в очередь существующего потока целевого процесса. Когда поток входит в alertable state, APC выполняется.

C:
// Псевдокод
HANDLE hThread = OpenThread(THREAD_SET_CONTEXT | THREAD_SUSPEND_RESUME, ...);
QueueUserAPC((PAPCFUNC)shellcode_addr, hThread, (ULONG_PTR)param);
// или через NtQueueApcThread для обхода хуков

Вариант Early Bird APC: создаётся приостановленный процесс (CREATE_SUSPENDED), в его главный поток ставится APC, затем процесс возобновляется. APC выполняется до точки входа основного образа.

Почему обходит классический детект:
  • Нет CreateRemoteThread — ключевой триггер многих правил отсутствует;
  • QueueUserAPC сам по себе легитимен (используется в I/O).

Артефакты для детектирования:
  • NtQueueApcThread / NtQueueApcThreadEx к потоку чужого процесса;
  • Последовательность OpenThread(THREAD_SET_CONTEXT)QueueUserAPC к потоку несвязанного процесса;
  • Для Early Bird: CreateProcess с CREATE_SUSPENDEDQueueUserAPCResumeThread — подозрительная цепочка;
  • Kernel callback PsSetCreateThreadNotifyRoutine не помогает (поток уже существует), но ObRegisterCallbacks может логировать OpenThread с подозрительными правами.

────────────────────

3. Process Hollowing (RunPE)​


Механизм:
  1. Создаётся легитимный процесс в приостановленном состоянии (CREATE_SUSPENDED).
  2. Оригинальный образ выгружается из памяти (NtUnmapViewOfSection).
  3. На его место маппится вредоносный PE.
  4. Контекст главного потока корректируется (новый entry point).
  5. Поток возобновляется.

Почему обходит детект:
  • Нет CreateRemoteThread, VirtualAllocEx в чужой процесс в классическом смысле;
  • Процесс выглядит как легитимный (правильный PID, имя, родитель).

Артефакты:
  • NtUnmapViewOfSection на image section процесса — крайне редкая операция в легитимном ПО;
  • Несоответствие между VAD (Virtual Address Descriptor) и содержимым памяти: VAD указывает на оригинальный image path, но содержимое секций не совпадает с файлом на диске;
  • NtQueryVirtualMemory + сравнение с файлом на диске — основа многих EDR-проверок;
  • ETW Microsoft-Windows-Kernel-Process фиксирует ProcessStart с флагом suspended.

────────────────────

4. Process Doppelgänging / Process Herpaderping​


Doppelgänging: использует транзакции NTFS для подмены образа процесса до его создания. Файл записывается в транзакцию, из транзакции создаётся section, секция маппится в новый процесс, транзакция откатывается — файл на диске остаётся чистым.

Herpaderping: образ записывается в файл, из него создаётся процесс, затем файл перезаписывается мусором. Процесс уже запущен с вредоносным кодом, но на диске файл выглядит безобидно.

Артефакты:
  • Для Doppelgänging: NtCreateTransactionNtCreateFileNtCreateSectionNtCreateProcessExNtRollbackTransaction — подозрительная цепочка;
  • Для Herpaderping: несоответствие между image на диске и содержимым памяти процесса;
  • Оба варианта детектируются через VAD-анализ и сравнение in-memory image с on-disk file.

────────────────────

5. Module Stomping (DLL Overwrite Injection)​


Механизм: вместо выделения новой памяти (VirtualAllocEx) малварь перезаписывает содержимое уже загруженной DLL в целевом процессе.

C:
// Находим адрес загруженного модуля в целевом процессе
// Перезаписываем его .text секцию своим кодом
WriteProcessMemory(hProcess, pModuleBase, shellcode, size, &written);
// Создаём поток с точкой входа на перезаписанный модуль
CreateRemoteThread(hProcess, NULL, 0, pModuleBase, NULL, 0, NULL);

Почему обходит детект:
  • Нет VirtualAllocEx — память уже выделена и имеет права на исполнение;
  • VAD показывает легитимный DLL path;
  • Некоторые EDR проверяют только newly allocated RWX regions.

Артефакты:
  • WriteProcessMemory в регион, принадлежащий загруженному модулю — аномалия;
  • Хеш содержимого модуля в памяти не совпадает с хешем файла на диске;
  • CreateRemoteThread с точкой входа на адрес внутри модуля, но не на экспортную функцию.

────────────────────

6. Thread Execution Hijacking​


Механизм: вместо создания нового потока малварь приостанавливает существующий поток целевого процесса, модифицирует его контекст (регистр RIP/EIP указывает на shellcode), и возобновляет.

C:
SuspendThread(hThread);
GetThreadContext(hThread, &ctx);
ctx.Rip = (DWORD64)shellcode_addr;
SetThreadContext(hThread, &ctx);
ResumeThread(hThread);

Почему обходит детект:
  • Нет CreateRemoteThread;
  • Нет нового потока в системе — существующий поток просто меняет точку выполнения.

Артефакты:
  • SuspendThreadSetThreadContextResumeThread к потоку чужого процесса;
  • SetThreadContext с изменением instruction pointer на адрес вне загруженных модулей;
  • Kernel callback PsSetCreateThreadNotifyRoutine не срабатывает (поток не создаётся), но ObRegisterCallbacks логирует OpenThread с THREAD_SET_CONTEXT.

────────────────────

7. Reflective DLL Loading​


Механизм: DLL не загружается через LoadLibrary. Вместо этого малварь сама парсит PE-заголовок, разрешает импорты, применяет релокации и вызывает DllMain — всё в памяти, без записи на диск и без записи в PEB loaded module list.

Почему обходит детект:
  • DLL отсутствует в списке загруженных модулей процесса (EnumProcessModules не видит);
  • Нет записи на диске;
  • Нет событий загрузки DLL в ETW.

Артефакты:
  • Регион памяти с правами PAGE_EXECUTE_READWRITE, не привязанный к VAD с image path;
  • PE-заголовок (MZ) в приватной памяти процесса;
  • EDR сканирует память процессов на наличие PE-сигнатур в RWX/RX регионах без backing file.

────────────────────

8. Mapping Injection (NtMapViewOfSection)​


Механизм: вместо VirtualAllocEx + WriteProcessMemory малварь создаёт shared section object и маппит его в целевой процесс.

C:
// Создаём section
NtCreateSection(&hSection, SECTION_ALL_ACCESS, NULL, &maxSize,
                PAGE_EXECUTE_READWRITE, SEC_COMMIT, NULL);
// Маппим в целевой процесс
NtMapViewOfSection(hSection, hTargetProcess, &pRemoteBase, ...);
// Записываем shellcode через локальный mapping
NtMapViewOfSection(hSection, NtCurrentProcess(), &pLocalBase, ...);
memcpy(pLocalBase, shellcode, size);
// Создаём поток
NtCreateThreadEx(&hThread, ..., hTargetProcess, pRemoteBase, ...);

Почему обходит детект:
  • Нет VirtualAllocEx и WriteProcessMemory — двух ключевых триггеров;
  • Данные записываются через локальный mapping, а не через WriteProcessMemory.

Артефакты:
  • NtCreateSection с PAGE_EXECUTE_READWRITE + SEC_COMMIT;
  • NtMapViewOfSection в чужой процесс с правами на исполнение;
  • Section object без backing file (не из image file);
  • NtCreateThreadEx с точкой входа на адрес из shared section.

────────────────────

9. Atom Bombing​


Механизм: использует Global Atom Table как канал передачи данных между процессами. Shellcode записывается в atom, затем через APC в целевом процессе вызывается GlobalGetAtomName, который копирует данные в память цели.

Артефакты:
  • NtAddAtom / GlobalAddAtom с подозрительно большим размером данных;
  • QueueUserAPC с GlobalGetAtomName как callback;
  • Крайне редкая комбинация в легитимном ПО.

────────────────────

Сводка: что остаётся для детектирования​


Независимо от техники обхода, фундаментальные артефакты сохраняются на уровне ядра:

  • Kernel callbacks: PsSetCreateProcessNotifyRoutine, PsSetCreateThreadNotifyRoutine, PsSetLoadImageNotifyRoutine, ObRegisterCallbacks — видят операции независимо от userland-хуков.
  • ETW (Event Tracing for Windows): провайдеры Microsoft-Windows-Kernel-Process, Microsoft-Windows-Kernel-Audit-API-Calls, Microsoft-Windows-DotNETRuntime фиксируют события на уровне ядра.
  • VAD analysis: несоответствие между VAD metadata и фактическим содержимым памяти.
  • Memory scanning: поиск PE-заголовков, shellcode-паттернов, RWX-регионов без backing file.
  • Behavioral correlation: ни одна отдельная API-вызов не является доказательством, но цепочка операций + контекст (кто, куда, зачем) даёт высокую уверенность.

────────────────────

Практический вывод для blue team​


Если ваше детектирование строится только на CreateRemoteThread + VirtualAllocEx — оно обходится тривиально. Эффективная защита требует:

  1. Kernel-level visibility (ETW, callbacks, driver);
  2. Memory integrity checks (VAD vs. actual content);
  3. Behavioral correlation (не один вызов, а цепочка + контекст);
  4. Coverage альтернативных API (NtQueueApcThread, NtMapViewOfSection, NtCreateSection, SetThreadContext, NtCreateThreadEx).
 
Назад
Верх Низ