Что происходит при системном вызове на Windows
Каждый запрос пользовательского режима к ядру Windows проходит через инструкцию
syscall (x64) или sysenter (x86). Библиотека ntdll.dll содержит тонкие обёртки — так называемые системные вызовы-заглушки (syscall stubs), каждая из которых выполняет строго определённую последовательность:
Код:
NtAllocateVirtualMemory PROC
mov r10, rcx ; первый аргумент в r10 (конвенция syscall)
mov eax, <SSN> ; System Service Number — индекс в таблице вызовов
syscall ; переход в ядро (ring 0)
ret
NtAllocateVirtualMemory ENDP
SSN (System Service Number) — это числовой идентификатор, по которому ядро находит обработчик в
System Service Descriptor Table (SSDT). Значение SSN для одной и той же функции меняется от версии к версии Windows и даже между сборками. Например, NtMapViewOfSection имеет SSN 0x25 на Windows 7 SP1, 0x26 на Windows 8, 0x27 на Windows 8.1 и 0x28 на всех сборках Windows 10 начиная с 1507.Почему атакующие уходят от ntdll.dll
EDR-агенты размещают хуки в пользовательском режиме именно на этих заглушках в
ntdll.dll. Типичный хук заменяет первые байты функции на jmp к коду агента, который логирует аргументы вызова, проверяет их на подозрительные паттерны и только затем передаёт управление оригиналу.Direct syscall — техника, при которой код вызывает
syscall напрямую, минуя заглушку в ntdll.dll. Если хук стоит на заглушке, а вызов происходит из другого места, хук не срабатывает. Это делает технику привлекательной для обхода userland-хуков.Три поколения техник получения SSN
Статическая таблица по версиям ОС
Самый ранний подход: захардкодить таблицу SSN для каждой версии и сборки Windows. Код читает PEB (Process Environment Block) через
gs:[60h], определяет major version, minor version и build number, затем выбирает соответствующий SSN из предзаполненной таблицы.
Код:
mov rax, gs:[60h] ; PEB
cmp dword ptr [rax+118h], 10 ; major version = 10?
je Check_Build_10
; ...
Недостаток очевиден: таблица устаревает с каждой новой сборкой, а на неизвестной версии вызов просто не выполняется.
Парсинг заглушек в рантайме (Hell's Gate)
Техника Hell's Gate читает байты заглушек
ntdll.dll в памяти процесса и извлекает SSN прямо из инструкции mov eax, <SSN>. Паттерн для поиска:0x4c 0x8b 0xd1—mov r10, rcx
0xb8— началоmov eax, imm32
- Байты на смещении +4 и +5 от
0xb8содержат SSN в little-endian
C:
if (*((PBYTE)pFunctionAddress + cw) == 0x4c &&
*((PBYTE)pFunctionAddress + 1 + cw) == 0x8b &&
*((PBYTE)pFunctionAddress + 2 + cw) == 0xd1 &&
*((PBYTE)pFunctionAddress + 3 + cw) == 0xb8) {
BYTE high = *((PBYTE)pFunctionAddress + 5 + cw);
BYTE low = *((PBYTE)pFunctionAddress + 4 + cw);
pVxTableEntry->wSystemCall = (high << 8) | low;
}
Если EDR заменил первые байты заглушки на
jmp, парсер сдвигает окно поиска вперёд, пока не найдёт оригинальный паттерн или не упрётся в syscall (0x0f 0x05) либо ret (0xc3).Хеш-таблицы и динамическое разрешение (SysWhispers)
Инструмент SysWhispers генерирует ассемблерный код, который вычисляет SSN по хешу имени функции. Хеш передаётся в функцию
SW3_GetSyscallNumber, которая ищет совпадение в предзаполненном списке. Дополнительно SW3_GetRandomSyscallAddress возвращает адрес инструкции syscall из случайной заглушки ntdll.dll, чтобы адрес возврата после syscall указывал внутрь ntdll.dll, а не на подозрительный регион.
Код:
mov ecx, 01A80161Bh ; хеш имени функции
call SW3_GetRandomSyscallAddress ; адрес syscall-инструкции в ntdll
mov r15, rax
call SW3_GetSyscallNumber ; SSN в eax
mov r10, rcx
jmp r15 ; переход на syscall внутри ntdll
Где EDR наблюдает за direct syscalls
Direct syscall обходит userland-хуки, но не делает процесс невидимым. Современные EDR-агенты используют несколько слоёв наблюдения, которые работают ниже уровня заглушек.
Callback-механизмы ядра
Ядро Windows предоставляет легитимные точки уведомления, которые EDR регистрирует из своего драйвера:
- Process/Thread/Image notification callbacks (
PsSetCreateProcessNotifyRoutine,PsSetCreateThreadNotifyRoutine,PsSetLoadImageNotifyRoutine) — срабатывают при создании процессов, потоков и загрузке модулей независимо от того, как именно был выполнен системный вызов.
- Object callbacks (
ObRegisterCallbacks) — позволяют фильтровать операции с объектами ядра (процессы, потоки, файлы) до их завершения.
- Minifilter-драйверы для файлового ввода-вывода.
Эти механизмы перехватывают результат системного вызова на уровне ядра, поэтому неважно, через
ntdll.dll или напрямую был выполнен syscall.ETW (Event Tracing for Windows)
ETW-провайдеры ядра генерируют события при ключевых операциях: выделение памяти, создание потоков, загрузка образов. EDR-агент подписывается на эти провайдеры и получает телеметрию асинхронно, без хуков в пользовательском режиме.
Анализ стека вызовов (call stack inspection)
Когда ядро обрабатывает
syscall, оно фиксирует адрес возврата. Если этот адрес не указывает внутрь ntdll.dll, а находится в регионе, не привязанном к известному модулю, это аномалия. Некоторые EDR проверяют стек при обработке чувствительных вызовов и помечают вызовы с подозрительным адресом возврата.Техника
SW3_GetRandomSyscallAddress как раз пытается замаскировать адрес возврата, выполняя jmp на инструкцию syscall внутри ntdll.dll. Однако EDR может проверять не только непосредственный адрес возврата, но и весь стек фреймов.Поведенческий анализ и корреляция
Даже если отдельный вызов не вызвал срабатывания, последовательность операций — выделение памяти с
PAGE_EXECUTE_READWRITE, запись шеллкода, создание удалённого потока — формирует паттерн, который EDR классифицирует как инъекцию. Прямые системные вызовы не меняют семантику операций, только точку входа.Что это значит для защитника
| Слой наблюдения | Обходится direct syscall? | Примечание |
|---|---|---|
| Userland-хуки в ntdll | Да | Основная причина использования техники |
| Kernel callbacks | Нет | Работают на уровне ядра |
| ETW-провайдеры | Нет | Асинхронная телеметрия |
| Call stack analysis | Частично | Зависит от реализации маскировки |
| Поведенческие правила | Нет | Семантика операций не меняется |
Практические выводы для построения детекта:
- Не полагайтесь только на userland-хуки. Они обходятся тривиально. Ядро и ETW — обязательные источники телеметрии.
- Проверяйте адрес возврата при обработке
syscall. Вызов из региона внеntdll.dll— индикатор прямого системного вызова.
- Коррелируйте последовательности. Одиночный
NtAllocateVirtualMemoryнормален; цепочка allocate → write → protect → create thread в контексте чужого процесса — нет.
- Мониторьте аномальные паттерны загрузки. Процесс, который выполняет
syscallиз региона с правами RWX или из heap-аллокации, подозрителен сам по себе.
Ограничения техники для атакующего
Direct syscall не является универсальным обходом. Он требует точного SSN для текущей сборки ОС, корректной подготовки регистров и стека, а также решения проблемы адресации. Ошибка в SSN приводит к
STATUS_INVALID_SYSTEM_SERVICE или краху процесса. Кроме того, некоторые EDR-продукты детектируют сам факт выполнения syscall из нестандартного региона через hardware breakpoints или hypercall-based мониторинг (виртуализационные агенты).Для исследователя защиты понимание этой техники позволяет правильно расставлять точки наблюдения и не создавать ложное чувство безопасности, основанное исключительно на хуках пользовательского режима.
