09 сентября 2025 в 18:33

Разбираем ELF файлы: readelf, objdump и nm

Загляните в ДНК Linux-программ! С помощью readelf – нашего "рентгена для ELF-файлов – вы научитесь расшифровывать магические байты, находить точку входа, различать .text и .bss, а также вскрывать динамические связи программ с библиотеками. После этой статьи бинарные файлы перестанут быть чёрными ящиками. Вы будете читать их как открытую книгу, срывая защитные покровы байт за байтом.

«Файл чистый!» – бодро отрапортовал антивирус. А через мгновение этот самый «чистый» файл спокойно вынес Дэвиду Льюису, техасскому майнеру, 140 тысяч баксов в крипте. Спасибо.

История Дэвида – классический случай «хотел как лучше, а получилось как всегда». Парень просто искал способ выжать из своей фермы больше битков, скачал якобы прокачанную версию майнера с GitHub. Перестраховался? Еще как! Прогнал файл через три разных антивируса. Все как один – зеленый свет, полный порядок. Статический анализ? Ни одной зацепки. Выглядело всё чище некуда, обычная рабочая лошадка для майнинга.

А через неделю... Пусто. Кошелек = ноль. Оказалось, гадость была впаяна в легитимный код так мастерски, что разглядеть подвох могли лишь те, кто умеет ковыряться в самых потаенных машинных кишках ELF-файлов, прямо на уровне этих самых нулей и единиц. Не для слабонервных, скажу я вам.

Так чем же отличается специалист по безопасности от обычного юзера, который тыкает на кнопки? Тем, что первый умеет читать настоящую «ДНК» программы. Видеть её скелет, находить спрятанные под полом люки, чуять подозрительное шевеление до того, как нажать «Запустить». И знаете что? Этим самым чтением ДНК мы с вами и займемся сегодня. Прямо сейчас.

Готовы? За пару минут я вручу вам три линуксовых «отмычки»: readelf, objdump и nm. Они превратят любой исполняемый файл из черного ящика в прозрачную вазу. Вы научитесь видеть, как программа устроена внутри (архитектура – это важно!), где она начинает свою работу (точки входа – ключевая штука!), на кого опирается (зависимости – слабое звено!) и как пытается спрятаться (маскировка – её мы тоже расколем!). И всё это – без единого запуска подозрительного кода. Безопасность прежде всего, как-никак.

Короче говоря, финал будет таким: вы посмотрите на какую-нибудь «безобидную» утилитку, покопаетесь в ней этими инструментами минут пять... и с удивлением обнаружите, что она не только считает, но и аккуратно сливает ваши пароли куда-то на левые сервера. Или этот «простой калькулятор»? Окажется, он мастер по скрытому майнингу. Добро пожаловать в мир настоящей безопасности, друзья. Здесь скучно не бывает. Скайнет бы прослезился от этой информации.

Не просто байты: Режем ELF-файлы как настоящие профи

(Кхе-кхе... простите, «введение» – скучное слово. Давайте начнем с середины мысли, ладно?)

Помните, как мы ковырялись в файлах? Тогда мы вооружились file – наш детектор лжи, strings – выуживатель спрятанных словечек, и xxd с hexdump – переводчиками с «шестнадцатеричного» на «человеческий» (ну, почти). Полезный наборчик, как швейцарский нож для первичного осмотра подозрительной цифровой рухляди. Но знаете что? Это только верхушка айсберга. Или, если хотите, крышка канализационного люка, а под ней целый мир.

Сегодня ныряем в темные, маслянистые воды ELF-файлов (Executable and Linkable Format). Это же основа всего, что бегает по линуксам! Без понимания внутренней кухни вы как механик, который только дверцу капота открыть умеет. А нам-то надо под капот залезть, прямо в самое нутро. Зачем? Ну, например:

  • Увидеть скелет программы: без запуска (опасно же!) и без исходников (которые кто-то «забыл» приложить). Как по рентгену.
  • Найти главный вход: ту самую точку, где всё начинается. Или спрятанную дверцу на черный ход.
  • Понять, на чем программа висит: какие библиотеки ей подавай? Слабое звено – вот оно!
  • Раскусить маскировку: о, эти хитрые способы спрятать зловредный код... как жуки-листоеды.
  • Вытащить метаданные – кто собрал, чем собирал, когда? Следы остаются всегда. Как отпечатки пальцев на стакане.

Чего ждать от этой прогулки по ELF-джунглям?

Спойлер: не будет скучной лекции. Будем рвать и метать. Точнее, анализировать. Вы узнаете:

  1. Как устроен ELF изнутри. Не просто «есть заголовок», а зачем эти байтики нужны. Практика – наше всё.
  2. Команда readelf – ваша супер-лупа. Для разглядывания заголовков, секций (это как отделы в здании) и символов (метки-указатели). Штука мощнее, чем кажется.
  3. Дизассемблирование с objdump. Да, это страшно выглядит, этот ассемблер. Но я покажу, как не залипнуть на mov-ах и найти главное. Клянусь, это как читать шифровки – захватывает!
  4. Утилита nm и его таблицы символов. Кто есть кто в этой программе? Функции, переменные... Ищем своих, чужих и подозрительных «незнакомцев».
  5. Разбор на косточках. Применим всё к реальным (безопасным!) программкам. Статические vs динамические? Разберемся, в чем подвох и где собака порылась.
  6. Задел на будущее. Освоили это? Дальше сможете разобраться с radare2 и Ghidra (это как Шерлок Холмс и доктор Ватсон в мире реверса). Но фундамент – вот он, сегодня. В этой статье с radare2 и Ghidra разбираться не будем.

Готовы заглянуть туда, куда обычные юзеры боятся даже щелкнуть мышкой? Поехали. Только предупреждаю: после этого обычные программы могут показаться вам... прозрачными. Почти как в том фильме про хакеров, только без глупых спецэффектов. И да, кофе под рукой лишним не будет.

Краткий обзор формата ELF

Что такое ELF (Executable and Linkable Format)

Формат ELF – это стандартный формат файлов для исполняемых программ, библиотек и объектных файлов в Unix-подобных системах, включая Linux. Разработанный в 1990-х годах, этот формат заменил более старые форматы типа a.out и COFF, предоставив более гибкую и расширяемую архитектуру.

Типы ELF файлов

Рассмотрим несколько примеров:

$ file hello

hello: ELF 64-bit LSB executable, x86-64, version 1 (GNU/Linux), statically linked, BuildID[sha1]=bde6f73fec85c41e19f94bb59ab46cc69c0c8d6c, for GNU/Linux 3.2.0, not stripped

Разделяемые библиотеки (DYN):

$ file /lib/x86_64-linux-gnu/libc.so.6

/lib/x86_64-linux-gnu/libc.so.6: ELF 64-bit LSB shared object, x86-64, version 1 (GNU/Linux), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=42c84c92e6f98126b3e2230ebfdead22c235b667, for GNU/Linux 3.2.0, stripped

Объектные файлы (REL):

$ file hello.o

hello.o: ELF 64-bit LSB relocatable, x86-64, version 1 (SYSV), not stripped

ELF-файлы: за кулисами линуксовых программ

Что за зверь этот ELF?

ELF (Executable and Linkable Format) – это ДНК для линуксовых программ. Представьте: всё, что бегает на вашем сервере: исполняшки, библиотеки, объектники – упакованы в эти штуки. Появился в лихие 90-е, вытеснив устаревшие форматы вроде a.out, как VHS кассеты на свалке истории. Гибкий? Ещё бы! Расширяемый? Как конструктор Лего.

Типовой зоопарк ELF'ов

Взгляните-ка:

$ file hello
hello: ELF 64-bit LSB executable, x86-64, version 1 (GNU/Linux), statically linked, BuildID[sha1]=bde6f73fec85c41e19f94bb59ab46cc69c0c8d6c, for GNU/Linux 3.2.0, not stripped  # "Я — программа!"

Библиотеки (DYN) – вечные нахлебники:

$ file /lib/x86_64-linux-gnu/libc.so.6
/lib/x86_64-linux-gnu/libc.so.6: ELF 64-bit LSB shared object, x86-64, version 1 (GNU/Linux), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=42c84c92e6f98126b3e2230ebfdead22c235b667, for GNU/Linux 3.2.0, stripped  # "Подключите меня!"

Объектники (REL) – полуфабрикаты:

$ file hello.o
hello.o: ELF 64-bit LSB relocatable, x86-64, version 1 (SYSV), not stripped  # "Я ещё не собрался!"

Анатомия ELF'а: разбираем по косточкам

1. Заголовок (ELF Header)

Прям как паспорт в начале файла. Узнаётся по магическим байтам 7F 45 4C 46. Вот его структура:

typedef struct {
    unsigned char e_ident[16];  // Магическое число и информация (7F 45 4C 46)
    uint16_t      e_type;       // Тип файла (ET_REL, ET_EXEC, ET_DYN)
    uint16_t      e_machine;    // Архитектура (x86, ARM, etc.)
    uint32_t      e_version;    // Версия ELF
    uint64_t      e_entry;      // Точка входа (адрес начала выполнения)
    uint64_t      e_phoff;      // Смещение к заголовкам программы
    uint64_t      e_shoff;      // Смещение к заголовкам секций
    uint32_t      e_flags;      // Флаги процессора
    uint16_t      e_ehsize;     // Размер ELF-заголовка
    uint16_t      e_phentsize;  // Размер заголовка программы
    uint16_t      e_phnum;      // Количество заголовков программы
    uint16_t      e_shentsize;  // Размер заголовка секции
    uint16_t      e_shnum;      // Количество заголовков секций
    uint16_t      e_shstrndx;   // Индекс секции с именами секций
} Elf64_Ehdr;

2. Заголовки программ (Program Headers)

Только для EXEC/DYN. Описывают сегменты: как программу разложить в памяти. То есть инструкция для загрузчика:

typedef struct {
    uint32_t   p_type;   // Тип сегмента (PT_LOAD, PT_DYNAMIC, etc.)
    uint32_t   p_flags;  // Флаги доступа (R/W/X)
    uint64_t   p_offset; // Смещение сегмента в файле
    uint64_t   p_vaddr;  // Виртуальный адрес в памяти
    uint64_t   p_paddr;  // Физический адрес (не используется)
    uint64_t   p_filesz; // Размер сегмента в файле
    uint64_t   p_memsz;  // Размер сегмента в памяти
    uint64_t   p_align;  // Выравнивание
} Elf64_Phdr;

3. Секции (Sections)

Сюда спрятаны все вкусности:

Секция Что хранит Особенности
`.text` Код программы 💻 Здесь процессор танцует
`.data` Инициализированные данные 📦 Готовые переменные
`.rodata` Данные только для чтения 🔒 Константы, строки
`.bss` Неинициализированные данные 👻 Место под будущие нули
`.symtab` Таблица символов 📇 Имена функций/переменных
`.strtab` Строки для .symtab 🔤 Текстовая кухня

4. Заголовки секций (Section Headers)

Аннотации к каждой секции: где лежит, какого размера:

typedef struct {
    uint32_t   sh_name;      // Индекс имени в `.shstrtab`
    uint32_t   sh_type;      // Тип секции (SHT_PROGBITS, SHT_NOBITS, etc.)
    uint64_t   sh_flags;     // Флаги (SHF_ALLOC, SHF_EXECINSTR)
    uint64_t   sh_addr;      // Виртуальный адрес
    uint64_t   sh_offset;    // Смещение в файле
    uint64_t   sh_size;      // Размер секции
    uint32_t   sh_link;      // Ссылка на связанную секцию
    uint32_t   sh_info;      // Доп. информация
    uint64_t   sh_addralign; // Выравнивание
    uint64_t   sh_entsize;   // Размер элемента (для таблиц)
} Elf64_Shdr;

Типы секций:

  • SHT_PROGBITS: данные программы (код, данные).
  • SHT_NOBITS: пустые данные (например, .bss).
  • SHT_SYMTAB: таблица символов (.symtab).
  • SHT_STRTAB: таблица строк (.strtab, .shstrtab).
  • SHT_RELA: релокации с аддитивными значениями.

5. Данные секций

Располагаются в теле файла. Содержимое секций хранится по смещениям, указанным в sh_offset заголовков секций.

6. Специальные структуры данных

Таблица символов (.symtab):

typedef struct {
    uint32_t      st_name;   // Индекс имени в `.strtab`
    unsigned char st_info;   // Тип и область видимости
    unsigned char st_other;  // Доп. флаги
    uint16_t      st_shndx;  // Индекс секции
    uint64_t      st_value;  // Значение (адрес)
    uint64_t      st_size;   // Размер объекта
} Elf64_Sym;

Таблица релокаций (.rela):

typedef struct {
    uint64_t    r_offset; // Адрес для применения релокации
    uint64_t    r_info;   // Индекс символа и тип релокации
    int64_t     r_addend; // Аддитивное значение
} Elf64_Rela;

Динамическая секция (.dynamic):

typedef struct {
    int64_t     d_tag;    // Тип тега (DT_NEEDED, DT_SONAME и т.д.)
    uint64_t    d_val;    // Значение/указатель
} Elf64_Dyn;

Как это всё уложено в файле

┌───────────────────────┐
│ ELF Header            │ ← Паспорт
├───────────────────────┤
│ Program Headers       │ ← Инструкция загрузчику (опционально)
├───────────────────────┤
│ .text                 │ ← Код
├───────────────────────┤
│ .data                 │ ← Данные
├───────────────────────┤
│ ...                   │ ← Другие секции
├───────────────────────┤
│ Section Headers Table │ ← Оглавление секций
└───────────────────────┘
💡 Примечание

Ключевые различия между сегментами и секциями:

  • Сегменты – используются загрузчиком операционной системы для загрузки программы в память;
  • Секции – используются компоновщиком и отладчиком для организации данных.

Исполняемые vs Библиотеки vs Объектники

Тип Готов к запуску? Особенности Пример использования
EXEC ✅ Да Фиксированные адреса, точка входа /bin/bash, ваши скрипты
DYN ❌ Нет (но помогает!) PIC-код, гибкая загрузка libc.so, libssl.so
REL ❌ Только кусочки Релокации, нелинкованные file.o после gcc -c

Зачем вам копаться в ELF?

Поглядим на hello через readelf -h:

Адрес точки входа:  0x4017b0   # ← Сюда прыгнет процессор!
Тип:                EXEC       # ← Исполняемый файл
Заголовков секций:  26         # ← Ух, сложненькая!

1. Оцениваем сложность на глаз

$ readelf -h malware | grep "Number of section headers"
Number of section headers: 69   # Ого, накрутили!

2. Статика vs Динамика: битва размеров

# Статическая сборка — монстр!
$ ls -lh static_app → 741K

# Динамическая — изящнее
$ ls -lh dynamic_app → 14K    # 🪶 Лёгкий как пёрышко!

3. Вскрываем защитные механизмы

Давайте разберём защитные механизмы ELF-файлов как профессионалы. Для этого вооружимся readelf, нашим основным инструментом статического анализа. Возьмём простую программу hello, собранную базово:

gcc hello.c -O3 -o build/hello  # ← Никаких специальных защит!
3.1. RELRO: Защищаем GOT от перезаписи

В динамически линкованных файлах есть уязвимое место – GOT (Global Offset Table). Это как адресная книга для вызовов библиотек. RELRO делает её read-only, блокируя популярные атаки типа GOT-overwrite.

Как проверить через readelf:

# Partial RELRO (базовая защита):
readelf -W -l build/hello | grep 'GNU_RELRO'  # ← Должен показать запись

# Full RELRO (максимальная защита):
readelf -W -d build/hello | grep 'BIND_NOW'   # ← Критично для полной блокировки
💡 Partial/Full RELRO
  • Partial RELRO: защищает только часть GOT (как замок на чемодане)
  • Full RELRO: полная защита + ранняя линковка (сейф с датчиком движения)
3.2. Stack Canary: сторож для стека

Эту «канарейку» (да-да, как в шахтах!) помещают между буфером и управляющими данными. При переполнении буфера она «умирает» первой, вызывая __stack_chk_fail.

Ищем защиту в бинарнике:

readelf -W -s build/hello | grep '__stack_chk_fail'  # ← Пусто? Значит защиты нет!

Почему не найдено? Мы забыли флаг компиляции! Исправляем:

gcc hello.c -O3 -fstack-protector-all -o build/hello  # ← Теперь с канарейкой!

Повторяем проверку – теперь функция должна обнаружиться. Ваша программа только что получила цифрового телохранителя!

3.3. NX: запрет исполнения в стеке/куче

По умолчанию в gcc включено: помечает стек и кучу как неисполняемые. Без этого шелл-код в буферах становится смертоносным оружием.

Проверяем флаги доступа:

readelf -W -l build/hello | grep 'GNU_STACK'
# Опасный вариант (RWE = Read+Write+Execute):
GNU_STACK  RWE 0x10  # ← Прямая угроза! Так компилируют только безумцы

# Нормальный вариант (RW = Read+Write):
GNU_STACK  RW  0x10  # ← Так и должно быть!
💡 Внимание

gcc -z execstack отключает NX. Серьёзно, зачем вам это?

3.4. PIE: рандомизация адресов

Position Independent Executable – позволяет загружать код по случайным адресам. Работает в паре с ASLR (проверьте /proc/sys/kernel/randomize_va_space).

Настройки ASLR:

0 → Без рандомизации (опасно!)  
1 → Рандомизация только библиотек  
2 → Полная рандомизация (рекомендуется)  

Как проверить PIE:

file build/hello
# С PIE:
build/hello: ELF 64-bit LSB pie executable...  # ← Хорошо!

# Без PIE:
build/hello: ELF 64-bit LSB executable...      # ← Плохо для безопасности!

Дополнительная проверка:

readelf -W -h build/hello | grep DYN  # ← PIE-файлы определяются как DYN!
3.5. Отладочные символы: двойной меч

Эти метаданные – подарок реверс-инженеру! Упрощают анализ, но увеличивают размер файла. В релизах их обычно удаляют.

Ищем символы:

# Способ 1 (через readelf):
readelf -W --symbols build/hello | grep '.symtab'  # ← Если есть вывод — символы присутствуют

# Способ 2 (через file):
file build/hello  → not stripped  # 🎁 Есть символы!
file build/hello  → stripped      # 🔒 Удалены!

Как удалить:

# При компиляции:
gcc ... -s -o program  # ← Волшебный флаг '-s'

# Или после компиляции:
strip build/hello       # ← Безжалостная очистка
3.6. Fortify Source: замена опасных функций

Автоматически подменяет strcpy, printf и др. на безопасные версии с суффиксом _chk. Но тут есть нюансы!

Пишем тестовую программу с уязвимостью:

#include <stdio.h>
#include <string.h>

int main(int argc, char **argv) {
    char buffer[5];
    printf("Buffer: %s, Size: %ld\n", buffer, sizeof(buffer));
    strcpy(buffer, argv[1]);  // ← Классика уязвимостей!
    printf("Buffer: %s, Size: %ld\n", buffer, sizeof(buffer));
}

Компилируем и ищем защиту:

gcc test.c -O2 -o build/vuln_prog
objdump -d build/vuln_prog | grep '_chk@plt'  # ← Найдём заменённые функции
0000000000001080 <__strcpy_chk@plt>  # ← Вот она защита!
0000000000001090 <__printf_chk@plt>

Почему сработало без флагов? Всё дело в макросах! Проверим препроцессор:

cat hello.c | gcc -O3 -dM -E - | grep FORTIFY
#define _FORTIFY_SOURCE 3  # ← Вот он виновник! Включен по умолчанию в некоторых дистрибутивах
3.7. Итоговая диагностика: checksec

Когда нужно проверить всё сразу, вызываем тяжёлую артиллерию:

checksec --file=build/hello
RELRO           STACK CANARY   NX        PIE             Symbols         FORTIFY
Full RELRO      Canary found   NX enabled  PIE enabled   No Symbols      Fortified 2/2

Эта сводка как медицинская карта для бинарника. Видите «Partial RELRO» или «NX disabled»? Срочно лечите компиляцию!

Ковыряемся в readelf: как разобрать ELF-файл на запчасти

Что умеет эта штука?

Слушайте, если вам надо докопаться до сути ELF-файлов, то readelf ваш лучший друг. Это как мультитул, только для кода. Штука покажет всё: от магических байтов до того, где какие переменные спрятались.

Команды, которые реально пригодятся:

-h, --file-header      # Самый верх – заголовок файла
-l, --program-headers  # Вот эти сегменты... как слои в луке
-S, --section-headers  # А это уже секции – внутренности
-s, --symbols          # Где кто живёт: функции, переменные
-r, --relocs           # Эти хитрые пересчёты адресов
-d, --dynamic          # Всё про динамику – зависимости и прочее
-n, --notes            # Заметки на полях, иногда полезно
-x SECTION             # Секцию – в шестнадцатеричный вид
-p SECTION             # А тут строки вытащит, если повезёт

Заголовок: лицо программы

ELF-заголовок – это как паспорт файла. Помните тот статически собранный hello? Вот его «личные данные»:

Заголовок ELF:
  Magic:   7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00
  Класс:                             ELF64
  Данные:                            дополнение до 2, от младшего к старшему
  Version:                           1 (current)
  OS/ABI:                            UNIX - System V
  Версия ABI:                        0
  Тип:                               EXEC (Исполняемый файл)
  Машина:                            Advanced Micro Devices X86-64
  Версия:                            0x1
  Адрес точки входа:                 0x4017b0
  Начало заголовков программы:       64 (байт в файле)
  Начало заголовков раздела:         704904 (байт в файле)
  Флаги:                             0x0
  Size of this header:               64 (bytes)
  Size of program headers:           56 (bytes)
  Number of program headers:         10
  Size of section headers:           64 (bytes)
  Number of section headers:         26
  Section header string table index: 25

Что тут важно-то? Ну, во-первых, 7f 45 4c 46 – это как отпечаток пальцев ELF'а. ELF64 – ясно дело, 64 бита. EXEC – значит, это программа, а не библиотека какая. А вот 0x4017b0 – это святое: отсюда стартует выполнение. Без этого – никуда.

Секции и сегменты: где что лежит

Запустим-ка readelf -S build/hello | head -20. Видите эту кашу из секций?

There are 26 section headers, starting at offset 0xac188:
Заголовки разделов:
  [Нм] Имя               Тип              Адрес             Смещение
       Размер            Разм.Ent         Флаги  Ссылк Инфо  Выравн
  [ 0]                   NULL             0000000000000000  00000000
       0000000000000000  0000000000000000           0     0     0
  [ 1] .note.gnu.pr[...] NOTE             0000000000400270  00000270
  ... # и так далее

А теперь выудим главное:

readelf -S build/hello | grep -E "(\.text|\.data|\.bss|\.rodata)"
  [ 7] .text             PROGBITS         0000000000401180  00001180
  [ 9] .rodata           PROGBITS         000000000047f000  0007f000
  [18] .data.rel.ro      PROGBITS         00000000004a5f80  000a4f80
  [21] .data             PROGBITS         00000000004aa0c0  000a90c0
  [22] .bss              NOBITS           00000000004abac0  000aaac0

Разберёмся:

  • .text – тут код, сердце программы. Без него – просто набор байт.
  • .data – глобальные переменные, которые уже проинициализированы.
  • .bss – а это «пустое место» для неинициализированных переменных. Хитро – в файле не занимает места!
  • .rodata – константы.

Символы: кто есть кто

Хотите знать, какие функции где живут? readelf -s build/hello вам в помощь. Но есть нюанс...

💡 Внимание!

Если бинарник «стриженый» (без debug-символов) – тут мало что узнаете. Увы.

Динамика и релокации: танцы с библиотеками

Для динамически линкованных программ это святое. Смотрим зависимости:

readelf -d build/hello | grep NEEDED
 0x0000000000000001 (NEEDED)             Совм. исп. библиотека: [libc.so.6]

Ну ясно, libc – наше всё. А теперь самое вкусное – релокации:

readelf -r build/hello
Раздел перемещения '.rela.dyn' at offset 0x550 contains 8 entries:
  Смещение        Инфо           Тип            Знач.симв.    Имя симв. + Addend
000000003db8  000000000008 R_X86_64_RELATIVE                    1160
... # куча записей
000000003ff8  000600000006 R_X86_64_GLOB_DAT 0000000000000000 __cxa_finalize@GLIBC_2.2.5 + 0

Раздел перемещения '.rela.plt' at offset 0x610 contains 1 entry:
  Смещение        Инфо           Тип            Знач.симв.    Имя симв. + Addend
000000003fd0  000300000007 R_X86_64_JUMP_SLO 0000000000000000 puts@GLIBC_2.2.5 + 0

Видите puts@GLIBC? Это и есть наши вызовы к внешним библиотекам. Без этого линковщик просто потеряется. Куча всего... хотя нет, не всё полезно, но иногда – золото.

Инструмент objdump: когда бинарник раскрывает карты

Чем objdump вас впечатлит

Этот инструмент ваш цифровой рентген с принтером для печати. Заставит любой исполняемый файл выложить подноготную: от ассемблерных команд до спрятанных секретов. Представьте хирурга, который режет не скальпелем, а флагом -M intel.

Основные ключики:

-d, --disassemble         # Вскрыть исполняемые секции (как консервы)
-D, --disassemble-all     # Резать всё – даже то, что не надо
-t, --syms                # Показать таблицу символов (имена функций – если повезёт)
-h, --section-headers     # Заголовки секций – как анатомический атлас
-x, --all-headers         # Всё и сразу – для мазохистов
-R, --dynamic-reloc       # Динамические релокации – кусочки пазла
-j SECTION                # Биопсия конкретной секции
-M OPTION                 # Синтаксис дизассемблера (intel/att – дело вкуса)

Дизассемблирование: чтение мыслей процессора

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

💡 Важная оговорка

Дизассемблированный код – он мёртвый, понимаете? Обратно в бинарник не соберёшь! И да – дизассемблеры врут как очевидцы в суде. Почему? О, это целое искусство... Но не сегодня.

objdump -M intel -d build/hello > dis  # Сохраняем потроха в файлик

Охота на системные вызовы

Системные вызовы – это как крики «Пожар!» в коде: сразу ясно: тут что-то важное.

objdump -d build/hello | grep -E "call.*<.*>"

А вот и наши шалуны:

106f:       e8 dc ff ff ff          call   1050 <puts@plt>  # Кричит в консоль
109f:       ff 15 33 2f 00 00       call   *0x2f33(%rip)   # Тайный вызов!
1142:       e8 f9 fe ff ff          call   1040 <__cxa_finalize@plt>  # Уборщик
💡 Внимание, слепота!

Objdump – слепой крот: он видит только прямые пути. А если программа прыгает по адресам как блоха, то он половину функционала пропустит. Это вам не IDA Pro с рекурсивным спуском!

Роемся в строках

Когда нужно найти спрятанные сообщения, дергаем за .rodata:

objdump -s -j .rodata build/hello  # Шерстим только read-only данные

Хотя честно? strings тут часто сподручнее. Но если нужны точные адреса, objdump вне конкуренции.

Инструмент nm: детектив по символам

Алфавит символов

Этот инструмент как бармен в баре функций: знает всех по именам (если у них стикеры на лбу наклеены). Расшифровка его маркировок:

T - Функции (главные звезды вечера)
D - Инициализированные данные (типа глобальных переменных)
B - BSS секция (неинициализированные данные – тёмная материя)
U - Неопределенные символы (функции извне – типа printf)
W - Слабые символы (как тушёная капуста – есть, но не главное)
t/d/b - Локальные версии (скромные работяги в тени)

Когда символы выжили

nm build/hello  # Покажет всё, что не удалил strip

Но чаще увидите:

nm: build/hello: no symbols – классика! Будто пришли на вечеринку, а гости все в масках.

Динамические символы: последняя надежда

Когда статические символы стёрты, смотрим на динамические:

nm -D build/hello  # То, что нужно для линковки

Вывод как обрывки разговора:

U puts@GLIBC_2.2.5  # Ага, зовёт библиотечную функцию
w __gmon_start__     # Странный тип в углу...
U __libc_start_main@GLIBC_2.34  # Стартовый мейн – где-то в libc

Итоги: ваш арсенал вырос

Что освоили с readelf

  • Вскрытие заголовков ELF: как аутопсия файла.
  • Навигацию по секциям: различали .text и .data как свои пять пальцев.
  • Расшифровку символов/релокаций: читали таблицы как детективы.
  • Магию линковки: понимали разницу между статикой и динамикой.

Что делали с objdump

  • Дизассемблировали: читали ассемблер как утреннюю газету.
  • Охотились на функции: находили пользовательские функции.
  • Извлекали строки: выуживали строковые данные как устриц из раковин.

Что выжали из nm

  • Поиск символов: идентифицировали функции по останкам.
  • Работу со stripped-бинарниками: не паниковали при «no symbols».
  • Анализ динамических связей: видели зависимости как на рентгене.

Куда движемся дальше

В следующей серии перейдём от статики к динамике:

  • strace – подслушиваем системные вызовы;
  • ltrace – следим за библиотечными вызовами (как детектив за подозреваемым);
  • ldd – вскрываем зависимости (ищем сообщников преступления).

Мы научимся понимать, что программа делает «под капотом», как она взаимодействует с системой, и как обнаружить скрытую функциональность, которая не видна при статическом анализе.

Помните: понимание формата ELF – это фундамент для всех продвинутых техник реверс-инжиниринга. Время, потраченное на изучение этих основ, многократно окупится при работе с более сложными задачами анализа.

До встречи в следующей статье, где мы увидим наши программы в действии!