Изображение взято из статьи «Linux Kernel EFI Boot Stub или «Сам себе загрузчик»»

Несколько месяцев назад я написал для скрипта mkinitcpio код, который позволяет ему создавать файлы UEFI с использованием заглушки systemd.

Само внесенное мной изменение можно найти на GitHub.

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

Процесс загрузки


В последнее десятилетие большинство компьютеров имеют два варианта загрузки. Режим Legacy BIOS и пришедший ему на замену UEFI. Возможности последнего весьма обширны, но один из наиболее интересных аспектов в том, что технически ядро Linux представляет собой исполняемый файл MS-DOS, и если считать его первые два байта, то мы увидим MZ.

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

Однако большинство из нас не хочет связываться непосредственно с UEFI, поэтому ради простоты мы используем загрузчики вроде grub или system-boot.

Защита цепочки загрузки


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

Все эти три файла лежат зашифрованные в загрузочном разделе*. С помощью безопасной загрузки можно подписать ядро, поскольку оно является исполняемым файлом UEFI, но это оставит полностью незащищенными конфигурацию и initramfs.

*Да, некоторые люди шифруют свои загрузочные разделы.

Решением будет вложить все эти составляющие в один исполняемый файл. Реализовать это доступным и вполне понятным способом позволяют образы EFI ядра.

Заглушки UEFI


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

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

#!/bin/bash
objcopy \
    --add-section .osrel="/etc/os-release" --change-section-vma .osrel=0x20000 \
    --add-section .cmdline="/etc/kernel/cmdline" --change-section-vma .cmdline=0x30000 \
    --add-section .splash="/usr/share/systemd/bootctl/splash-arch.bmp" --change-section-vma .splash=0x40000 \
    --add-section .linux="/boot/vmlinuz-linux" --change-section-vma .linux=0x2000000 \
    --add-section .initrd=<(cat /boot/intel-ucode.img /boot/initrd-linux.img) --change-section-vma .initrd=0x3000000 \
    "/usr/lib/systemd/boot/efi/linuxx64.efi.stub" "/efi/EFI/Linux/linux.efi"


Приведенный пример с Arch Linux создает исполняемый файл, содержащий информацию дистрибутива (os-release), считанные из файла параметры ядра, bmp-файл с логотипом дистрибутива, ядро и initramfs с микрокодом.

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

*Думаю, стоит упомянуть, что некоторые загрузчики не проверяют подписи выполняемых ими файлов. Это делает systemd-boot, но не делает grub. Имейте в виду.

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

MKINITCPIO


mkinitcpio – это генератор initramfs, разработанный и используемый в основном для Arch Linux, поэтому некоторые части текущего раздела будут ориентированы на этот конкретный дистрибутив. Тем не менее аналогичные возможности создания и работы с initramfs есть, к примеру, в dracut, где для этого используется - -uefi. Если ваша программа для созданияinitramfs такой функциональности не имеет, то ее добавление в проект не должно составить особого труда.

При желании проработать последующий пример можете взять его предвыпускную версию из репозитория проекта. Любые полезные изменения кода и документация приветствуются.

github.com/archlinux/mkinitcpio/releases/tag/v31_rc0

Начнем с изменения файла linux.preset, который в Arch указывает на конфигурацию ядра.

--- /etc/mkinitcpio.d/linux.preset
+++ /etc/mkinitcpio.d/linux.preset
@@ -2,13 +2,16 @@

 ALL_config="/etc/mkinitcpio.conf"
 ALL_kver="/boot/vmlinuz-linux"
+ALL_microcode=(/boot/*-ucode.img)

 PRESETS=('default' 'fallback')

 #default_config="/etc/mkinitcpio.conf"
 default_image="/boot/initramfs-linux.img"
-#default_options=""
+default_efi_image="/efi/EFI/Linux/archlinux-linux.efi"
+default_options="--splash /usr/share/systemd/bootctl/splash-arch.bmp"

 #fallback_config="/etc/mkinitcpio.conf"
 fallback_image="/boot/initramfs-linux-fallback.img"
-fallback_options="-S autodetect"
+fallback_efi_image="/efi/EFI/Linux/archlinux-linux-fallback.efi"
+fallback_options="-S autodetect --splash /usr/share/systemd/bootctl/splash-arch.bmp"

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

Далее исправляем параметры загрузки ядра. По умолчанию mkinitcpio считывает из /etc/kernel/cmdline. Если же вы не уверены, откуда происходит считывание в вашей системе, то можете проверить /proc/cmdline и использовать этот путь в качестве отправной точки. Но имейте в виду, что записи initrd, указывающие на микрокод и initramfs , нужно удалить.

# cat /etc/kernel/cmdline
rw quiet bgrt_disable

Содержимое файла должно быть похожим на приведенное выше. Также учтите, что все флаги root или cyptdevices должны остаться, если вы выполняете initramfs без systemd, обеспечивающего обнаружение разделов.

Мы также добавляем в командную строку ядра команду bgrt disable. Этот флаг отключает отображение логотипа OEM после загрузки таблиц ACPI. В результате загрузочная заставка вместо замены неким невзрачным логотипом будет отображаться на несколько секунд дольше.

При выполнении mkinitcpio -P вывод должен получиться примерно таким:

[..snip..]
==> Starting build: 5.13.10-arch1-1
  -> Running build hook: [base]
  -> Running build hook: [systemd]
  -> Running build hook: [autodetect]
  -> Running build hook: [modconf]
  -> Running build hook: [block]
  -> Running build hook: [keyboard]
  -> Running build hook: [sd-encrypt]
  -> Running build hook: [filesystems]
==> Generating module dependencies
==> Creating zstd-compressed initcpio image: /boot/initramfs-linux.img
==> Image generation successful
==> Creating UEFI executable: /efi/EFI/Linux/archlinux-linux.efi
  -> Using UEFI stub: /usr/lib/systemd/boot/efi/linuxx64.efi.stub
  -> Using kernel image: /lib/modules/5.13.10-arch1-1/vmlinuz
  -> Using os-release file /etc/os-release
==> UEFI executable generation successful

Вот и все! Мы сгенерировали с помощью mkinitcpio заглушку UEFI!

Если вы используете systemd-boot, то больше ничего настраивать не нужно.
Загрузчик для отображения в меню заглушек UEFI будет искать их в каталоге EFI/Linux. Это намного упрощает настройку загрузчика, так как для его создания нужно лишь выполнить bootctl install и сгенерировать исполняемый файл.

Совет


Если вы хотите работать со старыми ядрами, то эта возможность также все упростит. Извлеките при создании образа пакетную версию ядра linux. В случае использования systemd-boot ее можно будет задействовать для загрузки без дополнительных настроек.

default_efi_image="/efi/EFI/Linux/linux-$(pacman -Q linux | awk '{print $2}').efi"



Комментарии (2)


  1. fshp
    04.09.2021 14:01
    +1

    Использовать параметр EFI_STUB религия не позволяет?

    Запустите make menuconfig, там много интересного. И initrd можно встроить, и блобы, и опции, и версию и на выходе сразу получить готовый efi файл.


    1. Am1GO
      07.09.2021 05:49
      +1

      EFISTUB подходит только для тех, кто сам себе ядра собирает, а systemd'овый EFI stub позволяет встроить произвольный бинарник в EFI-обёртку и поэтому отлично подходит всем, включая тех, кто использует готовые бинарные сборки.

      Помимо прочего, systemd'овый EFI stub умеет взаимодействовать с TPM и в тех случаях, когда загрузка бинарника, полученного на основе этого stub осуществляется не напрямую, а через загрузчик, это даёт дополнительные преимущества: например, если реализовано автоматическое расшифровывание рутового раздела и при этом используется загрузчик, позволяющий передать произвольный cmdline, то его изменение приведёт к тому, что содержимое целевого PCR-регистра изменится и автоматической расшифровки не произойдёт. Всего этого, в случае упаковки ядра с предсобранным init'ом с помощью ядерного EFISTUB, разумеется, реализовать не удастся.

      Дополнительное преимущество - это время, требуемое на изменение конфигурации: если мне необходимо добавить или убрать поддержку устройств, инициализируемых при загрузке до переключения на рутовый раздел, то в случае внешнего init'а и systemd'ового EFI stub на это потребуется порядка 1 минуты на пересборку initramfs и ещё 5 секунд на сборку нового подписанного EFI stub, в случае же с init'ом, вкомпиленным в ядро, которое собирается в виде EFISTUB, на это потребуется существенно больше времени.

      Всё это не имеет ни малейшего отношения к религии и позволяет экономить собственное время для тех, кто его ценит.