Системные вызовы Windows: зачем исследователю понимать direct syscalls и где их наблюдает EDR

Что происходит при системном вызове на 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 0xd1mov 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ЧастичноЗависит от реализации маскировки
Поведенческие правилаНетСемантика операций не меняется

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

  1. Не полагайтесь только на userland-хуки. Они обходятся тривиально. Ядро и ETW — обязательные источники телеметрии.
  2. Проверяйте адрес возврата при обработке syscall. Вызов из региона вне ntdll.dll — индикатор прямого системного вызова.
  3. Коррелируйте последовательности. Одиночный NtAllocateVirtualMemory нормален; цепочка allocate → write → protect → create thread в контексте чужого процесса — нет.
  4. Мониторьте аномальные паттерны загрузки. Процесс, который выполняет syscall из региона с правами RWX или из heap-аллокации, подозрителен сам по себе.

Ограничения техники для атакующего​


Direct syscall не является универсальным обходом. Он требует точного SSN для текущей сборки ОС, корректной подготовки регистров и стека, а также решения проблемы адресации. Ошибка в SSN приводит к STATUS_INVALID_SYSTEM_SERVICE или краху процесса. Кроме того, некоторые EDR-продукты детектируют сам факт выполнения syscall из нестандартного региона через hardware breakpoints или hypercall-based мониторинг (виртуализационные агенты).

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

Источники​


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