Как проанализировать структуру файла ELF и сравнить её с PE‑форматом

Допустим, вам на руки попал исполняемый файл, и нужно понять — что он из себя представляет, собрал ли кто‑то его руками или получил естественным путём, можно ли его отладить. Или вы кросс‑компилируете под Linux и хотите убедиться, что результат принципиально отличается от Windows‑сборки. В обоих случаях первый шаг — заглянуть внутрь формата. ELF и PE устроены по‑разному, и разница не только в имени и расширении файла.

С чего начать анализ ELF

Сам ELF (Executable and Linkable Format) — это стандартный бинарный формат для Unix‑подобных систем. Он задаёт структуру исполняемых файлов, объектных файлов, разделяемых библиотек и дампов ядра. ELF‑файл состоит из нескольких ключевых частей, и анализ всегда начинается с заголовка.

ELF‑заголовок: базовая информация

Структура ELF определяется стандартом и включает:

  • идентификаторы — магическое число 0x7f и символы «E», «L», «F»;
  • указание архитектуры (32‑бит или 64‑бит);
  • тип файла (исполняемый, перемещаемый, динамическая библиотека);
  • точка входа (entry point);
  • смещение таблицы заголовков секций;
  • смещение таблицы программных заголовков.

Это минимальный объём данных, без которого невозможно понять, как интерпретировать остальную часть файла. Типичная ситуация: вы открываете файл в 16‑ричном редакторе или вызываете readelf -h, и первое, что вы увидите — именно эти поля.

Секции и сегменты: в чём разница

В ELF есть два похожих, но принципиально разных понятия: секции (sections) и сегменты (program headers). Секции — это минимальные единицы компиковки и линковки. В них лежит код, данные, отладочная информация, таблицы символов. Сегменты — это способы загрузить файл в память: один сегмент может включать несколько секций, если у них одинаковые флаги доступа.

Когда вы анализируете ELF‑файл, смотрите на оба списка. readelf -S покажет все секции, readelf -l — программные заголовки. Например, секция .text содержит исполняемый код, .data — инициализированные данные, .bss — обнуляемые данные. Секции могут быть пустыми, если код оптимизирован или отладка отключена.

Таблицы символов и имена

ELF хранит информацию о функциях и глобальных переменных в таблицах символов. Две основные секции:

  • .symtab — полная таблица символов, включая локальные;
  • .dynsym — таблица символов, нужных для динамической компоновки.

Можно посмотреть их через readelf -s или objdump -t. Если символы вырезаны (stripped), вы увидите только динамические имена. Это нормально для большинства бинарников, которые вы получаете в готовом виде.

Как устроен PE‑файл

PE (Portable Executable) — формат исполняемых файлов Windows. Он произошёл от olderформата MZ (DOS‑заголовок), который до сих пор часто можно найти в начальных байтах PE‑файла как дань обратной совместимости. Несмотря на это, основная структура PE радикально отличается от ELF.

DOS‑заголовок и переход

PE‑файл начинается с DOS‑заголовка, который содержит два поля:

  • магическое число 0x5A4D («MZ»);
  • смещение на PE‑заголовок (обычно по адресу e_lfanew).

Дальше идёт DOS‑код, который при запуске обычно выводит сообщение «This program cannot be run in DOS mode». Затем располагается PE‑заголовок с сигнатурой «PE\0\0». Уже здесь видно одно из первых отличий: в PE исторически заложена ступенька совместимости, которой нет в ELF.

PE‑заголовок и опциональный заголовок

После сигнатуры идёт стандартный (COFF) заголовок и опциональный заголовок. В стандартном заголовке указываются:

  • архитектура (Machine);
  • количество секций;
  • временная метка сборки;
  • смещение таблицы символов (если она есть);
  • размер опционального заголовка.

В опциональном заголовке — точка входа, предпочтительный базовый адрес загрузки, размер стека, кучи, подсистема (GUI, консоль, драйвер) и много других вещей. В Windows без опционального заголовка нормальный exe‑файл собрать нельзя.

Секции в PE

Каждая секция в PE описывается собственным заголовком с именем, размером, смещением, флагами доступа. Названия часто начинаются с точки: .text, .data, .rdata, .reloc. В отличие от ELF, секции в PE обычно не группируются в сегменты явно — загрузчик сам по флагам определяет, как отобразить секцию в память.

Где смотреть структуру на практике

Что смотреть ELF (Linux/Unix) PE (Windows)
Заголовок файла readelf -h CFF Explorer, PEview
Список секций readelf -S objdump -h (mingw)
Программные заголовки readelf -l видны в опциональном заголовке
Таблица символов readelf -s objdump -tили отладочные символы
Точка входа в ELF‑заголовке в опциональном заголовке
Отладочная информация секции .debug_*, DWARF PDB, CV‑записи, CodeView

Если вы работаете на Linux, но хотите посмотреть PE‑файл, удобно использовать objdump из пакета binutils‑mingw. Для серьёзного анализа Windows‑бинарей пригодятся инструменты вроде CFF Explorer, PEview и Ghidra.

Сравнение структур: что принципиально важно

ELF и PE — оба формата, описывающие, как загрузить код в память и отладить его. Но подходы к организации информации у них разные. Вот ключевые различия, которые сразу бросаются в глаза при анализе:

  • Метаданные и расширяемость. ELF легко добавляет новые секции и типы заголовков. Секции могут содержать пользовательские данные, релокации, информацию о версиях, зависимости от конкретных символов. PE более жёстко структурирован и меньше поддаётся расширению без изменения формата.
  • Типы содержимого. ELF описывает не только исполняемые файлы, но и объектные файлы, разделяемые библиотеки, core‑дампы. Это сделало формат универсальным для всей экосистемы сборки и отладки. PE в основном ориентирован на исполняемые файлы и DLL.
  • Модель загрузки. В Linux загрузчик (ld.so) и ядро операются сегментами ELF и динамической компоновкой, описанной в .dynamic. В Windows загрузчик PE сам ищет импортируемые библиотеки, обрабатывает таблицу импорта (IAT), и все адреса в PE обычно привязаны к предпочтительному базовому адресу, если нет ASLR.
  • Отладочная информация. В Linux отладочные данные часто страницы ELF‑файла (DWARF), в Windows — внешний PDB‑файл, хотя раньше использовались встроенные CodeView‑записи.
  • Флаги и выравнивание. Оба формата учитывают требования выравнивания секций, но у PE жёстче с минимальной гранулярностью отображения в памяти, что влияет на размер файла при большом количестве мелких секций.

Когда какой формат анализировать

Сценарий 1. Вам пришёл бинарник под Linux, и надо понять, собрал ли его GCC или Clang, есть ли отладочные данные, какие библиотеки нужны для запуска. Вы смотрите ELF‑заголовок, список секций, .dynamic, таблицу символов. Если формат странный — возможно, файл обфусцирован или получен кросс‑компиляцией.

Сценарий 2. Вы получили Windows‑exe, и нужно проверить, не повреждён ли заголовок, не зашифрованы ли секции, какие DLL импортируются. Смотрите DOS‑заголовок, PE‑сигнатуру, опциональный заголовок, таблицу импорта. Если в начале файла не «MZ», а что‑то другое — возможно, это не PE вообще.

Сценарий 3. Вы сравниваете один и тот же исходный код, скомпилированный под две платформы. Тогда полезно посмотреть, как распределён код и данные по секциям, как работают релокации, как организована динамическая компоновка. В ELF вы увидите .got, .plt, в PE — IAT и секцию .reloc.

Частые ошибки при анализе

  • Путать секции и сегменты в ELF. Секция .text может входить в один программный заголовок вместе с .rodata. Если вы смотрите только секции, можно не понять, как именно загрузчик отобразит файл в память.
  • Игнорировать DOS‑заголовок в PE. Если вы сразу ищете «PE» с начала файла, потратите время впустую. Надо сначала прочитать e_lfanew и перейти по смещению.
  • Считать, что отсутствие символов — признак вредоносности. В реальных продуктах символы часто вырезают на этапе сборки. Это не обязательно вирус, просто стандартная практика.
  • Не учитывать выравнивание. Размер секции на диске может отличаться от размера в памяти. Если вы вручную парсите файл, путаница с выравниванием приводит к неверным смещениям.
  • Полагаться только на один инструмент. readelf удобен, но иногда данные в секциях могут быть неполными или повреждёнными. Полезно перепроверять objdump или Ghidra.

Как лучше сделать анализ

  1. Определите формат. Первые 4 байта ELF — 7f 45 4c 46, PE — 4d 5a. Если вы видите что‑то другое, возможно, это скрипт, архив или файл другой ОС.
  2. Прочитайте основной заголовок. Убедитесь, что архитектура совпадает с ожидаемой (x86, x86_64, ARM). Проверьте точку входа — она должна быть в пределах адресного пространства секций.
  3. Изучите секции. Обратите внимание на названия, размеры, флаги. Если секция .text пустая, а файл позиционируется как исполняемый — это подозрительно.
  4. Посмотрите динамические зависимости. В ELF — .dynamic и readelf -d. В PE — таблица импорта. Так вы поймёте, какие библиотеки нужны для запуска.
  5. Проверьте отладочную информацию. Если она есть, анализ становится намного проще. Если её нет, придётся ориентироваться по именам динамических символов и строкам.
  6. Используйте дизассемблер. Ghidra, IDA, radare2 позволяют увидеть реальную структуру кода, перекрёстные ссылки, вызовы функций. Это следующий уровень после чтения заголовков.

Итог

Анализ ELF и PE — это не разовое чтение магического числа, а последовательное восстановление картины: как файл собран, какие данные содержит, как будет загружаться в память. ELF отличается гибкостью и универсальностью, PE — более жёсткой структурой с историческими наслоениями. Если вы знаете, где в каждом формате расположены заголовки, секции, таблицы символов и динамические зависимости, вы можете быстро оценить любой бинарник и понять, что с ним делать дальше.

Начните с простого: откройте несколько реальных файлов в readelf и objdump, посмотрите, как меняется структура при отключении отладки, при статической и динамической компоновке. После пары десятков примеров различия между ELF и PE станут не теоретическими, а видны «на глаз».

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