Что такое бинарники в linux

Обновлено: 04.07.2024

Чтобы сразу начать программировать, создадим еще один клон известной программы "Hello World". Что делает эта программа, вы знаете. Откройте свой любимый текстовый редактор и наберите в нем следующий текст:

Я назвал свой файл hello.c. Вы можете назвать как угодно, сохранив суффикс .c. Содержимое файла hello.c - это исходный код программы ('program source', 'source code' или просто 'source'). А hello.c - это исходный файл программы ('source file'). Hello World - очень маленькая программа, исходный код которой помещается в одном файле. В "настоящих" программах, как правило, исходный код разносится по нескольким файлам. В больших программах исходных файлов может быть больше сотни.

Наш исходный код написан на языке программирования C. Языки программирования были придуманы для того, чтобы программист мог объяснить компьютеру, что делать. Но вот беда, компьютер не понимает ни одного языка программирования. У компьютера есть свой язык, который называют машинным кодом или исполняемым кодом ('executable code'). Написать Hello World в машинном коде можно, но серьезные программы на нем не пишутся. Исполняемый код не только сложный по своей сути, но и очень неудобный для человека. Программа, которую можно написать за один день на языке программирования будет писаться целый год в машинном коде. Потом программист сойдет с ума. Чтобы этого не случилось, был придуман компилятор ('compiler'), который переводит исходный код программы в исполняемый код. Процесс перевода исходного кода программы в исполняемый код называют компиляцией.

Вы наверняка догадались, что опция -o компилятора gcc указывает на то, каким должно быть имя выходного файла. Как вы позже узнаете, выходным файлом может быть не только бинарник. Если не указать опцию -o, то бинарнику, в нашем случае, будет присвоено имя a.out.

Осталось только запустить полученный бинарник. Для этого набираем в командной строке следующую команду:

Мы рассмотрели идеальный случай, когда программа написана без синтаксических ошибок. Попробуем намеренно испортить программу таким образом, чтобы она не отвечала канонам языка C. Для этого достаточно убрать точку с запятой в конце вызова функции printf(): Теперь, если попытаться откомпилировать программу, то компилятор выругается, указав нам на то, что он считает неправильным: В первой строке говорится, что в файле hello.c (у нас он единственный) в теле функции main() что-то произошло. Вторая строка сообщает, что именно произошло: седьмая строка файла hello.c вызвала ошибку (error). Далее идет расшифровка: синтаксическая ошибка перед закрывающейся фигурной скобкой.

Заглянув в файл hello.c мы с удивлением обнаружим, что нахулиганили мы не в седьмой, а в шестой строке. Дело в том, что компилятор обнаружил нелады только в седьмой строке, но написал 'before' (до), что означает "прокручивай назад".

Естественно, пока мы не исправим ошибку, ни о каком бинарнике не может идти и речи. Если мы удалим старый бинарник hello, доставшийся нам от прошлой компиляции, то увидим, что компиляция испорченного файла не даст никакого результата. Однако иногда компилятор может лишь "заподозрить" что-то неладное, потенциально опасное для нормального существования программы. Тогда вместо 'error' пишется 'warning' (предупреждение), и бинарник все-таки появляется на свет (если в другом месте нет явных ошибок). Не следует игнорировать предупреждения, за исключением тех случаев, когда вы на 100% знаете, что делаете.

Парадокс программирования заключается в том, что можно наделать кучу ошибок (уже не синтаксических, как в нашем случае, а смысловых) по всем правилам языка программирования. В таком случае компилятор выдает бинарник, который делает не то, что мы хотели. В таком случае программу приходится отлаживать. Отладка - это обычное дело при написании любой достаточно сложной программы. Не ошибается только тот, кто ничего не делает.

2.2. Мультифайловое программирование

Как я уже говорил, если исходный код сколько-нибудь серьезной программы уместить в одном файле, то такой код станет просто нечитаемым. К тому же если программа компилируется достаточно долго (особенно это относится к языку C++), то после исправления одной ошибки, нужно перекомпилировать весь код.

Куда лучше разбросать исходный код по нескольким файлам (осмысленно, по какому-нибудь критерию), и компилировать каждый такой файл отдельно. Как вы вскоре узнаете, это очень даже просто.

Давайте сначала разберемся, как из исходного файла получается бинарник. Подобно тому как гусеница не сразу превращается в бабочку, так и исходный файл не сразу превращается в бинарник. После компиляции создается объектный код. Это исполняемый код с некоторыми "вкраплениями", из-за которых объектный код еще не способен к выполнению. Сразу в голову приходит стиральная машина: вы ее только что купили и она стоит у вас дома в коробке. В таком состоянии она стирать не будет, но вы все равно рады, потому что осталось только вытащить из коробки и подключить.

Вернемся к объектному коду. Эти самые "вкрапления" (самое главное среди них - таблица символов) позволяют объектному коду "пристыковываться" к другому объектному коду. Такой фокус делает компоновщик (линковщик) - программа, которая объединяет объектный код, полученный из "разных мест", удаляет все лишнее и создает полноценный бинарник. Этот процесс называется компоновкой или линковкой.

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

В Linux в качестве линковщика используется программа ld, обладающая приличным арсеналом опций. К счастью gcc самостоятельно вызывает компоновщик с нужными опциями, избавляя нас от "ручной" линковки.

Попробуем теперь, вооружившись запасом знаний, написать мультифайловый Hello World. Создадим первый файл с именем main.c: Теперь создадим еще один файл hello.c со следующим содержимым:

Здесь функция main() вызывает функцию print_hello(), находящуюся в другом файле. Функция print_hello() выводит на экран заветное приветствие. Теперь нужно получить два объектных файла. Опция -c компилятора gcc заставляет его отказаться от линковки после компиляции. Если не указывать опцию -o, то в имени объектного файла расширение .c будет заменено на .o (обычные объектные файлы имеют расширение .o): Итак, мы получили два объектных файла. Теперь их надо объединить в один бинарник: Компилятор "увидел", что вместо исходных файлов (с расширением .c) ему подбросили объектные файлы (с расширением .o) и отреагировал согласно ситуации: вызвал линковщик с нужными опциями.

Давайте разберемся, что же все-таки произошло. В этом нам поможет утилита nm. Я уже оговорился, что объектные файлы содержат таблицу символов. Утилита nm как раз позволяет посмотреть эту таблицу в читаемом виде. Те, кто пробовал программировать на ассемблере знают, что в исполняемом файле буквально все (функции, переменные) стоит на своей позиции: стоит только вставить или убрать из программы один байт, как программа тут же превратиться в груду мусора из-за смещенных позиций (адресов). У объектных файлов особая роль: они хранят в таблице символов имена некоторых позиций (глобально объявленных функций, например). В процессе линковки происходит стыковка имен и пересчет позиций, что позволяет нескольким объектным файлам объединиться в один бинарник. Если вызвать nm для файла hello.o, то увидим следующую картину: О смысловой нагрузке нулей и литер U,T мы будем говорить при изучении библиотек. Сейчас же важным является то, что в объектном файле сохранилась информация об использованных именах. Своя информация есть и в файле main.o: Таблицы символов объектных файлов содержат общее имя print_hello. В процессе линковки высчитываются и подставляются в нужные места адреса, соответствующие именам из таблицы. Вот и весь секрет.

2.3. Автоматическая сборка

В предыдущем разделе для создания бинарника из двух исходных файлов нам пришлось набрать три команды. Если бы программу пришлось отлаживать, то каждый раз надо было бы вводить одни и те же три команды. Казалось бы выход есть: написать сценарий оболочки. Но давайте подумаем, какие в этом случае могут быть недостатки. Во-первых, каждый раз сценарий будет компилировать все файлы проекта, даже если мы исправили только один из них. В нашем случае это не страшно. Но если речь идет о десятках файлов! Во-вторых, сценарий "намертво" привязан к конкретной оболочке. Программа тут же становится менее переносимой. И, наконец, простому скрипту не хватает функциональности (задание аргументов сборки и т. п.), а хороший скрипт (с многофункциональными прибамбасами) плохо модернизируется.

Выход из сложившейся ситуации есть. Это утилита make, которая работает со своими собственными сценариями. Сценарий записывается в файле с именем Makefile и помещается в репозиторий (рабочий каталог) проекта. Сценарии утилиты make просты и многофункциональны, а формат Makefile используется повсеместно (и не только на Unix-системах). Дошло до того, что стали создавать программы, генерирующие Makefile'ы. Самый яркий пример - набор утилит GNU Autotools. Самое главное преимущество make - это "интеллектуальный" способ рекомпиляции: в процессе отладки make компилирует только измененные файлы.

То, что выполняет утилита make, называется сборкой проекта, а сама утилита make относится к разряду сборщиков.

Любой Makefile состоит из трех элементов: комментарии, макроопределения и целевые связки (или просто связки). В свою очередь связки состоят тоже из трех элементов: цель, зависимости и правила.

Макроопределения позволяют назначить имя практически любой строке, а затем подставлять это имя в любое место сценария, где должна использоваться данная строка. Макросы Makefile схожи с макроконстантами языка C.

Связки определяют: 1) что нужно сделать (цель); 2) что для этого нужно (зависимости); 3) как это сделать (правила). В качестве цели выступает имя или макроконстанта. Зависимости - это список файлов и целей, разделенных пробелом. Правила - это команды передаваемые оболочке.

Теперь рассмотрим пример. Попробуем составить сценарий сборки для рассмотренного в предыдущем разделе мультифайлового проекта Hello World. Создайте файл с именем Makefile: Обратите внимание, что в каждой строке перед вызовом gcc, а также в строке перед вызовом rm стоят табуляции. Как вы уже догадались, эти строки являются правилами. Формат Makefile требует, чтобы каждое правило начиналось с табуляции. Теперь рассмотрим все по порядку.

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

Первая связка имеет цель hello. Цель отделяется от списка зависимостей двоеточием. Список зависимостей отделяется от правил символом новой строки. А каждое правило начинается на новой строке с символа табуляции. В нашем случае каждая связка содержит по одному правилу. В списке зависимостей перечисляются через пробел вещи, необходимые для выполнения правила. В первом случае, чтобы скомпоновать бинарник, нужно иметь два объектных файла, поэтому они оказываются в списке зависимостей. Изначально объектные файлы отсутствуют, поэтому требуется создать целевые связки для их получения. Итак, чтобы получить main.o, нужно откомпилировать main.c. Таким образом файл main.c появляется в списке зависимостей (он там единственный). Аналогичная ситуация с hello.o. Файлы main.c и hello.c изначально существуют (мы их сами создали), поэтому никаких связок для их создания не требуется.

Особую роль играет целевая связка clean с пустым списком зависимостей. Эта связка очищает проект от всех автоматически созданных файлов. В нашем случае удаляются файлы main.o, hello.o и hello. Очистка проекта бывает нужна в нескольких случаях: 1) для очистки готового проекта от всего лишнего; 2) для пересборки проекта (когда в проект добавляются новые файлы или когда изменяется сам Makefile; 3) в любых других случаях, когда требуется полная пересборка (напрмиер, для измерения времени полной сборки).

Теперь осталось запустить сценарий. Формат запуска утилиты make следующий: Опции make нам пока не нужны. Если вызвать make без указания целей, то будет выполнена первая попавшаяся связка (со всеми зависимостями) и сборка завершится. Нам это и требуется: В процессе сборки утилита make пишет все выполняемые правила. Проект собран, все работает.

Теперь давайте немного модернизируем наш проект. Добавим одну строку в файл hello.c: Теперь повторим сборку: Утилита make "пронюхала", что был изменен только hello.c, то есть компилировать нужно только его. Файл main.o остался без изменений. Теперь давайте очистим проект, оставив одни исходники: В данном случае мы указали цель непосредственно в командной строке. Так как целевая связка clean содержит пустой список зависимостей, то выполняется только одно правило. Не забывайте "чистить" проект каждый раз, когда изменяется список исходных файлов или когда изменяется сам Makefile.

2.4. Модель КИС

Любая программа имеет свой репозиторий - рабочий каталог, в котором находятся исходники, сценарии сборки (Makefile) и прочие файлы, относящиеся к проекту. Репозиторий рассмотренного нами проекта мультифайлового Hello World изначально состоит из файлов main.c, hello.c и, собственно, Makefile. После сборки репозиторий дополняется файлами main.o, hello.o и hello. Практика показывает, что правильная организация исходного кода в репозитории не только упрощает модернизацию и отладку, но и предотвращает возможность появления многих ошибок.

Модель КИС (Клиент-Интерфейс-Сервер) - это элегантная концепция распределения исходного кода в репозитории, в рамках которой все исходники можно поделить на клиенты, интерфейсы и серверы.

Итак, сервер предоставляет услуги. В нашем случае это могут быть функции, структуры, перечисления, константы, глобальные переменные и проч. В языке C++ это чаще всего классы или иерархии классов. Любой желающий (клиент) может воспользоваться предоставленными услугами, то есть вызвать функцию со своими фактическими параметрами, создать экземпляр структуры, воспользоваться константой и т. п. В C++, как правило, клиент использует класс как тип данных и использует его члены.

Часто бывает, что клиент сам становится сервером, точнее начинает играть роль промежуточного сервера. Хороший пример - наш мультифайловый Hello World. Здесь функция print_hello() (клиент) пользуется услугами стандартной библиотеки языка C (сервер), вызывая функцию printf(). Однако в дальнейшем функция print_hello() сама становится сервером, предоставляя свои услуги функции main(). В языке C++ довольно часто клиент создает производный класс, который наследует некоторые механизмы базового класса сервера. Таким образом клиент сам становится сервером, предоставляя услуги своего производного класса.

Клиент с сервером должны "понимать" друг друга, иначе взаимодействие невозможно. Интерфейс (протокол) - это условный набор правил, согласно которым взаимодействуют клиент и сервер. В нашем случае (мультифайловый Hello World) интерфейсом (протоколом) является общее имя в таблице символов двух объектных файлов. Такой способ взаимодействия может привести к неприятным последствиям. Клиент (функция main()) не знает ничего, кроме имени функции print_hello() и, наугад вызывает ее без аргументов и без присваивания. Иначе говоря, клиент не знает до конца правил игры. В нашем случае прототип функции print_hello() неизвестен.

Не смотря, на то, что большинство программ в операционной системе Linux (в моем случае Ubuntu) можно установить из Центра приложений, тем самым обеспечивая совместимость с системой, иногда вам может понадобится установить новую версию программы или наоборот, старую, которой может не оказаться в списке.

Такие программы чаще всего распространяются, в формате .bin также известные как бинарные файлы. Бывает, что они запакованы в архив, а иногда как исполняемые файлы. Установка bin linux, может быть произведена через терминал либо же через графический интерфейс. Рассмотрим оба варианта.

Установка bin файлов в Linux

Консольный вариант

1. Для начала в файловом менеджере, нужно найти наш бинарник.

Загрузки_007

2. Откроем терминал, сочетанием клавиш Ctrl+Alt+T.

Терминал_009

3. Зайдем в режим суперпользователя (под рутом). Для этого введем следующую команду в терминале:

Затем нужно ввести пароль.

Терминал_011

4. Теперь, в терминале нам нужно зайти в ту директорию, где расположен наш бинарник. В моем случае это будет так:

Терминал_012

5. Дайте права на выполнение файла .bin, командой:

chmod +x thefile.bin

В моем случае это выглядит так:

chmod +x clip2net.bin

Терминал_014

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

Терминал_015

7. После этой команды, установщик программы запустился и спрашивает меня на каком языке я хотел бы установить bin файл в linux.

Language Selection_016

8. Далее устанавливаем программу как обычно это делается в ОС Windows.

Установка_017

Графический вариант

Он ненамного проще, но для новичков, которые недавно перешли с Windows будет понятнее. Первые 3 пункта из консольного варианта все же придется выполнить. Затем нужно выбрать файл, нажать правой кнопкой мыши, выбрать пункт "свойства". На вкладке права, нужно поставить галочку напротив пункта "Разрешить выполнение файла как программы".

Свойства «clip2net.bin»_018

Закрываем окно. И перетаскиваем этот файл в терминал, запущенный от суперпользователя, имя файла и путь уже впишутся автоматически, остается лишь нажать на кнопку "Enter". Далее, вы можете установить bin файл, как и в первом варианте. На этом все.

Во время работы с Linux у вас есть возможность на выбор использовать два совсем разных способа установки программ, а именно:

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

Бинарный файл

Бинарный файл - это фактическая программа, которая уже полностью готовая к использованию. Это исполняемый файл, который создается при компиляции из исходного кода. Как правило, они имеют все необходимые библиотеки, встроенные в них, или устанавливают / разворачивают их по мере необходимости (в зависимости от того, как было написано ПО). В большинстве случаев предоставляются в архивном формате.

Для установки требуется специальная программа для распаковки этих файлов и помещения их на компьютер. То есть менеджер пакетов вашего дистрибутива Linux (например, apt, yum и т. д.). Менеджер пакетов также выполняет и другие полезные функции кроме распаковки, такие как отслеживание установленных файлов и управление обновлениями программного обеспечения.

Преимущества и плюсы использования бинарных файлов

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

Недостатки и минусы использования

  • Вы не можете видеть (иметь доступ) и редактировать исходный код, поэтому вы не имеете возможности получить оптимизацию программы под вашу систему, ваши потребности и предпочтения.

Исходные файлы

Исходные файлы - файлы для “сборки” утилиты/ПО в бинарный файл. Исходный код программного обеспечения для Linux поставляется в виде сжатых tar-файлов, которые обычно имеют расширения .tar.gz или .tar.bz2. Инструменты используются для упаковки исходного кода в tarballs, где «tar» (используется для объединения нескольких файлов в один), «gzip» или bzip2 (используется для сжатия).

Чтобы получить tar-архив с исходным кодом для определенного программного обеспечения, вам нужно знать URL-адрес к tar-архиву. После чего нужно распаковать скачанный tar-архив специальной командой tar для определенного типа расширения архива, чтобы получить доступ к файлам и возможность работать с исходником. Следующим шагом выполняются нужные настройки среды для компиляции и установки программного обеспечения из исходного кода.

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

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

Касательно вопроса, где можно найти исходный код к продукту, вариантов много, в большинстве случаев Вы можете загрузить исходный код проекта с таких сервисов, как GitHub или BitBucket. Некоторые владельцы ПО могут даже разместить его на личном веб-сайте.

Также шаг который лучше не упускать - это ознакомление с документацией к проекту, там могут содержаться важные данные о всех возможностях, последних обновлениях, детали и подсказки по компиляции и установке этого ПО.

Преимущества и плюсы использования исходных файлов

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

Недостатки и минусы использования

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

К минусам этот пункт можно и не относить, но для установки ПО с исходника потребуется уже наличие теоретических знаний и необходимых навыков в понимании документации к продукту, работы с терминалом и т.д., тут обычному пользователю может быть сложно.

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


TL;DR. В этой статье мы исследуем защитные схемы (hardening schemes), которые из коробки работают в пяти популярных дистрибутивах Linux. Для каждого мы взяли конфигурацию ядра по умолчанию, загрузили все пакеты и проанализировали схемы защиты во вложенных двоичных файлах. Рассматриваются дистрибутивы OpenSUSE 12.4, Debian 9, CentOS, RHEL 6.10 и 7, а также Ubuntu 14.04, 12.04 и 18.04 LTS.

Результаты подтверждают, что даже основные схемы, такие как стековые канарейки и независимый от позиции код, ещё не всеми используются. Ситуация ещё хуже у компиляторов, когда речь идёт о защите от уязвимостей вроде столкновения стека (stack clash), которые попали в центр внимания в январе после публикации информации об уязвимостях в systemd. Но не всё так безнадёжно. В значительной части бинарников реализованы базовые методы защиты, и их число растёт от версии к версии.

Проверка показала, что наибольшее количество методов защиты реализовано в Ubuntu 18.04 на уровне ОС и приложений, затем следует Debian 9. С другой стороны, в OpenSUSE 12.4, CentOS 7 и RHEL 7 тоже реализованы базовые схемы защиты, а защита от столкновения стека применяется ещё шире при гораздо более плотном наборе пакетов по умолчанию.

Трудно обеспечить высокое качество программного обеспечения. Несмотря на огромное количество продвинутых инструментов для статического анализа кода и динамического анализа во время выполнения, а также значительный прогресс в разработке компиляторов и языков программирования, современное ПО по-прежнему страдает от уязвимостей, которые постоянно эксплуатируются злоумышленниками. Ситуация ещё хуже в экосистемах, которые включают устаревший код. В таких случаях мы не только сталкиваемся с вечной проблемой поиска возможных эксплуатируемых ошибок, но и ограничены жёсткими рамками обратной совместимости, которые часто требуют сохранить ограниченный, а ещё хуже уязвимый или глючный код.

Здесь в игру вступают методы защиты или усиления программ (hardening). Некоторые типы ошибок мы не в силах предотвратить, зато можем сделать жизнь злоумышленника сложнее и частично решить проблему, предотвратив или помешав эксплуатации этих ошибок. Такая защита используется во всех современных ОС, однако методы сильно отличаются по сложности, эффективности и производительности: от стековых канареек (stack canaries) и ASLR до полноценных защит CFI и ROP. В этой статье рассмотрим, какие методы защиты применяются в самых популярных дистрибутивах Linux в конфигурации по умолчанию, а также изучим свойства бинарников, которые распространяются через системы управления пакетами каждого дистрибутива.

CVE и безопасность

Все мы видели статьи с названиями вроде «Самые уязвимые приложения года» или «Самые уязвимые операционные системы». Обычно там приводят статистику по общему количеству записей об уязвимостях типа CVE (Common Vulnerability and Exposures), полученной из Национальной базы уязвимости (NVD) от NIST и других источников. Впоследствии эти приложения или ОС ранжируются по количеству CVE. К сожалению, хотя CVE очень полезны для отслеживания проблем и информирования поставщиков и пользователей, они мало говорят о реальной безопасности программного обеспечения.

Для примера, рассмотрим общее количество CVE за последние четыре года для ядра Linux и пяти наиболее популярных серверных дистрибутивов, а именно Ubuntu, Debian, Red Hat Enterprise Linux и OpenSUSE.



Рис. 1

Разумеется, система CVE даёт полезную информацию, которая позволяет создавать соответствующие защиты. Чем лучше мы понимаем причины сбоя программы, тем проще определить возможные способы эксплуатации и разработать соответствующие механизмы обнаружения и реагирования. На рис. 2 показаны категории уязвимостей для всех дистрибутивов за последние четыре года (источник). Сразу видно, что большинство CVE попадают в следующие категории: отказ в обслуживании (DoS), выполнение кода, переполнение, повреждение памяти, утечка (эксфильтрация) информации и эскалация привилегий. Хотя многие CVE учтены несколько раз в разных категориях, в целом одни и те же проблемы сохраняются из года в год. В следующей части статьи мы оценим использование различных схем защиты для предотвращения эксплуатации указанных уязвимостей.



Рис. 2

Задачи

В этой статье мы намерены ответить на следующие вопросы:

  • Какова безопасность различных дистрибутивов Linux? Какие защитные механизмы существуют в ядре и приложениях пользовательского пространства?
  • Как со временем изменилось принятие механизмов защиты для различных дистрибутивов?
  • Каковы средние зависимости пакетов и библиотек у каждого дистрибутива?
  • Какие защиты реализованы для каждого бинарника?

Выбор дистрибутивов

Оказывается, сложно найти точную статистику по установкам дистрибутивов, так как в большинстве случаев количество загрузок не указывает на количество реальных установок. Тем не менее, варианты Unix составляют большинство серверных систем (на веб-серверах 69,2%, по статистике W3techs и других источников), и их доля постоянно растёт. Таким образом, для нашего исследования мы сосредоточились на дистрибутивах, доступных из коробки на платформе Google Cloud. В частности, мы выбрали следующие ОС:

Изучим конфигурацию ядра по умолчанию, а также свойства пакетов, доступных через пакетный менеджер каждого дистрибутива из коробки. Таким образом, мы рассматриваем только пакеты из зеркал по умолчанию каждого дистрибутива, игнорируя пакеты из нестабильных репозиториев (например, зеркал ‘testing’ в Debian) и сторонние пакеты (например, пакеты Nvidia со стандартных зеркал). Кроме того, мы не рассматриваем пользовательские компиляции ядра или конфигурации с повышенной защитой.

Анализ конфигурации ядра

Мы применили скрипт анализа на основе свободного чекера kconfig. Рассматриваем параметры защиты из коробки у названных дистрибутивов и сравниваем их со списком от Проекта самозащиты ядра (KSPP). Для каждого параметра конфигурации таблица 2 описывает желаемую настройку: галочка стоит для дистрибутивов, которые соответствуют рекомендациям KSSP (разъяснение терминов см. здесь; в будущих статьях мы расскажем, как появились многие из этих методов защиты и как взломать систему в их отсутствие).



В целом, в новых ядрах более строгие настройки из коробки. Например, у CentOS 6.10 и RHEL 6.10 на ядре 2.6.32 нет большинства критических функций, реализованных в новых ядрах, таких как SMAP, строгие разрешения RWX, рандомизация адресов или защита copy2usr. Следует отметить, что многие из вариантов конфигурации из таблицы отсутствуют в более старых версиях ядра и не применимы в реальности — в таблице это всё равно указано как отсутствие должной защиты. Аналогично, если параметр конфигурации отсутствует в данной версии, а для безопасности этот параметр нужно отключить, это считается разумной конфигурацией.

Ещё один момент при интерпретации результатов: некоторые конфигурации ядра, которые увеличивают поверхность атаки, одновременно могут использоваться для безопасности. Такие примеры включают uprobes и kprobes, модули ядра и BPF/eBPF. Наша рекомендация состоит в том, чтобы использовать вышеуказанные механизмы для обеспечения реальной защиты, поскольку они нетривиальны для использования, а их эксплуатация предполагает, что вредоносные субъекты уже закрепились в системе. Но если эти параметры включены, системный администратор должен активно следить за злоупотреблениями.

Изучая далее записи таблицы 2, мы видим, что современные ядра предоставляют несколько вариантов для защиты от эксплуатации таких уязвимостей, как утечка информации и переполнение стека/кучи. Однако мы замечаем, что даже самые последние популярные дистрибутивы ещё не реализовали более сложную защиту (например, с патчами grsecurity) или современную защиту от атак повторного использования кода (например, сочетание рандомизации со схемами типа R^X для кода). Что ещё хуже, даже эти более продвинутые средства защиты не защищают от полного спектра атак. Таким образом, системным администраторам крайне важно дополнять разумные конфигурации решениями, предлагающими обнаружение и предотвращение эксплойтов во время выполнения.

Анализ приложений

Неудивительно, что у разных дистрибутивов разные характеристики пакетов, параметры компиляции, зависимости библиотек и т. д. Различия существуют даже для родственных дистрибутивов и пакетов с небольшим количеством зависимостей (например, coreutils в Ubuntu или Debian). Чтобы оценить различия, мы загрузили все доступные пакеты, извлекли их содержимое и проанализировали бинарные файлы и зависимости. Для каждого пакета мы отслеживали другие пакеты, от которых он зависит, и для каждого бинарника отслеживали его зависимости. В этом разделе кратко изложим выводы.

Дистрибутивы

В общей сложности мы загрузили 361 556 пакетов для всех дистрибутивов, извлекая только пакеты с зеркал по умолчанию. Мы игнорировали пакеты без исполняемых файлов ELF, такие как исходные коды, шрифты и т. д. После фильтрации осталось 129 569 пакетов, содержащих в общей сложности 584 457 бинарных файлов. Распределение пакетов и файлов по дистрибутивам показано на рис. 3.



Рис. 3

Можно заметить, что чем современнее дистрибутив, тем больше в нём пакетов и двоичных файлов, что логично. При этом пакеты Ubuntu и Debian включают гораздо больше двоичных файлов (как исполняемых, так и динамических модулей и библиотек), чем CentOS, SUSE и RHEL, что потенциально влияет на поверхность атаки Ubuntu и Debian (нужно заметить, что цифры отражают все бинарники всех версий пакета, то есть некоторые файлы анализируются несколько раз). Это особенно важно, если учесть зависимости между пакетами. Таким образом, уязвимость в бинарнике одного пакета может повлиять на многие части экосистемы, как уязвимая библиотека может повлиять на все бинарные файлы, импортирующие её. В качестве точки отсчёта посмотрим на распределение числа зависимостей по пакетам в различных ОС:

Почти во всех дистрибутивах у 60% пакетов минимум по 10 зависимостей. Кроме того, у некоторых пакетов количество зависимостей значительно больше (более 100). То же самое относится и к обратным зависимостям пакетов: как и ожидалось, несколько пакетов используются многими другими пакетами в дистрибутиве, поэтому уязвимости в этих немногих избранных имеют высокий риск. В качестве примера в следующей таблице перечислены 20 пакетов с максимальным количеством обратных зависимостями в SLES, Centos 7, Debian 9 и Ubuntu 18.04 (в каждой ячейке указан обозначает пакет и количество обратных зависимостей).



Таблица 3

Интересный факт. Хотя все анализируемые ОС построены для архитектуры x86_64, а у большинства пакетов архитектура определена как x86_64 и x86, но пакеты часто содержат двоичные файлы для других архитектур, как показано на рис. 5.



Рис. 5

В следующем разделе углубимся в характеристики анализируемых бинарников.

Статистика защиты бинарных файлов

Как абсолютный минимум, нужно изучить базовый набор вариантов защиты для имеющихся бинарных файлов. Несколько дистрибутивов Linux поставляются со скриптами, которые выполняют такие проверки. Например, в Debian/Ubuntu есть такой скрипт. Вот пример его работы:

  • Position Independent Executable (PIE): указывает, можно ли переместить в памяти текстовый раздел программы, чтобы добиться рандомизации, если в ядре включен ASLR.
  • Stack Protected: включены ли стековые канарейки для защиты от атак на столкновение стека.
  • Fortify Source: заменяются ли небезопасные функции (например, strcpy) их более безопасными аналогами, а проверяемые в рантайме вызовы — их не проверяемыми аналогами (например, memcpy вместо __memcpy_chk).
  • Read-only relocations (RELRO): помечены ли записи таблицы перемещения как «только для чтения», если они сработали до начала выполнения.
  • Immediate binding (немедленная привязка): разрешает ли компоновщик среды выполнения все перемещения перед началом выполнения программы (это эквивалентно полному RELRO).

Мы хотели изучить, как много бинарных файлов в рассматриваемых дистрибутивах защищены этими, а также ещё тремя методами:

  • Неисполняемый бит (NX) предотвращает выполнение в любом регионе, который не должен быть исполняемым, например в куче стека и т. д. обозначает путь выполнения, используемый динамическим загрузчиком для поиска соответствующих библиотек. Первый является обязательным для любой современной системы: его отсутствие позволяет злоумышленникам произвольно записывать полезную нагрузку в память и выполнять её как есть. Для второго неверные конфигурации пути выполнения помогают в введении ненадёжного кода, который может привести к ряду проблем (например, эскалация привилегий, а также другие проблемы).
  • Защита от столкновения стека обеспечивает защиту от атак, которые заставляют стек наложиться на другие области памяти (например, на кучу). Учитывая недавние эксплойты, злоупотребляющие уязвимостями со столкновением кучи в systemd, мы сочли уместным включить этот механизм в наш набор данных.
  • Как можно заметить, защита NX реализована везде, за редкими исключениями. В частности, можно отметить несколько более низкое её использование в дистрибутивах Ubuntu и Debian по сравнению с CentOS, RHEL и OpenSUSE.
  • Стековые канарейки много где отсутствуют, особенно в дистрибутивах со старыми ядрами. Некоторый прогресс наблюдается в последних дистрибутивах Centos, RHEL, Debian и Ubuntu.
  • За исключением Debian и Ubuntu 18.04, в большинстве дистрибутивов плохая поддержка PIE.
  • Защита от столкновений стека слабо реализована в OpenSUSE, Centos 7 и RHEL 7 и практически отсутствует у остальных.
  • Все дистрибутивы с современными ядрами имеют некоторую поддержку RELRO, при этом лидирует Ubuntu 18.04, а второе место занимает Debian.



Таблица 4. Характеристики защиты для исполняемых файлов, показанных на рис. 3 (реализация соответствующих функций в процентах от общего количества исполняемых файлов)



Таблица 5. Характеристики защиты для библиотек, показанных на рис. 3 (реализация соответствующих функций в процентах от общего количества библиотек)

Так прогресс есть? Определённо есть: это видно из статистики по отдельным дистрибутивам (например, Debian), а также из приведённых выше таблиц. В качестве примера в рис. 6 показано внедрение защитных механизмов в трёх последовательных дистрибутивах Ubuntu LTS 5 (мы опустили статистику защиты от столкновения стека). Мы замечаем, что от версии к версии всё больше файлов поддерживают стековых канареек, а также последовательно всё больше бинарных файлов поставляются с полной защитой RELRO.

К сожалению, ряд исполняемых файлов в разных дистрибутивах по-прежнему не обладает ни одной из вышеуказанных защит. Например, взглянув на Ubuntu 18.04, можно заметить бинарник ngetty (замена getty), а также оболочки mksh и lksh, интерпретатор picolisp, пакеты nvidia-cuda-toolkit (популярный пакет для приложений с GPU-ускорением, таких как фреймворки машинного обучения) и klibc-utils. Аналогично, бинарник mandos-client (административный инструмент, позволяющий автоматически перезагружать машины с зашифрованными файловыми системами), а также rsh-redone-client (повторная реализация rsh и rlogin) поставляются без защиты NX, хотя у них права SUID :(. Кроме того, в нескольких suid-бинарниках нет базовой защиты, такой как стековые канарейки (например, бинарный файл Xorg.wrap из пакета Xorg).

В этой статье мы выделили несколько свойств безопасности современных дистрибутивов Linux. Анализ показал, что в последнем дистрибутиве Ubuntu LTS (18.04) реализована в среднем самая сильная защита уровня ОС и приложений среди дистрибутивов с относительно новыми ядрами, таких как Ubuntu 14.04, 12.04 и Debian 9. Однако рассмотренные дистрибутивы CentOS, RHEL и OpenSUSE в нашем наборе данных по умолчанию выдают более плотный набор пакетов, а в последних версиях (CentOS и RHEL) имеют более высокий процент реализации защиты от столкновения стека, по сравнению с конкурентами на основе Debian (Debian и Ubuntu). Сравнивая версии CentOS и RedHat, мы замечаем большие улучшения во внедрении стековых канареек и RELRO с версий 6 до 7, но в среднем в CentOS реализовано больше функций, чем в RHEL. В целом, всем дистрибутивам следует уделить особое внимание защите PIE, которая, за исключением Debian 9 и Ubuntu 18.04, реализована менее чем в 10% бинарных файлов из нашего набора данных.

Наконец, следует отметить: хотя мы провели исследование вручную, существует множество инструментов безопасности (например, Lynis, Tiger, Hubble), которые выполняют анализ и помогают избежать небезопасных конфигураций. К сожалению, даже сильная защита в разумных конфигурациях не гарантирует отсутствие эксплойтов. Вот почему мы твёрдо убеждены, что жизненно важно обеспечить надёжный мониторинг и предотвращение атак в реальном времени, сосредоточив внимание на моделях эксплуатации и предотвращая их.

Читайте также: