Почему direct syscalls усложняют детектирование
Классическая инъекция кода в Windows строится на цепочке Win32 API → Native API → системный вызов. Когда вредонос вызывает
VirtualAllocEx, WriteProcessMemory и CreateRemoteThread, EDR-агент, перехватывающий userland-функции в ntdll.dll или kernel32.dll, видит каждый шаг. Техника direct syscalls обходит этот слой: вместо вызова экспортируемой функции из ntdll.dll код напрямую выполняет инструкцию syscall, подставляя номер системного вызова в регистр eax/rax и передавая аргументы через стек или регистры по конвенции x64.Результат: userland-хуки EDR, установленные на
NtAllocateVirtualMemory, NtWriteVirtualMemory, NtProtectVirtualMemory и NtCreateThreadEx, не срабатывают, потому что исполнение не проходит через тело функции в ntdll.dll. Для защитника это означает потерю одного из самых удобных слоёв телеметрии — но не всех.Типичная последовательность системных вызовов при инъекции
Большинство реализаций классической инъекции через syscalls используют одну и ту же четвёрку вызовов:
| Шаг | Системный вызов | Назначение |
|---|---|---|
| 1 | NtAllocateVirtualMemory | Выделение региона памяти в целевом процессе с правами PAGE_READWRITE |
| 2 | NtWriteVirtualMemory | Запись полезной нагрузки в выделенный регион |
| 3 | NtProtectVirtualMemory | Смена защиты региона на PAGE_EXECUTE_READWRITE |
| 4 | NtCreateThreadEx | Создание потока с точкой входа в записанный код |
Каждый из этих вызовов принимает
ProcessHandle — дескриптор целевого процесса. Если дескриптор не является псевдодескриптором текущего процесса ((HANDLE)-1), значит операция направлена на чужой процесс. Это ключевой артефакт, который сохраняется вне зависимости от способа вызова.Что видит ядро: объектный менеджер и аудит
Системный вызов обрабатывается ядром независимо от того, как он был инициирован — через
ntdll.dll или напрямую инструкцией syscall. Это означает, что следующие механизмы продолжают работать:Object Manager callbacks
Ядро Windows предоставляет механизм
ObRegisterCallbacks, позволяющий драйверам фильтровать операции с объектами (процессами, потоками, дескрипторами). Когда любой код запрашивает дескриптор процесса с правами PROCESS_VM_WRITE | PROCESS_VM_OPERATION, callback может:- Зафиксировать факт запроса и идентификаторы источника и цели.
- Понизить права дескриптора (strip rights), сделав инъекцию невозможной.
- Заблокировать операцию полностью.
Этот механизм не зависит от того, какой API использовался в usermode. Он срабатывает на уровне ядра при любой попытке открыть или использовать дескриптор.
Kernel-mode ETW
Провайдеры ядра генерируют события на уровне системных вызовов. Например:
- Событие создания потока (
NtCreateThreadEx) фиксируется с указанием целевого процесса, адреса точки входа и флагов создания.
- Операции с виртуальной памятью (
NtAllocateVirtualMemory,NtProtectVirtualMemory) могут логироваться через соответствующие kernel ETW providers.
ETW-события ядра не зависят от userland-хуков. Даже если вызов пришёл напрямую через
syscall, ядро генерирует событие до возврата в usermode.Sysmon и драйверные источники
Sysmon (Event ID 8 — CreateRemoteThread, Event ID 10 — ProcessAccess) работает через драйвер ядра и фиксирует:
- Создание потока в чужом процессе с указанием адреса
StartAddress.
- Открытие процесса с подозрительными масками доступа.
Эти события не обходятся direct syscalls, потому что драйвер подписан на объектные операции и создание потоков на уровне ядра.
Что видит EDR: поведенческий анализ и память
Поведенческие цепочки
Современные EDR-решения не полагаются исключительно на хуки конкретных функций. Они строят поведенческие цепочки (behavioral chains):
- Процесс A открывает дескриптор процесса B с правами записи.
- В процессе B появляется новый регион памяти с правами RWX.
- В процессе B создаётся поток, указывающий на этот регион.
Такая последовательность подозрительна независимо от того, какие API использовались на каждом шаге. EDR может обнаружить её через:
- Периодическое сканирование памяти процессов на наличие RWX-регионов.
- Отслеживание изменений в VAD-дереве (Virtual Address Descriptor) через kernel callbacks.
- Анализ стека вызовов потока при создании: если
StartAddressуказывает на регион, не принадлежащий ни одному загруженному модулю, это аномалия.
Сканирование памяти
Даже если инъекция прошла успешно, записанный shellcode остаётся в памяти целевого процесса. EDR-агент с правами ядра может:
- Перечислить регионы памяти всех процессов и найти RWX-страницы.
- Проверить, соответствует ли содержимое региона известным паттернам (например, заголовок MZ или характерные последовательности).
- Обнаружить, что поток выполняется из региона, не привязанного к файлу на диске (unbacked memory).
Thread Start Address
При создании потока через
NtCreateThreadEx ядро сохраняет StartRoutine в структуре ETHREAD. EDR или Sysmon может прочитать это значение и проверить:- Указывает ли адрес на модуль из списка загруженных (легитимно).
- Указывает ли адрес на выделенную память без привязки к модулю (подозрительно).
Артефакты, которые остаются при любых обстоятельствах
| Артефакт | Источник | Почему не обходится direct syscalls |
|---|---|---|
| Дескриптор процесса с правами записи | Object Manager | Ядро фиксирует при любом способе получения |
| RWX-регион в целевом процессе | VAD-дерево, сканирование памяти | Физическое состояние памяти не зависит от способа вызова |
Поток с StartAddress вне модулей | ETHREAD, Sysmon Event ID 8 | Ядро создаёт поток и сохраняет метаданные |
| Изменение защиты памяти | NtProtectVirtualMemory на уровне ядра | Событие генерируется ядром |
| Аномальный стек вызовов при создании потока | Kernel stack walk | Прямой syscall даёт короткий стек без фреймов ntdll |
Ограничения защитника
Несмотря на сохраняющуюся телеметрию, существуют практические сложности:
- Объём событий. Ядро генерирует огромное количество событий. Фильтрация «шума» (легитимные аллокации, JIT-компиляция, загрузчики) требует точных правил и может давать ложные срабатывания.
- Unbacked memory не всегда злокачественна. Легитимные приложения (браузеры, .NET CLR, Java VM) создают исполняемую память без привязки к файлу. Правило «RWX без модуля = инъекция» даёт ложные срабатывания.
- Call stack analysis ограничен. Если атакующий использует технику «stack spoofing» (подделывает стек перед
syscall), анализ стека теряет надёжность.
- Производительность. Постоянное сканирование памяти всех процессов дорого. На практике EDR сканирует выборочно или по триггеру.
Практические правила детектирования для защитника
На основе сохраняющихся артефактов можно строить правила:
Правило 1: подозрительный StartAddress потока
Если
NtCreateThreadEx создаёт поток в процессе, отличном от текущего, и StartAddress не попадает в диапазон адресов ни одного загруженного модуля целевого процесса — это высокорелевантный индикатор.Правило 2: последовательность аллокация → запись → смена защиты → поток
Корреляция четырёх событий в коротком временном окне (менее нескольких секунд) для одного и того же целевого процесса и одного региона памяти.
Правило 3: NtProtectVirtualMemory с переходом в PAGE_EXECUTE_READWRITE
Смена защиты на RWX сама по себе не является злокачественной, но в сочетании с предшествующей записью данных из другого процесса — сильный сигнал.
Правило 4: дескриптор процесса с PROCESS_VM_WRITE из неожиданного источника
Если процесс, не являющийся отладчиком, системным сервисом или известным инструментом, открывает другой процесс с правами записи в память — это требует расследования.
Что не поможет: распространённые заблуждения
- Хуки на
ntdll.dllв usermode. Direct syscalls полностью их обходят. Полагаться на них как единственный слой — ошибка.
- Сигнатуры на shellcode в памяти. Атакующий может шифровать payload и расшифровывать его непосредственно перед выполнением, что делает статические сигнатуры бесполезными.
- Мониторинг только
CreateRemoteThread.NtCreateThreadEx— не единственный способ запуска кода. APC-инъекция (NtQueueApcThread), hijacking существующего потока черезNtSetContextThreadи другие техники не создают новый поток.
Связь с MITRE ATT&CK
Техника отображается как T1055 — Process Injection с подтехниками:
- T1055.003 — Thread Execution Hijacking
- T1055.012 — Process Hollowing
- T1055.001 — Dynamic-link Library Injection
Использование direct syscalls не меняет классификацию техники, но влияет на слой детектирования: защитник должен опираться на kernel-level телеметрию, а не на userland-хуки.
Итоговая карта слоёв телеметрии
| Слой | Видимость при direct syscalls | Надёжность |
|---|---|---|
| Userland-хуки (ntdll, kernel32) | Не видят | Низкая |
| ETW kernel providers | Видят | Средняя–Высокая |
| Object Manager callbacks | Видят | Высокая |
| Sysmon (драйвер) | Видит | Высокая |
| Сканирование памяти (RWX, unbacked) | Видит | Средняя (ложные срабатывания) |
| Call stack analysis | Частично | Средняя (обходится stack spoofing) |
| Поведенческая корреляция | Видит | Высокая при правильной настройке |
Защитник, который строит детектирование только на userland-хуках, теряет видимость при использовании direct syscalls. Перенос фокуса на ядерные источники телеметрии — Object Manager callbacks, kernel ETW, анализ VAD-дерева и поведенческую корреляцию — позволяет сохранять обнаружение даже при обходе usermode-слоя.
Источники
- Цикл статей "Изучение вредоносных программ" | Предельная техника-2. Практика. Реализуем техники инъекции через сисколы | Osint42.Org - Безопасность и код
- Process Injection, Technique T1055 - Enterprise | MITRE ATT&CK®
- The Linux Kernel documentation — The Linux Kernel documentation
- Welcome to QEMU’s documentation! — QEMU documentation
