
На прошлогоднем 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-сигнатуры выдают архитектуру, пакеры, стрипперы и антиотладочные механизмы быстрее, чем дизассемблер закончит автоанализ.
Прежде чем лезть в структуру заголовка 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-заголовок для 64-битного бинарника занимает ровно 64 байта, для 32-битного — 52 байта. Каждое поле имеет фиксированное смещение, так что при желании парсится в hex-редакторе без дополнительных утилит.
Самые наглядные примеры использования 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:
PT_GNU_STACK без флага X — стек не исполняемыйPT_GNU_RELRO — GOT частично или полностью защищён__stack_chk_fail в таблице символов (видно через readelf -s | grep stack_chk)ET_DYN вместо ET_EXEC — адреса рандомизируются при каждом запуске__memcpy_chk, __strcpy_chk в импортахДля pwn-задач комбинация NX off + no canaries = ret2shellcode. PIE off = фиксированные адреса для ROP-цепочек. Собственно, ради этого и читаем заголовки до загрузки в дизассемблер.
PE-формат тащит за собой наследие MS-DOS, из-за чего путь до реального заголовка длиннее, чем в ELF. Но логика та же: магические байты, метаданные, секции.
Каждый PE-файл начинается с DOS Header размером 64 байта. Для анализа исполняемых файлов перед динамическим анализом нужны два поля:
0x5A4D — литералы MZ. Подтверждение форматаМежду 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 в начале.
Сразу после PE Signature идёт COFF File Header (20 байт):
0x014C = i386, 0x8664 = x86-64 — определение архитектуры бинарного файла, то есть какой процессорный модуль ставить в Ghidra.text, .data, .rdata, .rsrc, .reloc). Больше 10 или секции с нестандартными именами — пахнет пакеромOptional Header (обязательный для исполняемых файлов, несмотря на название — спасибо мелкософту за нейминг) содержит:
0x010B = PE32, 0x020B = PE32+ (64-bit)e_entry в ELF0x400000 для EXEPE Header Import Table — самая информативная структура для быстрого триажа. Она показывает, какие DLL и функции бинарник импортирует:
kernel32.dll с парой функций) при большом размере бинарника — признак пакера, который резолвит API динамическиVirtualAlloc + WriteProcessMemory + CreateRemoteThread — классическая тройка для инъекции кодаIsDebuggerPresent, NtQueryInformationProcess — антиотладочные проверки (T1497.001, System Checks по MITRE ATT&CK)LoadLibrary/GetProcAddress в рантаймеExport Table обычно принадлежит DLL, а не EXE. Если EXE экспортирует функции — паттерн нетипичный, повод присмотреться.
Набор утилит для 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 используют те же приёмы, чтобы увеличить сложность таска.
После чтения заголовков собрано достаточно информации, чтобы настроить дизассемблер с умом:
e_machine (ELF) или Machine (PE) определяют процессорный модуль. Ghidra обычно угадывает сам, но для MIPS, PowerPC, RISC-V стоит проверить вручнуюET_DYN) Ghidra ставит базу 0x100000, реальная может отличаться. Для PE — сверить ImageBase из Optional Headerupx -d для UPX, ручная распаковка для остальных). Ghidra покажет мусор на запакованном бинарникеreadelf -d для ELF, Import Table для PE. libcrypto.so = криптография, ws2_32.dll = сетевое взаимодействие — это подсказывает функциональность до анализа кода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 комментариев
Пожалуйста, войдите, чтобы оставить комментарий.
Загрузка комментариев...