Инъекция shellcode в процесс: что меняется в памяти и потоке выполнения с точки зрения защитника

Любая инъекция кода в чужой процесс на Windows проходит через строго определённые механизмы управления памятью и планирования потоков. Ядро фиксирует каждое выделение региона, каждое изменение прав страницы и каждое создание потока в структурах данных, доступных для наблюдения. Для защитника это означает: полностью бесследная инъекция невозможна в принципе — вопрос лишь в том, на каком уровне и с какой задержкой эти следы будут обнаружены.

Что происходит при классической инъекции​


Типичный сценарий инъекции shellcode в чужой процесс состоит из последовательности вызовов WinAPI, каждый из которых оставляет наблюдаемый артефакт:

  1. Перечисление процессовCreateToolhelp32Snapshot с флагом TH32CS_SNAPPROCESS, затем итерация через Process32First/Process32Next.
  2. Открытие целевого процессаOpenProcess с правами PROCESS_ALL_ACCESS или комбинацией PROCESS_VM_WRITE | PROCESS_VM_OPERATION | PROCESS_CREATE_THREAD.
  3. Выделение памятиVirtualAllocEx с флагами MEM_COMMIT | MEM_RESERVE и начальными правами PAGE_READWRITE.
  4. Запись payloadWriteProcessMemory копирует shellcode в выделенный регион.
  5. Смена прав страницыVirtualProtectEx переводит регион в PAGE_EXECUTE_READWRITE (RWX).
  6. Создание удалённого потокаCreateRemoteThread с адресом shellcode в качестве стартовой рутины.

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

Артефакты в виртуальной памяти процесса​


VAD-дерево и новый регион​


Windows отслеживает все выделенные регионы виртуальной памяти через структуру VAD (Virtual Address Descriptor). Каждый вызов VirtualAllocEx создаёт новый узел в VAD-дереве целевого процесса. Для защитника это означает:

  • Появление региона с типом MEM_PRIVATE (не привязан к файлу на диске, не является mapped image).
  • Размер региона обычно мал — от нескольких сотен байт до нескольких килобайт, что нетипично для легитимных выделений.
  • Регион не имеет backing file: в MEMORY_BASIC_INFORMATION поле Type равно MEM_PRIVATE, а AllocationBase указывает на сам регион.

EDR-системы и инструменты вроде Volatility могут перечислить VAD-дерево и найти регионы без backing file с подозрительными правами.

Права страниц: переход в RWX​


Ключевой индикатор — смена защиты страниц на PAGE_EXECUTE_READWRITE. Легитимное ПО крайне редко использует RWX-регионы:

  • Компиляторы и линковщики разделяют секции: .text — RX, .data — RW, .rdata — R.
  • JIT-компиляторы (CLR, V8) выделяют память как RW, записывают код, затем переключают в RX — но не оставляют RWX.
  • Регион, который одновременно доступен на запись и исполнение, — сильный сигнал аномалии.

Переход PAGE_READWRITE → PAGE_EXECUTE_READWRITE через VirtualProtectEx фиксируется ядром. EDR-системы, работающие на уровне kernel-mode драйвера, могут перехватывать этот переход через хуки на NtProtectVirtualMemory или через подписку на соответствующие ETW-события. Конкретный набор доступных ETW-провайдеров для отслеживания изменений прав памяти зависит от версии Windows и уровня привилегий подписчика.

Содержимое региона​


Если защитник получает дамп памяти процесса, shellcode-регион можно идентифицировать по содержимому:

  • Высокая энтропия (если payload зашифрован или упакован).
  • Паттерны, характерные для position-independent кода: отсутствие абсолютных адресов, использование call $+5 / pop для получения EIP/RIP, обращение к PEB через gs:[0x60] (x64) или fs:[0x30] (x86).
  • Отсутствие валидных заголовков PE в начале региона.

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


Аномальный адрес старта потока​


При создании потока через CreateRemoteThread ядро заполняет структуру ETHREAD, включая поле StartAddress. Для легитимных потоков процесса этот адрес обычно указывает:

  • На код внутри загруженного модуля (.exe или .dll), зарегистрированного в PEB.
  • На известную системную функцию (RtlUserThreadStart, BaseThreadInitThunk).

Поток, чей StartAddress указывает на регион MEM_PRIVATE без backing file, — прямой индикатор инъекции. EDR-системы проверяют это при каждом событии создания потока.

Контекст создания потока​


CreateRemoteThread создаёт поток из контекста другого процесса. Ядро фиксирует:

  • ClientId процесса-создателя.
  • ClientId целевого процесса.
  • Дескриптор, через который выполнен вызов.

Если процесс A создаёт поток в процессе B, и при этом B не является дочерним по отношению к A, не является системным сервисом и не входит в известный паттерн (например, csrss.exe, svchost.exe), это вызывает подозрение.

Состояние потока до и после инъекции​


В некоторых реализациях атакующий создаёт поток в приостановленном состоянии (CREATE_SUSPENDED), чтобы модифицировать контекст через SetThreadContext перед возобновлением. В этом случае артефактом служит:

  • Поток, созданный с флагом CREATE_SUSPENDED.
  • Последующий вызов SetThreadContext, меняющий RIP/EIP на адрес вне загруженных модулей.
  • Вызов ResumeThread.

Артефакты на уровне дескрипторов​


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


OpenProcess с PROCESS_ALL_ACCESS (0x1FFFFF) — редкость для легитимного ПО. Чаще встречаются комбинации:

ПраваТипичный легитимный сценарий
PROCESS_QUERY_INFORMATIONМониторинг, диспетчер задач
PROCESS_VM_READОтладчик, profiler
PROCESS_VM_WRITE + PROCESS_VM_OPERATION + PROCESS_CREATE_THREADИнъекция

EDR-системы, подписанные на ObRegisterCallbacks, видят каждый запрос на открытие дескриптора процесса и могут оценить запрошенные права.

Последовательность операций​


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

Код:
OpenProcess → VirtualAllocEx → WriteProcessMemory → VirtualProtectEx → CreateRemoteThread

Корреляция этих событий по ProcessId источника и цели, по временной метке и по адресному пространству — основа поведенческого детекта.

Что видит защитник через ETW и kernel callbacks​


ETW-провайдеры​


Несколько встроенных провайдеров Windows генерируют события, релевантные для детекта инъекции:

  • Microsoft-Windows-Kernel-Process — события создания потоков, включая StartAddress.
  • Microsoft-Windows-Threat-Intelligence (PPL-protected) — расширенные события для EDR-вендоров, включая VirtualAllocEx, WriteProcessMemory, CreateRemoteThread с контекстом вызывающего процесса. Доступ к этому провайдеру ограничен защитой PPL (Protected Process Light), что делает его недоступным для пользовательских утилит без соответствующей подписи.

Kernel callbacks​


  • PsSetCreateThreadNotifyRoutine / PsSetCreateThreadNotifyRoutineEx — уведомление при создании потока, включая удалённые потоки. Позволяет EDR-драйверу проверить StartAddress нового потока и сопоставить его с загруженными модулями целевого процесса.
  • ObRegisterCallbacks — фильтрация запросов на открытие дескрипторов процессов и потоков. Позволяет оценить запрошенные права доступа до того, как дескриптор будет выдан.
  • PsSetLoadImageNotifyRoutine — если shellcode пытается загрузить DLL через LoadLibrary в качестве payload, это событие также фиксируется.

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


При расследовании подозрительного процесса аналитик может проверить:

  1. VAD-дерево — через WinDbg (!vad), Volatility (vadinfo, malfind) или Sysinternals VMMap. Искать регионы MEM_PRIVATE с правами RWX.
  2. Потоки процесса — через Process Explorer, WinDbg (!thread) или API. Проверять StartAddress каждого потока: если он не попадает в диапазон загруженных модулей, это аномалия.
  3. Дескрипторы — через Process Explorer (вкладка Handles) или NtQuerySystemInformation. Искать дескрипторы процессов с подозрительными правами.
  4. ETW-трассировка — собрать трассировку через logman или xperf с включённым провайдером Kernel-Process и проанализировать события создания потоков.

Ограничения и обходы, о которых нужно знать​


Атакующие используют техники, усложняющие детект:

  • Разделение этапов: VirtualAllocEx и WriteProcessMemory выполняются из одного процесса, а CreateRemoteThread — из другого.
  • Использование легитимных потоков: вместо создания нового потока атакующий приостанавливает существующий, меняет его контекст через SetThreadContext и возобновляет. Новый поток не создаётся, но StartAddress в ETHREAD не меняется — меняется только регистр RIP в сохранённом контексте.
  • APC-инъекция: QueueUserAPC к существующему потоку в alertable состоянии. Не требует CreateRemoteThread, но требует, чтобы целевой поток находился в alertable wait.
  • Memory-mapped секции: вместо VirtualAllocEx + WriteProcessMemory используется NtCreateSection + NtMapViewOfSection с правами PAGE_EXECUTE_READWRITE. VAD-запись будет иметь тип MEM_MAPPED, но backing file может быть в секции \KnownDlls или в pagefile.

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

Итоговая карта артефактов​


Этап инъекцииАртефактГде наблюдать
Перечисление процессовSnapshot handle, итерацияETW Kernel-Process, EDR behavioral
OpenProcessДескриптор с PROCESS_ALL_ACCESSObRegisterCallbacks, Process Explorer
VirtualAllocExVAD-узел MEM_PRIVATEVAD-дерево, Volatility malfind
WriteProcessMemoryСодержимое региона без backing fileДамп памяти, YARA in-memory scan
VirtualProtectEx → RWXСмена прав страницыEDR kernel hook, ETW (зависит от версии)
CreateRemoteThreadПоток с StartAddress вне модулейPsSetCreateThreadNotifyRoutine, Process Explorer

Источники​


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