ASLR: как рандомизация адресного пространства усложняет эксплуатацию уязвимостей

Введение​


ASLR (Address Space Layout Randomization) — механизм рандомизации расположения элементов в адресном пространстве процесса, применяемый в операционных системах для повышения безопасности. Его основная цель — сделать предсказуемым положение ключевых структур памяти, таких как стек, куча, библиотеки и исполняемые области, тем самым усложняя эксплуатацию уязвимостей типа buffer overflow, return-oriented programming (ROP), и других техник, зависящих от известных адресов.

ASLR был впервые внедрён в Linux и Windows как часть комплексной стратегии защиты от эксплуатации уязвимостей. Хотя он не является абсолютной защитой, он значительно повышает сложность атак, требуя от злоумышленника дополнительных усилий для определения адресов целевых объектов в памяти.

Как работает ASLR в Linux​


В Linux ASLR реализуется через механизм рандомизации адресов при загрузке процесса и его компонентов. Это включает:

  • Расположение библиотек (shared objects) в случайных адресах;
  • Случайное смещение стека и кучи;
  • Рандомизацию области памяти, содержащей исполняемый код;
  • Использование ASLR при загрузке ядра и модулей ядра (в зависимости от конфигурации).

Эта рандомизация применяется при каждом запуске процесса, что делает невозможным использование заранее известных адресов в случае атаки.

Параметры управления ASLR в Linux​


В Linux ASLR управляется через файлы в /proc/sys/kernel/. Основные параметры:

  • randomize_va_space — управляет уровнем рандомизации адресного пространства. Возможные значения:
    • 0: отключено;
    • 1: частичная рандомизация (например, стек и куча);
    • 2: полная рандомизация (все компоненты).

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

Примеры рандомизации в Linux​


При запуске процесса в Linux, адреса следующих элементов памяти могут изменяться:

  • Адрес стека (stack)
  • Адрес кучи (heap)
  • Адреса библиотек (shared libraries)
  • Адреса исполняемого кода (text segment)

Пример:

Bash:
## Проверка уровня рандомизации
cat /proc/sys/kernel/randomize_va_space

Если результат 2, значит включена полная рандомизация.

Проверка рандомизации в реальном времени​


Для проверки рандомизации в Linux можно использовать следующий подход:

  1. Запустите процесс несколько раз и проверьте, меняются ли адреса.
  2. Используйте readelf или objdump для анализа памяти.
  3. Сравните вывод cat /proc/[pid]/maps для разных запусков.

Пример:

Bash:
## Запуск программы и проверка адресов
for i in {1..5}; do
    ./test_program &
    echo "PID: $!"
    sleep 1
    cat /proc/$!/maps
    echo "---"
done

Если адреса различаются — ASLR работает.

Как работает ASLR в Windows​


В Windows ASLR реализуется через механизм DEP (Data Execution Prevention) и дополнительные политики управления памятью. Основные принципы:

  • Память, выделенная для данных (например, стек, куча), помечается как недоступная для выполнения;
  • Память, предназначенная для исполняемого кода, помечается как выполнимая;
  • Система может использовать рандомизацию адресов для библиотек и других областей.

Политики DEP и ASLR в Windows​


В Windows DEP и ASLR могут быть настроены через:

  • Политики системы (через Group Policy)
  • Параметры процесса (через SetProcessDEPPolicy)

Пример использования SetProcessDEPPolicy:

C:
#include <windows.h>

int main() {
    SetProcessDEPPolicy(PROCESS_DEP_ENABLE);
    return 0;
}

Эта функция включает DEP для текущего процесса, что делает невозможным выполнение кода из памяти, помеченной как данные.

Механизмы защиты ASLR​


ASLR работает совместно с другими механизмами безопасности, такими как:

  • DEP (Data Execution Prevention)
  • Stack Canaries
  • Control Flow Guard (CFG)
  • SafeSEH

Эти технологии вместе создают многослойную защиту, которая затрудняет эксплуатацию уязвимостей.

Как ASLR противостоит ROP и JIT​


ROP (Return-Oriented Programming) — техника, при которой злоумышленник собирает цепочки вызовов из существующих фрагментов кода (gadgets) в памяти. ASLR усложняет эту технику, поскольку адреса этих фрагментов становятся неизвестными.

JIT (Just-In-Time) компиляция — часто используется в веб-браузерах и скриптовых языках. В Windows и Linux JIT-код может быть выделен в отдельную область памяти с правами на выполнение. ASLR может помешать эксплуатации JIT-кода, если адреса неизвестны.

Проблемы и ограничения ASLR​


Несмотря на свои преимущества, ASLR имеет ряд ограничений:

  • Не все компоненты памяти могут быть рандомизированы (например, в некоторых случаях библиотеки могут быть загружены по фиксированным адресам)
  • Утечка данных может позволить злоумышленнику получить информацию о текущих адресах
  • Некоторые уязвимости могут быть использованы для обхода ASLR (например, через переполнение буфера, которое позволяет получить адреса)

Обход ASLR через утечку данных​


Если приложение утекает информацию о своих адресах (например, через уязвимость в выводе), злоумышленник может использовать эти данные для обхода ASLR. Например:

  • Утечка адреса функции printf может быть использована для расчета адреса system()
  • Утечка адреса стека может помочь в расчете адреса ret возврата

Практическая проверка ASLR​


Для проверки уровня рандомизации в Linux можно использовать следующий подход:

  1. Запустите процесс несколько раз и проверьте, меняются ли адреса.
  2. Используйте readelf или objdump для анализа памяти.
  3. Сравните вывод cat /proc/[pid]/maps для разных запусков.

Пример:

Bash:
## Запуск программы и проверка адресов
for i in {1..5}; do
    ./test_program &
    echo "PID: $!"
    sleep 1
    cat /proc/$!/maps
    echo "---"
done

Если адреса различаются — ASLR работает.

Безопасные практики при разработке ПО​


Разработчики ПО должны учитывать следующие аспекты при работе с ASLR:

  • Не использовать жёстко заданные адреса в коде
  • Избегать использования функций, которые могут быть уязвимы к переполнению
  • Использовать библиотеки и фреймворки, поддерживающие ASLR
  • Проверять работу приложения с включённым ASLR

Сравнение ASLR с другими механизмами защиты​


| Механизм | Описание | Уровень защиты |
|----------|----------|----------------|
| ASLR | Рандомизация адресов | Средний |
| DEP | Предотвращение выполнения данных | Средний |
| Stack Canaries | Защита от переполнения стека | Низкий |
| CFG | Проверка потока управления | Высокий |

ASLR — один из базовых механизмов защиты, но его эффективность возрастает при использовании в сочетании с другими технологиями.

Дополнительные аспекты безопасности в Linux​


В Linux ASLR может быть дополнительно усилен с помощью других механизмов:

  • KASLR (Kernel Address Space Layout Randomization) — рандомизация адресов ядра
  • Stack canaries — защита от переполнения стека
  • SMEP (Supervisor Mode Execution Prevention) — запрет выполнения пользовательского кода в режиме ядра
  • SMAP (Supervisor Mode Access Prevention) — запрет доступа к пользовательским данным в режиме ядра

Эти механизмы работают вместе для создания более безопасной среды выполнения.

Контроль и мониторинг ASLR в Linux​


В Linux можно отслеживать состояние ASLR через системные журналы и специальные утилиты:

  • dmesg — для просмотра сообщений ядра, связанных с ASLR
  • /proc/sys/kernel/randomize_va_space — для проверки текущего уровня рандомизации
  • cat /proc/[pid]/maps — для анализа адресного пространства процесса

Пример:

Bash:
## Проверка состояния ASLR
sysctl kernel.randomize_va_space

Влияние ASLR на производительность​


ASLR может оказывать незначительное влияние на производительность:

  • Увеличение времени загрузки процессов из-за необходимости рандомизации
  • Дополнительные накладные расходы на управление памятью
  • Возможное снижение производительности при частых перезапусках процессов

Однако эти эффекты обычно минимальны и не влияют на общую производительность системы.

Заключение​


ASLR — важный элемент защиты в современных операционных системах. Его работа основана на рандомизации адресного пространства, что делает эксплуатацию уязвимостей гораздо сложнее. Однако, как и любой механизм безопасности, он не является безусловно надёжным. Важно понимать его ограничения и использовать его в сочетании с другими защитными технологиями.

Понимание ASLR необходимо как для разработчиков ПО, так и для специалистов по безопасности. Оно даёт представление о том, как современные ОС защищают себя от атак, и помогает разрабатывать более безопасные приложения.

Источники​


 
Расскажи подробней про Control Flow Guard (CFG) и SafeSEH
 

Коротко​


SafeSEH и Control Flow Guard (CFG) — это Windows-механизмы защиты, которые ограничивают, куда может перейти управление при эксплуатации ошибок памяти.

  • SafeSEH защищает 32-битные обработчики структурированных исключений Windows, чтобы атакующий не мог подменить Handler в SEH-цепочке на произвольный адрес.
  • CFG защищает косвенные вызовы: указатели на функции, виртуальные вызовы, callback'и, некоторые системные переходы. Он проверяет, что целевой адрес входит в заранее известный набор допустимых целей.

Оба механизма дополняют ASLR, DEP/NX, Stack Canaries, SEHOP, CET Shadow Stack/IBT, но не заменяют их.

────────────────────

SafeSEH​


Что SafeSEH защищает​


В 32-битном Windows SEH исторически хранится прямо в стеке. Упрощённо структура выглядит так:

C:
typedef struct _EXCEPTION_REGISTRATION_RECORD {
    struct _EXCEPTION_REGISTRATION_RECORD *Next;
    void *Handler;
} EXCEPTION_REGISTRATION_RECORD;

Начало цепочки находится через FS:[0].

При возникновении исключения система проходит по цепочке и вызывает Handler. Если атакующий переполняет буфер и перезаписывает Handler, он может заставить процесс выполнить переход по контролируемому адресу.

SafeSEH решает именно эту задачу: он не даёт использовать произвольный адрес обработчика исключения.

────────────────────

Как работает SafeSEH​


При сборке 32-битного модуля линкер может создать таблицу допустимых обработчиков исключений.

Во время диспетчеризации исключения Windows проверяет:

  1. В каком модуле находится адрес обработчика.
  2. Есть ли у этого модуля SafeSEH-таблица.
  3. Входит ли конкретный адрес обработчика в разрешённый список.

Если обработчик не проходит проверку, система не передаёт ему управление.

Упрощённая логика:

C:
if (!HandlerIsInSafeSEHTable(module, handler_address)) {
    // обработчик считается недопустимым
    // исключение не будет выполнено через него
}

────────────────────

Где хранятся метаданные SafeSEH​


SafeSEH-таблица находится в PE-файле, в Load Config Directory.

Для 32-битных модулей важны поля:

Код:
IMAGE_LOAD_CONFIG_DIRECTORY32
    SEHandlerTable
    SEHandlerCount

То есть PE-файл содержит:

  • адрес таблицы безопасных обработчиков;
  • количество записей в таблице.

Проверить наличие SafeSEH-метаданных можно через dumpbin:

Код:
dumpbin /loadconfig app.exe

В выводе стоит искать похожие строки:

Код:
SE Handler Table
SE Handler Count

────────────────────

Сборка с SafeSEH​


Для 32-битного MSVC-кода:

Код:
cl /W4 /GS /EHsc source.cpp /link /SAFESEH /DYNAMICBASE /NXCOMPAT

Важно:

  • /SAFESEH актуален именно для 32-битной x86-модели SEH.
  • Для x64 и ARM64 классический SafeSEH обычно не применяется, потому что там другая модель исключений.
  • Все значимые модули процесса должны быть собраны с корректными метаданными, иначе защита может быть неполной.

────────────────────

Что SafeSEH не защищает​


SafeSEH — важный, но узкий механизм.

Он не защищает от:

  • перезаписи возвращаемого адреса;
  • повреждения данных без изменения потока управления;
  • атак через vectored exception handlers, если они не покрыты конкретной проверкой;
  • использования допустимого обработчика с вредоносными аргументами или состоянием;
  • атак на 64-битные приложения через классический 32-битный SEH, потому что там другой механизм исключений;
  • ROP как класса техник.

Для защиты SEH-цепочки также важен SEHOP, который проверяет целостность всей цепочки обработчиков, а не только адрес конкретного обработчика.

────────────────────

SafeSEH и x64​


В x64 Windows обработчики исключений не хранятся в стеке как linked list. Вместо этого используются таблицы:

Код:
.pdata
.xdata

Они описывают runtime-функции и unwind-информацию.

Поэтому классическая атака через перезапись FS:[0] и Handler в стеке для x64 неактуальна в том же виде.

Но это не значит, что x64-исключения полностью неуязвимы. Поэтому современные защиты включают:

  • CFG;
  • EH continuation metadata;
  • CET Shadow Stack;
  • IBT;
  • контроль целостности кода и страниц.

────────────────────

Control Flow Guard (CFG)​


Что защищает CFG​


CFG защищает косвенные переходы, например:

C:
void (*callback)(void) = get_callback();
callback();

или:

C++:
obj->VirtualMethod();

В обоих случаях целевой адрес берётся из памяти:

  • из указателя на функцию;
  • из vtable;
  • из callback-таблицы;
  • из структуры объекта;
  • из API-указателя.

Если атакующий может перезаписать такой указатель, он может направить выполнение на произвольный код.

CFG проверяет, что целевой адрес является допустимой целью косвенного вызова.

────────────────────

Основная идея CFG​


При сборке компилятор и линкер анализируют код и определяют допустимые цели косвенных вызовов.

Например:

  • функции, адрес которых берётся;
  • функции, используемые как callback;
  • виртуальные функции;
  • экспортируемые функции, если они не подавлены;
  • специальные цели, зарегистрированные через API для динамического кода.

Эти сведения попадают в PE-файл.

При загрузке процесса Windows строит или использует готовую структуру проверки, часто называемую CFG bitmap.

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

Код:
если target не входит в допустимые цели CFG:
    fast fail

────────────────────

Упрощённая модель проверки CFG​


Упрощённо проверка выглядит так:

C:
bool IsCfgValidTarget(void *target) {
    size_t bit_index = (uintptr_t)target >> 4;
    return BitmapTest(CfgBitmap, bit_index);
}

То есть существует битовая карта адресного пространства, где отмечены допустимые цели.

Гранулярность зависит от архитектуры и реализации, но смысл именно такой: быстро проверить, разрешён ли адрес как цель косвенного вызова.

────────────────────

Инструментация косвенных вызовов​


MSVC при включённом CFG может генерировать вызов через специальную диспетчерную функцию.

Концептуально вместо:

Код:
call rax

может быть что-то вроде:

Код:
mov  rax, [function_pointer]
call qword ptr [__guard_dispatch_icall_fptr]

Диспетчер:

  1. получает целевой адрес;
  2. проверяет его по CFG-метаданным;
  3. если адрес допустим — передаёт управление;
  4. если недопустим — вызывает fast fail.

────────────────────

PE-метаданные CFG​


CFG-информация также находится в Load Config Directory.

Важные поля:

Код:
GuardFlags
GuardCFCheckFunctionPointer
GuardCFDispatchFunctionPointer
GuardCFFunctionTable
GuardCFFunctionCount

Проверка через dumpbin:

Код:
dumpbin /loadconfig app.exe

В выводе можно увидеть:

Код:
Guard Flags
Guard CF Check Function Pointer
Guard CF Dispatch Function Pointer
Guard CF Function Table
Guard CF Function Count

Если этих элементов нет, модуль, скорее всего, не собран с полноценной поддержкой CFG.

────────────────────

Сбор​

 
Продолжение.

Сборка с Control Flow Guard​


Для MSVC поддержка CFG включается на этапах компиляции и линковки.

Для C/C++:

Код:
cl /W4 /GS /EHsc /guard:cf source.cpp /link /guard:cf /DYNAMICBASE /NXCOMPAT

Для более современного уровня защиты можно добавить:

Код:
cl /W4 /GS /EHsc /guard:cf /EHCONT source.cpp ^
   /link /guard:cf /DYNAMICBASE /NXCOMPAT /CETCOMPAT

Где:

  • /guard:cf — включает инструментацию Control Flow Guard;
  • /DYNAMICBASE — включает ASLR для модуля;
  • /NXCOMPAT — помечает модуль как совместимый с DEP/NX;
  • /CETCOMPAT — включает совместимость с Intel CET / Shadow Stack и IBT, если поддерживается системой;
  • /EHCONT — включает EH continuation metadata, полезную для защиты C++ exception handling.

Для clang/LLVM на Windows обычно используются совместимые флаги через clang-cl:

Код:
clang-cl /guard:cf source.cpp /link /guard:cf

или, в зависимости от версии toolchain:

Bash:
clang-cl -mguard=cf source.cpp -fuse-ld=lld -Wl,/guard:cf

Важно: CFG эффективен только тогда, когда им покрыты все значимые модули процесса:

  • основной исполняемый файл;
  • все DLL;
  • сторонние библиотеки;
  • плагины;
  • JIT-код, если он корректно регистрирует допустимые цели.

Если в процессе присутствует старый модуль без CFG, он может стать слабым звеном.

────────────────────

Проверка SafeSEH и CFG в PE-файле​


Для проверки можно использовать dumpbin из Visual Studio Developer Command Prompt.

Проверка заголовков:

Код:
dumpbin /headers app.exe

Для ASLR и DEP стоит искать флаги:

Код:
Dynamic base
NX compatible

Для CFG могут присутствовать признаки guard-метаданных и флага:

Код:
Guard CF

Проверка Load Config:

Код:
dumpbin /loadconfig app.exe

Для SafeSEH в 32-битном модуле важны:

Код:
SE Handler Table
SE Handler Count

Для CFG важны примерно такие поля:

Код:
Guard Flags
Guard CF Check Function Pointer
Guard CF Dispatch Function Pointer
Guard CF Function Table
Guard CF Function Count

Если Guard CF Function Table и связанные поля отсутствуют, модуль, скорее всего, не содержит полноценной CFG-инструментации.

────────────────────

Проверка политик процесса в Windows​


В Windows можно проверить mitigation-политики процесса через PowerShell.

Для конкретного процесса:

Код:
Get-ProcessMitigation -Name app.exe

Для системных настроек:

Код:
Get-ProcessMitigation -System

В выводе можно увидеть параметры, связанные с:

  • DEP;
  • ASLR;
  • CFG;
  • SEHOP;
  • Shadow Stack;
  • IBT;
  • Control Flow Guard suppression;
  • strict handle checks;
  • other exploit protection settings.

Также можно использовать Windows Security Center:

Код:
Windows Security
 -> App & browser control
 -> Exploit protection settings

Там настраиваются глобальные и per-application политики, включая CFG и related mitigations.

────────────────────

Как CFG выглядит на уровне кода​


Допустим, есть косвенный вызов:

C:
#include <stdio.h>

typedef void (*callback_t)(void);

void allowed_callback(void)
{
    puts("allowed callback");
}

int main(void)
{
    callback_t cb = allowed_callback;
    cb();
    return 0;
}

При компиляции с CFG компилятор может заменить прямой косвенный вызов на проверенный переход через системный dispatch helper.

Концептуально вместо:

Код:
call rax

может быть:

Код:
mov  rax, qword ptr [cb]
call qword ptr [__guard_dispatch_icall_fptr]

Диспетчер проверяет rax перед передачей управления.

Если rax указывает на недопустимую цель, процесс аварийно завершится через fast fail.

────────────────────

Что считается допустимой целью CFG​


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

Например:

  • функции, адрес которых берётся явно;
  • виртуальные методы;
  • callback-функции;
  • функции, передаваемые в API;
  • экспортируемые функции, если они не подавлены;
  • зарегистрированные динамические цели для JIT-кода.

Пример:

C:
void (*fp)(void) = some_function;

Здесь some_function может быть помечена как допустимая цель, потому что её адрес используется в косвенном вызове.

А вот произвольный адрес, например:

C:
void (*fp)(void) = (void (*)(void))attacker_controlled_value;
fp();

при включённом CFG должен привести к проверке и, если адрес не входит в bitmap допустимых целей, к fast fail.

────────────────────

CFG bitmap​


Windows использует структуру, которую часто называют CFG bitmap.

Упрощённо:

Код:
адрес цели -> бит в карте

Если бит установлен, адрес считается допустимой целью косвенного вызова.

Грубая модель:

C:
bool cfg_check(void *target)
{
    uintptr_t aligned = (uintptr_t)target & ~0xFULL;
    size_t index = aligned >> 4;

    return bitmap_test(cfg_bitmap, index);
}

Реальная реализация сложнее, но смысл именно такой: быстро проверить, входит ли адрес в множество разрешённых целей.

────────────────────

Динамически генерируемый код и CFG​


CFG изначально рассчитан на статически известные цели. Но браузеры, JavaScript-движки, .NET, Java VM и другие среды часто генерируют код во время выполнения.

Для таких случаев Windows предоставляет механизмы регистрации допустимых целей.

Один из relevant API:

C:
SetProcessValidCallTargets()

Он позволяет процессу зарегистрировать допустимые call targets для динамически созданного кода.

Упрощённая идея:

C:
CFG_CALL_TARGET_INFO info;

info.Offset = (ULONG_PTR)(code_buffer + entry_offset);
info.Flags  = CFG_CALL_TARGET_VALID;

SetProcessValidCallTargets(
    GetCurrentProcess(),
    section_handle,
    1,
    &info
);

Это позволяет JIT-коду быть совместимым с CFG, не отключая защиту.

Если JIT-движок не регистрирует свои цели корректно, CFG может блокировать переходы в динамический код.

────────────────────

SafeSEH: сборка и проверка​


Для 32-битного x86-приложения SafeSEH включается линкером:

Код:
cl /W4 /GS /EHsc source.cpp /link /SAFESEH /DYNAMICBASE /NXCOMPAT

Если модуль не использует SEH и не должен содержать обработчики исключений, можно использовать:

Код:
link /SAFESEH:NO

Это уменьшает поверхность атаки и сообщает системе, что модуль не рассчитывает на классическую SEH-цепочку.

Проверка:

Код:
dumpbin /loadconfig app.exe

Для 32-битного модуля с SafeSEH должны быть видны обработчики:

Код:
SE Handler Table
SE Handler Count

────────────────────

SafeSEH и SEHOP​


SafeSEH проверяет конкретный обработчик исключений.

SEHOP проверяет целостность всей цепочки SEH.

Упрощённо:

  • SafeSEH: "Этот Handler находится в списке разрешённых обработчиков модуля?"
  • SEHOP: "Вся цепочка EXCEPTION_REGISTRATION_RECORD выглядит корректно и не повреждена?"

SEHOP помогает против атак, которые пытаются исказить саму цепочку обработчиков, например подменить Next или разрушить структуру цепочки.

В Windows SEHOP может управляться через exploit protection settings и через реестр:

Код:
HKLM\SYSTEM\CurrentControlSet\Control\Session Manager\kernel
DisableExceptionChainValidation

Значение:

  • 0 — проверка цепочки исключений включена;
  • 1 — проверка цепочки исключений отключена.

На практике лучше не отключать SEHOP без серьёзной диагностической причины.

────────────────────

SafeSEH и x64​


Классический SafeSEH — это в основном защита 32-битной x86-модели SEH.

В x64 Windows исключения устроены иначе.

Вместо цепочки в стеке используются таблицы:

Код:
.pdata
.xdata

Они содержат:

  • runtime function entries;
  • unwind information;
  • exception handler addresses;
  • scope tables;
  • language-specific exception data.

Поэтому классическая атака через перезапись FS:[0] и Handler в стеке для x64 не работает так же, как в 32-битном коде.

Но это не означает, что исключения в x64 не нуждаются в защите. Поэтому применяются:

  • CFG;
  • EH continuation metadata;
  • CET Shadow Stack;
  • IBT;
  • kernel patch protection и code integrity;
  • control-flow enforcement на уровне CPU.

────────────────────

Что CFG защищает, а что нет​


CFG защищает косвенные переходы.

Он помогает против:

  • подмены указателя на функцию;
  • повреждения vtable;
  • перезаписи callback-указателя;
  • атак на indirect call через повреждённую структуру;
  • некоторых техник control-flow hijacking.

Но CFG не является полной защитой от всех классов атак.

Он не защищает полностью от:

  • direct call/jump в произвольный адрес, если переход не инструментирован;
  • ROP-цепочек, построенных на ret, если нет CET Shadow Stack или аналогичной защиты;
  • data-only attacks, где атакующий меняет данные, а не поток управления;
  • вызова допустимой функции с вредоносными аргументами;
  • type confusion, если цель формально допустима;
  • атак через модули без CFG;
  • атак на логику приложения;
  • утечек памяти, которые раскрывают адреса и данные.

Пример проблемы: если атакующий не может вызвать произвольный адрес, но может вызвать допустимую функцию с контролируемыми аргументами, это всё ещё может быть опасно.

Такой класс техник иногда называют COOP — counterfeit object-oriented programming.

────────────────────

Ограничения SafeSEH​


SafeSEH важен для legacy 32-битного SEH, но он узкий.

Он не защищает от:

  • перезаписи return address;
  • stack buffer overflow как класса;
  • heap corruption;
  • use-after-free;
  • type confusion;
  • ROP;
  • атак через данные;
  • повреждений вне SEH-цепочки;
  • 64-битных исключений в том же виде.

Поэтому SafeSEH нужно рассматривать вместе с:

  • /GS stack cookies;
  • DEP/NX;
  • ASLR;
  • SEHOP;
  • CFG;
  • CET/IBT;
  • safe exception handling design.

────────────────────

Практический лабораторный пример​


В локальной VM можно собрать два варианта одного и того же 32-битного приложения.

Вариант без SafeSEH:

Код:
cl /W4 /GS /EHsc test.cpp /link /DYNAMICBASE /NXCOMPAT

Вариант с SafeSEH:

Код:
cl /W4 /GS /EHsc test.cpp /link /SAFESEH /DYNAMICBASE /NXCOMPAT

Затем сравнить:

Код:
dumpbin /loadconfig test_no_safeseh.exe
dumpbin /loadconfig test_safeseh.exe

У второго должна появиться SE Handler Table.

Для CFG:

Код:
cl /W4 /GS /EHsc /guard:cf test.cpp /link /guard:cf /DYNAMICBASE /NXCOMPAT

Проверка:

Код:
dumpbin /loadconfig test_cfg.exe

В Load Config должны быть CFG-поля.

────────────────────

Диагностика CFG-срабатываний​


Если CFG блокирует недопустимый косвенный вызов, процесс обычно завершается через fast fail.

В crash-телеметрии можно увидеть:

  • exception code 0xC0000409;
  • STATUS_STACK_BUFFER_OVERRUN;
  • fail fast / guard failure indicators;
  • аварийное завершение процесса без обычного SEH-обработчика;
  • WER-отчёты;
  • EDR/Sysmon process termination events.

В Windows Event Log стоит смотреть:

Код:
Application Error
Windows Error Reporting

В Sysmon может быть полезен Event ID 5 — process terminated.

Для глубокой диагностики:

  • собрать memory dump;
  • проверить exception record;
  • посмотреть модуль, где произошёл fail fast;
  • проверить целевой адрес косвенного вызова;
  • проверить, входит ли адрес в CFG-таблицы модулей;
  • проверить, нет в процессе модулей без CFG;
  • проверить JIT-код и dynamically registered call targets.

В WinDbg можно начать с:

Код:
!analyze -v
.exr -1
.cxr -1
~* k
lm

Для анализа SEH в 32-битном процессе:

Код:
!teb
!exchain

Для x64 exception analysis:

Код:
.exr -1
.cxr -1
!analyze -v

────────────────────

Что смотреть в memory dump​


При подозрении на CFG-срабатывание полезно проверить:

  • адрес инструкции, которая выполняла косвенный вызов;
  • значение регистра или памяти, содержащего target;
  • модуль, которому принадлежит target;
  • наличие у модуля Load Config с CFG;
  • наличие target в guard function table;
  • был ли target в dynamically registered call targets;
  • не является ли target адресом в heap, stack или non-executable region;
  • не было ли перед этим повреждения памяти.

Для просмотра памяти:

Код:
u <address>
db <address>
dq <address>
ln <address>
lmvm <module>

────────────────────

Типичные ошибки разработчиков​


Частые проблемы:

  • CFG включён только в основном EXE, но не в DLL;
  • сторонняя библиотека собрана без /guard:cf;
  • плагин загружается как старый модуль без CFG;
  • JIT-движок генерирует код, но не регистрирует call targets;
  • используется assembly-код без учёта CFG;
  • функция приводится к несовместимому типу указателя;
  • callback сохраняется в структуре, которая может быть перезаписана;
  • приложение рассчитывает на старый SEH и не включает SafeSEH;
  • 32-битный модуль использует ручной SEH без safe handler table;
  • защита отключается ради совместимости без анализа последствий.

────────────────────

Рекомендации по hardening​


Для новых Windows-приложений разумный baseline:

Код:
cl /W4 /GS /EHsc /guard:cf /EHCONT source.cpp ^
   /link /guard:cf /DYNAMICBASE /NXCOMPAT /CETCOMPAT

Для 32-битных legacy-приложений дополнительно:

Код:
link /SAFESEH /DYNAMICBASE /NXCOMPAT

Если модуль не использует SEH:

Код:
link /SAFESEH:NO

Также стоит:

  • включать ASLR для всех модулей;
  • включать DEP/NX;
  • не использовать executable stack;
  • не использовать executable heap без необходимости;
  • проверять все сторонние DLL;
  • использовать CET/Shadow Stack, если доступно;
  • использовать EH continuation metadata;
  • минимизировать использование ручного SEH;
  • избегать self-modifying code;
  • для JIT использовать официальные API регистрации call targets;
  • мониторить crash-телеметрию на guard failures.

────────────────────

Связка с ASLR​


ASLR и CFG решают разные задачи.

ASLR:

  • скрывает адреса;
  • усложняет использование известных адресов;
  • мешает ROP, если нет утечки адресов;
  • усложняет поиск gadgets.

CFG:

  • ограничивает допустимые цели косвенных вызовов;
  • мешает переходу на произвольный адрес через указатель;
  • проверяет легитимность indirect call target;
  • уменьшает полезность control-flow corruption.

Вместе они работают лучше, чем по отдельности.

Например:

  • ASLR мешает угадать адрес;
  • CFG мешает вызвать даже угаданный или утёкший адрес, если он не является допустимой целью;
  • DEP/NX мешает выполнению данных;
  • Shadow Stack/IBT дополнительно защищают return flow и indirect branches.

────────────────────

Краткое сравнение​


SafeSEH:

  • актуален для 32-битного x86 SEH;
  • проверяет адреса SEH-обработчиков;
  • использует SE Handler Table в PE;
  • защищает от подмены Handler в SEH-цепочке;
  • не защищает от ROP, data-only attacks и произвольных indirect calls.

CFG:

  • актуален для современных Windows-приложений;
  • проверяет цели косвенных вызовов;
  • использует guard metadata и CFG bitmap;
  • защищает от подмены function pointers, vtable, callbacks;
  • не является полной CFI-защитой и не закрывает все control-flow атаки.

────────────────────

Итог​


SafeSEH — это более старый, но всё ещё важный механизм защиты 32-битного SEH. Он проверяет, что обработчик исключений находится в списке разрешённых обработчиков модуля.

CFG — более современный механизм. Он проверяет, что косвенный вызов переходит только на заранее разрешённую цель. Это сильно усложняет атаки через повреждённые указатели на функции, vtable и callback-структуры.

Но оба механизма нужно рассматривать как часть общей защиты:

  • ASLR;
  • DEP/NX;
  • Stack Canaries;
  • SafeSEH;
  • SEHOP;
  • CFG;
  • EH continuation metadata;
  • CET Shadow Stack;
  • IBT;
  • code integrity;
  • memory-safe development practices.
 
Назад
Верх Низ