Поиск по сайту:

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


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

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

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

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

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

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

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

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

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

ls -hl

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

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

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

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

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

file build_instructions.xyz

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

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

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

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

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

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

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

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

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

file wd
file wd.o
file hello
file 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 в новом исполняемом файле и посмотрим, что изменилось:

file hello

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

ls -hl hello

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

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

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

cat hello.c

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

ls -hl hello

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

strings hello | less

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

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

ldd hello

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

  • 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 hello

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

Первый байт всех двоичных файлов 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.
  • Машина: архитектура набора инструкций. Это указывает на целевую платформу, для которой был создан двоичный файл.
  • Версия: для этой версии ELF всегда установлено значение 1.
  • Адрес точки входа: адрес памяти в двоичном файле, с которого начинается выполнение.

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

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

hexdump -C -n 8 hello

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

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

objdump -d hello | less

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

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

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

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

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