Как заглянуть внутрь двоичных файлов из командной строки Linux
Опубликовано: 2022-01-29 Есть секретный файл? Команда file
Linux быстро сообщит вам, какой это тип файла. Однако, если это двоичный файл, вы можете узнать о нем еще больше. file
имеет целый ряд стабильных товарищей, которые помогут вам проанализировать его. Мы покажем вам, как использовать некоторые из этих инструментов.
Определение типов файлов
Файлы обычно имеют характеристики, которые позволяют программным пакетам определять, какой это тип файла, а также какие данные в нем представляются. Было бы бессмысленно пытаться открыть файл PNG в музыкальном проигрывателе MP3, поэтому полезно и прагматично, если файл несет в себе некоторую форму идентификатора.
Это может быть несколько байтов подписи в самом начале файла. Это позволяет файлу явно указать его формат и содержимое. Иногда тип файла выводится из отличительного аспекта внутренней организации самих данных, известного как файловая архитектура.
Некоторые операционные системы, такие как Windows, полностью зависят от расширения файла. Вы можете назвать это легковерным или доверчивым, но Windows предполагает, что любой файл с расширением DOCX действительно является файлом обработки текста DOCX. Linux не такой, как вы скоро увидите. Он хочет доказательств и заглядывает в файл, чтобы найти их.
Описанные здесь инструменты уже были установлены в дистрибутивах Manjaro 20, Fedora 21 и Ubuntu 20.04, которые мы использовали для исследования этой статьи. Начнем наше исследование с команды file
.
Использование файловой команды
У нас есть коллекция различных типов файлов в нашем текущем каталоге. Они представляют собой смесь документов, исходного кода, исполняемых и текстовых файлов.
Команда ls
покажет нам, что находится в каталоге, а параметр -hl
(удобочитаемые размеры, длинный список) покажет нам размер каждого файла:
лс-хл
Давайте попробуем file
на нескольких из них и посмотрим, что мы получим:
файл build_instructions.odt
файл build_instructions.pdf
файл COBOL_Report_Apr60.djvu
Три формата файлов определены правильно. Там, где это возможно, file
дает нам немного больше информации. Сообщается, что файл PDF имеет формат версии 1.5.
Даже если мы переименуем файл ODT, чтобы он имел расширение с произвольным значением XYZ, файл по-прежнему правильно идентифицируется как в файловом браузере Files
, так и в командной строке с помощью file
.
В файловом браузере Files
ему присваивается правильный значок. В командной строке file
игнорирует расширение и просматривает файл, чтобы определить его тип:
файл build_instructions.xyz
Использование file
на носителе, таких как изображения и музыкальные файлы, обычно дает информацию об их формате, кодировке, разрешении и т. д.:
скриншот файла.png
файл скриншот.jpg
файл Pachelbel_Canon_In_D.mp3
Интересно, что даже с обычными текстовыми файлами file
не судит о файле по его расширению. Например, если у вас есть файл с расширением «.c», содержащий стандартный обычный текст, но не исходный код, file
не примет его за подлинный файл исходного кода C:
файл function+headers.h
файл makefile
файл hello.c
file
правильно идентифицирует заголовочный файл («.h») как часть набора файлов исходного кода C, и он знает, что makefile является скриптом.
Использование файла с двоичными файлами
Двоичные файлы являются скорее «черным ящиком», чем другие. Файлы изображений можно просматривать, звуковые файлы можно воспроизводить, а файлы документов можно открывать с помощью соответствующего программного пакета. Двоичные файлы, однако, являются более сложной задачей.
Например, файлы «hello» и «wd» являются исполняемыми двоичными файлами. Это программы. Файл с именем «wd.o» является объектным файлом. Когда исходный код компилируется компилятором, создается один или несколько объектных файлов. Они содержат машинный код, который компьютер в конечном итоге выполнит при запуске готовой программы, вместе с информацией для компоновщика. Компоновщик проверяет каждый объектный файл на наличие вызовов функций к библиотекам. Он связывает их с любыми библиотеками, которые использует программа. Результатом этого процесса является исполняемый файл.
Файл «watch.exe» — это двоичный исполняемый файл, который был кросс-компилирован для запуска в Windows:
файл wd
файл wd.o
файл привет
файл watch.exe
Взяв сначала последнее, file
сообщает нам, что файл «watch.exe» является исполняемой консольной программой PE32+ для семейства процессоров x86 в Microsoft Windows. PE означает переносимый исполняемый формат, который имеет 32- и 64-разрядные версии. PE32 — это 32-разрядная версия, а PE32+ — 64-разрядная версия.
Все остальные три файла идентифицируются как файлы исполняемого и связываемого формата (ELF). Это стандарт для исполняемых файлов и общих объектных файлов, таких как библиотеки. Вскоре мы рассмотрим формат заголовка ELF.
Что может привлечь ваше внимание, так это то, что два исполняемых файла («wd» и «hello») идентифицируются как общие объекты Linux Standard Base (LSB), а объектный файл «wd.o» идентифицируется как перемещаемый LSB. Слово исполняемый очевидно в его отсутствии.
Объектные файлы перемещаемы, то есть код внутри них может быть загружен в память в любом месте. Исполняемые файлы перечислены как общие объекты, поскольку они были созданы компоновщиком из объектных файлов таким образом, что они наследуют эту возможность.
Это позволяет системе рандомизации адресного пространства (ASMR) загружать исполняемые файлы в память по адресам по своему выбору. Стандартные исполняемые файлы имеют адрес загрузки, закодированный в их заголовках, которые определяют, где они загружаются в память.
ASMR — это техника безопасности. Загрузка исполняемых файлов в память по предсказуемым адресам делает их уязвимыми для атак. Это связано с тем, что их точки входа и расположение их функций всегда будут известны злоумышленникам. Позиционно-независимые исполняемые файлы (PIE), расположенные по случайному адресу, преодолевают эту уязвимость.
Если мы скомпилируем нашу программу с помощью компилятора gcc
и укажем параметр -no-pie
, мы сгенерируем обычный исполняемый файл.
Параметр -o
(выходной файл) позволяет нам указать имя для нашего исполняемого файла:
gcc -o hello -no-pie hello.c
Мы будем использовать file
в новом исполняемом файле и посмотрим, что изменилось:
файл привет
Размер исполняемого файла такой же, как и раньше (17 КБ):
лс-хл привет
Двоичный файл теперь идентифицируется как стандартный исполняемый файл. Мы делаем это только в демонстрационных целях. Если вы компилируете приложения таким образом, вы потеряете все преимущества ASMR.
Почему исполняемый файл такой большой?
Наш пример hello
программы весит 17 КБ, так что ее трудно назвать большой, но все относительно. Исходный код 120 байт:
кот привет.с
Что увеличивает объем двоичного файла, если все, что он делает, это выводит одну строку в окно терминала? Мы знаем, что есть заголовок ELF, но его длина составляет всего 64 байта для 64-битного двоичного файла. Очевидно, это должно быть что-то другое:
лс-хл привет
Давайте просканируем двоичный файл с помощью команды strings
в качестве простого первого шага, чтобы узнать, что внутри него. Мы передадим его в less
:
строки привет | меньше
Внутри бинарника много строк, помимо «Hello, Geek world!» из нашего исходного кода. Большинство из них представляют собой метки для областей внутри двоичного файла, а также имена и информацию о связывании общих объектов. К ним относятся библиотеки и функции в этих библиотеках, от которых зависит двоичный файл.
Команда ldd
показывает нам общие зависимости объектов бинарного файла:
лдд привет
В выходных данных есть три записи, и две из них включают путь к каталогу (первая не содержит):
- linux-vdso.so: виртуальный динамический общий объект (VDSO) — это механизм ядра, который позволяет двоичному файлу пользовательского пространства получить доступ к набору подпрограмм пространства ядра. Это позволяет избежать накладных расходов на переключение контекста из пользовательского режима ядра. Общие объекты VDSO придерживаются формата Executable and Linkable Format (ELF), что позволяет динамически связывать их с двоичным файлом во время выполнения. VDSO выделяется динамически и использует преимущества ASMR. Возможности VDSO предоставляются стандартной библиотекой GNU C, если ядро поддерживает схему ASMR.
- libc.so.6: общий объект GNU C Library.
- /lib64/ld-linux-x86-64.so.2: это динамический компоновщик, который хочет использовать двоичный файл. Динамический компоновщик опрашивает двоичный файл, чтобы узнать, какие у него есть зависимости. Он запускает эти общие объекты в память. Он подготавливает двоичный файл к запуску и позволяет находить и получать доступ к зависимостям в памяти. Затем он запускает программу.
Заголовок ELF
Мы можем проверить и расшифровать заголовок ELF, используя утилиту readelf
и параметр -h
(заголовок файла):
readelf -h привет
Заголовок интерпретируется для нас.
Первый байт всех двоичных файлов ELF устанавливается в шестнадцатеричное значение 0x7F. Следующие три байта имеют значения 0x45, 0x4C и 0x46. Первый байт — это флаг, который идентифицирует файл как двоичный файл ELF. Чтобы сделать это кристально ясным, следующие три байта представляют собой «ELF» в ASCII:
- Класс: указывает, является ли двоичный файл 32- или 64-разрядным исполняемым файлом (1 = 32, 2 = 64).
- Данные: указывает порядок байтов в использовании. Кодировка Endian определяет способ хранения многобайтовых чисел. В кодировке с обратным порядком байтов число сначала хранится со старшими битами. В кодировке с прямым порядком байтов число сохраняется с младшими битами первыми.
- Версия: Версия ELF (на данный момент это 1).
- OS/ABI: представляет тип используемого бинарного интерфейса приложения. Это определяет интерфейс между двумя двоичными модулями, такими как программа и разделяемая библиотека.
- Версия ABI: версия ABI.
- Тип: тип двоичного файла ELF. Общие значения:
ET_REL
для перемещаемого ресурса (например, объектного файла),ET_EXEC
для исполняемого файла, скомпилированного с флагом-no-pie
, иET_DYN
для исполняемого файла с поддержкой ASMR. - Машина: Архитектура набора инструкций. Это указывает на целевую платформу, для которой был создан двоичный файл.
- Версия: всегда устанавливается на 1 для этой версии ELF.
- Адрес точки входа: адрес памяти в двоичном файле, с которого начинается выполнение.
Другие записи — это размеры и количество областей и разделов в двоичном файле, чтобы можно было вычислить их расположение.
Быстрый просмотр первых восьми байтов двоичного файла с помощью hexdump
покажет байт подписи и строку «ELF» в первых четырех байтах файла. Параметр -C
(канонический) дает нам ASCII-представление байтов вместе с их шестнадцатеричными значениями, а параметр -n
(число) позволяет нам указать, сколько байтов мы хотим видеть:
hexdump -C -n 8 привет
objdump и детальное представление
Если вы хотите увидеть мельчайшие детали, вы можете использовать команду objdump
с параметром -d
(разобрать):
objdump -d привет | меньше
Это дизассемблирует исполняемый машинный код и отображает его в шестнадцатеричных байтах вместе с эквивалентом на ассемблере. Расположение адреса первого пока в каждой строке показано в крайнем левом углу.
Это полезно, только если вы умеете читать на ассемблере или вам интересно, что происходит за кулисами. Выходных данных много, поэтому мы перенаправили их в less
файлов .
Компиляция и компоновка
Есть много способов скомпилировать бинарник. Например, разработчик решает, включать ли отладочную информацию. Способ связывания двоичного файла также влияет на его содержимое и размер. Если двоичные ссылки совместно используют объекты как внешние зависимости, они будут меньше, чем те, на которые зависимости ссылаются статически.
Большинство разработчиков уже знакомы с описанными здесь командами. Однако для других они предлагают несколько простых способов порыться и посмотреть, что находится внутри бинарного черного ящика.