Содержание статьи
Сразу отмечу, что в отличие от Windows в Linux от расширения файла не зависит практически ничего (за совсем небольшим исключением). Тип и формат файла определяется его внутренним содержимым и наличием тех или иных атрибутов, поэтому файлы в формате Executable and Linkable Format могут иметь любое расширение.
Что нам понадобится
Вообще, можно позавидовать людям, обитающим в мире Linux, — как правило, в системе «из коробки» идет большое число утилит и программ, которые в Windows необходимо где‑то искать и устанавливать дополнительно, да еще и не всегда бесплатно. В нашем случае для анализа ELF-файлов в Linux присутствует вполне состоятельный арсенал встроенных средств и утилит:
- readelf — с помощью этой утилиты можно практически полностью просматривать все потаенные места ELF-файлов в удобочитаемом виде;
- hexdump — простой просмотрщик файлов в шестнадцатеричном представлении (конечно, до hiew из мира Windows ему далеко, но, во‑первых, он присутствует в системе по умолчанию, а во‑вторых, делает это совершенно бесплатно);
- strings — с помощью этой известной утилиты можно увидеть имена всех импортируемых (или экспортируемых) функций, а также библиотек, из которых эти функции импортированы, названия секций и еще много чего интересного;
- ldd — позволяет выводить имена разделяемых библиотек, из которых импортируются те или иные функции, используемые исследуемой программой;
-
nm — может показывать таблицу имен из состава отладочной информации, которая добавляется в ELF-файлы при их компиляции (эта отладочная информация с помощью команды
strip
может быть удалена из файла, и в этом случае утилита nm ничем не поможет); - objdump — способна вывести информацию и содержимое всех элементов исследуемого файла, в том числе и в дизассемблированном виде.
Часть перечисленного (кроме hexdump и ldd) входит в состав пакета GNU Binutils. Если этого пакета в твоей системе нет, его легко установить. К примеру, в Ubuntu это выглядит следующим образом:
sudo apt install binutils
В принципе, имея все перечисленное, можно уже приступать к анализу и исследованию ELF-файлов без привлечения дополнительных средств. Для большего удобства и наглядности можно добавить к нашему инструментарию известный в кругах реверс‑инженеров дизассемблер IDA в версии Freeware (этой версии для наших целей будет более чем достаточно, хотя никто не запрещает воспользоваться версиями Home или Pro, если есть возможность за них заплатить).
Также неплохо было бы использовать вместо hexdump что‑то поудобнее, например 010 Editor или wxHex Editor. Первый hex-редактор — достойная альтернатива Hiew для Linux (в том числе и благодаря возможности использовать в нем большое количество шаблонов для различных типов файлов, среди них и шаблон для парсинга ELF-файлов). Однако он небесплатный (стоимость лицензии начинается с 49,95 доллара, при этом есть 30-дневный триальный период).
Говоря о дополнительных инструментах, которые облегчают анализ ELF-файлов, нельзя не упомянуть Python-пакет lief. Используя этот пакет, можно писать Python-скрипты для анализа и модификации не только ELF-файлов, но и файлов PE и MachO. Скачать и установить этот пакет получится традиционным для Python-пакетов способом:
pip install lief
Подопытные экземпляры
В Linux (да и во многих других современных UNIX-подобных операционных системах) формат ELF используется в нескольких типах файлов.
-
Исполняемый файл — содержит все необходимое для создания системой образа процесса и запуска этого процесса. В общем случае это инструкции и данные. Также в файле может присутствовать описание необходимых разделяемых объектных файлов, а также символьная и отладочная информация. Исполняемый файл может быть позиционно зависимым (в этом случае он грузится всегда по одному и тому же адресу, для 32-разрядных программ обычно это
0x8048000
, для 64-разрядных —0x400000
) и позиционно независимым исполняемым файлом (PIE — Position Independent Execution или PIC — Position Independent Code). В этом случае адрес загрузки файла может меняться при каждой загрузке. При построении позиционно независимого исполняемого файла используются такие же принципы, как и при построении разделяемых объектных файлов. -
Перемещаемый файл — содержит инструкции и данные, при этом они могут быть статически связаны с другими объектными файлами, в результате чего получается разделяемый объектный или исполняемый файл. К этому типу относятся объектные файлы статических библиотек (как правило, для статических библиотек имя начинается с lib и применяется расширение
*.
), однако, как мы уже говорили, расширение в Linux практически ничего не определяет. В случае статических библиотек это просто дань традиции, а работоспособность библиотеки будет обеспечена с любым именем и любым расширением.a -
Разделяемый объектный файл — содержит инструкции и данные, может быть связан с другими перемещаемыми файлами или разделяемыми объектными файлами, в результате чего будет создан новый объектный файл. Такие файлы могут выполнять функции разделяемых библиотек (по аналогии с DLL-библиотеками Windows). При этом в момент запуска программы на выполнение операционная система динамически связывает эту разделяемую библиотеку с исполняемым файлом программы, и создается исполняемый образ приложения. Опять же традиционно разделяемые библиотеки имеют расширение
*.
(от английского Shared Object).so - Файл дампа памяти — файл, который содержит образ памяти того или иного процесса на момент его завершения. В определенных ситуациях ядро может создавать файл с образом памяти аварийно завершившегося процесса. Этот файл также создается в формате ELF, однако мы о такого рода файлах говорить не будем, поскольку задача исследования дампов и содержимого памяти достаточно объемна и требует отдельной статьи.
Для наших изысканий нам желательно иметь все возможные варианты исполняемых файлов из перечисленных выше, чем мы сейчас и займемся.
Делаем исполняемые файлы
Не будем выдумывать что‑то сверхоригинальное, а остановимся на классическом хелловорлде на С:
#include <stdio.h>int main(int argc, char* argv[]) { printf("Hello world"); return 0;}
Компилировать это дело мы будем с помощью GCC. Современные версии Linux, как правило, 64-разрядные, и входящие в их состав по умолчанию средства разработки (в том числе и компилятор GCC) генерируют 64-разрядные приложения. Мы в своих исследованиях не будем отдельно вникать в 32-разрядные ELF-файлы (по большому счету отличий от 64-разрядных ELF-файлов в них не очень много) и основные усилия сосредоточим именно на 64-разрядных версиях программ. Если у тебя возникнет желание поэкспериментировать с 32-разрядными файлами, то при компиляции в GCC нужно добавить опцию -m32
, при этом, возможно, потребуется установить библиотеку gcc-multilib. Сделать это можно примерно вот так:
sudo apt-get install gcc-multilib
Итак, назовем наш хелловорлд example.
(кстати, здесь как раз один из немногих случаев, когда в Linux расширение имеет значение) и начнем с исполняемого позиционно зависимого кода:
gcc -no-pie example.c -o example_no_pie
Как ты уже догадался, опция -no-pie
как раз и говорит компилятору собрать не позиционно независимый код.
Вообще, если говорить правильно, то GCC — это не совсем компилятор. Это комплексная утилита, которая в зависимости от расширения входного файла и опций вызывает нужный компилятор или компоновщик с соответствующими входными данными. Причем из С или другого высокоуровневого языка сначала исходник транслируется в ассемблерный код, а уже затем все это окончательно преобразуется в объектный код и собирается в нужный нам ELF-файл.
В целом можно выделить четыре этапа работы GCC:
- препроцессирование;
- трансляция в ассемблерный код;
- преобразование ассемблерного кода в объектный;
- компоновка объектного кода.
Чтобы посмотреть на промежуточный результат, к примеру в виде ассемблерного кода, используй в GCC опцию -S
:
gcc -S -masm=intel example.c
Обрати внимание на два момента. Первый — мы в данном случае не задаем имя выходного файла с помощью опции -o
(GCC сам определит его из исходного, добавив расширение *.
, что и означает присутствие в файле ассемблерного кода). Второй момент — опция -masm=intel
, которая говорит о том, что ассемблерный код в выходном файле необходимо генерировать с использованием синтаксиса Intel (по умолчанию будет синтаксис AT&T, мне же, как и, наверное, большинству, синтаксис Intel ближе). Также в этом случае опция -no-pie
не имеет смысла, поскольку ассемблерный код в любом случае будет одинаковый, а переносимость обеспечивается на этапе получения объектного файла и сборки программы.
На выходе получим файл example.
с таким вот содержимым (полностью весь файл показывать не будем, чтобы не занимать много места):
.file "example.c" .intel_syntax noprefix .text .section .rodata.LC0: .string "Hello world" .text .globl main .type main, @functionmain:.LFB0: .cfi_startproc endbr64 push rbp .cfi_def_cfa_offset 16 .cfi_offset 6, -16 mov rbp, rsp .cfi_def_cfa_register 6 lea rdi, .LC0[rip] call puts@PLT mov eax, 0 ...
Продолжение доступно только участникам
Вариант 1. Присоединись к сообществу «Xakep.ru», чтобы читать все материалы на сайте
Членство в сообществе в течение указанного срока откроет тебе доступ ко ВСЕМ материалам «Хакера», позволит скачивать выпуски в PDF, отключит рекламу на сайте и увеличит личную накопительную скидку! Подробнее
Вариант 2. Открой один материал
Заинтересовала статья, но нет возможности стать членом клуба «Xakep.ru»? Тогда этот вариант для тебя! Обрати внимание: этот способ подходит только для статей, опубликованных более двух месяцев назад.
Я уже участник «Xakep.ru»