![](/uploads/posts/2022-08/pict-2.png)
Содержание статьи
Сразу отмечу, что в отличие от 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, если есть возможность за них заплатить).
![Анализ заголовка ELF-файла в IDA Freeware Анализ заголовка ELF-файла в IDA Freeware](/uploads/posts/2022-08/pict-1.png)
Также неплохо было бы использовать вместо hexdump что‑то поудобнее, например 010 Editor или wxHex Editor. Первый hex-редактор — достойная альтернатива Hiew для Linux (в том числе и благодаря возможности использовать в нем большое количество шаблонов для различных типов файлов, среди них и шаблон для парсинга ELF-файлов). Однако он небесплатный (стоимость лицензии начинается с 49,95 доллара, при этом есть 30-дневный триальный период).
![Анализ заголовка ELF-файла в 010 Editor Анализ заголовка ELF-файла в 010 Editor](/uploads/posts/2022-08/pict-2.png)
Говоря о дополнительных инструментах, которые облегчают анализ 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-pieexample.c -oexample_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, @function
main:
.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
...
Скачать:
Скриншоты:
Важно:
Все статьи и материал на сайте размещаются из свободных источников. Приносим свои глубочайшие извинения, если Ваша статья или материал была опубликована без Вашего на то согласия.
Напишите нам, и мы в срочном порядке примем меры.