
На соревновании получаешь бинарник — 6 КБ, stripped, а file сообщает: архитектура Motorola Coldfire, OS/ABI Novell Modesto. Новичок пишет организаторам — файл битый. Опытный участник открывает readelf -h, видит e_phoff: 4022250974 (при размере файла 6 КБ — явная аномалия), распознаёт в hex-значении 0xdeadbeef, патчит три поля в hex-редакторе и получает рабочий x86-64 бинарник с флагом. Это уровень 3 из CTF-челленджа книги "Practical Binary Analysis" Денниса Андриесса, и решается он исключительно пониманием формата ELF и тремя стандартными утилитами Linux: readelf, strings, objdump.
Все примеры воспроизводятся в минимальном окружении:
binutils (включает readelf, objdump, strings, nm), file, xxd. Пакет binutils — часть GNU, обновляется с каждым релизом GCC-тулчейна. В большинстве дистрибутивов стоит из коробкиПроверить наличие: which readelf objdump strings file. Если чего-то нет — sudo apt install binutils file.
Каждый разбор ELF бинарников на CTF начинается с команды file. Не формальность — её вывод сразу даёт три критических ориентира для выбора дальнейшей стратегии.
Типичный вывод: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 3.2.0, stripped. Разберём, что каждое поле значит для решения задачи.
ELF 64-bit LSB — формат, разрядность и порядок байтов. LSB (little-endian) означает: при ручном чтении hex-дампа через xxd младший байт идёт первым. Последовательность a8 2b 40 00 в дампе — это адрес 0x00402ba8. Путаница с порядком байтов — одна из самых частых ошибок при ручном разборе заголовков. Я сам на это наступал не раз.
x86-64 — архитектура процессора. Определяет набор инструкций для дизассемблера. Если здесь указано что-то неожиданное (Motorola Coldfire, ARM, MIPS при работе на x86-машине) — либо задача на кросс-архитектурный анализ, либо поле e_machine в ELF Header намеренно подпорчено. В CTF второй вариант встречается регулярно.
dynamically linked vs statically linked — принципиальное различие. Динамически слинкованный бинарник зависит от системных библиотек, и таблица .dynsym содержит имена импортированных функций — сильно упрощает понимание логики. Статически слинкованный включает код библиотек внутри: файл жирнее, а в выводе objdump -d тысячи функций из libc перемешаны с пользовательским кодом. Искать main в таком — то ещё удовольствие.
stripped vs not stripped — ключевое поле для CTF. Not stripped сохраняет символьную таблицу: имена функций, переменных, метки. readelf -s покажет всё, а objdump -d разметит функции по именам. Stripped-бинарник лишён этой информации — вместо <main> в дизассемблере безымянные адреса. В классификации MITRE ATT&CK удаление символов соответствует технике Stripped Payloads (T1027.008, Defense Evasion): атакующие удаляют метаданные из payload, чтобы затруднить реверс. На CTF stripped-бинарники встречаются чаще — организаторы не хотят отдавать подсказки бесплатно.
Перед запуском бинарника выставьте execute-бит: chmod +x ./challenge. Скачанные файлы в Linux по умолчанию не имеют права на выполнение — это нормальное поведение, а не признак проблемы.
readelf показывает метаданные ELF-файла в человекочитаемом виде. В отличие от objdump, он не дизассемблирует код — только структурные данные. Для reverse engineering CTF заданий это инструмент первой линии после file.
Команда readelf -h ./challenge выводит ELF Header — первые 64 байта файла (для 64-bit). Разберём поля, которые непосредственно влияют на решение задачи.
Entry point address — адрес начала выполнения. Нюанс, на котором спотыкаются: это НЕ адрес функции main. Entry point указывает на _start, которая вызывает __libc_start_main, и уже та передаёт управление в main. Для CTF это критично в stripped-бинарниках: entry point — единственный известный адрес. От него можно добраться до main через objdump, найдя вызов __libc_start_main и посмотрев, какой адрес загружается в регистр rdi (первый аргумент по x86-64 calling convention) перед этим вызовом.
Type — EXEC или DYN. Для EXEC адреса в выводе objdump совпадают с runtime-адресами. Для DYN (PIE-бинарник) загрузчик разместит код по случайному базовому адресу из-за ASLR, и адреса из objdump не совпадут с тем, что покажет gdb. При динамическом анализе DYN-бинарника придётся вычислять реальные адреса: адрес в gdb = базовый адрес загрузки + смещение из objdump.
Start of program headers (e_phoff) и Start of section headers (e_shoff) — смещения к таблицам. Если e_phoff содержит значение, превышающее размер файла (классика — 0xdeadbeef), заголовок повреждён намеренно. Стандартный CTF-приём. Починка: открыть файл в hex-редакторе (bvi, hexedit или xxd -r) и записать корректное значение. Для стандартного 64-bit ELF таблица program headers обычно начинается сразу после ELF Header, то есть e_phoff = 0x40 (64 байта).
Machine — поле e_machine. Значение 0x3e — AMD64. Если стоит 0x34 (Motorola Coldfire) для бинарника, который явно предназначен для x86-64 — поле подменено. Патч: записать 0x3e по смещению 0x12 от начала файла.
Вот тут спотыкается большинство новичков. Разница между секциями и сегментами — из тех вещей, которые проще показать на практике, чем объяснять в теории.
Секции (sections) — представление для линкера и аналитика. Каждая имеет имя и тип. readelf -S ./challenge показывает полный список. Для CTF ключевые:
.text — машинный код программы. Тип должен быть SHT_PROGBITS. Если readelf -S показывает SHT_NOBITS для .text — тип секции подменён, и objdump откажется дизассемблировать. Починка: найти sh_type для .text в таблице section headers и записать 0x01.rodata — read-only данные: строковые литералы, константные массивы, нередко фрагменты флага.data — инициализированные переменные, иногда тут валяются захардкоженные ключи.symtab — полная символьная таблица (отсутствует в stripped-бинарниках).dynsym — динамические символы, сохраняется даже в stripped-бинарниках с динамической линковкойСегменты (segments) — представление для загрузчика ОС. Сегмент LOAD с флагами R E — код, с RW — данные. readelf -l ./challenge покажет program headers. Для большинства CTF-задач секции важнее сегментов.
Отдельно проверьте динамические символы: readelf --dyn-syms ./challenge. Даже в stripped-бинарнике эта таблица покажет имена импортированных функций. На что смотреть:
strcmp / strncmp — программа сравнивает строки, одна из них может быть флагомgetenv — флаг читается из переменной окруженияrand / srand — генерация псевдослучайных значений, вывод может меняться при каждом запускеsystem — вызов внешней команды (например, md5sum)ptrace — вероятный anti-debug (MITRE ATT&CK T1622, Debugger Evasion)strings находит в бинарнике все последовательности печатных ASCII-символов длиной от 4 знаков (по умолчанию). Самый быстрый способ получить первые зацепки — иногда флаг лежит в открытом виде и задача решается за 10 секунд.
Базовый вызов — strings ./challenge. Но для CTF есть нюансы. По умолчанию strings сканирует только инициализированные секции с данными. Флаг -a заставляет сканировать весь файл целиком, включая заголовки и padding между секциями. Если флаг спрятан в нестандартном месте, без -a его пропустите. Минимальную длину строки меняем через -n: strings -n 8 ./challenge покажет только строки от 8 символов, отсеивая мусор.
Для поиска флага конкретного CTF: strings -a ./challenge | grep -iE 'flag|ctf|correct|wrong|key'. Что может всплыть:
flag{this_was_easy}"What's the flag?", "That's not it...", "Correct!" указывают на логику проверки вводаmd5sum, libcrypto намекают на хэширование или шифрованиеXaDht-+1432=/as4?0129mklqt!@cnz^ могут быть XOR-результатомFLAG может означать, что программа вызывает getenv("FLAG")Если strings ничего полезного не дал — переходите к objdump -s --section .rodata ./challenge. Эта команда покажет hex-дамп секции .rodata с ASCII-интерпретацией. Байты, которые не формируют строку длиной 4+, strings пропускает, а в hex-дампе видны даже однобайтовые фрагменты. Именно так решается класс задач, где флаг разбит на отдельные байты, разделённые нулевыми символами — каждая пара видна в hex-колонке.
objdump превращает машинный код в ассемблерные инструкции. Для CTF это тот инструмент, который показывает, что программа делает с вводом пользователя: где сравнивает строки, какие условия проверяет, какие преобразования выполняет.
objdump -d ./challenge дизассемблирует все исполняемые секции. Для крупных бинарников вывод массивный — статически слинкованный hello world содержит тысячи функций из libc. На практике полезнее objdump -dj .text ./challenge — только секция .text.
Если бинарник not stripped, ищите метку <main>. Если stripped, начинайте с entry point (из readelf -h): найдите инструкцию call к __libc_start_main и определите адрес main из загрузки первого аргумента.
На CTF начального и среднего уровня не нужно читать каждую инструкцию. Ищите конкретные паттерны:
401acf: e8 3c fc ff ff callq 401710 <getopt_long@plt>
401ad4: 83 f8 ff cmp $0xffffffff,%eax
401add: 83 f8 62 cmp $0x62,%eax
401ae8: 83 f8 73 cmp $0x73,%eax
401af8: 83 f8 75 cmp $0x75,%eax
401b04: 83 f8 76 cmp $0x76,%eax
Тут 0x62 — ASCII-код b, 0x73 — s, 0x75 — u, 0x76 — v. Программа парсит аргументы командной строки через getopt_long и обрабатывает флаги -b, -s, -u, -v. Этот приём описан на Reverse Engineering Stack Exchange: даже без документации бинарника его интерфейс восстанавливается из дизассемблированного switch-конструкта.
Перед call strcmp в регистры rdi и rsi загружаются адреса двух строк. Один из адресов может указывать на .rodata — найдите его в hex-дампе секции, и у вас эталонная строка, с которой сравнивается ввод.
XOR-циклы — последовательность mov → xor → cmp в цикле — классика шифрования/дешифрования флага. Программа XOR'ит пользовательский ввод с ключом и сравнивает результат с эталоном. Для решения нужны эталонная последовательность байтов и ключ — обе части обычно лежат в .rodata или .data. XOR'нуть их между собой — и флаг готов. Этот паттерн соответствует технике Deobfuscate/Decode Files or Information (T1140, MITRE ATT&CK) — в реальных вредоносных бинарниках строки часто обфусцированы аналогичным образом.
Отдельного внимания заслуживает objdump -s --section .rodata ./challenge. Содержимое секции в формате hex + ASCII:
4006c0 01000200 30330034 66006334 00663600 ....03.4f.c4.f6.
4006d0 61350033 36006632 00626600 37340066 a5.36.f2.bf.74.f
4006e0 38006436 00643300 38310036 63006466 8.d6.d3.81.6c.df
4006f0 00383800 .88.
В этом примере (из CTF-уровня книги "Practical Binary Analysis") между нулевыми байтами спрятаны пары символов: 03, 4f, c4, f6, a5, 36, f2, bf и так далее. Конкатенация всех пар даёт 32-символьную строку — готовый флаг. strings эти фрагменты не покажет, потому что каждый элемент короче порога в 4 символа. А в hex-дампе — вот они, как на ладони.
Объединяем инструменты в рабочую последовательность. Этот workflow я использую на каждом CTF для задач категории reverse, и он закрывает 80% задач уровня beginner-intermediate.
file ./challenge — определяем формат, архитектуру, тип линковки, stripped или нет. Записываем всё, что выглядит аномально (неожиданная архитектура, странный OS/ABI)strings -a ./challenge | grep -iE 'flag|ctf|correct|key' — быстрый поиск флага и маркерных строк. Если флаг найден — задача решена, идём пить кофеreadelf -h ./challenge — читаем ELF Header. Проверяем entry point, значения e_phoff и e_machine. Аномалии в этих полях — повод открыть hex-редакторreadelf -S ./challenge — список секций. Есть ли .symtab (признак not stripped)? Какой размер у .rodata? Не подменён ли тип .text на NOBITS?readelf --dyn-syms ./challenge — динамические символы. Ищем strcmp, getenv, rand, ptrace и другие функции, указывающие на логикуobjdump -s --section .rodata ./challenge — hex-дамп .rodata. Ищем фрагменты флага, ключи шифрования, строки, которые strings пропустилobjdump -d ./challenge | less — дизассемблирование. От entry point или <main> ищем паттерны: вызовы strcmp, XOR-циклы, ветвление по условиюНа шаге 3, если readelf выдаёт ошибки чтения заголовков — нужен патчинг. Минимальный набор полей для проверки: e_ident[EI_OSABI] (должен быть 0x00 для System V), e_machine (должен быть 0x3e для AMD64), e_phoff (должен указывать на реальное смещение внутри файла). Для ELF64 размер ELF Header — 64 байта, и program headers обычно идут сразу после, то есть e_phoff = 0x40.
readelf, strings и objdump — чистый статический анализ. Есть классы CTF-задач, где этих инструментов объективно не хватает.
Упакованные бинарники — если файл сжат пакером (UPX или кастомный — MITRE ATT&CK T1027.002, Software Packing), objdump -d покажет код распаковщика вместо реальной логики. Для UPX достаточно upx -d ./challenge, но кастомные пакеры — это уже динамический анализ через gdb.
Динамические ключи — если ключ шифрования не захардкожен в .rodata, а генерируется в runtime (из адреса функции, системного времени или состояния PRNG), статический анализ не даст его значение. Нужен отладчик — ставим breakpoint и снимаем значение прямо из регистра.
Anti-debug и anti-analysis — некоторые задачи проверяют наличие отладчика через ptrace (T1622, Debugger Evasion) или определяют запуск в виртуальной среде (T1497.001, System Checks). Статические инструменты не покажут результат этих проверок, но readelf --dyn-syms подскажет: видите ptrace в импортах — готовьтесь к anti-debug.
Самомодифицирующийся код — программа перезаписывает собственные инструкции во время выполнения. objdump показывает только начальное состояние — по сути, «в чём мать родила». Реальная логика появится только при запуске под gdb с breakpoint на нужном адресе.
Для этих случаев переходите к gdb (динамический анализ), radare2 (продвинутый консольный анализ, версия 5.x+, активно поддерживается) или Ghidra (декомпиляция в псевдо-C, бесплатный инструмент от NSA). Но фундамент остаётся: даже при работе в Ghidra данные из readelf -h и readelf --dyn-syms дают контекст, который декомпилятор не всегда отображает явно.
| Инструмент | Что даёт | Когда достаточен | Когда не хватает |
|---|---|---|---|
file |
Тип, архитектура, линковка | Всегда первый шаг | Не анализирует содержимое |
strings |
Читаемые строки | Флаг в открытом виде | Зашифрованные/фрагментированные данные |
readelf |
Структура ELF, символы | Разведка структуры, повреждённые заголовки | Не показывает логику кода |
objdump |
Дизассемблирование, hex-дамп секций | Простые сравнения, XOR, .rodata | Обфусцированный/упакованный код |
| Ghidra / r2 | Декомпиляция, граф вызовов | Сложная логика, anti-debug | Не заменяет базовый triage |
Три инструмента из binutils — не «набор для начинающих». Это рабочий арсенал, который закрывает первые 15-20 минут анализа любого ELF-бинарника. На практике я вижу один и тот же паттерн: участники ставят Ghidra, открывают бинарник, ждут декомпиляцию — и тонут в псевдо-C, не понимая контекста. Потому что не посмотрели readelf -h и не знают, stripped бинарник или нет, PIE включён или нет, какие функции импортированы. Декомпилятор без контекста — шум, а не информация.
На CTF разница между первым и десятым местом на beginner-intermediate уровне — не экзотические инструменты, а скорость triage. Участник, который за 30 секунд прогоняет file → strings → readelf -h → readelf --dyn-syms и получает полную картину, решает задачу быстрее того, кто сразу ныряет в граф вызовов. Workflow важнее инструментария, а readelf, strings и objdump — его фундамент.
Если ваши задачи на CTF ограничиваются только reverse — попробуйте провернуть тот же triage на pwn-задаче с .rodata-строками. Удивитесь, сколько информации лежит на поверхности.
🚀 Хочешь закрепить на практике? Реши задачи по теме на HackerLab — категория «pentest-machines».
0 комментариев
Пожалуйста, войдите, чтобы оставить комментарий.
Загрузка комментариев...