Если вы работаете с PE-файлами — будь то анализ вредоносного ПО, реверс-инжиниринг или отладка — рано или поздно столкнётесь с тем, что файл использует структурированную обработку исключений (SEH) и таблицы исключений. Они описывают, как программа реагирует на сбои: деление на ноль, недопустимый доступ к памяти, переполнение стека. И если вы хотите понять, что делает файл при падении — или найти скрытую логику — нужно уметь находить и читать эти таблицы.
Разберёмся, где в PE-файле лежит таблица исключений, как она устроена, какие инструменты помогают её вытащить и на что смотреть при анализе.
- Зачем вообще смотреть на исключения в PE
- Где искать таблицу исключений в PE
- Для 64-битных PE (PE32+)
- Для 32-битных PE (PE32)
- Как извлечь таблицу исключений: инструменты
- 1. Через заголовки PE вручную
- 2. C помощью WinDbg
- 3. IDA Pro и Ghidra
- 4. pefile (Python)
- 5. dumpbin и llvm-readobj
- Как читать UNWIND_INFO и что там искать
- Сравнение подходов к извлечению
- Что делать в зависимости от вашей задачи
- Частые ошибки при работе с таблицей исключений
- Практические рекомендации
- Итог
Зачем вообще смотреть на исключения в PE
Таблица исключений — это не просто формальность компилятора. Она показывает:
- есть ли у функции обработчики ошибок (try/except, __try/__except и т.п.);
- какие именно исключения перехватываются;
- где находится фильтр исключений — функция, которая решает, обрабатывать ошибку или передавать дальше;
- есть ли в коде антиотладочные трюки, спрятанные через SEH;
- используется ли VEH (Vectored Exception Handling) — более редкий, но встречающийся механизм.
Для аналитика это источник информации о том, какие участки кода разработчик считал «опасными». Для разработчика — способ проверить, правильно ли компилятор сгенерировал защитные фреймы.
Где искать таблицу исключений в PE
В отличие от таблицы импорта или релокаций, таблица исключений — не обязательная часть PE. Она живёт в разделе .pdata (в 64-битных файлах) или прятена в другом месте для 32-битных сборок с определёнными настройками компилятора.
Для 64-битных PE (PE32+)
Здесь всё относительно просто. Таблица исключений хранится в разделе .pdata и описывается записью в таблице директорий данных (DataDirectory). Конкретно — элемент с индексом IMAGE_DIRECTORY_ENTRY_EXCEPTION (индекс 3).
Структура записей — IMAGE_RUNTIME_FUNCTION_ENTRY. Каждая запись описывает одну функцию или часть функции:
- BeginAddress — RVA начала функции.
- EndAddress — RVA конца функции.
- UnwindInfoAddress — RVA структуры UNWIND_INFO, которая описывает, как разворачивать стек при исключении.
Это стандартная схема для x64 Windows. Компиляторы (MSVC, Clang, MinGW) генерируют именно такой формат.
Для 32-битных PE (PE32)
Здесь ситуация сложнее. Официальной директории для таблицы исключений в 32-битном PE нет. Вместо этого компиляторы используют стековый фрейм с цепочкой SEH-регистраций, а таблица исключений может быть:
- встроена в секцию кода (например, через
__try/__exceptна уровне пролога функции); - описана в структурах, которые компиладор генерирует сам и не публикует в DataDirectory;
- найдена по сигнатурам в коде (например, по характерным вызовам
RtlUnwindили по паттернам пролога).
Поэтому для 32-битных файлов автоматическое извлечение таблицы исключений работает хуже — приходится анализировать код напрямую.
Как извлечь таблицу исключений: инструменты
1. Через заголовки PE вручную
Если вы пишете свой инструмент или скрипт, алгоритм для x64 выглядит так:
- Открыть PE, найти DataDirectory[IMAGE_DIRECTORY_ENTRY_EXCEPTION].
- Взять VirtualAddress и Size — это RVA и размер таблицы.
- Прочитать массив структур
IMAGE_RUNTIME_FUNCTION_ENTRY(каждая 12 байт, но выравнивание может добавить до 16). - Для каждой записи перейти по RVA UnwindInfoAddress и распарсить
UNWIND_INFOи массивUNWIND_CODE.
Структура UNWIND_INFO содержит:
- версию и флаги;
- размер пролога;
- коды разворачивания (массив
UNWIND_CODE) — описывают, какие регистры сохраняются, сколько стека выделяется и т.д.; - опционально — указатель на обработчик исключений (ExceptionHandler) и его данные (ExceptionData).
2. C помощью WinDbg
WinDbg умеет показывать таблицу исключений напрямую. Команда:
!exchain
Она выведет цепочку обработчиков исключений для текущего потока. Но это скорее для рантайма, а не для статического анализа файла.
Для статического анализа загрузите файл в WinDbg и используйте:
ln *<address>
!fnent <address>
!fnent показывает информацию о функции, включая unwind-данные и обработчики исключений.
3. IDA Pro и Ghidra
IDA Pro автоматически распознаёт .pdata и показывает функции с их SEH-информацией. В дизассемблере можно увидеть комментариях к функциям пометки о наличии обработчиков.
Ghidra тоже парсит .pdata при импорте файла. В разделе .pdata вы увидите записи функций, а в аннотациях — информацию об unwind-кодах. Если функция имеет обработчик исключений, Ghidra может показать ссылку на фильтр.
4. pefile (Python)
Библиотека pefile умеет читать DataDirectory для исключений, но парсинг самих UNWIND_INFO и UNWIND_CODE придётся делать вручную. Примерный подход:
import pefile
pe = pefile.PE("sample.exe")
# Проверяем, есть ли таблица исключений
if hasattr(pe, 'DIRECTORY_ENTRY_EXCEPTION'):
for entry in pe.DIRECTORY_ENTRY_EXCEPTION.entries:
begin = entry.BeginAddress
end = entry.EndAddress
unwind = entry.UnwindInfoAddress
print(f"Func: 0x{begin:08x} - 0x{end:08x}, Unwind: 0x{unwind:08x}")
Дальше нужно по RVA unwind прочитать структуру UNWIND_INFO и разобрать коды. Это не встроено в pefile — пишется самостоятельно.
5. dumpbin и llvm-readobj
Если у вас MSVC тулчейн:
dumpbin /unwindinfo sample.exe
Для LLVM-инструментов:
llvm-readobj --coff-exception-handling sample.obj
llvm-readobj --unwind sample.exe
Обе утилиты выведут таблицу исключений в читаемом текстовом виде.
Как читать UNWIND_INFO и что там искать
Когда вы вытащили таблицу, видите набор записей. Вот на что обращать внимание:
- Версия UNWIND_INFO — обычно 1 или 2. Влияет на формат флагов.
- Флаги — если установлен флаг
UNW_FLAG_EHANDLER, функция имеет обработчик исключений. ЕслиUNW_FLAG_UHANDLER— есть обработчик разворачивания (unwind handler). - Массив UNWIND_CODE — описывает действия в прологе: сохранение регистров, выделение стека, установка FP. Полезно для понимания, какие регистры функция использует.
- ExceptionHandler — адрес функции-фильтра. Это та самая функция, которая решает, перехватывать исключение или нет. Именно её нужно анализировать в первую очередь.
- ExceptionData — дополнительные данные, которые передаются обработчику. Часто это указатель на локальные переменные или контекст.
Если вы видите, что у функции есть обработчик, но фильтр указывает на адрес внутри подозрительной области кода — это повод изучить эту функцию детальнее. Часто так прячут антиотладочную логику.
Сравнение подходов к извлечению
| Инструмент | Что умеет | Для какого случая | Ограничения |
|---|---|---|---|
| pefile (Python) | Читает DataDirectory, даёт доступ к записям | Скриптовый анализ, автоматизация | Не парсит UNWIND_INFO автоматически |
| WinDbg | Показывает unwind-данные и обработчики | Отладка и динамический анализ | Требует запуска или загрузки дампа |
| IDA Pro | Автоматически распознаёт SEH, аннотирует функции | Глубокий статический анализ | Платный, ресурсоёмкий |
| Ghidra | Парсит .pdata, показывает структуру таблицы | Бесплатная альтернатива IDA | Автоматический парсинг не всегда корректен для нестандартных случаев |
| dumpbin / llvm-readobj | Текстовый вывод таблицы исключений | Быстрый просмотр без интерактивного анализа | Только для файлов, собранных соответствующим компилятором |
Что делать в зависимости от вашей задачи
Если вы анализируете малварь и ищете антиотладку: ищите функции с обработчиками исключений, которые вызывают IsDebuggerPresent, проверяют флаги отладки или модифицируют код. Часто обработчик исключений используется как способ изменить поток выполнения — например, через возврат ExceptionContinueExecution после модификации контекста.
Если вы отлаживаете краш в своём коде: найдите функцию, в которой произошёл сбой, и проверьте её unwind-данные. Если обработчик исключений не срабатывает — возможно, таблица исключений повреждена или отсутствует.
Если вы пишете инструмент для анализа PE: начните с парсинга DataDirectory, затем реализуйте разбор UNWIND_INFO и UNWIND_CODE. Для x64 это относительно прямолинейно. Для x86 будьте готовы к тому, что таблицы может не оказаться — придёт fallback-ить на анализ кода.
Частые ошибки при работе с таблицей исключений
- Путаница между RVA и файловым смещением. Все адреса в таблице — это RVA. Если вы читаете файл напрямую, не забудьте конвертировать их через секции.
- Ожидание таблицы в 32-битном PE. В x86 нет DataDirectory для исключений. Если ваш инструмент ищет только там — он ничего не найдёт.
- Неправильный размер записи.
IMAGE_RUNTIME_FUNCTION_ENTRY— 12 байт, но в некоторых случаях выравнивание добавляет 4 байта. Проверяйте документацию и реальные файлы. - Игнорирование UNWIND_CODE. Без разбора кодов вы не поймёте, какие регистры сохраняются и как устроен стек. А это критично для анализа.
- Предположение, что все функции имеют обработчики. Большинство функций не имеют обработчиков исключений. Флаги в UNWIND_INFO явно указывают на их наличие.
Практические рекомендации
- Всегда проверяйте наличие таблицы. Смотрите DataDirectory[3]. Если он пуст — таблицы нет.
- Используйте несколько инструментов. Если Ghidra что-то не показывает — проверьте WinDbg или dumpbin. Инструменты могут по-разному интерпретировать данные.
- Начните с простого. Для x64 PE таблица хорошо документирована и легко парсится. Начните с неё, прежде чем лезть в x86.
- Документируйте структуру. Если пишете парсер — сохраняйте не только адреса, но и unwind-коды. Они помогут позже при анализе стека.
- Проверяйте обработчики. Фильтр исключений — это функция. Если она делает что-то неочевидное — это красный флаг.
Итог
Таблица исключений в PE-файле — это карта того, как программа реагирует на ошибки. Для 64-битных файлов она лежит в .pdata и описывается через DataDirectory. Для 32-битных — всё сложнее, и автоматическое извлечение может не сработать.
Чтобы извлечь таблицу: используйте pefile для доступа к записям, WinDbg или IDA для интерактивного анализа, dumpbin или llvm-readobj для быстрого просмотра. При парсинге не забывайте про конвертацию RVA и проверку флагов в UNWIND_INFO.
Если вы анализируете малварь — обращайте внимание на обработчики с неочевидной логикой. Если отлаживаете свой код — проверяйте, что таблица корректна и обработчики срабатывают. Главное — не полагайтесь на один инструмент и всегда перепроверяйте результат.
