Как извлечь и проанализировать таблицу исключений в PE-файле

Если вы работаете с PE-файлами — будь то анализ вредоносного ПО, реверс-инжиниринг или отладка — рано или поздно столкнётесь с тем, что файл использует структурированную обработку исключений (SEH) и таблицы исключений. Они описывают, как программа реагирует на сбои: деление на ноль, недопустимый доступ к памяти, переполнение стека. И если вы хотите понять, что делает файл при падении — или найти скрытую логику — нужно уметь находить и читать эти таблицы.

Разберёмся, где в PE-файле лежит таблица исключений, как она устроена, какие инструменты помогают её вытащить и на что смотреть при анализе.

Зачем вообще смотреть на исключения в 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 выглядит так:

  1. Открыть PE, найти DataDirectory[IMAGE_DIRECTORY_ENTRY_EXCEPTION].
  2. Взять VirtualAddress и Size — это RVA и размер таблицы.
  3. Прочитать массив структур IMAGE_RUNTIME_FUNCTION_ENTRY (каждая 12 байт, но выравнивание может добавить до 16).
  4. Для каждой записи перейти по 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 явно указывают на их наличие.

Практические рекомендации

  1. Всегда проверяйте наличие таблицы. Смотрите DataDirectory[3]. Если он пуст — таблицы нет.
  2. Используйте несколько инструментов. Если Ghidra что-то не показывает — проверьте WinDbg или dumpbin. Инструменты могут по-разному интерпретировать данные.
  3. Начните с простого. Для x64 PE таблица хорошо документирована и легко парсится. Начните с неё, прежде чем лезть в x86.
  4. Документируйте структуру. Если пишете парсер — сохраняйте не только адреса, но и unwind-коды. Они помогут позже при анализе стека.
  5. Проверяйте обработчики. Фильтр исключений — это функция. Если она делает что-то неочевидное — это красный флаг.

Итог

Таблица исключений в PE-файле — это карта того, как программа реагирует на ошибки. Для 64-битных файлов она лежит в .pdata и описывается через DataDirectory. Для 32-битных — всё сложнее, и автоматическое извлечение может не сработать.

Чтобы извлечь таблицу: используйте pefile для доступа к записям, WinDbg или IDA для интерактивного анализа, dumpbin или llvm-readobj для быстрого просмотра. При парсинге не забывайте про конвертацию RVA и проверку флагов в UNWIND_INFO.

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

Оцените статью
PEFile — Безопасность и технологии простым языком