Главная / Блог / Анализ ELF и PE файлов: читаем заголовки до Ghidra

11 мин.00

Анализ ELF и PE файлов: читаем заголовки до Ghidra

Анализ ELF и PE файлов: читаем заголовки до Ghidra

На прошлогоднем CTF таск из категории reverse решился за полторы минуты — без дизассемблера. readelf -h показал entry point, указывающий внутрь .bss, три секции назывались .UPX0/.UPX1/.UPX2, энтропия .text выдавала 7.9+. Решение: upx -d, strings | grep flag — готово. Весь анализ ELF и PE файлов на соревнованиях и в пентесте начинается не с Ghidra, а с заголовков. 64 байта ELF-хедера или путь от MZ-стаба до PE-сигнатуры выдают архитектуру, пакеры, стрипперы и антиотладочные механизмы быстрее, чем дизассемблер закончит автоанализ.

Magic bytes — первые 4 байта определяют формат

Прежде чем лезть в структуру заголовка ELF файла или структуру PE файла, стоит понять, что за бинарник перед тобой. file в Linux делает это за миллисекунды — читает magic bytes и выдаёт сводку: Подробнее — в нашем материале про бинарный анализ уязвимостей.

$ file ./challenge
challenge: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, not stripped
$ file ./task.exe
task.exe: PE32+ executable (console) x86-64, for MS Windows

Одна строка — и уже ясно: архитектура, битность, тип линковки, наличие таблицы символов. За этим стоят конкретные байты. ELF открывается последовательностью 7F 45 4C 46 (байт \x7F + ASCII ELF). PE начинается с 4D 5A (ASCII MZ — инициалы Mark Zbikowski, одного из разработчиков MS-DOS). Через xxd ./binary | head -4 они видны голыми руками.

Зачем проверять вручную, если file справляется? На CTF авторы регулярно отдают бинарник без расширения, с подменённым или обрезанным заголовком. В анализе малвари атакующие применяют подмену типа файла — Masquerade File Type (T1036.008, Defense Evasion по MITRE ATT&CK): расширение .jpg или .pdf, а внутри — исполняемый код. Расширение — метаданные файловой системы, magic bytes — часть самого файла. Первое переименовывается одной командой, второе — если изменить, загрузчик ОС откажется работать с бинарником.

Структура заголовка ELF файла

ELF-заголовок для 64-битного бинарника занимает ровно 64 байта, для 32-битного — 52 байта. Каждое поле имеет фиксированное смещение, так что при желании парсится в hex-редакторе без дополнительных утилит.

ELF Header: readelf -h и что в нём читать

Самые наглядные примеры использования readelf — начинать с ключа -h. Вот типичный результат на CTF-бинарнике:

$ readelf -h ./challenge
ELF Header:
  Magic:   7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00
  Class:                             ELF64
  Data:                              2's complement, little endian
  Type:                              EXEC (Executable file)
  Machine:                           Advanced Micro Devices X86-64
  Entry point address:               0x400720
  Start of program headers:          64 (bytes into file)
  Start of section headers:          6440 (bytes into file)
  Number of section headers:         29

На что смотреть за 30 секунд при статическом анализе бинарных файлов:

Magic (16 байт, смещение 0x00). Первые 4 байта 7F 45 4C 46 подтверждают формат. Пятый байт — класс: 01 = ELF32, 02 = ELF64. Шестой — порядок байт: 01 = little-endian, 02 = big-endian. Для x86/x86-64 всегда 01. Видишь 02 — MIPS big-endian или другая экзотика, и настройки дизассемблера сразу меняются.

Type (e_type, смещение 0x10). Что перед тобой: - ET_REL (1) — объектный файл (.o), не слинкован - ET_EXEC (2) — исполняемый файл с фиксированным базовым адресом - ET_DYN (3) — позиционно-независимый код. Нюанс, на котором спотыкаются: современные компиляторы по умолчанию генерируют ET_DYN даже для обычных исполняемых файлов (PIE). Это не библиотека — это PIE-бинарник - ET_CORE (4) — core dump

Machine (e_machine, смещение 0x12). Определение архитектуры бинарного файла: EM_386 (3) = i386, EM_X86_64 (62) = x86-64, EM_ARM (40) = ARM, EM_AARCH64 (183) = AArch64. На CTF от этого поля зависит, какой процессорный модуль выбирать в дизассемблере.

Entry point (e_entry, смещение 0x18 для ELF64). Виртуальный адрес точки входа. Нормальный entry point лежит внутри .text. Если он указывает в .data, .bss или секцию с нестандартным именем — красный флаг: пакер, обфускатор или самомодифицирующийся код.

Секции и сегменты бинарного файла

Одна из самых частых ошибок при чтении заголовков исполняемых файлов формата ELF — путать секции с сегментами. Это два параллельных представления одного файла.

Сегменты (Program Headers) описывают, как ядро загружает файл в память. readelf -l выводит их полный список. Типы сегментов, критичные для триажа:

Тип сегмента Назначение На что смотреть
PT_LOAD Загружаемый сегмент Флаги R/W/X: код (R+X), данные (R+W)
PT_INTERP Путь к динамическому линкеру Нет PT_INTERP = статическая линковка
PT_DYNAMIC Информация для динамической линковки Зависимости, PLT/GOT
PT_GNU_STACK Флаги стека Флаг X = исполняемый стек (NX off)
PT_GNU_RELRO Read-Only relocations GOT защищён от перезаписи

В CTF/pwn задачах PT_GNU_STACK с флагом X — прямой намёк на shellcode-решение. PT_GNU_RELRO без Full RELRO означает, что GOT-overwrite ещё в игре.

Секции (Section Headers) — логическая структура для линкера и отладчика. readelf -S выводит таблицу. Основные:

Секция Содержимое Флаги
.text Машинный код AX (allocatable + executable)
.data Инициализированные переменные WA (writable + allocatable)
.rodata Константы, строки (read-only) A
.bss Неинициализированные данные WA
.plt / .got Таблицы динамической линковки
.symtab / .strtab Таблица символов и строк

Принцип простой: секции нужны для анализа, сегменты — для загрузки. Один сегмент объединяет несколько секций. Секции можно удалить командой strip — бинарник запустится (ядро использует сегменты), но в дизассемблере пропадут имена функций. Это техника Stripped Payloads (T1027.008, Defense Evasion по MITRE ATT&CK), и на CTF она встречается повсеместно.

Защитные механизмы — видны из заголовков

По заголовкам можно определить компиляторные защиты до запуска checksec:

  • NX: PT_GNU_STACK без флага X — стек не исполняемый
  • RELRO: наличие PT_GNU_RELRO — GOT частично или полностью защищён
  • Stack Canaries: символ __stack_chk_fail в таблице символов (видно через readelf -s | grep stack_chk)
  • PIE: тип ET_DYN вместо ET_EXEC — адреса рандомизируются при каждом запуске
  • FORTIFY_SOURCE: символы __memcpy_chk, __strcpy_chk в импортах

Для pwn-задач комбинация NX off + no canaries = ret2shellcode. PIE off = фиксированные адреса для ROP-цепочек. Собственно, ради этого и читаем заголовки до загрузки в дизассемблер.

Структура PE файла — от MZ до Import Table

PE-формат тащит за собой наследие MS-DOS, из-за чего путь до реального заголовка длиннее, чем в ELF. Но логика та же: магические байты, метаданные, секции.

DOS Header и PE Signature

Каждый PE-файл начинается с DOS Header размером 64 байта. Для анализа исполняемых файлов перед динамическим анализом нужны два поля:

  • e_magic (смещение 0x00): 0x5A4D — литералы MZ. Подтверждение формата
  • e_lfanew (смещение 0x3C): 4-байтное смещение до PE-сигнатуры. Через это поле загрузчик и аналитик находят реальный заголовок. Как справедливо замечают в разборах chuongdong.com — это «самое важное поле DOS Header»

Между DOS Header и PE Signature лежит DOS Stub — программа реального режима, обычно выводящая "This program cannot be run in DOS mode". Размер стаба — как правило, 38 байт, но он может быть произвольным. На CTF авторы иногда прячут подсказки или даже флаги внутри DOS Stub (да, прямо там). Стоит заглядывать через xxd.

По смещению из e_lfanew лежит PE Signature: 0x50450000 (ASCII PE\0\0). Нет этой сигнатуры — файл не валидный PE, даже при наличии MZ в начале.

COFF Header, Optional Header и секции PE

Сразу после PE Signature идёт COFF File Header (20 байт):

  • Machine: 0x014C = i386, 0x8664 = x86-64 — определение архитектуры бинарного файла, то есть какой процессорный модуль ставить в Ghidra
  • NumberOfSections: стандартный PE содержит 3-6 секций (.text, .data, .rdata, .rsrc, .reloc). Больше 10 или секции с нестандартными именами — пахнет пакером
  • TimeDateStamp: время компиляции. Нулевое значение или дата из далёкого будущего — подозрительно. Пакеры и билдеры малвари часто обнуляют это поле

Optional Header (обязательный для исполняемых файлов, несмотря на название — спасибо мелкософту за нейминг) содержит:

  • Magic: 0x010B = PE32, 0x020B = PE32+ (64-bit)
  • AddressOfEntryPoint — RVA точки входа, аналог e_entry в ELF
  • ImageBase — предпочтительный базовый адрес загрузки, по умолчанию 0x400000 для EXE
  • Data Directories — массив из 16 записей, указывающих на Import Table, Export Table, Resource Table

PE Header Import Table — самая информативная структура для быстрого триажа. Она показывает, какие DLL и функции бинарник импортирует:

  • Минимальный набор импортов (только kernel32.dll с парой функций) при большом размере бинарника — признак пакера, который резолвит API динамически
  • VirtualAlloc + WriteProcessMemory + CreateRemoteThread — классическая тройка для инъекции кода
  • IsDebuggerPresent, NtQueryInformationProcess — антиотладочные проверки (T1497.001, System Checks по MITRE ATT&CK)
  • Полное отсутствие Import Table — бинарник разрешает импорты через LoadLibrary/GetProcAddress в рантайме

Export Table обычно принадлежит DLL, а не EXE. Если EXE экспортирует функции — паттерн нетипичный, повод присмотреться.

Инструменты для статического анализа ELF PE

Набор утилит для reverse engineering бинарников, который отрабатывает быстрее любого дизассемблера:

ELF-тулкит (Linux): file для определения формата; readelf с ключами -h (заголовок), -l (сегменты), -S (секции), -d (динамические зависимости), -s (таблица символов); objdump -x для сводной информации; strings -a для извлечения ASCII/Unicode-строк; xxd для raw hex-дампа.

PE-тулкит (кроссплатформенный): file работает и для PE на Linux; Python-библиотека pefile парсит все структуры из скрипта; PE-bear и CFF Explorer — GUI-анализаторы на Windows.

import pefile
pe = pefile.PE('task.exe')
print(f"Machine: {hex(pe.FILE_HEADER.Machine)}")
print(f"Entry:   {hex(pe.OPTIONAL_HEADER.AddressOfEntryPoint)}")
print(f"Sections: {pe.FILE_HEADER.NumberOfSections}")
for s in pe.sections:
    name = s.Name.decode().rstrip(chr(0))
    print(f"  {name:8s} VSize={hex(s.Misc_VirtualSize)} Raw={hex(s.SizeOfRawData)}")

Шесть строк — и на экране архитектура, entry point, список секций с размерами. Если виртуальный размер секции в разы больше raw-размера — данные распаковываются в памяти, классический признак Software Packing (T1027.002).

Красные флаги в заголовках: пакеры и обфускация

При анализе ELF и PE файлов — на CTF или при исследовании малвари — заголовки выдают подозрительные бинарники ещё до дизассемблирования. Собственно, ради этого весь триаж и затевается.

Software Packing (T1027.002, Defense Evasion): - Секции с именами .UPX0, .UPX1, .aspack, .petite — открытый признак конкретного пакера - Секции с нечитаемыми именами вместо ASCII - Entry point указывает не в .text, а в последнюю секцию (паттерн распаковщика: код в конце, переход на начало после распаковки) - Минимальная Import Table в PE при большом размере файла - Энтропия секций выше 7.0 (обычный код — 5.0-6.5, запакованный — 7.5+)

Stripped Payloads (T1027.008): - file показывает stripped - readelf -S не содержит .symtab и .strtab - Дизассемблер показывает sub_401000 вместо реальных имён функций

Obfuscated Files or Information (T1027): - Нестандартные значения в e_ident (неожиданный OS/ABI-байт) - Энтропия всего файла выше 7.0 - Несоответствие заявленной архитектуры и реального кода — на CTF встречается как намеренное усложнение

Логика тут простая: атакующие пакуют и стриппят бинарники, чтобы обойти антивирусные сигнатуры и затруднить анализ. Авторы CTF используют те же приёмы, чтобы увеличить сложность таска.

Подготовка к анализу в Ghidra — чеклист перед загрузкой

После чтения заголовков собрано достаточно информации, чтобы настроить дизассемблер с умом:

  1. Архитектура и битность. e_machine (ELF) или Machine (PE) определяют процессорный модуль. Ghidra обычно угадывает сам, но для MIPS, PowerPC, RISC-V стоит проверить вручную
  2. Base address. Для PIE-бинарников (ET_DYN) Ghidra ставит базу 0x100000, реальная может отличаться. Для PE — сверить ImageBase из Optional Header
  3. Stripped или нет. Если stripped — включить Function ID (Ghidra) или FLIRT (IDA) для восстановления имён библиотечных функций по сигнатурам
  4. Пакер? Признаки упаковки — сначала распаковать (upx -d для UPX, ручная распаковка для остальных). Ghidra покажет мусор на запакованном бинарнике
  5. Динамические зависимости. readelf -d для ELF, Import Table для PE. libcrypto.so = криптография, ws2_32.dll = сетевое взаимодействие — это подсказывает функциональность до анализа кода
  6. Security features. NX, RELRO, canaries, PIE — определяют векторы эксплуатации в pwn-задачах
  7. Строки. strings -a ./binary | less — самый недооценённый инструмент. Флаги CTF, URL командных серверов, пути, отладочные сообщения лежат в .rodata и .data

Весь чеклист занимает 2-3 минуты. После него запуск Ghidra — осмысленное действие с пониманием, что искать, а не слепое ожидание автоанализа.

Большинство гайдов по reverse engineering бинарников начинают с «Установите Ghidra» — и сразу бросают новичка в дизассемблер. Человек смотрит на тысячи строк ассемблера, не понимая, что перед ним запакованный бинарник или файл под ARM, открытый в x86-режиме. За годы участия в CTF убедился: 80% успеха в реверсе определяется в первые пять минут, когда дизассемблер ещё не запущен. readelf, file, strings, xxd — четыре утилиты, которые дают больше контекста, чем час блуждания по графу вызовов. Выглядит непрезентабельно в writeup-ах, но когда entry point смотрит в .bss, секции носят имена UPX, а Import Table пуста — Ghidra не поможет, пока не разберёшься с этим на уровне заголовков. Навык быстрого триажа бинарников — то, что отличает реверсера с опытом от новичка с дорогим инструментом. Если хочешь не просто читать writeup-ы, а пройти цепочку от триажа до полной эксплуатации самому — на WAPT это разбирают с лабой на каждый этап.

🚀 Хочешь закрепить на практике? CTF-задачи и лабы ждут на HackerLab.

Поделиться

0 комментариев

Пожалуйста, войдите, чтобы оставить комментарий.

Загрузка комментариев...

Читайте также

Как написать write-up для CTF: чеклист

12 мин.

3

Как написать write-up для CTF: чеклист

Markdown-шаблон write-up, чеклист из 16 пунктов, таблица «хороший vs плохой» — практический гайд по оформлению CTF-разборов для новичков

9 НОЯБРЬ, 2026

Nmap для начинающих: сканируем сеть в CTF

12 мин.

5

Nmap для начинающих: сканируем сеть в CTF

Трёхфазная стратегия nmap: типы скана, NSE-скрипты, тайминг T0-T5 и обход IDS. Пошаговый гайд с командами для CTF — без потери сервисов.

8 НОЯБРЬ, 2026

Race condition эксплуатация в CTF через Turbo Intruder

16 мин.

5

Race condition эксплуатация в CTF через Turbo Intruder

Три сценария Turbo Intruder для race condition в CTF: limit overrun, file upload race, multi-endpoint. Чек-лист дебага когда гонка не срабатывает. CVE-2022-4037, CVE-2022-24800.

5 НОЯБРЬ, 2026