Как заглянуть внутрь двоичных файлов из командной строки Linux

Есть загадочный файл? Команда Linux file быстро сообщит вам, какой это тип файла. Однако если это двоичный файл, вы можете узнать о нем еще больше. В файле есть множество товарищей по конюшне, которые помогут вам его проанализировать. Мы покажем вам, как использовать некоторые из этих инструментов.

Определение типов файлов

Файлы обычно имеют характеристики, которые позволяют программным пакетам определять, какой это тип файла, а также то, что представляют собой данные в нем. Нет смысла пытаться открыть файл PNG в музыкальном проигрывателе MP3, поэтому полезно и прагматично, чтобы файл содержал в себе какой-либо идентификатор.

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

Некоторые операционные системы, например Windows, полностью ориентируются на расширение файла. Вы можете назвать это легковерным или доверчивым, но Windows предполагает, что любой файл с расширением DOCX действительно является файлом текстового редактора DOCX. Linux не такой, как вы скоро увидите. Ему нужны доказательства, и он ищет их в файле.

Описанные здесь инструменты уже были установлены в дистрибутивах Manjaro 20, Fedora 21 и Ubuntu 20.04, которые мы использовали при исследовании этой статьи. Начнем наше расследование с использования команда файла.

Использование файловой команды

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

Команда ls покажет нам, что находится в каталоге, а опция -hl (удобочитаемые размеры, длинный список) покажет нам размер каждого файла:

ls -hl

ls -hl в окне терминала.

Давайте попробуем записать несколько из них и посмотрим, что у нас получится:

file build_instructions.odt
file build_instructions.pdf
file COBOL_Report_Apr60.djvu

файл build_instructions.odt в окне терминала.

Правильно определены три формата файлов. Где возможно, файл дает нам немного больше информации. Сообщается, что файл PDF находится в папке формат версии 1.5.

Даже если мы переименуем файл ODT, чтобы он имел расширение с произвольным значением XYZ, файл все равно будет правильно идентифицирован как в браузере файлов файлов, так и в командной строке с использованием файла.

Файл OpenDocument правильно идентифицирован в браузере файлов файлов, даже если его расширение - XYZ.

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

file build_instructions.xyz

файл build_instructions.xyz в окне терминала.

Использование файлов на носителе, таких как файлы изображений и музыкальные файлы, обычно дает информацию об их формате, кодировке, разрешении и т. Д.

file screenshot.png
file screenshot.jpg
file Pachelbel_Canon_In_D.mp3

файл screenshot.png в окне терминала.

Интересно, что даже с текстовыми файлами файл не оценивает файл по его расширению. Например, если у вас есть файл с расширением «.c», содержащий стандартный простой текст, но не исходный код, файл не принимает его за подлинный C файл исходного кода:

file function+headers.h
file makefile
file hello.c

файл function + headers.h в окне терминала.

file правильно определяет заголовочный файл («.h») как часть набора файлов исходного кода C и знает, что make-файл является сценарием.

  Как записывать звонки Skype в Linux с помощью OpenBroadcaster

Использование файла с двоичными файлами

Двоичные файлы — это больше «черный ящик», чем другие. Можно просматривать файлы изображений, воспроизводить звуковые файлы и открывать файлы документов с помощью соответствующего программного пакета. Бинарные файлы, однако, представляют собой большую проблему.

Например, файлы «hello» и «wd» являются двоичными исполняемыми файлами. Это программы. Файл с именем «wd.o» является объектным файлом. Когда исходный код компилируется компилятором, создаются один или несколько объектных файлов. Они содержат машинный код, который компьютер в конечном итоге выполнит при запуске законченной программы, вместе с информацией для компоновщика. Компоновщик проверяет каждый объектный файл на наличие вызовов функций в библиотеки. Он связывает их с любыми библиотеками, которые использует программа. Результатом этого процесса является исполняемый файл.

Файл «watch.exe» — это двоичный исполняемый файл, который был кросс-скомпилирован для работы в Windows:

file wd
file wd.o
file hello
file watch.exe

файл wd в окне терминала.

Если взять последнее в первую очередь, файл сообщает нам, что файл «watch.exe» является исполняемым файлом PE32 +, консольной программой для семейства процессоров x86 в Microsoft Windows. PE означает переносимый исполняемый формат, который имеет 32- и 64-битные версии. PE32 — это 32-битная версия, а PE32 + — 64-битная версия.

Остальные три файла обозначены как Исполняемый и связываемый формат (ELF) файлы. Это стандарт для исполняемых файлов и файлов общих объектов, таких как библиотеки. Вскоре мы рассмотрим формат заголовка ELF.

Что может броситься в глаза, так это то, что два исполняемых файла («wd» и «hello») идентифицированы как Стандартная база Linux (LSB) общие объекты, а объектный файл «wd.o» идентифицируется как перемещаемый LSB. Слово «исполняемый файл» очевидно в его отсутствии.

Объектные файлы можно перемещать, то есть код внутри них может быть загружен в память в любом месте. Исполняемые файлы перечислены как общие объекты, потому что они были созданы компоновщиком из объектных файлов таким образом, что они унаследовали эту возможность.

Это позволяет Рандомизация макета адресного пространства (ASMR) для загрузки исполняемых файлов в память по выбранным ею адресам. Стандартные исполняемые файлы имеют адрес загрузки, закодированный в их заголовках, которые определяют, куда они загружаются в память.

ASMR — это техника безопасности. Загрузка исполняемых файлов в память по предсказуемым адресам делает их уязвимыми для атак. Это связано с тем, что их точки входа и местоположение их функций всегда будут известны злоумышленникам. Независимые от позиции исполняемые файлы (PIE), расположенный по случайному адресу, преодолевает эту восприимчивость.

  4 лучших приложения для заметок для Linux

Если мы скомпилировать нашу программу с компилятором gcc и предоставив параметр -no-pie, мы сгенерируем обычный исполняемый файл.

Параметр -o (выходной файл) позволяет нам указать имя для нашего исполняемого файла:

gcc -o hello -no-pie hello.c

Мы воспользуемся файлом для нового исполняемого файла и посмотрим, что изменилось:

file hello

Размер исполняемого файла такой же, как и раньше (17 КБ):

ls -hl hello

gcc -o hello -no-pie hello.c в окне терминала.

Бинарный файл теперь идентифицируется как стандартный исполняемый файл. Мы делаем это только в демонстрационных целях. Если вы компилируете приложения таким образом, вы потеряете все преимущества ASMR.

Почему исполняемый файл такой большой?

В нашем примере программа hello имеет размер 17 КБ, поэтому ее сложно назвать большой, но все относительно. Исходный код составляет 120 байт:

cat hello.c

Что делает двоичный файл объемным, если он только выводит одну строку в окно терминала? Мы знаем, что есть заголовок ELF, но для 64-битного двоичного файла он имеет длину всего 64 байта. Ясно, что это должно быть что-то другое:

ls -hl hello

cat hello.c в окне терминала.

Давайте сканировать двоичный файл с strings как простой первый шаг, чтобы узнать, что внутри нее. Мы перейдем к меньшему:

strings hello | less

строки привет |  меньше в окне терминала.

Внутри двоичного файла есть много строк, помимо «Hello, Geek world!» из нашего исходного кода. Большинство из них являются метками для областей в двоичном файле, а также именами и связующей информацией общих объектов. К ним относятся библиотеки и функции в этих библиотеках, от которых зависит двоичный файл.

В команда ldd показывает нам зависимости общих объектов двоичного файла:

ldd hello

ldd hello в окне терминала.

В выводе есть три записи, две из которых включают путь к каталогу (первая — нет):

linux-vdso.so: Виртуальный динамический общий объект (VDSO) — это механизм ядра, который позволяет двоичному файлу пространства пользователя получить доступ к набору процедур пространства ядра. Этот позволяет избежать накладных расходов на переключение контекста из пользовательского режима ядра. Общие объекты VDSO придерживаются формата исполняемых и связываемых файлов (ELF), что позволяет им динамически связываться с двоичным файлом во время выполнения. VDSO выделяется динамически и использует ASMR. Возможность VDSO обеспечивается стандартом Библиотека GNU C если ядро ​​поддерживает схему ASMR.
libc.so.6: Библиотека GNU C общий объект.
/lib64/ld-linux-x86-64.so.2: это динамический компоновщик, который хочет использовать двоичный файл. Динамический компоновщик опрашивает двоичный файл, чтобы узнать, какие у него зависимости. Он запускает эти общие объекты в память. Он подготавливает двоичный файл к запуску и может найти и получить доступ к зависимостям в памяти. Затем он запускает программу.

Заголовок ELF

Мы можем изучить и расшифровать заголовок ELF с помощью утилиты readelf и опции -h (заголовок файла):

readelf -h hello

readelf -h привет в окне терминала.

Заголовок интерпретируется для нас.

  Как перенести файловые системы Ext2 или Ext3 на Ext4 в Linux

Вывод команды readelf -h hello в окне терминала.

Первый байт всех двоичных файлов ELF имеет шестнадцатеричное значение 0x7F. Следующие три байта устанавливаются в 0x45, 0x4C и 0x46. Первый байт — это флаг, который идентифицирует файл как двоичный файл ELF. Чтобы сделать это кристально ясным, следующие три байта пишут «ELF» в ASCII:

Класс: указывает, является ли двоичный файл 32- или 64-разрядным исполняемым файлом (1 = 32, 2 = 64).
Данные: указывает порядок байтов в использовании. Порядок байтов определяет способ хранения многобайтовых чисел. В кодировании с прямым порядком байтов число хранится в первую очередь с его старшими битами. В кодировании с прямым порядком байтов число сначала сохраняется с младшими значащими битами.
Версия: версия ELF (в настоящее время это 1).
OS / ABI: представляет тип двоичный интерфейс приложения в использовании. Это определяет интерфейс между двумя двоичными модулями, такими как программа и общая библиотека.
Версия ABI: версия ABI.
Тип: тип двоичного файла ELF. Общие значения: ET_REL для перемещаемого ресурса (например, объектного файла), ET_EXEC для исполняемого файла, скомпилированного с флагом -no-pie, и ET_DYN для исполняемого файла с поддержкой ASMR.
Машина: The архитектура набора команд. Это указывает на целевую платформу, для которой был создан двоичный файл.
Версия: всегда устанавливается в 1 для этой версии ELF.
Адрес точки входа: адрес памяти в двоичном файле, с которого начинается выполнение.

Другие записи — это размеры и количество регионов и секций в двоичном файле, поэтому их расположение можно вычислить.

Быстрый взгляд на первые восемь байтов двоичного файла с hexdump покажет байт подписи и строку «ELF» в первых четырех байтах файла. Параметр -C (канонический) дает нам представление байтов в формате ASCII вместе с их шестнадцатеричными значениями, а параметр -n (число) позволяет указать, сколько байтов мы хотим видеть:

hexdump -C -n 8 hello

hexdump -C -n 8 hello в окне терминала.

objdump и подробное представление

Если вы хотите увидеть мельчайшие подробности, вы можете использовать команду objdump с параметром -d (дизассемблировать):

objdump -d hello | less

objdump -d привет |  меньше в окне терминала.

Это дизассемблирует исполняемый машинный код и отображает его в шестнадцатеричных байтах вместе с эквивалентом на языке ассемблера. Расположение адреса первого свидания в каждой строке показано слева.

Это полезно только в том случае, если вы умеете читать на ассемблере или вам любопытно, что происходит за кулисами. Выводов много, поэтому мы сократили их.

Putput from objdump -d привет |  меньше в окне терминала.

Компиляция и компоновка

Есть много способов скомпилировать двоичный файл. Например, разработчик решает, включать ли отладочную информацию. Способ связывания двоичного файла также играет роль в его содержимом и размере. Если двоичные ссылки разделяют объекты как внешние зависимости, он будет меньше, чем тот, на который зависимости статически связываются.

Большинство разработчиков уже знакомы с описанными здесь командами. Для других же они предлагают несколько простых способов порыться и посмотреть, что находится внутри двоичного черного ящика.