Elf формат для линукс

Содержание
  1. Как запускается функция main() в Linux
  2. Вступление
  3. Что находится внутри исполняемого файла?
  4. Что такое ELF?
  5. Что находится по адресу «0x080482d0», то есть по адресу запуска (starting
  6. Вопрос 1> Что за числа кладутся в стек?
  7. Вопрос 2> Что находится по адресу 80482bc?
  8. Дополнительно о формате ELF и динамическом связывании
  9. Что такое __libc_start_main?
  10. Вопрос 3> Что делает ядро?
  11. Что можно сказать по-поводу остальных регистров?
  12. Подведение итогов
  13. Заключение
  14. ELF для разных операционных систем — в чем отличия?
  15. Рецепты для ELFов
  16. Инструменты
  17. Тестовые эльфы
  18. Чтение, получение информации
  19. Тип файла, заголовок, секции
  20. 010Editor
  21. readelf
  22. Информация о компиляторе
  23. objdump
  24. readelf
  25. Я вычислю тебя по… RPATH
  26. Как появляется RPATH?
  27. readelf
  28. Проверка эльфа на безопасность
  29. Radare2
  30. «Сырой код» из эльфа (binary from ELF)
  31. objcopy
  32. Mangled — demangled имена функций
  33. Сборка, запись, модификация эльфа
  34. Эльф без метаинформации
  35. Удаление символьной информации
  36. strip
  37. sstrip
  38. Удаление таблицы секций
  39. Изменение и удаление RPATH
  40. chrpath, PatchELF

Как запускается функция main() в Linux

Вступление

Так ли прост вопрос: «Как запускается функция main() в Linux»? Для ответа на него я возьму, в качестве примера, простенькую программу на языке C — «simple.c»

Что находится внутри исполняемого файла?

Для того, чтобы рассмотреть внутреннее устройство исполняемого файла воспользуемся утилитой «objdump»

Отсюда видно, что файл, во-первых, имеет формат «ELF32», а во-вторых — адрес запуска программы «0x080482d0»

Что такое ELF?

ELF — это аббревиатура от английского Executable and Linking Format (Формат Исполняемых и Связываемых файлов). Это одна из разновидностей форматов для исполняемых и объектных файлов, используемых в UNIX-системах. Для нас особый интерес будет представлять заголовок файла. Каждый файл формата ELF имеет ELF-заголовок следующей структуры:

В этой структуре, поле «e_entry» содержит адрес запуска программы.

Что находится по адресу «0x080482d0», то есть по адресу запуска (starting

Для ответа на этот вопрос попробуем дизассемблировать программу «simple». Для дизассемблирования исполняемых файлов я использую objdump.

Утилита objdump выдаст очень много информации, поэтому я не буду приводить её всю. Нас интересует только адрес 0x080482d0. Вот эта часть листинга:

Похоже на то, что первой запускается процедура «_start». Все, что она делает — это очищает регистр ebp, «проталкивает» какие-то значения в стек и вызывает подпрограмму. Согласно этим инструкциям содержимое стека должно выглядеть так:

Теперь вопросов становится еще больше

  • Что за числа кладутся в стек?
  • Что находится по адресу 80482bc, который вызывается инструкцией call в процедуре _start?
  • В приведенном листинге отсутствуют инструкции, инициализирующие регистры (имеются ввиду eax, ecx, edx прим. перев.). Где они инициализируются?

Попробуем ответить на все эти вопросы.

Вопрос 1> Что за числа кладутся в стек?

Если внимательно просмотреть весь листинг, создаваемый утилитой objdump, то можно легко найти ответ

0x80483d0 : Это адрес функции main().

0x8048274 : адрес функции _init.

0x8048420 : адрес функции _fini. Функции _init и _fini — это функции инициализации и финализации (завершения) приложения, генерируемые компилятором GCC.

Таким образом все приведенные числа являются указателями на функции (точнее — адресами функций прим. перев.)

Вопрос 2> Что находится по адресу 80482bc?

Снова обратимся к листингу.

Здесь *0x8049548 означает указатель.

Это просто косвенный переход по адресу, хранящемуся в памяти по адресу 0x8049548.

Дополнительно о формате ELF и динамическом связывании

Формат ELF предполагает возможность динамического связывания исполняемой программы с библиотеками. Где под словами «динамическое связывание» следует понимать то, что связывание производится во время исполнения. В противоположность динамическому связыванию существует «статическое связывание», т.е. когда связывание с библиотеками происходит на этапе сборки программы, что, как правило, приводит к «раздуванию» исполняемого файла до огромных размеров. Если вы запустите команду:

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

  1. На этапе сборки программы адреса переменных и функций в динамической библиотеке не известны. Они становятся известны только на этапе исполнения
  2. Для того, чтобы иметь возможность обращаться к компонентам динамической библиотеки (переменные, функции и т.д. прим. перев.) необходимо предусмотреть указатели на эти компоненты Указатели заполняются фактическими адресами во время загрузки.
  3. Приложение может обращаться к динамическим компонентам только косвенно, используя для этого указатели. Пример такой косвенной адресации можно увидеть в листинге, приведенном выше, по адресу 80482bc, когда осуществляется косвенный переход. Фактический адрес перехода сохраняется по адресу 0x8049548 во время загрузки программы.

Косвенные ссылки можно посмотреть, выполнив команду

Что такое __libc_start_main?

Теперь «карты сдает» библиотека libc. __libc_start_main — это функция из библиотеки libc.so.6. Если отыскать функцию __libc_start_main в исходном коде библиотеки glibc, то увидите примерно такое объявление.

Читайте также:  Windows authentication user name

Теперь становится понятен смысл ассемблерных инструкций из листинга, приведенного выше — они кладут на стек входные параметры и вызывают функцию __libc_start_main.

В задачу этой функции входят некоторые действия по инициализации среды исполнения и вызов функции main().

Рассмотрим содержимое стека с новых позиций.

Согласно такому представлению стека, понятно, что перед вызовом __libc_start_main() в регистры esi, ecx, edx, esp и eax должны быть записаны соответствующие значения. Совершенно очевидно, что дизассемблированный код, показанный выше, ничего в эти регистры не пишет. Тогда кто? Остается только одно предположение — ядро. А теперь перейдем к третьему вопросу.

Вопрос 3> Что делает ядро?

Когда программа запускается из командной строки, выполняются следующие действия.

  1. Командная оболочка (shell) делает системный вызов «execve» с параметрами argc/argv.
  2. Обработчик системного вызова в ядре получает управление и начинает его обработку. В ядре обработчик называется «sys_execve». На платформе x86, пользовательское приложение передает аргументы вызова в ядро через регистры.
    • ebx : указатель на строку с именем программы
    • ecx : указатель на массив argv
    • edx : указатель на массив переменных окружения
  3. Универсальный обработчик системного вызова в ядре называется do_execve. Он создает и заполняет определенные структуры данных, копирует необходимую информацию из пространства пользователя в пространство ядра и, наконец, вызывает search_binary_handler().

Linux поддерживает множество форматов исполняемых файлов, например a.out и ELF. Для обеспечения такой поддержки в ядре имеется структура «struct linux_binfmt», которая содержит указатели на загрузчики каждого из поддерживаемых форматов. Таким образом, search_binary_handler() просто отыскивает нужный загрузчик и вызывает его. В нашем случае — это load_elf_binary(). Описывать эту функцию в подробностях слишком долгая и нудная работа, так что я не буду заниматься этим здесь. За подробностями обращайтесь к специальной литературе по данной тематике. (от себя могу предложить ссылку на статью «Внутреннее устройство ядра Linux 2.4» прим. перев. )

Вкратце процесс загрузки выглядит примерно так.

Сначала создаются и заполняются структуры в пространстве ядра и файл программы считывается в память. Затем производится установка дополнительных значений — определяется размер сегмента кода, определяется начало сегмента данных и сегмента стека и т.д.. В пользовательском режиме выделяется память, в которую копируются входные параметры (argv) и переменные окружения. Затем функция create_elf_tables(), в пользовательском режиме, кладет на стек argc, указатели на argv и массив переменных окружения, после чего start_thread() запускает программу на исполнение.

Когда управление передается в точку _start, стек выглядит примерно так:

Теперь наш дизассемблированный листинг выглядит еще более определенным.

Теперь все готово к запуску программы.

Что можно сказать по-поводу остальных регистров?

esp используется для указания вершины стека в прикладной программе. После того как со стека будет снята вся необходимая информация, процедура _start просто скорректирует указатель стека (esp), сбросив 4 младших бита в регистре esp. В регистр edx заносится указатель на, своего рода деструктор приложения — rtlf_fini. На платформе x86 эта особенность не поддерживается, поэтому ядро заносит туда число 0 макрокомандой.

Откуда взялся весь этот дополнительный код

Подведение итогов

Итак, выводы следующие.

  1. При сборке программы, GCC присоединяет к ней код из объектных модулей crtbegin.o/crtend.o/gcrt1.o а другие библиотеки, по-умолчанию, связывает динамически. Адрес запуска приложения (в ELF-заголовке прим. перев.) указывает на точку _start.
  2. Ядро загружает программу и устанавливает сегменты text/data/bss/stack, распределяет память для входных параметров и переменных окружения и помещает на стек всю необходимую информацию.
  3. Управление передается в точку _start. Здесь информация снимается со стека, на стеке размещаются входные параметры для функции __libc_start_main, после чего ей передается управление.
  4. Функция __libc_start_main выполняет все необходимые действия по инициализации среды исполнения, особенно это касается библиотеки C (malloc и т.п.) и вызывает функцию main() программы.
  5. Функции main() передаются входные аргументы — main(argc, argv). Здесь есть один интересный момент. __libc_start_main «представляет» себе сигнатуру функции main() как main(int, char **, char **). Если вам это любопытно, то попробуйте запустить следующую программу:

Заключение

В Linux запуск функции main() является результатом взаимодействия GCC, libc и загрузчика.

Источник

ELF для разных операционных систем — в чем отличия?

В чем отличия между эльфами, скажем, для FreeBSD и эльфами для Linux? Как отличается код, генерируемый gcc?

Алсо, немного не в тему — можно ли собирать линуксядро фряшным gcc?

код и не отличатется, в основном
различия на уровне линкера

А где можно почитать что-нибудь по теме подробнее? Насколько я понял, там различия только в сисколлах для подгрузки libc, например.

на ibm есть статья с хорошими ссылками.

Спасибо, действительно познавательная статья, принялся за чтение ссылок 🙂

Читайте также:  Filenote для windows 10

например, в Haiku используется свой, особый ELF, в целях совместимости с ABI BeOS.
Для сборки там используются свои ldscripts, скрипты линкера.

gcc собирает в *.o, эти файлы более-менее переносимы между разными OS, для одинаковых версий gcc/binuntils.
затем из *.o собранной программы собирается статическая библиотека *.lib через ar, или динамическая *.so через ld (статический линкер).
Затем ld из binutils, используя ldscripts и нужный формат выходного бинарника генерирует исполняемый бинарник (ELF для Linux/FreeBSD/Haiku, PE EXE для Windows, и т.п.)
Полученный бинарник является полностью независимым только при статической линковке, когда из *.lib + из рантайма языка все нужные функции собираются в один окончательный ELF/PE EXE, и т.п.
Если использовались динамические библиотеки *.so , то в бинарник прилинковывается stub, к которому её в момент запуска бинарника ядром OS подключаются через mmap нужные библиотеки. Это выполняет динамический линкер, реализованный в ядре ОС, рядом с загрузчиком (той частью ядра, которая реализует сисколл exec, подгружая бинарник, вызывая динамический линкер, создавая из бинарника процесс и запуская процесс на выполнение).

Подробнее это расписано в книжке Linkers and loaders, в Gentoo есть linkers-and-loaders в Portage.

Форматы бинарников в разных ОС разные — понятно, например COFF PE EXE vs. ELF , ELF в основном универсальный формат, но и в разных ОС разные ELF-ы, с точностью до ldscripts, которые использовались gcc тулчейном при сборке бинарника.

Подробнее про ldscripts есть в мануале про binutils и GCC, также может быть полезна книжка «GCC Internals».

в принципе ничего не мешает реализовать свой лоадер своего формата бинарников — в линуксе для этого есть binmisc_fmt, в MacOSX например, дефолтный формат бинарников Mach-O, но можно реализовать и загрузчик ELF-ов : http://www.osxbook.com/software/xbinary/

в Haiku например, используется свой формат ELF, со своими особыми ldscripts, и динамический загрузчик в ядре ОС немного другой, упрощённый — ELF Хайку не поддерживает все фичи ELF Linux, например.

Как отличается код, генерируемый gcc?

гугли на тему OS ABI, syscall interface, readelf и linuxemu в FreeBSD

То-есть, чтоб запустить, ELF от системы1 на системе2 нужно иметь копию системы1 в системе 2 и подсовывать бинарнику файлы системы1 а не родной? Или еще нужно делать ядерную прослойку?

То-есть, чтоб запустить, ELF от системы1 на системе2 нужно иметь
копию системы1 в системе 2 и подсовывать бинарнику файлы
системы1 а не родной? Или еще нужно делать ядерную прослойку?

Ядерная прослойка конечно нужна, или нечто выполняющие ее функции.

Во времена распространенности разных интел юниксов была такая штука как iBCS . С ее помощью гоняли оракл под SCO под линуксом, например.

Естественно всякие системные сервисы сами собой не появятся, типа графики. Так что для систем с ELF но не-юникс речь может идти в таком упрощенном варианте только о запуске CLI приложений.

То-есть, чтоб запустить, ELF от системы1 на системе2 нужно иметь копию системы1 в системе 2 и подсовывать бинарнику файлы системы1 а не родной?

Источник

Рецепты для ELFов

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

Подразумевается, что читатель на базовом уровне знаком с форматом ELF (в противном случае рекомендуем цикл статей Executable and Linkable Format 101).

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

— Я тоже эльф… Синий в красный… Эльфы очень терпеливы… Синий в красный… А мы эльфы. Синий в красный… От магии одни беды…
(с) Маленькое королевство Бена и Холли

Инструменты

В большинстве случаев примеры можно выполнить как на Linux, так и на Windows.

В рецептах мы будем использовать следующие инструменты:

  • утилиты из набора binutils (objcopy, objdump, readelf, strip);
  • фреймворк radare2;
  • hex-редактор с поддержкой шаблонов файлов (в примерах показан 010Editor, но можно использовать, например, свободный Veles);
  • Python и библиотеку LIEF;
  • другие утилиты (ссылки указаны в рецепте).

Тестовые эльфы

В качестве «подопытного» будем использовать ELF-файл simple из таска nutcake’s PieIsMyFav на crackmes.one, но подойдёт любой представитель «эльфийского» семейства. Если готовый файл с требуемыми характеристиками не был найден в свободном доступе, то будет приведён способ создания такого эльфа.

«Свободных» эльфов можно также найти по ссылкам:

Читайте также:  Сравнение безопасности windows unix

Чтение, получение информации

Тип файла, заголовок, секции

В зависимости от задачи интерес могут представлять:

  • тип файла (DYN — библиотека, EXEC — исполняемый, RELOC — линкуемый);
  • целевая архитектура (E_MACHINE — x86_64, x86, ARM и т.д.);
  • точка входа в приложение (Entry Point);
  • информация о секциях.

010Editor

HEX-редактор 010Editor предоставляет систему шаблонов. Для ELF-файлов шаблон называется, как ни странно, ELF.bt и находится в категории Executable (меню Templates — Executable).
Интерес может представлять, например, точка входа в исполняемый файл (entry point) (записана в заголовке файла).

readelf

Утилиту readelf можно считать стандартом де-факто для получения сведений об ELF-файле.

Для удобства чтения адреса приведены к 32-битному формату:

Для удобства чтения адреса приведены к 32-битному формату:

Вывод сокращён для удобства чтения:

Опция -W нужна для увеличения ширины консольного вывода (по умолчанию, 80 символов).

Прочитать заголовок и информацию о секциях можно с использованием кода на Python и библиотеки LIEF (предоставляет API не только для Python):

Информация о компиляторе

Для получения информации о компиляторе и сборке следует смотреть секции .comment и .note .

objdump

readelf

Я вычислю тебя по… RPATH

Эльфы могут сохранять пути для поиска динамически подключаемых библиотек. Чтобы не задавать системную переменную LD_LIBRARY_PATH перед запуском приложения, можно просто «вшить» этот путь в ELF-файл.

Для этого используется запись в секции .dynamic с типом DT_RPATH или DT_RUNPATH (см. главу Directories Searched by the Runtime Linker в документации).

И будь осторожен, юный разработчик, не «спали» свою директорию проекта!

Как появляется RPATH?

Основная причина появления RPATH-записи в эльфе — опция -rpath линковщика для поиска динамической библиотеки. Примерно так:

Такая команда создаст в секции .dynamic RPATH-запись со значением /run/media/pablo/disk1/projects/cheat_sheets/ELF/lib/ .

readelf

Посмотреть элементы из секции .dynamic (среди которых есть и RPATH) можно так:

Для удобства чтения результат команды сокращён:

С помощью библиотеки LIEF также можно прочитать RPATH-запись в эльфе:

Проверка эльфа на безопасность

Скрипт проверки безопасности checksec.sh от исследователя Tobias Klein (автора книги A Bug Hunter’s Diary) не обновлялся с 2011 года. Данный скрипт для ELF-файлов выполняет проверку наличия опций RelRO (Read Only Relocations), NX (Non-Executable Stack), Stack Canaries, PIE (Position Independent Executables) и для своей работы использует утилиту readelf.

Можно сделать свой аналог на коленке Python и LIEF (чуть короче прародителя и с дополнительной проверкой опции separate-code):

Radare2

Спасибо dukebarman за дополнение по использованию Radare2 для вывода информации аналогично checksec:

«Сырой код» из эльфа (binary from ELF)

Бывают ситуации, когда «эльфийские одёжи» в виде ELF-структуры не нужны, а нужен только «голый» исполняемый код приложения.

objcopy

Использование objcopy вероятно знакомо тем, кто пишет прошивки:

  • -S — для удаления символьной информации;
  • -g — для удаления отладочной информации.

Никакой магии. Просто взять содержимое загружаемых секций и слепить из них бинарь:

Mangled — demangled имена функций

В ELF-ах, созданных из С++ кода, имена функций декорированы (манглированы) для упрощения поиска соответствующей функции класса. Однако читать такие имена при анализе не очень удобно.

Для представления имён в удобочитаемом виде можно использовать утилиту nm из набора binutils:

Вывод имён символов в деманглированном виде с использованием библиотеки LIEF:

Сборка, запись, модификация эльфа

Эльф без метаинформации

После того как приложение отлажено и выпускается в дикий мир, имеет смысл удалить метаинформацию:

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

Удаление символьной информации

Символьная информация — это имена объектов и функций. Без неё реверс приложения немного усложняется.

strip

В самом простом случае можно воспользоваться утилитой strip из набора binutils. Для удаления всей символьной информации достаточно выполнить команду:

  • для исполняемого файла:
  • для динамической библиотеки:

sstrip

Для тщательного удаления символьной информации (в том числе ненужных нулевых байтов в конце файла) можно воспользоваться утилитой sstrip из набора ELFkickers. Для удаления всей символьной информации достаточно выполнить команду:

C использованием библиотеки LIEF также можно сделать быстрый strip (удаляется таблица символов — секция .symtab ):

Удаление таблицы секций

Как упоминалось выше, наличие/отсутствие таблицы секций не оказывает влияния на работу приложения. Но при этом без таблицы секций реверс приложения становится чуть сложнее.
Воспользуемся библиотекой LIEF под Python и примером удаления таблицы секций:

Изменение и удаление RPATH

chrpath, PatchELF

Для изменения RPATH под Linux можно воспользоваться утилитами chrpath (доступна в большинстве дистрибутивов) или PatchELF.

Источник

Оцените статью