- Как запускается функция main() в Linux
- Вступление
- Что находится внутри исполняемого файла?
- Что такое ELF?
- Что находится по адресу «0x080482d0», то есть по адресу запуска (starting
- Вопрос 1> Что за числа кладутся в стек?
- Вопрос 2> Что находится по адресу 80482bc?
- Дополнительно о формате ELF и динамическом связывании
- Что такое __libc_start_main?
- Вопрос 3> Что делает ядро?
- Что можно сказать по-поводу остальных регистров?
- Подведение итогов
- Заключение
- ELF для разных операционных систем — в чем отличия?
- Рецепты для ELFов
- Инструменты
- Тестовые эльфы
- Чтение, получение информации
- Тип файла, заголовок, секции
- 010Editor
- readelf
- Информация о компиляторе
- objdump
- readelf
- Я вычислю тебя по… RPATH
- Как появляется RPATH?
- readelf
- Проверка эльфа на безопасность
- Radare2
- «Сырой код» из эльфа (binary from ELF)
- objcopy
- Mangled — demangled имена функций
- Сборка, запись, модификация эльфа
- Эльф без метаинформации
- Удаление символьной информации
- strip
- sstrip
- Удаление таблицы секций
- Изменение и удаление RPATH
- 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 динамически. Вкратце, концепция динамического связывания выглядит так.
- На этапе сборки программы адреса переменных и функций в динамической библиотеке не известны. Они становятся известны только на этапе исполнения
- Для того, чтобы иметь возможность обращаться к компонентам динамической библиотеки (переменные, функции и т.д. прим. перев.) необходимо предусмотреть указатели на эти компоненты Указатели заполняются фактическими адресами во время загрузки.
- Приложение может обращаться к динамическим компонентам только косвенно, используя для этого указатели. Пример такой косвенной адресации можно увидеть в листинге, приведенном выше, по адресу 80482bc, когда осуществляется косвенный переход. Фактический адрес перехода сохраняется по адресу 0x8049548 во время загрузки программы.
Косвенные ссылки можно посмотреть, выполнив команду
Что такое __libc_start_main?
Теперь «карты сдает» библиотека libc. __libc_start_main это функция из библиотеки libc.so.6. Если отыскать функцию __libc_start_main в исходном коде библиотеки glibc, то увидите примерно такое объявление.
Теперь становится понятен смысл ассемблерных инструкций из листинга, приведенного выше они кладут на стек входные параметры и вызывают функцию __libc_start_main.
В задачу этой функции входят некоторые действия по инициализации среды исполнения и вызов функции main().
Рассмотрим содержимое стека с новых позиций.
Согласно такому представлению стека, понятно, что перед вызовом __libc_start_main() в регистры esi, ecx, edx, esp и eax должны быть записаны соответствующие значения. Совершенно очевидно, что дизассемблированный код, показанный выше, ничего в эти регистры не пишет. Тогда кто? Остается только одно предположение ядро. А теперь перейдем к третьему вопросу.
Вопрос 3> Что делает ядро?
Когда программа запускается из командной строки, выполняются следующие действия.
- Командная оболочка (shell) делает системный вызов «execve» с параметрами argc/argv.
- Обработчик системного вызова в ядре получает управление и начинает его обработку. В ядре обработчик называется «sys_execve». На платформе x86, пользовательское приложение передает аргументы вызова в ядро через регистры.
- ebx : указатель на строку с именем программы
- ecx : указатель на массив argv
- edx : указатель на массив переменных окружения
- Универсальный обработчик системного вызова в ядре называется 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 макрокомандой.
Откуда взялся весь этот дополнительный код
Подведение итогов
Итак, выводы следующие.
- При сборке программы, GCC присоединяет к ней код из объектных модулей crtbegin.o/crtend.o/gcrt1.o а другие библиотеки, по-умолчанию, связывает динамически. Адрес запуска приложения (в ELF-заголовке прим. перев.) указывает на точку _start.
- Ядро загружает программу и устанавливает сегменты text/data/bss/stack, распределяет память для входных параметров и переменных окружения и помещает на стек всю необходимую информацию.
- Управление передается в точку _start. Здесь информация снимается со стека, на стеке размещаются входные параметры для функции __libc_start_main, после чего ей передается управление.
- Функция __libc_start_main выполняет все необходимые действия по инициализации среды исполнения, особенно это касается библиотеки C (malloc и т.п.) и вызывает функцию main() программы.
- Функции main() передаются входные аргументы main(argc, argv). Здесь есть один интересный момент. __libc_start_main «представляет» себе сигнатуру функции main() как main(int, char **, char **). Если вам это любопытно, то попробуйте запустить следующую программу:
Заключение
В Linux запуск функции main() является результатом взаимодействия GCC, libc и загрузчика.
Источник
ELF для разных операционных систем — в чем отличия?
В чем отличия между эльфами, скажем, для FreeBSD и эльфами для Linux? Как отличается код, генерируемый gcc?
Алсо, немного не в тему — можно ли собирать линуксядро фряшным gcc?
код и не отличатется, в основном
различия на уровне линкера
А где можно почитать что-нибудь по теме подробнее? Насколько я понял, там различия только в сисколлах для подгрузки libc, например.
на ibm есть статья с хорошими ссылками.
Спасибо, действительно познавательная статья, принялся за чтение ссылок 🙂
например, в 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, но подойдёт любой представитель «эльфийского» семейства. Если готовый файл с требуемыми характеристиками не был найден в свободном доступе, то будет приведён способ создания такого эльфа.
«Свободных» эльфов можно также найти по ссылкам:
Чтение, получение информации
Тип файла, заголовок, секции
В зависимости от задачи интерес могут представлять:
- тип файла (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.
Источник