Как проанализировать структуру ELF и сравнить её с PE — практическое руководство для разработчика и аналитика

Как проанализировать структуру ELF и сравнить её с PE — практическое руководство для разработчика и аналитика

Ты работаешь с бинарниками на Linux — может, отлаживаешь краш, анализируешь вредоносное ПО, или пишешь свой загрузчик. И вдруг тебе нужно понять: а как устроена эта штука внутри? И почему на Windows всё иначе? Ты открываешь objdump, видишь кучу секций, заголовков, смещений — и теряешься. Или ты только что получил .exe-файл с Windows, а тебе нужно понять, как он отличается от .so или .elf, который ты привык видеть.

Эта статья — не теоретический обзор. Это пошаговый гайд, как реально смотреть внутрь ELF и PE, что искать, на что обращать внимание, и как не запутаться. Я покажу, как это делают в реальной работе — без воды, с конкретными командами, примерами и сравнением.

Что такое ELF и PE — просто, без заумностей

ELF (Executable and Linkable Format) — стандартный формат исполняемых файлов в Linux, BSD, Android и других Unix-подобных системах. Это не просто «файл программы». Это структура, которая говорит операционной системе: где лежат код, данные, таблицы импорта, символы отладки, и как всё это запустить.

PE (Portable Executable) — аналогичный формат, но для Windows. Он используется для .exe, .dll, .sys и других бинарников. По сути, это то же самое, что ELF, но с другим дизайном, другим синтаксисом и другими правилами.

Оба формата — это не просто «запускаемый код». Это контейнеры с метаданными, которые ОС читает перед запуском. Если ты не понимаешь, как устроен этот контейнер, ты не можешь уверенно анализировать, почему программа не запускается, почему она крашится, или как она взаимодействует с системой.

Как посмотреть структуру ELF — пошагово

Допустим, у тебя есть файл myapp на Linux. Ты хочешь понять, из чего он состоит. Вот что ты делаешь:

  1. Проверь, что это вообще ELF — просто запусти: file myapp. Если видишь ELF 64-bit LSB executable — всё в порядке. Если пишет «ASCII text» или «PE32+» — это не ELF.
  2. Посмотри заголовок ELF: readelf -h myapp. Ты увидишь:
    • Entry point address — где начинается выполнение.
    • Type — это executable, shared object (.so), или relocatable?
    • Machine — x86-64, ARM, MIPS?
    • Number of program headers — сколько сегментов (LOAD, LOAD, DYNAMIC и т.д.)
  3. Смотри сегменты (Program Headers): readelf -l myapp. Это то, что ОС читает при загрузке. Каждый сегмент — это кусок файла, который нужно загрузить в память. Важные типы:
    • LOAD — код или данные, которые должны быть загружены в память.
    • DYNAMIC — содержит информацию о динамических зависимостях (например, libc.so.6).
    • INTERP — путь к интерпретатору (например, /lib64/ld-linux-x86-64.so.2).
  4. Смотри секции (Section Headers): readelf -S myapp. Это для линковщика и отладчика. Не все секции попадают в память при запуске. Важные:
    • .text — исполняемый код.
    • .data — инициализированные глобальные переменные.
    • .bss — неинициализированные глобальные переменные (в файле не занимают места, но выделяются в памяти).
    • .dynsym, .dynstr — таблицы импорта/экспорта для динамической линковки.
    • .symtab, .strtab — символы для отладки (если не обрезаны).
  5. Проверь зависимости: ldd myapp. Покажет, какие библиотеки он ищет в системе. Если видишь not found — это причина краша.
  6. Если нужно глубже — используй objdump -d myapp для дизассемблирования кода, или hexdump -C myapp | less для просмотра сырых байтов.

Это не всё. Но этого достаточно, чтобы понять: где код, какие библиотеки нужны, где инициализируются данные, и как программа запускается.

Как посмотреть структуру PE — аналогично, но по-другому

Теперь возьми Windows-файл: myapp.exe. Ты не можешь использовать readelf — он не поймёт PE. Тебе нужны другие инструменты.

  1. Проверь тип файла: file myapp.exe — если пишет PE32+ или PE32, значит, это Windows-бинарник.
  2. Посмотри заголовок PE: используй objdump -x myapp.exe (если у тебя установлен MinGW или Cygwin) или dumpbin /headers myapp.exe в командной строке Windows. Ты увидишь:
    • Machine — x64, x86, ARM64.
    • Number of sections — сколько разделов в файле.
    • Time date stamp — когда был скомпилирован.
    • Size of optional header — ключевой параметр, он есть только у исполняемых файлов и DLL.
  3. Смотри разделы (Sections): в выводе objdump -x или dumpbin /sections ты увидишь:
    • .text — код (аналогично ELF).
    • .data — инициализированные данные.
    • .rdata — только чтение (константы, строки).
    • .bss — неинициализированные данные.
    • .rsrc — ресурсы (иконки, строки, манифесты).
    • .reloc — таблица релокаций (нужна для ASLR).
  4. Импорт и экспорт: dumpbin /imports myapp.exe покажет, какие DLL он использует (например, kernel32.dll, msvcrt.dll). dumpbin /exports — если это DLL, какие функции она экспортирует.
  5. Динамическая линковка: в PE всё хранится в таблице импорта (IAT — Import Address Table). В ELF это делается через .dynsym и .got. В PE таблица импорта — это просто массив указателей на функции, которые заполняются загрузчиком при старте.

В PE есть ещё один важный элемент — директории (Data Directories). Их 16, и они указывают, где в файле находятся таблицы импорта, экспорта, ресурсов, исключений и т.д. Это нечто вроде «содержания» файла. Если ты смотришь на PE в Hex-редакторе — ищи сигнатуру 50 45 00 00 («PE\0\0»), а потом смещение к директориям — оно указано в заголовке.

Сравнение ELF и PE — таблица, которую реально использовать

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

Элемент ELF (Linux) PE (Windows)
Основной заголовок Elf64_Ehdr — 64 байта, фиксированная структура IMAGE_NT_HEADERS — переменный размер, начинается с сигнатуры PE\0\0
Загрузка в память Сегменты (PT_LOAD) — указывают, где и как загружать Разделы (.text, .data) — загружаются по описанию в Optional Header
Точка входа Поле e_entry в заголовке ELF Поле AddressOfEntryPoint в Optional Header
Динамическая линковка .dynamic секция + .dynsym, .dynstr, .got Таблица импорта (IAT) — в разделе .idata
Импорт функций Символы в .dynsym, имена в .dynstr Таблица импорта с именами DLL и функций, адреса заполняются при загрузке
Экспорт функций Нет стандартного механизма — используется .symtab для отладки Таблица экспорта в .edata — обязательна для DLL
Ресурсы Нет встроенного механизма — хранятся как данные в .data Отдельный раздел .rsrc с деревом ресурсов (иконки, строки, манифесты)
Релокации При компиляции обычно отключены (PIE — положение независимый код) Обязательны для ASLR — таблица .reloc в PE
Интерпретатор PT_INTERP — путь к /lib64/ld-linux-x86-64.so.2 Нет — загрузчик Windows встроен в ОС
Инструменты анализа readelf, objdump, hexdump, nm dumpbin, objdump (MinGW), PE Explorer, CFF Explorer

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

Что выбрать: ELF или PE — в зависимости от твоей задачи

Ты не выбираешь между ELF и PE. Ты выбираешь, как анализировать тот бинарник, который у тебя есть.

  • Если ты работаешь с Linux-бинарником — используй readelf -h и readelf -l как первую команду. Если хочешь понять, какие библиотеки нужны — ldd. Если нужно посмотреть код — objdump -d.
  • Если ты получил .exe с Windows — открой его в dumpbin (если есть Visual Studio) или в objdump -x (если есть MinGW). Ищи /imports и /exports. Если нет — возможно, это упакованный или обфусцированный файл.
  • Если ты переносишь код с Linux на Windows — не жди, что .so станет .dll. Это разные форматы. Тебе нужно перекомпилировать исходники под PE. Анализ ELF не поможет тебе понять, как работает PE-эквивалент.
  • Если ты анализируешь вредоносное ПО — смотри на отсутствие импорта в PE (это признак статической линковки или обфускации), или на наличие множества PT_LOAD сегментов в ELF (возможно, это мульти-стадийный загрузчик).
  • Если ты пишешь свой загрузчик — ELF проще: ты читаешь сегменты, загружаешь их в память, прыгаешь по e_entry. В PE тебе нужно парсить Optional Header, искать IAT, обрабатывать релокации — сложнее, но более стандартизировано.

Частые ошибки — и как их избежать

Я видел, как люди тратят часы, потому что делали одно из этого:

  • Смотрят только на .text — и думают, что это весь код. А в ELF там может быть только часть. Остальное — в других PT_LOAD сегментах. В PE — в .rdata или .text с разными атрибутами.
  • Путают секции и сегменты — в ELF readelf -S показывает секции (для линковки), а readelf -l — сегменты (для загрузки). В PE dumpbin /sections показывает и то, и другое — потому что в PE нет чёткого разделения. Это путает.
  • Игнорируют зависимости — если ldd показывает not found, это не «ошибка компиляции», это причина краша. Аналогично — если в PE нет импорта kernel32.dll, но программа использует CreateFile — это невозможно. Значит, ты смотришь не на тот файл, или он упакован.
  • Думают, что PE-файл — это «более сложный ELF» — нет. Это другой формат с другой логикой. Не пытайся «перевести» ELF-понятия в PE. Они не совпадают.
  • Используют только GUI-инструменты — CFF Explorer или IDA Pro — это круто, но если ты не умеешь читать readelf -h, ты не поймёшь, что видишь. GUI скрывает структуру. Сначала — команды, потом — графика.

Как лучше делать — практические рекомендации

Вот что я делаю в реальной работе:

  1. Всегда начинай с file — это 2 секунды, но спасает от траты времени на неправильный файл.
  2. Для ELF: readelf -hreadelf -llddobjdump -d. Это цепочка, которую я повторяю для каждого файла. Не пропускаю.
  3. Для PE: dumpbin /headersdumpbin /importsdumpbin /exportsdumpbin /disasm. Если dumpbin недоступен — использую objdump -x в MinGW.
  4. Сравниваю размеры: если ELF-файл весит 800 КБ, а PE-аналог — 12 МБ — это не «лучше». Это значит, что в PE всё статически скомпилировано, или есть ресурсы. Проверяю .rsrc в PE — часто там встроенные иконки, манифесты, логотипы.
  5. Ищу аномалии: в ELF — если есть 5 PT_LOAD сегментов, а в .text только один — это подозрительно. В PE — если нет .reloc и программа запускается на любой системе — это либо старый код, либо обфускация.
  6. Сохраняю выводы в текст: я копирую вывод readelf -h и readelf -l в файл и отмечаю ключевые моменты: «импорт: libc.so.6», «точка входа: 0x401230», «интерпретатор: /lib64/ld-linux-x86-64.so.2».

Итог: что делать прямо сейчас

У тебя есть бинарник. Не знаешь, что с ним делать?

Если это Linux-файл (.elf, .so, без расширения):

  • Запусти: file имя_файла
  • Запусти: readelf -h имя_файла — запомни точку входа и тип
  • Запусти: readelf -l имя_файла — посмотри, какие сегменты загружаются
  • Запусти: ldd имя_файла — убедись, что все библиотеки есть

Если это Windows-файл (.exe, .dll):

  • Запусти: file имя_файла
  • Запусти: dumpbin /headers имя_файла (или objdump -x) — найди Machine и Number of Sections
  • Запусти: dumpbin /imports имя_файла — посмотри, какие DLL требуются
  • Если нет импорта — проверь, не упакован ли файл (например, UPX)

Если ты это сделал — ты уже знаешь больше 90% людей, которые «анализируют бинарники». Ты не просто «открыл в Hex-редакторе». Ты понимаешь, как файл устроен. И теперь ты можешь ответить на вопрос: «Почему он не запускается?», «Что он делает?», «Как его перенести?» — без паники и без гугления.

Практикуй это на 3-5 файлах. Сначала на простых — /bin/ls, /usr/bin/python3. Потом на своих. Через неделю ты будешь смотреть на бинарник и сразу понимать: «Это ELF, статический, без импорта — значит, не использует динамические библиотеки». Или: «Это PE с ресурсами, но без импорта — значит, всё вшито». Это не магия. Это опыт. И ты уже на пути к нему.

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

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