- Как работает SafeSEH и где он прописан в PE-структуре — практическое руководство
- Что такое SafeSEH и зачем он нужен
- Где SafeSEH прописан в PE-структуре
- Как SafeSEH работает на практике
- Когда SafeSEH включён, а когда нет — таблица
- Частые ошибки, которые ломают SafeSEH
- Как правильно настроить SafeSEH в Visual Studio
- Что выбрать в зависимости от ситуации
- Как лучше сделать — практические рекомендации
- Что делать, если SafeSEH ломает твой код
- Итог: что делать прямо сейчас
Как работает SafeSEH и где он прописан в PE-структуре — практическое руководство
Если ты разрабатываешь или тестируешь приложение на Windows, особенно в низкоуровневом коде — на C/C++, — ты наверняка сталкивался с крашами, которые не объясняются логикой программы. Иногда они возникают только на определённых версиях ОС, или только при включённом DEP. А потом ты узнаёшь, что всё дело в SafeSEH. Это не магия, не настройка компилятора по умолчанию, а конкретный механизм защиты, который прописан прямо в PE-файле. И если ты его не понимаешь — ты не можешь контролировать, как твой код ведёт себя на реальных системах.
Я не буду рассказывать про «безопасность в целом». Я покажу, где и как SafeSEH живёт в PE-структуре, как он работает на уровне процессора, и почему твой код может не запуститься, если ты его не настроил правильно.
Что такое SafeSEH и зачем он нужен
SafeSEH (Safe Structured Exception Handling) — это защита от перехвата управления через повреждённые указатели на обработчики исключений. В Windows, когда происходит ошибка (например, деление на ноль или обращение к недопустимой памяти), система ищет подходящий обработчик исключения — функцию, которая должна его обработать. Эти обработчики хранятся в структуре EXCEPTION_REGISTRATION_RECORD, которая цепляется к стеку. Каждый обработчик — это адрес функции, и если злоумышленник может переписать этот адрес, он перехватывает управление.
SafeSEH решает эту проблему простым способом: он разрешает запускать только те обработчики, которые заранее зарегистрированы в PE-файле. Другими словами: если обработчик исключения не указан в списке разрешённых — система его игнорирует, даже если он физически есть в памяти.
Это не панацея. Это не замена DEP. Это дополнение. DEP блокирует выполнение кода в данных, а SafeSEH блокирует перехват управления через повреждённые указатели на обработчики. Вместе они создают слой защиты, который делает эксплуатацию уязвимостей гораздо сложнее.
Где SafeSEH прописан в PE-структуре
Все данные о SafeSEH хранятся в директиве IMAGE_LOAD_CONFIG_DIRECTORY — это часть Data Directory в заголовке PE-файла. Конкретно — в поле SEHandlerTable и SEHandlerCount.
Представь, что PE-файл — это книга с оглавлением. В оглавлении есть раздел «Обработчики исключений». Там перечислены все адреса функций, которые Windows разрешает использовать как обработчики. Если ты попытался вызвать обработчик, которого нет в этом списке — система просто не пойдёт по этому адресу. Она не проверяет, есть ли там код. Она проверяет: «А разрешён ли он вообще?»
Этот список — это массив 32-битных адресов (в 32-битных приложениях), каждый из которых указывает на функцию-обработчик. Их порядок не важен — важен только сам факт наличия в списке.
Чтобы увидеть это в реальности — открой свой EXE-файл в dnSpy (для .NET) или в IDA Pro (для нативного кода). Перейди в NT Headers → Optional Header → Data Directory → Load Configuration. Там ты увидишь поле SEHandlerTable — это указатель на массив, и SEHandlerCount — количество элементов.
Если ты компилируешь код в Visual Studio — по умолчанию SafeSEH включён для 32-битных сборок. Для 64-битных он не нужен: там используется SEHOP (Structured Exception Handling Overwrite Protection), который работает иначе. Но если ты пишешь 32-битный код — ты обязан понимать, как он работает.
Как SafeSEH работает на практике
Представь, что у тебя есть функция:
void __declspec(naked) MyExceptionHandler()
{
__asm
{
ret
}
}
И ты регистрируешь её в коде через __try/__except:
__try
{
int* p = nullptr;
*p = 42; // вызовет исключение
}
__except(MyExceptionHandler())
{
// обработка
}
Когда компилятор видит, что ты используешь пользовательский обработчик, он автоматически добавляет его адрес в список SafeSEH, если включена опция /SAFESEH. Этот список попадает в PE-файл, и при загрузке Windows проверяет: «А есть ли этот обработчик в списке?»
Если ты используешь динамический код, например, генерируешь обработчик на лету через VirtualAlloc и пишешь туда байты — он не попадёт в список SafeSEH. И если на него ссылается повреждённый указатель — система его не запустит. Это не баг. Это фича.
А теперь представь, что ты используешь стороннюю библиотеку, которая не была скомпилирована с SafeSEH. Тогда при попытке вызвать её обработчик — твой процесс упадёт. Это не ошибка в твоём коде. Это защита. И если ты не понимаешь, почему твой EXE не запускается на Windows 7 — возможно, именно в этом причина.
Когда SafeSEH включён, а когда нет — таблица
Не все сборки имеют SafeSEH. Он зависит от компилятора, флагов и целевой архитектуры. Вот что важно знать:
| Ситуация | SafeSEH включён? | Почему |
|---|---|---|
| 32-битный EXE, скомпилированный в VS 2019 с /SAFESEH (по умолчанию) | Да | Компилятор автоматически добавляет обработчики в PE |
| 32-битный EXE, скомпилированный с /SAFESEH:NO | Нет | Явное отключение. Опасно, если есть исключения |
| 64-битный EXE | Нет | Используется SEHOP, а не SafeSEH |
| PE-файл, собранный вручную (без компилятора) | Нет | Нет структуры Load Configuration |
| DLL, скомпилированная без /SAFESEH, загружаемая в 32-битный процесс | Нет | Если DLL не имеет списка — она не может использоваться как обработчик |
| EXE, скомпилированный в VS 2005, без /SAFESEH | Нет | До VS 2005 SafeSEH не был включён по умолчанию |
Если ты разрабатываешь библиотеку, которая будет использоваться в других проектах — ты обязан собирать её с /SAFESEH. Иначе твой код может сломать безопасность всего процесса, даже если ты не используешь исключения напрямую.
Частые ошибки, которые ломают SafeSEH
Я видел десятки случаев, когда разработчики сталкивались с проблемами из-за SafeSEH, и почти все они — одни и те же:
- Отключают SafeSEH, чтобы «сделать быстрее». Нет, ты не ускоряешь код. Ты просто убираешь защиту. На 100% случаев это не оправдано.
- Используют inline-асм-код с обработчиками. Если ты пишешь обработчик на ассемблере и не регистрируешь его через
__except— компилятор его не добавит в список. Он будет в памяти, но SafeSEH его не пустит. - Собирают 32-битный код с /LARGEADDRESSAWARE, но без /SAFESEH. Это частая ошибка при миграции старых проектов. Windows 7+ может отказать в запуске.
- Смешивают 32- и 64-битные библиотеки. Если ты загружаешь 64-битную DLL в 32-битный процесс — она не имеет SafeSEH, и система может упасть при исключении.
- Пишут собственный загрузчик PE. Если ты сам читаешь PE-файл и загружаешь его в память — ты обязан воссоздать структуру Load Configuration. Иначе SafeSEH не сработает, даже если он был в исходном файле.
Особенно часто ошибки возникают при использовании старых библиотек, например, из 2005 года. Ты подключаешь их, компилируешь свой проект с /SAFESEH — и получаешь ошибку линковки: error LNK2019: unresolved external symbol __except_handler4. Это значит: библиотека не имеет SafeSEH, а твой проект требует его. Решение? Либо пересобрать библиотеку с /SAFESEH, либо отключить его в своём проекте — но тогда ты теряешь защиту.
Как правильно настроить SafeSEH в Visual Studio
Если ты используешь Visual Studio — вот как это делается:
- Открой свойства проекта → Configuration Properties → C/C++ → Command Line.
- Проверь, нет ли там
/SAFESEH:NO. Если есть — удали. - Перейди в Linker → Advanced.
- Убедись, что SafeSEH стоит на Yes (/SAFESEH).
- Если ты используешь макросы или скрипты сборки — проверь, что там тоже нет отключения.
Для командной строки: если компилируешь через cl.exe — просто не передавай /SAFESEH:NO. По умолчанию он включён для 32-битных сборок.
Если ты используешь MinGW или GCC — SafeSEH не поддерживается напрямую. Там используется другой механизм — и ты не можешь включить его через флаги. В этом случае ты либо переходишь на MSVC, либо полагаешься на DEP и другие механизмы.
Что выбрать в зависимости от ситуации
Ты не можешь просто «включить SafeSEH и забыть». Ты должен выбирать, исходя из контекста:
- Если ты пишешь 32-битное приложение для Windows 7 и выше — включи SafeSEH. Без вариантов. Это стандарт безопасности. Отключение — это риск, который не оправдан.
- Если ты пишешь 64-битное приложение — не думай о SafeSEH. Он не используется. Тебе важен SEHOP и DEP. Проверь, что включён DEP, и что ты не отключаешь его через
/NXCOMPAT:NO. - Если ты используешь стороннюю 32-битную DLL без SafeSEH — и она вызывает исключения — твой процесс упадёт. Ты не можешь «обойти» это. Единственное решение — заменить DLL на версию с SafeSEH, или отказаться от неё.
- Если ты пишешь вирус-анализатор или динамический инструмент — ты можешь отключить SafeSEH в своём процессе, чтобы анализировать поведение вредоносного кода. Но только в изолированной среде. И только если ты понимаешь, что делаешь.
Если ты не знаешь, какая архитектура у целевой системы — всегда собирай 32-битную версию с SafeSEH. Это гарантирует совместимость с Windows XP SP2 и выше.
Как лучше сделать — практические рекомендации
Вот что я рекомендую делать на практике:
- Всегда включай SafeSEH для 32-битных сборок. Это не опция, это базовая защита. Даже если ты не используешь исключения — компилятор может сгенерировать их внутри стандартных библиотек.
- Проверяй, есть ли SafeSEH в твоём EXE. Открой его в PE-bear или pe-sieve. Посмотри, есть ли в Load Configuration поле SEHandlerTable. Если нет — ты не защищён.
- Не используй inline-асм-обработчики без регистрации. Если ты пишешь обработчик на ассемблере — используй
__try/__exceptилиSetUnhandledExceptionFilter— и убедись, что компилятор его видит. - Не отключай SafeSEH ради «оптимизации». Это не ускоряет код. Это только делает его уязвимым.
- Тестируй на Windows 7 и 10 с DEP включённым. SafeSEH не работает, если DEP отключён. Но на практике DEP включён у всех.
- Если ты используешь старый код — пересобери его. Код 2008 года может не иметь SafeSEH. Это не значит, что он «работает». Это значит, что он уязвим.
Если ты не уверен — включи SafeSEH. Это не вредит. Это защищает. И если твой код упадёт на клиенте — ты сразу поймёшь, что это не баг, а защита.
Что делать, если SafeSEH ломает твой код
Иногда ты получаешь ошибку при запуске: This application has failed to start because the application configuration is incorrect. — и это не про .NET, а про PE.
Вот что проверить:
- У тебя есть 32-битный EXE? Если да — проверь, есть ли в нём SEHandlerTable.
- Ты используешь стороннюю DLL? Открой её в PE-редакторе — есть ли у неё SafeSEH? Если нет — она несовместима.
- Ты используешь компилятор, который не поддерживает SafeSEH? (например, старый GCC). Тогда ты не можешь использовать его в 32-битном проекте с SafeSEH.
- Ты используешь
SetUnhandledExceptionFilter? Это не добавляет обработчик в SafeSEH-список. Он работает отдельно.
Если ты не можешь пересобрать DLL — и она критична — ты можешь отключить SafeSEH в своём проекте. Но только если ты понимаешь, что ты теряешь защиту. И только если это временно. И только если ты тестируешь в изолированной среде.
Итог: что делать прямо сейчас
Если ты читаешь это — ты, скорее всего, столкнулся с проблемой: твой EXE не запускается, или падает при исключении, или ты не понимаешь, почему защита «не работает».
Вот что тебе делать:
- Открой свой EXE в PE-bear или pe-sieve.
- Найди
Load Configuration→SEHandlerTable. - Если он пустой или отсутствует — ты не защищён.
- Включи
/SAFESEHв Visual Studio (по умолчанию включён, но проверь). - Пересобери проект.
- Проверь снова — теперь SEHandlerTable должен содержать адреса твоих обработчиков.
- Если ты используешь сторонние библиотеки — проверь их тоже. Если они без SafeSEH — замени их или отключи защиту в своём проекте (и понимай, что ты делаешь).
Это не сложнее, чем проверить, что у тебя есть .NET Framework. Это базовая настройка. И если ты её игнорируешь — ты не разработчик. Ты просто запускаешь код, надеясь, что всё будет хорошо.
Проверь свой EXE. Включи SafeSEH. Пересобери. Запусти. И больше не задавай вопросов «почему падает» — ты уже знаешь ответ.
Информация, представленная в этой статье, носит ознакомительный характер. Настройка механизмов защиты в приложениях требует понимания архитектуры системы и тестирования в реальных условиях. Перед внедрением изменений в продуктивную среду рекомендуется проконсультироваться с экспертом по безопасности ПО.
