Если вы когда-нибудь разбирали эксплойт переполнения стека на Windows или смотрели на защиты исполняемого файла, вы наверняка натыкались на аббревиатуру SafeSEH. Это не просто флаг в заголовке — это конкретный механизм, который меняет то, как операционная система обрабатывает исключения. И если вы реверсите бинарники, пишете эксплойты или просто хотите понимать, почему ваш старый трюк с SEH больше не работает, нужно знать, как он устроен изнутри.
Ниже я разберу именно практику: как SafeSEH работает на уровне загрузчика, где в PE-структуре хранится информация о валидных обработчиках, и как это реально влияет на эксплуатацию.
Зачем вообще понадобился SafeSEH
Классическая атака через SEH (Structured Exception Handler) выглядит так: вы переполняете буфер на стеке, перезаписываете указатель обработчика исключения на адрес своего кода (обычно на jmp esp или pop pop ret), вызываете исключение — и выполнение переходит туда, куда вы указали.
Проблема в том, что злоумышленнику не нужно знать точный адрес в коде. Он просто указывает на фиксированный адрес в памяти, где стоит нужная инструкция. Защита DEP не всегда спасает, потому что SEH-перехват происходит до того, как ASLR и DEP могут заблокировать выполнение на стеке.
SafeSEH решает именно эту проблему: он не даёт системе вызвать обработчик исключения, если его адрес не находится в списке заранее одобренных. То есть даже если вы перезаписали SEH-запись произвольным адресом, система проверит — есть ли этот адрес в белом списке. Нет — и процесс просто падает.
Как это работает под капотом
Когда компилятор собирает код с поддержкой SafeSEH, он делает две вещи:
- Прописывает адреса всех валидных обработчиков исключений в таблицу. Каждый
__try/__exceptблок в коде получает запись в этой таблице — с указателем на функцию-обработчик. - Помечает секцию или модуль как поддерживающий SafeSEH. Это делается через специальную директорию в PE-заголовке.
Когда происходит исключение и система идёт по цепочке SEH, загрузчик (или сама ОС на этапе обработки исключения) проверяет: содержится ли адрес вызываемого обработчика в таблице валидных обработчиков. Если нет — обработчик не вызывается, программа аварийно завершается.
Важный нюанс: SafeSEH проверяет адрес обработчика, а не адрес, куда ведёт перезаписанная запись. То есть если вы перезаписали указатель на адрес, которого нет в таблице — всё, конец.
Где это хранится в PE-файле
Информация о SafeSEH живёт в директории загрузочной конфигурации — Load Configuration Directory. Это одна из стандартных директив в PE-заголовке, и она описана в спецификации Microsoft.
Конкретно:
- Директория
IMAGE_DIRECTORY_ENTRY_LOAD_CONFIG(индекс 10 в массиве DataDirectory) указывает на структуруIMAGE_LOAD_CONFIG_DIRECTORY. - В этой структуре есть поле
SEHandlerTable— это RVA (Relative Virtual Address) таблицы валидных SEH-обработчиков. - Поле
SEHandlerCountсодержит количество записей в этой таблице.
Таблица представляет собой массив RVA-адресов — каждый указывает на функцию-обработчик исключений в коде модуля. Когда система обрабатывает исключение, она просто ищет адрес вызываемого обработчика в этом массиве.
Если поля SEHandlerTable и SEHandlerCount равны нулю — модуль не поддерживает SafeSEH, и классическая SEH-атака может работать (если нет других защит).
Как проверить наличие SafeSEH в конкретном файле
Есть несколько способов посмотреть, есть ли SafeSEH у PE-файла:
- Через CFF Explorer или PE-bear. Открываете файл, идёте в раздел Load Config, смотрите на SEHandlerTable и SEHandlerCount. Если оба нули — SafeSEH нет.
- Через dumpbin:
dumpbin /loadconfig file.exe— покажет ту же информацию в текстовом виде. - Через WinDbg: команда
!pebили анализ директории загрузки даст нужные данные.
Также обратите внимание на флаги в характеристиках секции и в загрузочной конфигурации. Флаг IMAGE_DLLCHARACTERISTICS_NO_SEH в поле DllCharacteristics заголовка означает, что модуль вообще не использует SEH — это ещё более жёсткая защита, чем SafeSEH.
Что это значит для эксплуатации
SafeSEH закрывает самый простой вектор атаки — перезапись SEH на произвольный адрес. Но он не всеобъемлющ. Вот что реально блокируется и что нет:
| Сценарий | SafeSEH блокирует? | Почему |
|---|---|---|
| Перезапись SEH на адрес в стеке или куче | Да | Адреса стека и кучи никогда не попадают в таблицу валидных обработчиков |
| Перезапись SEH на адрес в секции .text без регистрации | Да | Адрес есть в коде, но его нет в SEHandlerTable |
| Использование обработчика из модуля без SafeSEH | Нет | Если целевой модуль не имеет SafeSEH, его обработчики не проверяются |
| Перезапись на адрес в другом модуле с SafeSEH | Нет | Проверяется только таблица того модуля, где определён обработчик |
| Атака через VEH вместо SEH | Нет | SafeSEH не покрывает Vectored Exception Handling |
Из таблицы видно, что SafeSEH — это не серебряная пуля. Если в адресном пространстве есть модуль без SafeSEH (например, старый сторонний DLL), вы можете использовать его обработчики как промежуточный шаг.
Типичные ошибки при анализе SafeSEH
Когда люди впервые сталкиваются с SafeSEH на практике, они часто делают одни и те же ошибки:
- Путают SafeSEH с SEHOP. SEHOP (SEH Overwrite Protection) — это другая защита, которая проверяет целостность всей цепочки SEH, а не только адрес конкретного обработчика. Они часто работают вместе, но это разные механизмы.
- Смотрят только на флаги компилятора, но не на реальное содержимое Load Config. Флаг
/SAFESEHпри компиляции — это лишь инструкция компилятору. Факт наличия таблицы нужно проверять в самом файле. - Забывают, что ASLR усложняет поиск адресов. Даже если вы нашли модуль без SafeSEH, с ASLR его базовый адрес будет меняться, и вам нужен информационный лик или другой способ узнать адрес заранее.
- Предполагают, что SafeSEH есть во всех модулях. На практике в сложных приложениях бывают DLL без SafeSEH — и именно они становятся точкой входа.
Как действовать в зависимости от ситуации
Ситуация 1: Все модули защищены SafeSEH + ASLR + DEP.
Классическая SEH-перезапись не работает. Нужно искать другие векторы: use-after-free, форматные строки, или пытаться обойти защиты через цепочку ROP. Это уже совсем другой уровень сложности.
Ситуация 2: Есть модуль без SafeSEH, но с ASLR.
Ищите информационный лик, который раскроет базовый адрес. Или используйте техники, не требующие знания точного адреса (например, heap spraying в старых браузерах).
Ситуация 3: Есть модуль без SafeSEH и без ASLR.
Это классический сценарий для атаки. Находите в таком модуле инструкцию pop pop ret, перезаписываете SEH, вызываете исключение — и получаете контроль над выполнением.
Ситуация 4: Вы разработчик и хотите защитить своё приложение.
Компилируйте все модули с /SAFESEH, включайте ASLR (/DYNAMICBASE), DEP (/NXCOMPAT), и регулярно проверяйте зависимости — нет ли в вашем проекте старых DLL без этих защит.
Практические рекомендации
Вот что я бы посоветовал делать на практике при анализе безопасности PE-файлов:
- Всегда проверяйте Load Config Directory. Не полагайтесь на настройки компилятора — смотрите реальную структуру файла.
- Составляйте карту модулей. Для каждого DLL в процессе определяйте: есть ли SafeSEH, ASLR, DEP. Модули хотя бы без одной из этих защит — потенциальная цель.
- Используйте инструменты автоматизации. Mona.py для WinDbg умеет анализировать защиты модулей и показывать, какие DLL уязвимы для SEH-атак.
- Не забывайте про CFG (Control Flow Guard). В современных системах SafeSEH часто дополняется CFG, который проверяет не только SEH, но и косвенные вызовы. Это делает атаки ещё сложнее.
Итог
SafeSEH — это механизм, который хранит таблицу валидных обработчиков исключений в Load Configuration Directory PE-файла и проверяет каждый вызов SEH-обработчика против этой таблицы. Он эффективно блокирует классическую перезапись SEH, но не защищает от атак через модули без SafeSEH, VEH или более сложных техник вроде ROP.
Если вы анализируете безопасность приложения — начните с проверки SEHandlerTable и SEHandlerCount в Load Config. Если они пусты, ищите другие модули. Если заполнены — ищите обходные пути. И помните: SafeSEH — это один слой защиты, а не стена. Реальная безопасность — это комбинация механизмов, а не одного флага компилятора.
