Рекомендации по настройке доверенной загрузки ядра
Содержание:
1. Подготовка программного обеспечения
2. Построение цепочки доверия в UEFI Secure Boot
2.1. Создание ключей и генерация сертификатов
2.2. Конвертация сертификатов в формат EFI Signature List
2.3. Формирование базы разрешенных ключей db
2.4. Формирование базы отозванных ключей dbx
2.5. Подпись цепочки сертификатов
2.6. Создание загрузочной флешки с KeyTool
3. Создание Unified Kernel Image и отказ от стороннего загрузчика
3.1. Создание файла аргументов ядра Linux
3.2. Подготовка initramfs. Встраивание микрокода процессора
3.3. Создание EFI-образа Unified Kernel Image
3.3.1. В производных от Debian дистрибутивах
3.3.2. В производных от Arch Linux дистрибутивах
3.3.3. В дистрибутиве Alt Linux
3.4. Подпись созданного Unified Kernel Image
3.5. Автоматизация генерации Unified Kernel Image
3.6. Добавление Unified Kernel Image в загрузку
4. Подпись ядра, модулей ядра, функция Kernel Lockdown
4.1. Подпись ядра Linux
4.2. Подпись модулей ядра Linux
4.3. Kernel Lockdown
5. Конфигурация UEFI Secure Boot и установка ключей
5.1. Настройка UEFI Secure Boot в средах виртуализации
5.1.1 VMWare Fusion / VMWare Workstation
5.1.2 QEMU + OVMF
5.1.3 VirtualBox
5.2. Установка сертификатов с помощью KeyTool
Введение
Этот документ рассматривает настройку доверенной загрузки преимущественно для рабочих станций с архитектурой x86, x86_64 и не затрагивает вопросы централизованного управления, но при этом не противоречит им. Централизованное управление, в частности, развертывание ключей Secure Boot может быть организовано с помощью специального программного обеспечения разработчиками дистрибутивов или иными средствами, применяемыми системными администраторами на местах.
Данные рекомендации призваны обезопасить процесс запуска дистрибутивов семейства Linux. Основная концепция доверенной загрузки заключается в том, что управление операционной системе должно быть передано только в том случае, если механизмы доверенной загрузки (МДЗ) платформы могут криптографически проверить загрузочные компоненты на соответствие предварительно сконфигурированному набору открытых ключей, подписей и хэшей, а также ряд других параметров.
Средства доверенной загрузки (СДЗ) подразделяются на следующие типы:
- средства доверенной загрузки уровня базовой системы ввода-вывода (СДЗ уровня БСВВ);
- средства доверенной загрузки уровня платы расширения (СДЗ уровня ПР);
- средства доверенной загрузки уровня загрузочной записи (СДЗ уровня ЗЗ).
Важно отметить, что стандартный механизм UEFI Secure Boot не может быть сертифицирован в качестве СДЗ, но при этом на концептуальном уровне он соответствует СДЗ уровня БСВВ.
Популярные дистрибутивы, в том числе и отечественные, поддерживающие реализацию доверенной загрузки через механизм UEFI Secure Boot и по умолчанию содержат ряд недостатков:
- Список доверенного ПО, порождаемый сертификатами Microsoft и Canonical через shim, является практически бесконечным и бесконтрольным даже для тех дистрибутивов, загрузчику которых доверяет shim.
- Отзыв уязвимых версий загрузчика происходит медленно и влечёт за собой либо отзыв загрузчиков всех дистрибутивов, либо приводит к значительному росту размера чёрного списка сертификатов и хэшей (dbx), который хранится в NVRAM и уже сейчас составляет более 14 КБ.
- Ключ владельца аппаратуры (MOK), также используемый shim для аутентификации загрузчиков UEFI и модулей ядра Linux может быть изменён на произвольный любым пользователем с правами root, который обладает физическим доступом к компьютеру.
- Аргументы загрузки ядра хранятся на изменяемой файловой системе (в конфигурации загрузчика), в NVRAM переменной загрузочной записи (Boot####) или редактируются прямо из интерфейса загрузчика, позволяя получить root-доступ.
- Цепочка доверия заканчивается на модулях ядра и не проверяет ни файловую систему начальной загрузки (initrd), ни какие-либо пользовательские программы.
- Постоянно выявляемые уязвимости в загрузчике операционных систем GRUB2 делают его ненадежным для использования в конфигурации Secure Boot, что компрометирует всю цепочку доверия, равно как и использование любого уязвимого промежуточного звена в виде загрузчиков операционных систем, которые перехватывают функции BootServices.
Количество потенциальных угроз можно было бы значительно сократить за счёт:
- использования отдельного CA каждым дистрибутивом, устанавливаемого разработчиком аппаратной платформы или системным администратором;
- использования подписанного Unified Kernel Image (UKI) вместо grub2 и initrd;
- отказа от MOK и возможности влиять на список загружаемых модулей ядра.
если реализовать следующую схему загрузки ОС Linux:
Использование СДЗ уровня ПР также возможно в предложенной схеме. Преимуществом использования UKI является возможность сократить список контролируемых файлов с помощью СДЗ до одного файла (UKI-образа) и гарантировать проверку целостности компонентов родным для Linux-ядра способом.
Тем не менее при использовании СДЗ уровня ПР возникает ряд нюансов, в частности это проблема совместимости платы расширения конкретного СДЗ с UEFI SecureBoot, а также проблема организации доверенного хранилища сертификатов, которое формируется ядром Linux из ключей платформы.
С учетом вышеупомянутых проблем, для создания замкнутой программной среды (ЗПС) с СДЗ уровня ПР требуется реализация одного из перечисленных ниже подходов:
- UEFI Secure Boot не поддерживается платой расширения СДЗ, ключ платформы можно хранить в переменной MOK-ключа, которая должна быть установлена СДЗ на стадии загрузки до передачи управления ядру Linux в виде volatile переменной во избежание возможности её дальнейшей перезаписи. MOK-ключ так же как и переменные SecureBoot платформы загружается в хранилище ключей .platform.
- UEFI Secure Boot должен быть включён и совместим с платой расширения СДЗ. Т.е. СДЗ уровня ПР должен аутентифицировать ключи UEFI Secure Boot до их использования прошивкой.
- Все сертификаты должны быть интегрированы в ядро на стадии сборки, а загрузка внешних сертификатов должна быть запрещена.
Для защиты от TOCTOU атак проверка UKI-образа должна быть произведена СДЗ уровня ПР только в оперативной памяти и последующей загрузкой его без повторного чтения с носителя.
Основной задачей руководства продемонстрировать представленную схему загрузки, а также дать рекомендации по обеспечению цепочки доверия от старта встроенного программного обеспечения стандарта UEFI до пользовательского окружения операционной системы семейства Linux.
1. Подготовка программного обеспечения
Для настройки UEFI Secure Boot необходимо установить дополнительное программное обеспечение, а именно:
- Пакет openssl, одноименная утилита из которого используется для генерации ключевых пар и преобразования сертификатов.
- Пакет efitools, который предоставляет утилиты cert-to-efi-sig-list, sign-efi-sig-list для преобразования сертификатов в формат ESL[1] и осуществления подписи файлов в этом формате; утилиту hash-to-efi-sig-list для создания ESL из хешей отдельных исполняемых файлов, а также KeyTool.efi для управления ключами системы, находящихся в SetupMode.
- Пакет sbsigntool, из которого используется утилита sbsign для подписи исполняемых EFI-файлов с помощью вашего ключа.
- Пакет efibootmgr, который содержит одноименное приложение для управления загрузочными записями UEFI.
- Пакет uuid-runtime, который содержит утилиты для работы с UUID; необходима утилита uuidgen.
- Пакет binutils, который предоставляет программы для ассемблирования, компоновки и манипуляций с двоичными и объектными файлами. Из этого пакета будут использованы утилиты objcopy и objdump.
- Пакет linux-headers-$(uname -r), который предоставляет заголовки ядра Linux. $(uname -r) указывает версию запущенного ядра в настоящий момент. Заголовки являются частью ядра, хотя и поставляются отдельно; они действуют как интерфейс между внутренними компонентами ядра, а также между пользовательским пространством и ядром. В их комплекте поставляются утилиты для операций по модификации ядра vmlinuz (перепаковка, распаковка), а также инструмент для подписи модулей ядра sign-file.
Обработка двоичных файлов UEFI SecureBoot (PK/KEK/db/dbx/dbxupdate) будет осуществляться скриптом Python.
#!/usr/bin/env python3
from dataclasses import dataclass, field
import enum
import uuid
class Bytes(bytes):
def __str__(self):
return self.hex()
def __repr__(self):
return self.hex()
class Enum(enum.Enum):
def __str__(self):
return self.name
def __repr__(self):
return self.name
def u16(data):
assert len(data) == 2
return int.from_bytes(data, byteorder='little')
def u32(data):
assert len(data) == 4
return int.from_bytes(data, byteorder='little')
class EFI_SIGNATURE_TYPE(Enum):
EFI_CERT_SHA256_GUID = uuid.UUID('c1c41626-504c-4092-aca9-41f936934328')
EFI_CERT_RSA2048_GUID = uuid.UUID('3c5766e8-269c-4e34-aa14-ed776e85b3b6')
EFI_CERT_RSA2048_SHA256_GUID = uuid.UUID('e2b36190-879b-4a3d-ad8d-f2e7bba32784')
EFI_CERT_SHA1_GUID = uuid.UUID('826ca512-cf10-4ac9-b187-be01496631bd')
EFI_CERT_RSA2048_SHA1_GUID = uuid.UUID('67f8444f-8743-48f1-a328-1eaab8736080')
EFI_CERT_X509_GUID = uuid.UUID('a5c059a1-94e4-4aa7-87b5-ab155c2bf072')
class EFI_SIGNATURE_OWNER(Enum):
EFI_SIGNATURE_OWNER_CANONICAL = uuid.UUID('685984e3-5d0f-4682-94c1-0f85ecb55d34')
EFI_SIGNATURE_OWNER_MICROSOFT = uuid.UUID('77fa9abd-0359-4d32-bd60-28f4e78f784b')
@dataclass
class EFI_SIGNATURE_DATA:
Owner: uuid.UUID
Data: Bytes
@classmethod
def from_bytes(cls, data):
return cls(
Owner=EFI_SIGNATURE_OWNER(uuid.UUID(bytes_le=data[:0x10])),
Data=Bytes(data[0x10:]),
)
@dataclass
class EFI_SIGNATURE_LIST(list):
Type: EFI_SIGNATURE_TYPE
Size: int
Header: Bytes
@classmethod
def from_bytes(cls, data):
Type = EFI_SIGNATURE_TYPE(uuid.UUID(bytes_le=data[:0x10]))
ListSize = u32(data[0x10:0x14])
assert len(data) >= ListSize
HeaderSize = u32(data[0x14:0x18])
SignatureSize = u32(data[0x18:0x1C])
assert SignatureSize > 0
offsetof_Signatures = 0x1C + HeaderSize
sizeof_Signatures = ListSize - offsetof_Signatures
assert sizeof_Signatures % SignatureSize == 0
Signatures = data[offsetof_Signatures:ListSize]
self = cls(
Type=Type,
Size=ListSize,
Header=Bytes(data[0x1C:0x1C+HeaderSize]),
)
self += [
EFI_SIGNATURE_DATA.from_bytes(Signatures[offset:offset+SignatureSize])
for offset in range(0, sizeof_Signatures, SignatureSize)
]
return self
def __repr__(self):
Signatures = '\n'.join(f' {item},' for item in self)
return f'EFI_SIGNATURE_LIST(Type={self.Type}, Header={self.Header}, Signatures=[\n{Signatures}\n])'
@dataclass
class EFI_TIME:
Year: int
Month: int
Day: int
Hour: int
Minute: int
Second: int
Nanosecond: int
TimeZone: int
Daylight: int
@classmethod
def from_bytes(cls, data):
assert len(data) == 16
Pad1 = data[7]
Pad2 = data[15]
assert Pad1 == Pad2 == 0
return cls(
Year=u16(data[0:2]),
Month=data[2],
Day=data[3],
Hour=data[4],
Minute=data[5],
Second=data[6],
Nanosecond=u32(data[8:12]),
TimeZone=u16(data[12:14]),
Daylight=data[14],
)
class WIN_CERT_TYPE(Enum):
WIN_CERT_TYPE_PKCS_SIGNED_DATA = 0x0002
WIN_CERT_TYPE_EFI_PKCS115 = 0x0EF0
WIN_CERT_TYPE_EFI_GUID = 0x0EF1
@dataclass(init=False)
class WIN_CERTIFICATE:
Length: int
Revision: int
CertificateType: WIN_CERT_TYPE
Certificate: Bytes
@classmethod
def from_bytes(cls, data):
self = cls()
self.Length = u32(data[0:4])
assert self.Length > 8
self.Revision = u16(data[4:6])
assert self.Revision == 0x0200, f'Revision=0x{self.Revision:X}'
self.CertificateType = WIN_CERT_TYPE(u16(data[6:8]))
self.Certificate = data[8:self.Length]
return self
def __len__(self):
return self.Length
class EFI_CERT_TYPE(Enum):
EFI_CERT_TYPE_PKCS7_GUID = uuid.UUID('4aafd29d-68df-49ee-8aa9-347d375665a7')
EFI_CERT_TYPE_RSA2048_SHA256_GUID = uuid.UUID('a7717414-c616-4977-9420-844712a735bf')
@dataclass(init=False)
class WIN_CERTIFICATE_UEFI_GUID(WIN_CERTIFICATE):
Certificate: Bytes = field(repr=False)
CertType: EFI_CERT_TYPE
CertData: Bytes
@classmethod
def from_bytes(cls, data):
self = super().from_bytes(data)
self.CertType = EFI_CERT_TYPE(uuid.UUID(bytes_le=self.Certificate[:16]))
self.CertData = Bytes(self.Certificate[16:])
return self
@dataclass
class EFI_VARIABLE_AUTHENTICATION_2:
TimeStamp: EFI_TIME
AuthInfo: WIN_CERTIFICATE_UEFI_GUID
@classmethod
def from_bytes(cls, data):
return cls(
TimeStamp=EFI_TIME.from_bytes(data[:16]),
AuthInfo=WIN_CERTIFICATE_UEFI_GUID.from_bytes(data[16:]),
)
def __len__(self):
return 0x10 + len(self.AuthInfo)
def __repr__(self):
return f'EFI_VARIABLE_AUTHENTICATION_2(\n TimeStamp={self.TimeStamp},\n AuthInfo={self.AuthInfo},\n)'
def parse_binary(data):
if data[:4] == b'\x27\x00\x00\x00':
# skip 4 bytes attributes exposed by efivarfs filesystem
data = data[4:]
if data[22:24] != b'\x00\x00':
var_auth_2 = EFI_VARIABLE_AUTHENTICATION_2.from_bytes(data)
print(var_auth_2)
data = data[len(var_auth_2):]
offset = 0
while offset < len(data):
sig_list = EFI_SIGNATURE_LIST.from_bytes(data[offset:])
print(sig_list)
offset += sig_list.Size
def parse_file(path):
import mmap
with open(path, 'rb') as f:
try:
data = mmap.mmap(f.fileno(), 0, access=mmap.ACCESS_READ)
except OSError:
data = f.read()
parse_binary(data)
def main():
import argparse
parser = argparse.ArgumentParser('Secure Boot related blob parser (for PK/KEK/db/dbx/dbxupdate)')
parser.add_argument('path', help='path to input blob')
args = parser.parse_args()
parse_file(args.path)
if __name__ == '__main__':
main()
Команды для установки программного обеспечения в различных дистрибутивах:
Debian/Ubuntu/Astra Linux (раскрыть)
sudo apt update
sudo apt install openssl efitools efibootmgr sbsigntool uuid-runtime linux-headers-$(uname -r) binutils keyutils
Для Astra Linux дополнительно необходимо установить пакет astra-safepolicy, более подробно в официальной документации.
sudo apt install astra-safepolicy
Стоит отметить, что в указанной статье используется скрипт astra-secureboot, который выполняет большую часть шагов этого руководства.
sudo pacman -Sy
sudo pacman -S openssl efitools sbsigntools efibootmgr util-linux linux-headers binutils keyutils
В производных от Arch Linux дистрибутивах (раскрыть)
Команды, исполняемые с правами суперпользователя, в данной статье вызываются с помощью утилиты sudo. По умолчанию в дистрибутиве Alt Linux "Рабочая станция" команда sudo выключена. Для того чтобы включить команду sudo, необходимо отредактировать файл /etc/sudoers, раскомментировав строки root ALL=(ALL) ALL и WHEEL_USERS ALL=(ALL) ALL из учетной записи суперпользователя, используя команду visudo:
su -
visudo
sudo apt-get update
sudo apt-get install openssl efitools sbsigntools binutils util-linux kernel-headers-std-def kernel-headers-un-def efibootmgr keyutils
1 EFI Signature List - формат хранения файлов сертификатов и подписей, описанный в спецификации UEFI (в п. 32.4.1 вер. 2.10). Основной особенностью является возможность соединять файлы друг с другом конкатенацией (прим. cat bootmgfw.efi.esl bootx64.efi.esl > final_siglist.esl).
2. Построение цепочки доверия в UEFI Secure Boot
Сегодня в большинстве конфигураций UEFI Secure Boot платформы по умолчанию главный ключ (PK - Platform Key) предоставляется производителем материнской платы, а в хранилище ключей (KEK - Key Exchange Key) лежит единственный сертификат Microsoft Corporation KEK CA 2011, хотя иногда к нему добавляют сертификат Canonical и сертификат производителя платформы. Помимо этого, в хранилище есть также база разрешенных ключей (db - Signature Database Key) и база отозванных ключей (dbx - Forbidden Signature Database).
Здесь PK – это главный ключ, которым подписан KEK, в свою очередь ключами из KEK подписываются db и dbx. Для запуска подписанного исполняемого файла, он должен быть подписан ключом, который есть в db и отсутствует в dbx. Первым шагом на пути к построению цепочки доверия является создание сертификатов и приватных ключей.
2.1 Создание ключей и генерация сертификатов
Приватные ключи, созданные в процессе данной процедуры, важно хранить в защищенном хранилище. Утечка приватного ключа означает компрометацию всей цепочки доверия и, как следствие, всей защищаемой платформы. Рекомендуется использовать для каждого ключа отдельный, криптостойкий пароль.
С помощью приведенных ниже команд выполняется создание ключевых пар UEFI Secure Boot с использованием алгоритма RSA2048/SHA256.
Создание ключевой пары владельца платформы - PlatformKey:
openssl req -new -x509 -newkey rsa:2048 -sha256 -days 365 -subj "/CN=Platform Key" -keyout PK.key -out PK.pem
Пример выполнения команды (раскрыть)
Создание ключевой пары KEK:
openssl req -new -x509 -newkey rsa:2048 -sha256 -days 365 -subj "/CN=Key Exchange Key" -keyout KEK.key -out KEK.pem
Пример выполнения команды (раскрыть)
- Создание ключевой пары для подписи исполняемых EFI-файлов ISK (их может быть несколько):
openssl req -new -x509 -newkey rsa:2048 -sha256 -days 365 -subj "/CN=Image Signing Key" -keyout ISK.key -out ISK.pem
Пример выполнения команды (раскрыть)
Далее необходимо выполнить преобразование открытых ключей в формат UEFI Secure Boot – EFI Signature List.
2.2 Преобразование открытых ключей в формат EFI Signature List
Ранее созданные сертификаты необходимо преобразовать из контейнера PEM в формат ESL с помощью утилиты cert-to-efi-sig-list, как показано ниже.
owner=$(uuidgen owner=$(uuidgen --namespace @dns --name mega_secured_pc.local --sha1))
echo -n $owner > owner_uuid.txt
cert-to-efi-sig-list -g $owner PK.pem PK.esl
cert-to-efi-sig-list -g $owner KEK.pem KEK.esl
cert-to-efi-sig-list -g $owner ISK.pem ISK.esl
Здесь в качестве аргумента -g передается случайный идентификатор, сгенерированный с помощью утилиты uuidgen. Данный UUID идентифицирует владельца ключа. Обратите внимание, что в этих командах у всех трех ключей владелец (owner) один. Тем не менее, эта опция полезна для дифференциации ключей в системе, когда их много, если присваивать каждому ключу свой уникальны идентификатор. По умолчанию, если не указать свой UUID при преобразовнии, используется пустой (00000000-0000-0000-0000-000000000000).
Следующим этапом выполняется создание базы разрешенных ключей db.
2.3 Формирование базы разрешенных ключей db
На этом этапе необходимо принять решение, добавлять ли ключи подписи Microsoft в доверенные (в db) к ранее созданному ISK.esl или не добавлять.
Чтобы добавить возможность запуска подписанных загрузчиков операционных систем Windows, достаточно добавить ключи Microsoft Windows Production CA 2011[2]. Для поддержки драйверов различного оборудования необходимо также добавить ключ Microsoft UEFI driver signing CA[3], которым подписываются компоненты третьих сторон.
Для этого выполняется преобразование сертификата Microsoft Windows Production CA 2011 из формата DER в совместимый с утилитой cert-to-efi-sig-list формат PEM:
openssl x509 -in MicWinProPCA2011_2011-10-19.crt -inform DER -out MicWinProPCA2011_2011-10-19.pem -outform PEM
Аналогичные действия производятся для Microsoft UEFI driver signing CA:
openssl x509 -in MicCorUEFCA2011_2011-06-27.crt -inform DER -out MicCorUEFCA2011_2011-06-27.pem -outform PEM
Далее выполняется преобразование полученных сертификатов из контейнера PEM в ESL:
cert-to-efi-sig-list -g "$(uuidgen)" MicWinProPCA2011_2011-10-19.pem MicWinProPCA2011_2011-10-19.esl
cert-to-efi-sig-list -g "$(uuidgen)" MicCorUEFCA2011_2011-06-27.pem MicCorUEFCA2011_2011-06-27.esl
После этого создается база разрешенных ключей db:
cat ISK.esl > db.esl
cat MicWinProPCA2011_2011-10-19.esl ISK.esl > db.esl
ISK + PCA 2011 + CorUEFCA2011 (раскрыть)
cat MicWinProPCA2011_2011-10-19.esl MicCorUEFCA2011_2011-06-27.esl ISK.esl > db.esl
Альтернативным путем является добавление не сертификатов Microsoft, а доверенных хешей исполняемых EFI-файлов, а именно - файлов загрузчика Windows. Как показала практика, исполняемые файлы загрузчика Windows изменяются крайне редко, поэтому регулярно обновлять базу разрешенных хэш-значений не придётся.
Для этого необходимо преобразовать SHA-256 от исполняемых файлов загрузчика Windows в ESL формат:
- EFI/BOOT/bootx64.efi
- EFI/Microsoft/Boot/bootmgfw.efi
- EFI/Microsoft/Boot/bootmgr.efi
- EFI/Microsoft/Boot/memtest.efi
hash-to-efi-sig-list EFI/BOOT/bootx64.efi ms_1.esl
hash-to-efi-sig-list EFI/Microsoft/Boot/bootmgfw.efi ms_2.esl
hash-to-efi-sig-list EFI/Microsoft/Boot/bootmgr.efi ms_3.esl
hash-to-efi-sig-list EFI/Microsoft/Boot/memtest.efi ms_4.esl
Пример выполнения команд (раскрыть)
В результате, полученные ESL-файлы добавляются в базу разрешенных ключей db.esl командой:
cat ms_1.esl ms_2.esl ms_3.esl ms_4.esl ISK.esl > db.esl
В таком случае необходимо добавить хеши всех исполняемых файлов, в том числе драйверов периферийных устройств, GOP и т.д Также вы можете добавлять любой хеш приложения или драйвера, какой пожелаете в качестве доверенного.
Не добавляйте UEFI Shell в качестве доверенного, не подписывайте его и приложения со схожим функционалом, поскольку это может позволить обойти механизм UEFI Secure Boot. В частности, не подписывайте и не доверяйте загрузчику GRUB. Также не подписывайте KeyTool!
2 http://go.microsoft.com/fwlink/?LinkID=321192
3 http://go.microsoft.com/fwlink/?LinkId=321194
Следующий шаг - это создание базы отозванных ключей dbx. Он опционален, поскольку dbx необходим только в том случае, если вы добавили сторонние ключи Microsoft или Canonical в базу разрешенных ключей db или в KEK.
2.4 Формирование базы отозванных ключей dbx
Принцип создания базы отозванных ключей dbx не отличается от принципа создания db. Файлы обновления для базы отозванных ключей публикуются на сайте UEFI консорциума по ссылке. Чтобы заполнить базу отозванных ключей из файла обновления, предварительно выполняется его преобразование в формат ESL.
Файл обновления содержит структуру EFI_VARIABLE_AUTHENTICATION_2, в которой располагается электронная подпись Microsoft KEK, а также сам список отозванных хешей в формате ESL. Необходимо удалить заголовок с подписью для преобразования файла обновления dbx в формат ESL. Для этого с помощью sb_blob_parser определяется размер структуры EFI_VARIABLE_AUTHENTICATION_2, содержащей электронную подпись:
python3 ~/sb_blob_parser.py DBXUpdate.bin
К полученному значению Length добавляется еще 16 байт (размер EFI_TIME Timestamp) и результирующим значением в этом примере будет: 3318+16 = 3334.
Затем с помощью утилиты dd отрезается часть файла обновления, содержащая EFI Signature List командой:
dd if=DBXUpdate.bin of=dbx.esl bs=1 skip=3334
Для проверки корректности полученного dbx.esl, можно загрузить его в sb_blob_parser:
python3 sb_blob_parser.py dbx.esl
Пример выполнения команд (раскрыть)
Когда все компоненты готовы (PK, KEK, db, dbx), можно перейти к их подписи.
2.5 Подпись цепочки сертификатов
На данном этапе выполняется подпись сертификатов по цепочке PK->KEK->db,dbx.
PK подписывается самим собой командой:
sign-efi-sig-list -k PK.key -c PK.pem PK PK.esl PK.auth
Сертификатом PK подписывается KEK командой:
sign-efi-sig-list -k PK.key -c PK.pem KEK KEK.esl KEK.auth
Сертификатом KEK подписывается база разрешенных ключей db командой:
sign-efi-sig-list -k KEK.key -c KEK.pem db db.esl db.auth
Если был сформирован dbx, то он подписывается также сертификатом KEK:
sign-efi-sig-list -k KEK.key -c KEK.pem dbx dbx.esl dbx.auth
Следующим шагом необходимо создать загрузочный накопитель с утилитой KeyTool и сгенерированными ключами UEFI Secure Boot (*.auth).
2.6 Создание загрузочной флешки с KeyTool
Флеш-накопитель необходимо отформатировать в файловую систему FAT-32. После чего скопировать утилиту KeyTool.efi на флешку по пути /EFI/BOOT/bootx64.efi
. Утилита KeyTool поставляется в составе пакета efitools. На системах семейства Debian она расположена по пути /usr/lib/efitools/x86_64-linux-gnu/KeyTool.efi
, а в Arch Linux – /usr/share/efitools/efi/KeyTool.efi
.
Файлы .auth также необходимо скопировать на созданный раздел загрузочного накопителя.
3. Создание Unified Kernel Image и отказ от стороннего загрузчика
При использовании загрузчика GRUB, ему предоставляется файл конфигурации, initramfs, и образ системы vmlinuz, содержащий ядро Linux. Файл initramfs включает в себя базовый набор компонентов дистрибутива Linux, отвечающих за расшифровку разделов, монтирование файловой системы и других разделов с последующим запуском подсистемы инициализации init.
Идеей является вложить все эти составляющие в один исполняемый файл. Для этого существует несколько способов:
- использовать заготовку EFI Stub из пакета systemd для создания PE-исполняемого файла с помощью утилиты objcopy;
- скомпилировать ядро самостоятельно с включенной опцией ядра
CONFIG_EFI_STUB=y
, встроить initramfs и аргументы ядра.
Нами рекомендуется первый способ, ввиду того, что даже в случае если вам понадобиться скомпилировать ядро самостоятельно, такой подход позволяет без особых трудностей обновлять секции Unified Kernel Image, в частности, содержимое initramfs и значения аргументов ядра (root=UUID..., --verbose, ro, rw, quite и пр.).
Создаваемые образы, следуя systemd спецификации загрузчика, размещаются по пути /EFI/Linux/
на разделе EFI System Partition.
3.1 Создание файла с аргументами загрузки ядра Linux
Перед созданием Unified Kernel Image необходимо подготовить файл с аргументами загрузки ядра. Текущие аргументы ядра, которые использовались при загрузке системы, можно посмотреть по пути /proc/cmdline
.
В качестве примера на следующем рисунке представлены стандартные аргументы, передаваемые ядру ОС Debian 11 во время загрузки:
Здесь аргумент root=
указывает ядру, какое устройство должно быть использовано в качестве корневой файловой системы, параметр ro
указывает ядру монтировать корневую файловую систему в режиме «только для чтения», для того чтобы программа проверки целостности файловой системы (fsck) могла работать с неизменяющейся файловой системой. Аргумент BOOT_IMAGE=
необходимо отбросить, потому что vmlinuz будет расположен в секции .linux Unified Kernel Image. Более подробную информацию об аргументах ядра Linux можно получить в официальном руководстве администратора – https://www.kernel.org/doc/html/<версия ядра>/admin-guide/kernel-parameters.html, где версия ядра может иметь значение latest для mainline ветки или цифровое обозначение, например:
Таким образом, создается файл со всеми требуемыми аргументами загрузки ядра по пути /etc/kernel/cmdline
:
sudo bash -c "cat /proc/cmdline | tr -d '\n' | sed 's/.*root=/root=/g' > /etc/kernel/cmdline"
Либо вручную, как показано в примере команды ниже:
sudo bash -c "echo -n 'root=UUID=cc9e7638-ca8e-47d2-bab1-13e4e61037da ro quiet' > /etc/kernel/cmdline"
3.2 Подготовка initramfs. Встраивание микрокода процессора
Немаловажным моментом является встраивание микрокода в initramfs.
В производных от Debian дистрибутивах (раскрыть)
В дистрибутивах, производных от Debian, в том числе Astra Linux, необходимо установить пакет intel-microcode для процессоров семейства Intel или amd64-microcode для процессоров AMD соответственно. Установленный пакет с микрокодом автоматически добавит hook в утилиту генерации initramfs, который будет добавлять необходимый файл обновления микрокода в initrd при каждом обновлении.
После установки необходимого пакета с микрокодом требуется выполнить его конфигурацию. Для amd64-microcode надо раскомментировать строкуAMD64UCODE_INITRAMFS
и присвоить ей значение early
в файле /etc/default/amd64-microcode
, что соответствует раннему запуску обновления микрокода процессора (very early microcode updates).
А для intel-microcode в файле /etc/default/intel-microcode
выполнить такие же действия для переменной IUCODE_TOOL_INITRAMFS
.
После этого выполняется обновление initramfs командой:
sudo update-initramfs -u -k all
Более подробно про микрокод в Debian рассказывется в официальной Wiki
Также обратите внимание, что файлы микрокода на Debian расположены по путям:
/usr/lib/firmware/amd-ucode/
/usr/lib/firmware/intel-ucode/
В дистрибутиве Alt Linux (раскрыть)
Для дистрибутива Alt Linux требуемый программный пакет - firmware-linux. По умолчанию initrd генерируется с микрокодом процессора платформы и дополнительных действий не требуется, достаточно обновить initrd командой:
sudo make-initrd --kernel=$(uname -r)
Больше информации про утилиту make-initrd в Alt Linux можно найти в официальной Wiki и в репозитории одноименного проекта на Github.
В производных от Arch Linux дистрибутивах (раскрыть)
cat /boot/intel-ucode.img /boot/initramfs-linux.img > /boot/initramfs-linux-with-microcode.img
Командой выше показан пример с микрокодом Intel, но, в общем, нейминг выполняется в следующей форме: производитель_цп-ucode.img, т.е. для AMD будет amd-ucode.img.
Файлы *-ucode.img в /boot могут отсутствовать, в этом случае необходимо установить пакеты amd-ucode, intel-ucode с помощью пакетного менеджера pacman командами:
sudo pacman -Sy
sudo pacman -S amd-ucode
или
sudo pacman -S intel-ucode
Дополнительную информацию про настройку обновлений микрокода можно найти в официальной Wiki Arch Linux.
PS: Хотелось еще упомянуть неофициальный, но постоянно обновляемый, репозиторий с микрокодами для разных CPU - https://github.com/platomav/CPUMicrocodes.
На этом манипуляции с initramfs можно считать завершенными и перейти к созданию Unified Kernel Image с помощью утилиты objcopy.
3.3 Создание EFI-образа Unified Kernel Image
На данном этапе в EFI-заготовку из пакета systemd в качестве дополнительных секций будут встроены необходимые загрузочные файлы с помощью утилиты objcopy из пакета binutils. Обязательными секциями являются .osrel и .cmdline. В этом руководстве также добавляется ядро и рамдиск, что в дальнейших шагах позволит подписать все компоненты, требуемые для загрузки ядра, как единое целое.
Доступные секции для добавления в файл-заготовку:
.osrel
, в ней указывается информация о дистрибутиве, как правило содержимое расположено по пути/etc/os-release
(в некоторых дистрибутивах в/usr/lib/os-release
)..cmdline
, в этой секции хранятся аргументы ядра. В случае если ядро не добавляется в качестве секции в EFI-образ, то необходимо указать его расположение в качестве аргумента в этой секции. Необходимые аргументы можно взять из/proc/cmdline
, но вы можете записать свои собственные в файл и использовать их вместо этого. Рекомендуется хранить аргументы ядра по пути/etc/kernel/cmdline
..linux
, в данной секции хранится образ системы vmlinuz или vmlinux, содержащий ядро Linux..initrd
, а в эту секцию записывается initramfs..dtb
, в эту секцию можно записать свой DeviceTree, который заменит текущий в EFI configuration table.
Более подробно с systemd-stub можно ознакомиться на ресурсе freedesktop.
Важно заметить, что на платформах с архитектурой ARM64 существует две проблемы в дистрибутивах Linux. Первая связана с тем, что на большинстве LTS-дистрибутивах используется довольно старый пакет binutils, и это обязательно вызовет ошибку при использовании утилит objdump и objcopy на устройствах с этой архитектурой из-за бага, который исправили только в декабре 2021 года. К сожалению, пакеты в таких дистрибутивах как Debian обновляются очень медленно, поэтому возможным решением для них будет аналог утилиты objcopy, реализованный, например, на python, либо самостоятельная компиляция пакета binutils свежей версии из исходного кода, который можно скачать с официального сайта в виде тарбола. А вторая проблема заключается в отсутствии декомпрессора в Linux ядре для архитектуры aarch64, что ломает загрузку сжатых образов системы vmlinuz с использованием EFI-заготовки из состава systemd. Поэтому предварительно следует распаковать ядро (vmlinux), как временное решение (ref).
Также, обратите внимание, что в приведенных командах ниже создаются обезличенные UKI-образы с названием файла linux.efi
или linux-fallback.efi
, что может не соответствовать политике по именованию ядер Linux в вашем конкретном дистрибутиве. Для того чтобы иметь возможность загрузить конкретную версию ядра, вы можете именовать ваши UKI-образы более информативно соответствующим образом - linux-kernel_version.efi
, прим. linux-5.10.0-15-amd64.efi
. Для этого вы можете указывать в качестве сохраняемого файла имя - $(uname -r).efi
, что будет соответствовать текущей версии загруженного ядра Linux. В таком случае, имейте в виду, что для каждого такого ядра требуется создавать свою загрузочную запись в EFI Boot Manager (см. п.п. 3.6)
3.3.1 В производных от Debian дистрибутивах
Для производных от Debian дистрибутивов, в том числе Astra Linux, команды следующие:
sudo mkdir -p /boot/efi/EFI/Linux
efistub_path=$(case $(uname -m) in "x86_64") echo "/usr/lib/systemd/boot/efi/linuxx64.efi.stub";; \
"i686") echo "/usr/lib/systemd/boot/efi/linuxia32.efi.stub";; \
"aarch64") echo "/usr/lib/systemd/boot/efi/linuxaa64.efi.stub";; \
*) echo "unsupported arch" $(uname -m);; esac;)
vmlinuz_path=$(sudo realpath /vmlinuz)
initrd_path=$(sudo realpath /initrd.img)
osrelease_path=$(realpath /etc/os-release)
stub_line=$(objdump -h $efistub_path | tail -2 | head -1)
stub_size=0x$(echo "$stub_line" | awk '{print $3}')
stub_offs=0x$(echo "$stub_line" | awk '{print $4}')
osrel_offs=$((stub_size + stub_offs))
cmdline_offs=$((osrel_offs + $(stat -c%s $osrelease_path)))
linux_offs=$((cmdline_offs + $(stat -c%s "/etc/kernel/cmdline")))
initrd_offs=$((linux_offs + $(sudo stat -c%s "$vmlinuz_path")))
sudo objcopy \
--add-section .osrel=$osrelease_path \
--change-section-vma .osrel=$(printf 0x%x $osrel_offs) \
--add-section .cmdline="/etc/kernel/cmdline" \
--change-section-vma .cmdline=$(printf 0x%x $cmdline_offs) \
--add-section .linux=$vmlinuz_path \
--change-section-vma .linux=$(printf 0x%x $linux_offs) \
--add-section .initrd=$initrd_path \
--change-section-vma .initrd=$(printf 0x%x $initrd_offs) \
$efistub_path "/boot/efi/EFI/Linux/linux.efi"
Предполагается, что initrd уже содержит файл-обновление микрокода процессора (т.е. ранее был установлен программный пакет, соответствующий процессору аппаратной платформы: amd64-microcode, intel-microcode). В этих командах используются символьные ссылки, расположенные в корне файловой системы, которые ссылаются на последнюю версию ядра (vmlinuz) и рамдиска (initrd.img) в системной папке /boot
.
Для того чтобы была возможность загрузиться с прошлой версии ядра, можно создать резервный EFI-образ для /vmlinuz.old
и /initrd.img.old
:
sudo mkdir -p /boot/efi/EFI/Linux
efistub_path=$(case $(uname -m) in "x86_64") echo "/usr/lib/systemd/boot/efi/linuxx64.efi.stub";; \
"i686") echo "/usr/lib/systemd/boot/efi/linuxia32.efi.stub";; \
"aarch64") echo "/usr/lib/systemd/boot/efi/linuxaa64.efi.stub";; \
*) echo "unsupported arch" $(uname -m);; esac;)
vmlinuz_path=$(sudo realpath /vmlinuz.old)
initrd_path=$(sudo realpath /initrd.img.old)
osrelease_path=$(realpath /etc/os-release)
stub_line=$(objdump -h $efistub_path | tail -2 | head -1)
stub_size=0x$(echo "$stub_line" | awk '{print $3}')
stub_offs=0x$(echo "$stub_line" | awk '{print $4}')
osrel_offs=$((stub_size + stub_offs))
cmdline_offs=$((osrel_offs + $(stat -c%s $osrelease_path)))
linux_offs=$((cmdline_offs + $(stat -c%s "/etc/kernel/cmdline")))
initrd_offs=$((linux_offs + $(sudo stat -c%s "$vmlinuz_path")))
sudo objcopy \
--add-section .osrel=$osrelease_path \
--change-section-vma .osrel=$(printf 0x%x $osrel_offs) \
--add-section .cmdline="/etc/kernel/cmdline" \
--change-section-vma .cmdline=$(printf 0x%x $cmdline_offs) \
--add-section .linux=$vmlinuz_path \
--change-section-vma .linux=$(printf 0x%x $linux_offs) \
--add-section .initrd=$initrd_path \
--change-section-vma .initrd=$(printf 0x%x $initrd_offs) \
$efistub_path "/boot/efi/EFI/Linux/linux-fallback.efi"
Подписанный резервный EFI-образ дает возможность загрузки прошлой версии ядра, что негативно может сказаться на безопасности системы. Тем не менее, это существенно повышает надежность платформы, предоставляя возможность отката в случае неуспешного обновления основного ядра.
3.3.2 В производных от Arch Linux дистрибутивах
Для дистрибутива Arch Linux и производных от него ОС (например: Manjaro Linux, Garuda Linux и пр.) команды принимают следующий вид:
sudo mkdir -p /boot/efi/EFI/Linux
efistub_path=$(case $(uname -m) in "x86_64") echo "/usr/lib/systemd/boot/efi/linuxx64.efi.stub";; \
"i686") echo "/usr/lib/systemd/boot/efi/linuxia32.efi.stub";; \
"aarch64") echo "/usr/lib/systemd/boot/efi/linuxaa64.efi.stub";; \
*) echo "unsupported arch" $(uname -m);; esac;)
vmlinuz_path="/boot/vmlinuz-linux"
initrd_path="/boot/initramfs-linux.img"
osrelease_path=$(realpath /etc/os-release)
stub_line=$(objdump -h $efistub_path | tail -2 | head -1)
stub_size=0x$(echo "$stub_line" | awk '{print $3}')
stub_offs=0x$(echo "$stub_line" | awk '{print $4}')
osrel_offs=$((stub_size + stub_offs))
cmdline_offs=$((osrel_offs + $(stat -c%s $osrelease_path)))
linux_offs=$((cmdline_offs + $(stat -c%s "/etc/kernel/cmdline")))
initrd_offs=$((linux_offs + $(sudo stat -c%s "$vmlinuz_path")))
sudo objcopy \
--add-section .osrel=$osrelease_path \
--change-section-vma .osrel=$(printf 0x%x $osrel_offs) \
--add-section .cmdline="/etc/kernel/cmdline" \
--change-section-vma .cmdline=$(printf 0x%x $cmdline_offs) \
--add-section .linux=$vmlinuz_path \
--change-section-vma .linux=$(printf 0x%x $linux_offs) \
--add-section .initrd=$initrd_path \
--change-section-vma .initrd=$(printf 0x%x $initrd_offs) \
$efistub_path "/boot/efi/EFI/Linux/linux.efi"
Названия файлов ядра и рам-диска (переменные vmlinuz_path, initrd_path) в вашей системе могут отличаться от представленных выше. Например, если вы выполняли склейку initramfs с файлом обновления микрокода, то необходимо указать initrd_path в /boot/initramfs-linux-with-microcode.img
.
Так же как и в дистрибутиве Debian, вы можете создать резервный EFI-образ, только в данном случае выбирается не предыдущее установленное ядро, а ядро с длительным сроком поддержки (LTS):
sudo mkdir -p /boot/efi/EFI/Linux
efistub_path=$(case $(uname -m) in "x86_64") echo "/usr/lib/systemd/boot/efi/linuxx64.efi.stub";; \
"i686") echo "/usr/lib/systemd/boot/efi/linuxia32.efi.stub";; \
"aarch64") echo "/usr/lib/systemd/boot/efi/linuxaa64.efi.stub";; \
*) echo "unsupported arch" $(uname -m);; esac;)
vmlinuz_path="/boot/vmlinuz-linux-lts"
initrd_path="/boot/initramfs-linux-lts-fallback.img"
osrelease_path=$(realpath /etc/os-release)
stub_line=$(objdump -h $efistub_path | tail -2 | head -1)
stub_size=0x$(echo "$stub_line" | awk '{print $3}')
stub_offs=0x$(echo "$stub_line" | awk '{print $4}')
osrel_offs=$((stub_size + stub_offs))
cmdline_offs=$((osrel_offs + $(stat -c%s $osrelease_path)))
linux_offs=$((cmdline_offs + $(stat -c%s "/etc/kernel/cmdline")))
initrd_offs=$((linux_offs + $(sudo stat -c%s "$vmlinuz_path")))
sudo objcopy \
--add-section .osrel=$osrelease_path \
--change-section-vma .osrel=$(printf 0x%x $osrel_offs) \
--add-section .cmdline="/etc/kernel/cmdline" \
--change-section-vma .cmdline=$(printf 0x%x $cmdline_offs) \
--add-section .linux=$vmlinuz_path \
--change-section-vma .linux=$(printf 0x%x $linux_offs) \
--add-section .initrd=$initrd_path \
--change-section-vma .initrd=$(printf 0x%x $initrd_offs) \
$efistub_path "/boot/efi/EFI/Linux/linux-fallback.efi"
Названия файлов ядра и рам-диска (переменные vmlinuz_path, initrd_path) на вашей системе могут отличаться от представленных выше!
Создание EFI-образов с отображением картинки на этапе загрузки (boot splash) (раскрыть)
В рамках этой статьи данная опция будет рассмотрена только для Arch Linux, ввиду того, что дистрибутивы, основанные на этой ОС, имеют предустановленный файл с изображением на этапе загрузки в составе systemd, но это не означает, что эта опция недоступна на других дистрибутивах, просто для них необходимо создавать картинку самостоятельно, или брать из тем загрузчика GRUB или Plymouth.
Команды для создания Unified Kernel Image с изображением на этапе загрузки:
sudo mkdir -p /boot/efi/EFI/Linux
efistub_path=$(case $(uname -m) in "x86_64") echo "/usr/lib/systemd/boot/efi/linuxx64.efi.stub";; \
"i686") echo "/usr/lib/systemd/boot/efi/linuxia32.efi.stub";; \
"aarch64") echo "/usr/lib/systemd/boot/efi/linuxaa64.efi.stub";; \
*) echo "unsupported arch" $(uname -m);; esac;)
vmlinuz_path="/boot/vmlinuz-linux"
initrd_path="/boot/initramfs-linux.img"
osrelease_path=$(realpath /etc/os-release)
stub_line=$(objdump -h $efistub_path | tail -2 | head -1)
stub_size=0x$(echo "$stub_line" | awk '{print $3}')
stub_offs=0x$(echo "$stub_line" | awk '{print $4}')
osrel_offs=$((stub_size + stub_offs))
cmdline_offs=$((osrel_offs + $(stat -c%s $osrelease_path)))
splash_offs=$((cmdline_offs + $(stat -c%s "/etc/kernel/cmdline")))
linux_offs=$((splash_offs + $(stat -c%s "/usr/share/systemd/bootctl/splash-arch.bmp")))
initrd_offs=$((linux_offs + $(sudo stat -c%s "/boot/vmlinuz-linux")))
sudo objcopy \
--add-section .osrel=$osrelease_path \
--change-section-vma .osrel=$(printf 0x%x $osrel_offs) \
--add-section .cmdline="/etc/kernel/cmdline" \
--change-section-vma .cmdline=$(printf 0x%x $cmdline_offs) \
--add-section .splash="/usr/share/systemd/bootctl/splash-arch.bmp" \
--change-section-vma .splash=$(printf 0x%x $splash_offs) \
--add-section .linux=$vmlinuz_path \
--change-section-vma .linux=$(printf 0x%x $linux_offs) \
--add-section .initrd=$initrd_path \
--change-section-vma .initrd=$(printf 0x%x $initrd_offs) \
$efistub_path "/boot/efi/EFI/Linux/linux.efi"
Команды для создания резервного EFI-образа:
sudo mkdir -p /boot/efi/EFI/Linux
efistub_path=$(case $(uname -m) in "x86_64") echo "/usr/lib/systemd/boot/efi/linuxx64.efi.stub";; \
"i686") echo "/usr/lib/systemd/boot/efi/linuxia32.efi.stub";; \
"aarch64") echo "/usr/lib/systemd/boot/efi/linuxaa64.efi.stub";; \
*) echo "unsupported arch" $(uname -m);; esac;)
vmlinuz_path="/boot/vmlinuz-linux-lts"
initrd_path="/boot/initramfs-linux-lts-fallback.img"
osrelease_path=$(realpath /etc/os-release)
stub_line=$(objdump -h $efistub_path | tail -2 | head -1)
stub_size=0x$(echo "$stub_line" | awk '{print $3}')
stub_offs=0x$(echo "$stub_line" | awk '{print $4}')
osrel_offs=$((stub_size + stub_offs))
cmdline_offs=$((osrel_offs + $(stat -c%s $osrelease_path)))
splash_offs=$((cmdline_offs + $(stat -c%s "/etc/kernel/cmdline")))
linux_offs=$((splash_offs + $(stat -c%s "/usr/share/systemd/bootctl/splash-arch.bmp")))
initrd_offs=$((linux_offs + $(sudo stat -c%s "$vmlinuz_path")))
sudo objcopy \
--add-section .osrel=$osrelease_path \
--change-section-vma .osrel=$(printf 0x%x $osrel_offs) \
--add-section .cmdline="/etc/kernel/cmdline" \
--change-section-vma .cmdline=$(printf 0x%x $cmdline_offs) \
--add-section .splash="/usr/share/systemd/bootctl/splash-arch.bmp" \
--change-section-vma .splash=$(printf 0x%x $splash_offs) \
--add-section .linux=$vmlinuz_path \
--change-section-vma .linux=$(printf 0x%x $linux_offs) \
--add-section .initrd=$initrd_path \
--change-section-vma .initrd=$(printf 0x%x $initrd_offs) \
$efistub_path "/boot/efi/EFI/Linux/linux-fallback.efi"
3.3.3 В дистрибутиве Alt Linux
В дистрибутиве Alt Linux именование бинарных пакетов с Linux образами отличается от других дистрибутивов. Схема именования Linux ядер описывается в официальной Wiki и там же про их разновидности. В рамках данного руководства рассматривается создание EFI-образа для стандартного, основного (production) ядра std-def:
sudo mkdir -p /boot/efi/EFI/Linux
efistub_path=$(case $(uname -m) in "x86_64") echo "/usr/lib/systemd/boot/efi/linuxx64.efi.stub";; \
"i686") echo "/usr/lib/systemd/boot/efi/linuxia32.efi.stub";; \
"aarch64") echo "/usr/lib/systemd/boot/efi/linuxaa64.efi.stub";; \
*) echo "unsupported arch" $(uname -m);; esac;)
vmlinuz_path=$(sudo realpath /boot/vmlinuz-std-def)
initrd_path=$(sudo realpath /boot/initrd-std-def.img)
osrelease_path=$(realpath /etc/os-release)
stub_line=$(objdump -h $efistub_path | tail -2 | head -1)
stub_size=0x$(echo "$stub_line" | awk '{print $3}')
stub_offs=0x$(echo "$stub_line" | awk '{print $4}')
osrel_offs=$((stub_size + stub_offs))
cmdline_offs=$((osrel_offs + $(stat -c%s $osrelease_path)))
linux_offs=$((cmdline_offs + $(stat -c%s "/etc/kernel/cmdline")))
initrd_offs=$((linux_offs + $(sudo stat -c%s "$vmlinuz_path")))
sudo objcopy \
--add-section .osrel=$osrelease_path \
--change-section-vma .osrel=$(printf 0x%x $osrel_offs) \
--add-section .cmdline="/etc/kernel/cmdline" \
--change-section-vma .cmdline=$(printf 0x%x $cmdline_offs) \
--add-section .linux=$vmlinuz_path \
--change-section-vma .linux=$(printf 0x%x $linux_offs) \
--add-section .initrd=$initrd_path \
--change-section-vma .initrd=$(printf 0x%x $initrd_offs) \
$efistub_path "/boot/efi/EFI/Linux/linux.efi"
3.4 Подпись созданного Unified Kernel Image
На данном этапе полученный EFI-образ подписывается ранее созданным Image Signing сертификатом (ISK):
sudo sbsign --key ISK.key --cert ISK.pem /boot/efi/EFI/Linux/linux.efi --output /boot/efi/EFI/Linux/linux.signed.efi
В случае если был создан резервный загрузочный образ, то подписывается и он тоже командой:
sudo sbsign --key ISK.key --cert ISK.pem /boot/efi/EFI/Linux/linux-fallback.efi --output /boot/efi/EFI/Linux/linux-fallback.signed.efi
3.5 Автоматизация генерации Unified Kernel Image
Для дистрибутивов, производных от Debian, автоматическое создание Unified Kernel Image реализуется посредством post-update.d hook'а пакета initramfs-tools.
Для этого создается папка /etc/initramfs/post-update.d
командой:
sudo mkdir -p /etc/initramfs/post-update.d
Затем создается скрипт, который будет выполнять генерацию EFI-образа командой:
sudo bash -c "cat > /etc/initramfs/post-update.d/create_unified_kernel" << EOL
#!/bin/sh
mkdir -p /boot/efi/EFI/Linux
efistub_path=\$(case \$(uname -m) in "x86_64") echo "/usr/lib/systemd/boot/efi/linuxx64.efi.stub";; \\
"i686") echo "/usr/lib/systemd/boot/efi/linuxia32.efi.stub";; \\
"aarch64") echo "/usr/lib/systemd/boot/efi/linuxaa64.efi.stub";; \\
*) echo "unsupported arch" \$(uname -m);; esac;)
vmlinuz_path=\$(realpath /vmlinuz)
initrd_path=\$(realpath /initrd.img)
osrelease_path=\$(realpath /etc/os-release)
stub_line=\$(objdump -h \$efistub_path | tail -2 | head -1)
stub_size=0x\$(echo "\$stub_line" | awk '{print \$3}')
stub_offs=0x\$(echo "\$stub_line" | awk '{print \$4}')
osrel_offs=\$((stub_size + stub_offs))
cmdline_offs=\$((osrel_offs + \$(stat -c%s "\$osrelease_path")))
linux_offs=\$((cmdline_offs + \$(stat -c%s "/etc/kernel/cmdline")))
initrd_offs=\$((linux_offs + \$(stat -c%s "\$vmlinuz_path")))
objcopy \\
--add-section .osrel=\$osrelease_path \\
--change-section-vma .osrel=\$(printf 0x%x \$osrel_offs) \\
--add-section .cmdline="/etc/kernel/cmdline" \\
--change-section-vma .cmdline=\$(printf 0x%x \$cmdline_offs) \\
--add-section .linux=\$vmlinuz_path \\
--change-section-vma .linux=\$(printf 0x%x \$linux_offs) \\
--add-section .initrd=\$initrd_path \\
--change-section-vma .initrd=\$(printf 0x%x \$initrd_offs) \\
\$efistub_path "/boot/efi/EFI/Linux/linux.efi"
EOL
Если вы планируете загружать резервный EFI-образ, то скрипт необходимо расширить, и команда будет выглядеть следующим образом:
sudo bash -c "cat > /etc/initramfs/post-update.d/create_unified_kernel" << EOL
#!/bin/sh
mkdir -p /boot/efi/EFI/Linux
efistub_path=\$(case \$(uname -m) in "x86_64") echo "/usr/lib/systemd/boot/efi/linuxx64.efi.stub";; \\
"i686") echo "/usr/lib/systemd/boot/efi/linuxia32.efi.stub";; \\
"aarch64") echo "/usr/lib/systemd/boot/efi/linuxaa64.efi.stub";; \\
*) echo "unsupported arch" \$(uname -m);; esac;)
vmlinuz_path=\$(realpath /vmlinuz)
initrd_path=\$(realpath /initrd.img)
osrelease_path=\$(realpath /etc/os-release)
stub_line=\$(objdump -h \$efistub_path | tail -2 | head -1)
stub_size=0x\$(echo "\$stub_line" | awk '{print \$3}')
stub_offs=0x\$(echo "\$stub_line" | awk '{print \$4}')
osrel_offs=\$((stub_size + stub_offs))
cmdline_offs=\$((osrel_offs + \$(stat -c%s "\$osrelease_path")))
linux_offs=\$((cmdline_offs + \$(stat -c%s "/etc/kernel/cmdline")))
initrd_offs=\$((linux_offs + \$(stat -c%s "\$vmlinuz_path")))
objcopy \\
--add-section .osrel=\$osrelease_path \\
--change-section-vma .osrel=\$(printf 0x%x \$osrel_offs) \\
--add-section .cmdline="/etc/kernel/cmdline" \\
--change-section-vma .cmdline=\$(printf 0x%x \$cmdline_offs) \\
--add-section .linux=\$vmlinuz_path \\
--change-section-vma .linux=\$(printf 0x%x \$linux_offs) \\
--add-section .initrd=\$initrd_path \\
--change-section-vma .initrd=\$(printf 0x%x \$initrd_offs) \\
\$efistub_path "/boot/efi/EFI/Linux/linux.efi"
vmlinuz_path=\$(realpath /vmlinuz.old)
initrd_path=\$(realpath /initrd.img.old)
initrd_offs=\$((linux_offs + \$(stat -c%s "\$vmlinuz_path")))
objcopy \\
--add-section .osrel=\$osrelease_path \\
--change-section-vma .osrel=\$(printf 0x%x \$osrel_offs) \\
--add-section .cmdline="/etc/kernel/cmdline" \\
--change-section-vma .cmdline=\$(printf 0x%x \$cmdline_offs) \\
--add-section .linux=\$vmlinuz_path \\
--change-section-vma .linux=\$(printf 0x%x \$linux_offs) \\
--add-section .initrd=\$initrd_path \\
--change-section-vma .initrd=\$(printf 0x%x \$initrd_offs) \\
\$efistub_path "/boot/efi/EFI/Linux/linux-fallback.efi"
EOL
После этого необходимо дать скрипту права на исполнение:
sudo chmod a+x /etc/initramfs/post-update.d/create_unified_kernel
Для проверки корректной работы скрипта можно инициировать обновление initramfs командой:
sudo update-initramfs -u -k all
В результате должны создаться EFI-образы по путям /boot/efi/EFI/Linux/linux.efi
, /boot/efi/EFI/Linux/linux-fallback.efi
.
В дистрибутивах ОС, производных от Arch Linux, существует множество различных способов автоматизации создания Unified Kernel Image, описанных на официальной Wiki.
В данном руководстве рассматривается способ с использованием утилиты mkinitcpio. Подробно про mkinitcpio можно ознакомиться по ссылкам: mkinitcpio[en] | mkinitcpio[ru]
Положительной особенностью является то, что mkinitcpio умеет автоматически склеивать файлы обновления микрокода с initrfamfs с помощью конфигурационного префикса *_microcode (прим. ALL_microcode,
Убедитесь, что папка по пути /boot/efi/EFI/Linux
присутствует. Если её нет, то создайте её командой:
sudo mkdir -p /boot/efi/EFI/Linux
Файлы-пресеты для Linux-ядер расположены по пути /etc/mkinitcpio.d/
. Пример файла-пресета для пакета linux (linux.preset):
# mkinitcpio preset file for the 'linux' package
ALL_config="/etc/mkinitcpio.conf"
ALL_kver="/boot/vmlinuz-linux"
ALL_microcode="/boot/*-ucode.img"
PRESETS=('default')
default_image="/boot/initramfs-linux.img"
default_efi_image="/boot/efi/EFI/Linux/linux.efi"
default_options="--splash /usr/share/systemd/bootctl/splash-arch.bmp"
Файл конфигурации mkinitcpio, который дополнительно генерирует резервный EFI-образ (fallback) linux-lts ядра:
# mkinitcpio preset file for the 'linux' package
ALL_config="/etc/mkinitcpio.conf"
ALL_microcode="/boot/*-ucode.img"
PRESETS=('default' 'fallback')
default_kver="/boot/vmlinuz-linux"
default_image="/boot/initramfs-linux.img"
default_efi_image="/boot/efi/EFI/Linux/linux.efi"
default_options="--splash /usr/share/systemd/bootctl/splash-arch.bmp"
fallback_kver="/boot/vmlinuz-linux-lts"
fallback_efi_image="/boot/efi/EFI/Linux/linux-fallback.efi"
fallback_image="/boot/initramfs-linux-lts-fallback.img"
fallback_options="-S autodetect --splash /usr/share/systemd/bootctl/splash-arch.bmp"
После того как вы создали файл-пресет, его можно проверить, запустив mkinitcpio командой:
sudo mkinitcpio -p linux
В результате должны создаться исполняемые файлы EFI по пути /boot/efi/EFI/Linux
.
Заметьте, что при автоматической генерации EFI-образов они не подписываются! Мы считаем, что приватный ключ сертификата подписи ISK.key защищен паролем, что не позволяет осуществить подпись исполняемых файлов в автоматическом режиме без ввода пароля. Поэтому после каждой генерации Unified Kernel Image, его необходимо подписать в ручном режиме, как описано в прошлом пункте.
Теперь нужно создать загрузочную запись Unified Kernel Image.
3.6 Добавление Unified Kernel Image в загрузку
Для этого создается загрузочная запись в EFI Boot Manager командой:
efibootmgr --disk /dev/sda --part 1 --create --label "Linux Signed Boot" --loader /EFI/Linux/linux.signed.efi
Здесь /dev/sda
- это диск с системным EFI разделом, а --part 1
указывает порядковый номер этого раздела.
Если ранее Вы создавали резервный EFI-образ, то сделайте дополнительно загрузочную запись и для него командой:
efibootmgr --disk /dev/sda --part 1 --create --label "Linux Fallback Signed Boot" --loader /EFI/Linux/linux-fallback.signed.efi
В случае, если вы указываете конкретную версию ядра Linux в названии образа Unified Kernel Image, то следует это также учесть. Пример команды для текущего ядра:
efibootmgr --disk /dev/sda --part 1 --create --label "Linux $(uname -r) Signed Boot" --loader /EFI/Linux/linux-$(uname -r).efi
Теперь нужно убедиться, что загрузочная запись присутствует в порядке загрузки, и следует указать её в качестве первой - для этого используется аргумент --bootorder:
sudo efibootmgr --bootorder XXXX,YYYY,ZZZZ
здесь XXXX,YYYY,ZZZZ - порядковые номера загрузочных записей.
Если все в порядке, то далее надо выполнить перезагрузку и убедиться в том, что созданный и подписанный образ успешно запускается, после чего предлагается выполнить подпись ядра, его модулей и включить LSM Kernel Lockdown.
4. Подпись ядра, модулей ядра, функция Kernel Lockdown
Данный пункт посвящен настройке элементов замкнутой программный среды (ЗПС) для их сопряжения с механизмом безопасной загрузки. Как правило в состав ЗПС входит:
- Контроль загрузки модулей ядра - проверка встроенной в модуль подписи при загрузке его в ядро ОС.
- Контроль исполняемых файлов (библиотек, скриптов и модулей ядра) - проверка подписи, наложенной в расширенных атрибутах файла (Linux IMA/EVM - Integrity Measurement Architecture and Extended Verification Module).
В качестве подготовительных действий следует убедиться в наличии включенных опций ядра, отвечающих за поддержку подписи модулей ядра и ее проверку, а именно:
CONFIG_MODULE_SIG
CONFIG_MODULE_SIG_HASH
CONFIG_INTEGRITY_SIGNATURE
Конфигурационный файл запущенного ядра может быть расположен по-разному в зависимости от дистрибутива, как правило его можно найти по путям:
/proc/config.gz
/boot/config
/boot/config-$(uname -r)
/usr/src/linux-headers-$(uname -r)/.config
Для того чтобы проверить, включена та или иная опция, Вы можете воспользоваться командой:
zgrep <Опция> <Путь до файла конфигурации>
В ядре Linux существует несколько хранилищ ключей (keyrings), с ними подробно можно ознакомиться в документации ядра Linux. Для использования ключей Secure Boot в Linux необходимо наличие связки .platform и загрузки публичных ключей из базы разрешенных ключей (db). За формирование этого хранилища ключей отвечают две опции ядра:
CONFIG_LOAD_UEFI_KEYS
CONFIG_INTEGRITY_PLATFORM_KEYRING
Помимо формирования хранилища ключей .platform, необходимо также наличие патча ядра, который добавляет проверку подписи модулей с использованием публичных сертификатов из него. К сожалению, данная модификация функции mod_verify_sig
не была принята в upstream ядра Linux, поэтому большинство дистрибутивов добавляют его в ядро системы самостоятельно (прим. Ubuntu 22.04, Ubuntu 20.04, Fedora 28 и т.д). Помимо этого патча, повлиять на загрузку операционной системы может также патч принудительно включающий режим Kernel Lockdown (см. п.п. 4.3) в UEFI Secure Boot. В случае, если модули ядра не подписаны или отсутствует возможность организации доверенного хранилища для их проверки, режим Kernel Lockdown не позволит загрузить операционную систему, ввиду того, что он включает принудительную проверку подписи ядерных модулей.
В таблице ниже представлен список протестированных дистрибутивов с пометкой соответветствующих особенностей, влияющих на функцию проверки модулей ядра, Kernel Lockdown и работу UEFI Secure Boot в целом:
Особенность/Дистрибутив | Ubuntu 22.04 "Jammy Jellyfish"; Ubuntu 20.04 "Focal Fossa" | Debian 11 "Bullseye", Debian 10 "Buster" (Linux 5.10+) | Fedora 28 - 38 | RHEL 8, RHEL 9 | Alt Linux WorkStation 10 | Arch Linux и дистрибутивы производные от него (BlackArch, Manjaro, Garuda, и т.д) | RED OS МУРОМ 7.3.1 | ROSA Fresh Desktop 12.2 | Astra Linux 1.7.2 |
---|---|---|---|---|---|---|---|---|---|
Патч mod_verify_sig для проверки подписи модулей ядра открытыми ключами из .platform | + | + | + | + | - | - | - | - | - |
Патч принудительно включающий Kernel Lockdown при активном UEFI Secure Boot (LOCK_DOWN_IN_SECURE_BOOT или LOCK_DOWN_IN_EFI_SECURE_BOOT) | + | + | + | + | - | - | - | - | + |
Модули ядра подписаны на этапе сборки встроенным в ядро CA сертификатом (прим. Build time autogenerated kernel key) | + | + | + | + | + | + | + | - | * |
Наличие модуля Kernel Lockdown (SECURITY_LOCKDOWN_LSM) | + | + | + | + | + | + | + | - | + |
Установена политика Kernel Lockdown отличная от NONE (LOCK_DOWN_KERNEL_FORCE_INTEGRITY или LOCK_DOWN_KERNEL_FORCE_CONFIDENTIALITY) | - | - | - | - | - | - | - | - | - |
здесь
- ____ - Kernel Lockdown и проверка модулей ядра работает с самоподписанными модулями; используются предзагрузочные открытые ключи из базы разрешенных ключей UEFI Secure Boot.
- ____ - Kernel Lockdown и штатная функция ядра по проверке подписи модулей не работает ввиду того, что используется собственная реализация режима замкнутой программной среды, основанная на не выгружаемом модуле ядра
digsig_verif
, который использует криптографические алгоритмы ГОСТ. Соответственно модули ядра, как и исполняемые ELF-файлы подписаны ГОСТовым CA сертификатом. Более подробно с настройкой режима замкнутой программной среды в Astra Linux можно ознакомиться в официальной странице Wiki дистрибутива. На текущий момент версия дистрибутива 1.7.2 не совместима с включенным режимом UEFI Secure Boot, ввиду наличия патча принудительно включающего Kernel Lockdown. Дело в том, что Kernel Lockdown форсирует проверку подписи модулей используя механизм ядра, что не совместимо с используемым модулемdigsig_verif
. - ____ - модули ядра подписаны CA сертификатом дистрибутива, вшитым в ядро на этапе компиляции; Kernel Lockdown и проверка модулей работает только с комплектными модулями ядра, проверка самоподписанных пользователем модулей не поддерживается.
- ____ - комплектные модули ядра не подписаны, проверка модулей ядра не функционирует, может отсутствовать Kernel Lockdown; возможность проверки самоподписанных модулей отсутствует; включение UEFI SecureBoot ломает процесс загрузки.
Таким образом, для того, чтобы использовать проверку самоподписанных модулей ядра публичным сертификатом из хранилища ключей .platform, необходимо наличие патча mod_verify_sig. Для дистрибутивов отмеченных оранжевым цветом в таблице выше, необходимо пропустить пункты руководства, описывающие процедуру подписи модулей ядра, а для отмеченных красным цветом включение проверки модулей невозможно в принципе.
В данном руководстве в качестве примера подпись модулей будет осуществляться ранее созданным ключом Image Signing Key, который защищен паролем. Тем не менее, вы можете создать свою новую ключевую пару без парольной фразы, например, kernel.pem с параметром утилиты openssl -nodes
, затем добавить его в базу разрешенных ключей db и подписывать им модули ядра в автоматическом режиме. Но в этом случае вы должны убедиться в том, что он надежно хранится. Ввиду того, что с помощью такого ключа можно подписывать не только модули ядра, но и исполняемые файлы, такие как загрузчики и приложения EFI.
4.1 Подпись ядра Linux
По умолчанию ядро подписывается CA-сертификатом дистрибутива ОС. Убедиться в этом можно, проверив, какие ключи присутствуют в связке ключей .builtin_trusted_keys, с помощью команды:
sudo keyctl list %:.builtin_trusted_keys
В операционной системе Debian 11 вы увидите следующие ключи:
Проверить, какими ключами подписано ядро, можно с помощью sbverify:
sudo sbverify --list /boot/vmlinuz-$(uname -r)
В этом случае ядро подписано ключом Debian, который и присутствует в связке ключей. Поскольку ядро подписано ключом Debian, а не какой-то третьей стороной вроде Canonical, то нет никакой необходимости также добавлять вашу подпись, Secure Boot заработает без ошибок и в такой конфигурации без нареканий.
В любом случае вы можете добавить в него и свою подпись командой:
sudo sbsign --key ISK.key --cert ISK.pem /boot/vmlinuz-$(uname -r) --output /boot/vmlinuz-$(uname -r)
В результате, ядро теперь подписано ключом дистрибутива ОС и вашим Image Signing Key:
Подпись ядра на этом завершена, следующим шагом является подпись модулей ядра.
4.2 Подпись модулей ядра Linux
В первую очередь нужно проверить, подписаны ли модули до вас. Например, в Debian они поставляются подписанные ключом Debian Secure Boot CA, а в Astra Linux модули поставляются не подписанными.
Для проверки нужно выбрать любой модуль, допустим из списка загруженных, посмотреть которые можно командой lsmod:
Рассмотрим на примере модуля vfat. Для этого выведем информацию о его подписанте командой:
sudo modinfo -F signer vfat
Как видно на рисунке выше, модуль подписан Debian Secure Boot CA. Модуль может быть также подписан автоматически сгенерированным ключом в процессе сборки ядра (Build time autogenerated kernel key). А вот в случае, если модуль не подписан, команда вернет пустой результат.
Подпись модуля ядра проверяется ключом из .builtin_trusted_keys и .platform.
Стоит отметить, что подписывать модули ядра надо алгоритмом, который используется текущим ядром. Для этого нужно проверить конфиг ядра, например, с помощью команды:
cat /boot/config-5.10.0-17-amd64 | grep CONFIG_MODULE_SIG
Соответственно, если указан CONFIG_MODULE_SIG_SHA256=y
, то для подписи необходимо указывать алгоритм sha256 в качестве аргумента утилиты signfile, т.е. соответствие опций с аргументами следующее:
Опция ядра | Аргумент signfile |
---|---|
CONFIG_MODULE_SIG_SHA256 | sha256 |
CONFIG_MODULE_SIG_SHA224 | sha224 |
CONFIG_MODULE_SIG_SHA384 | sha384 |
CONFIG_MODULE_SIG_SHA512 | sha512 |
Предварительно, стоит удалить старые подписи у всех модулей ядра командой:
sudo find /lib/modules/$(uname -r)/ -name .ko -exec strip --strip-debug '{}' \;
После чего следует убедиться, что подпись удалена, на примере любого модуля, например, vfat:
sudo modinfo -F signer vfat
Затем, для того чтобы подписать все модули ядра разом, воспользуйтесь командами:
export SIG_ALGO="sha256"
export KEYDIR="/root/sb_keys"
read -s KBUILD_SIGN_PIN
export KBUILD_SIGN_PIN
sudo --preserve-env=KBUILD_SIGN_PIN find /usr/lib/modules/$(uname -r)/ -name .ko \
-exec /usr/src/linux-headers-$(uname -r)/scripts/sign-file ${SIG_ALGO} ${KEYDIR}/ISK.key ${KEYDIR}/ISK.pem '{}' \;
unset KBUILD_SIGN_PIN
здесь
SIG_ALGO
задает хэш-функцию алгоритма подписи, которую ранее вы определили в соответствии с таблицей;KEYDIR
указывает путь до папки с вашим ключом и сертификатом подписи ISK;KBUILD_SIGN_PIN
- в этой переменной указывается ваша парольная фраза от ISK.key.
Повторная проверка подписи модуля должна вернуть вам ваш CN ключа ISK:
sudo modinfo -F signer vfat
После этого надо перегенерировать initramfs:
Debian-производные дистрибутивы (раскрыть)
sudo update-initramfs -u -k $(uname -r)
Дистрибутивы основанные на Arch Linux (раскрыть)
sudo mkinitcpio -p linux
sudo make-initrd --kernel=$(uname -r)
В результате выполненных действий были подписаны модули текущего ядра и добавлены в initrafms. Теперь нужно задать политику проверки модулей с помощью аргумента ядра module.sig_enforce
. Этот аргумент принимает значения:
- 0, что означает разрешающую политику в отношении модулей ядра; т.е. загрузка неподписанных модулей разрешена, но ядро и соответствующие непрошедшие проверку модули (дополнительно промаркируются буквой "E") будет помечены как испорченные;
- 1, включает ограничительную политику, которая запрещает загрузку неподписанных модулей.
Независимо от настройки здесь, если модуль имеет блок подписи, который не может быть проанализирован, он будет сразу же отклонен, если опция module.sig_enforce
инициализирована.
В данном руководстве предлагается использовать режим с ограничительной политикой, что соответветствует максимальному уровню защиты, т.е. необходимо добавить module.sig_enforce=1
в аргументы ядра, которые ранее были созданы по пути /etc/kernel/cmdline
, и пересоздать Unified Kernel Image.
sudo bash -c "echo -n ' module.sig_enforce=1' >> /etc/kernel/cmdline"
Не забудьте создать новый Unified Kernel Image и подписать вашим сертификатом ISK!
Внимание! Если вы подписали модули ядра вашим сертификатом Secure Boot, то не выполняйте на данном этапе перезагрузку, потому что ваш Unified Kernel Image не запустится, т.к. созданные вами ключи еще не установлены, и UEFI
Secure Boot не переведен в User mode, а значит при загрузке связка ключей .platform будет пуста.
Вообще говоря, module.sig_enforce включается по умолчанию в режиме Kernel Lockdown; про него будет рассказано далее.
4.3 Kernel Lockdown
Kernel Lockdown предназначен для лучшего отделения кода ядра от пользовательских процессов. Она позволяет защитить ядро от суперпользователя, лишая его права изменять код ядра, мешая ему, например, установить руткит. Таким образом, если этот пользователь будет взломан или иным образом скомпрометирован, активация данного режима существенно затруднит для злоумышленника вмешательство в другие части системы.
Kernel Lockdown может быть сконфигурирован на этапе сборки ядра, для конфигурации доступны следующие конфигурационные опции:
CONFIG_SECURITY_LOCKDOWN_LSM
- включает поддержку модуля Lockdown;CONFIG_SECURITY_LOCKDOWN_LSM_EARLY
- запускает его раньше всех модулей безопасности;CONFIG_LOCK_DOWN_KERNEL_FORCE_NONE
- lockdown выключен по умолчанию, но может быть включен через аргумент ядраlockdown=<политика>
;CONFIG_LOCK_DOWN_KERNEL_FORCE_INTEGRITY
- lockdown включен по умолчанию с политикой integrity;CONFIG_LOCK_DOWN_KERNEL_FORCE_CONFIDENTIALITY
- lockdown включен по умолчанию с политикой confidentiality;CONFIG_LOCK_DOWN_IN_SECURE_BOOT
- включает lockdown автоматически при включенном UEFI Secure Boot.
Почти во всех дистрибутивах CONFIG_LOCK_DOWN_IN_SECURE_BOOT включен, а это значит, что lockdown будет включен автоматически после включения SecureBoot. Обязательно обратите на этот момент внимание! Ведь Lockdown форсирует проверку модулей ядра, и, если они не подписаны, как, например, в дистрибутиве Astra Linux, то оперционная система не загрузится после включения Secure Boot!
Тем не менее, рекомендуется однозначно указать режим Lockdown через аргумент ядра lockdown=
. Доступно два режима:
- integrity: считается наиболее подходящим для большинства случаев;
- confidentiality: лучше всего подходит для систем, хранящих чувствительные данные, защищённые даже от рут-пользователя. Режим конфиденциальности полностью запрещает администраторам доступ к памяти ядра.
Соответственно, для integrity:
sudo bash -c "echo -n ' lockdown=integrity' >> /etc/kernel/cmdline"
А для включения политики confidentiality:
sudo bash -c "echo -n ' lockdown=confidentiality' >> /etc/kernel/cmdline"
После чего нужно пересоздать Unified Kernel Image. Больше информации про Lockdown доступно в записке его автора Мэттью Гаррета.
Теперь необходимо перейти к конфигурации Secure Boot в Setup Utility и добавлению созданных ранее ключей.
5. Конфигурация UEFI Secure Boot и установка ключей
Конфигурация в Setup Utility сводится к тому, что необходимо перевести Secure Boot в режим настройки (Setup Mode), а хранилище ключей - в Custom, для того, чтобы иметь возможность установить свои сертификаты вместо тех, что поставляются производителем материнской платы. Переход в Setup Mode осуществляется удалением ключа PK, но не все производители встроенного программного обеспечения позволяют это сделать прозрачно.
Требуемые опции для включения Secure Boot в режиме настройки варьируются от прошивки к прошивке. Рекомендуется ознакомиться с документацией производителя материнской платы о возможностях по конфигурации UEFI. Достаточно подробно об этом рассказал в своей статье Укрощаем UEFI SecureBoot Николай Шлей (@coderush).
После того как вы успешно перевели платформу в Setup Mode и включили SecureBoot, настоятельно рекомендуется поставить пароль на прошивку (Supervisor Password) для предотвращения изменений настроек прошивки, в частности, отключения SecureBoot и/или подмены его ключей.
Конфигурация Secure Boot в виртуальных средах требует дополнительных действий, что будет описано в следующем пункте.
5.1 Настройка UEFI Secure Boot в средах виртуализации
5.1.1 VMWare Fusion / VMWare Workstation
Для конфигурации Secure Boot виртуальной машины необходимо добавить следующие строки в файл vmx:
uefi.secureBoot.enabled = "TRUE"
uefi.allowAuthBypass = "TRUE"
bios.forceSetupOnce = "TRUE"
При последующей загрузке виртуальная машина загрузится в окно прошивки из которого нужно зайти в Setup Utility и перевести платформу в Setup Mode, как показано на схеме ниже.
После этого обязательно удалите ранее добавленную строку конфигурации uefi.allowAuthBypass = "TRUE"
из файла vmx, предварительно завершив работу виртуальной машины. Теперь можно приступить к установке ключей с помощью KeyTool.
5.1.2 QEMU + OVMF
Для использования UEFI Secure Boot в средстве виртуализации QEMU, требуется проект Open Virtual Machine Firmware (OVMF), который представляет из себя сборку EDK II для виртуальной машины архитектуры x86. Прошивка OVMF реализует полную поддержку стандарта UEFI, включая механизм Secure Boot, позволяя использовать в виртуальной машине UEFI вместо традиционного BIOS (SeaBIOS).
Всё, что требуется в этой связке, - это скомпилировать образ OVMF с параметром -D SECURE_BOOT_ENABLE
, который включает поддержку UEFI Secure Boot, как показано ниже:
git clone https://github.com/acidanthera/audk.git
cd audk
git submodule update --init --recursive
. edksetup.sh
make -C BaseTools
build -a X64 -t GCC5 -b RELEASE -p OvmfPkg/OvmfPkgX64.dsc -D SECURE_BOOT_ENABLE
Артефакты сборки находятся в директории Build
. Файл прошивки OVMF расположен по пути Build/OvmfX64/RELEASE_GCC5/FV/OVMF.fd
.
Пример запуска виртуальной машины с OVMF:
qemu-system-x86_64 -m 2048 -drive if=pflash,format=raw,unit=0,file=Build/OvmfX64/RELEASE_GCC5/FV/OVMF.fd
По умолчанию OVMF находится в режиме Setup Mode, дополнительных настроек в Setup Utility не требуется, поэтому можно переходить сразу установке ключей с помощью KeyTool.
5.1.3 VirtualBox
Механизм UEFI SecureBoot и поддержка TPM доступна только в версии VirtualBox 7.0 и выше. Поэтому, если у вас виртуальная машина с включенным EFI, то можете сразу переходить к установке сертификатов с помощью утилиты KeyTool.
5.2 Установка сертификатов с помощью KeyTool
Выполняется загрузка с ранее подготовленного флеш-накопителя, содержащего утилиту KeyTool и ключи Secure Boot. Из главного меню KeyTool выбирается Edit Keys, затем открывается меню со списком ключей (PK, KEK, db, dbx, dbt, MOK), в котором по очереди с каждым ключом выполняется операция "Replace's Key(s)", как показано на схеме ниже.
В результате система должна перейти в User Mode, и при следующей перезагрузке система будет проверять подпись исполняемых файлов загруженными ключами.
Проверить статус Secure Boot в Linux из операционной системы можно командой:
sudo bootctl status 2>&1 | grep -E 'Secure|Setup'
Заключение
В результате выполненных действий, вы включили безопасную загрузку UEFI с помощью пользовательских ключей, создали Unified Kernel Image и включили функцию Kernel Lock Down. Не забудьте установить пароль на прошивку. Любой, кто получит доступ к Setup Utility, может легко сбросить ваши пользовательские ключи и получить контроль над вашим компьютером.
Обратите внимание, что предложенное решение позволяет устранить только часть недостатков безопасной загрузки, поскольку многие из них возникают на уровне архитектуры прошивки и настройками дистрибутива ОС их не устранить. Подробно про архитектурные проблемы рассказывается в нашем докладе Безопасная загрузка ядра Linux в UEFI окружении: проблемы и перспективы на конференции OS DAY 2022./p>