Unified kernel image

De Wiki doc


Une Image Noyau Unifiée (INU) ou Unified Kernel Image (UKI) est la combinaison d'un programme de démarrage UEFI, d'une image noyau Linux, d'un initrd ainsi que d'autres ressources optionnelles dans un seul fichier UEFI PE. Il peut alors être invoqué directement par :

  • le micrologiciel UEFI : utile notamment dans certains environnements d'informatique en nuage ou confidentiels
  • un chargeur de démarrage : généralement utile pour permettre plusieurs versions de noyau avec une sélection interactive ou automatique de la version dans laquelle démarrer (Systemd-boot ou Grub permettent cela)

L'amorçage de cette image unifiée est permise par le stub, programme logé dans celle-ci et pouvant être interprété par l'UEFI. Il constitue donc la partie exécutable initiale de l'image combinée et charge par la suite d'autres ressources à partir du reste de l'INU, en particulier le noyau et l'initrd.

La spécification officielle définit le format et les composants (obligatoires et optionnels) des UKI qui sont fournis en tant que sections PE/COFF de l'exécutable.

L'intérêt principal de cette approche est qu'elle permet de mieux sécuriser le démarrage d'un système en exposant un unique binaire (UEFI PE) en clair sur le disque d'amorçage (EFI System Partition - ESP) au lieu d'une multitude de fichiers pouvant être modifiés par le premier pirate venu. Cet exécutable pourra alors être signé par Secure Boot et permettre, entre-autre, le déchiffrement de la racine en s'assurant de l’absence de sa propre altération (enregistreur de frappes par exemple).

Image Noyau Unifiée

Installation des outils

La génération d'une INU nécessite quelques outils. Le projet Systemd met à disposition un programme Python nommé Ukify à partir de la version 253 de l'init. Il est logé à l'emplacement /usr/lib/systemd/ukify mais nécessite la bibliothèque python3-pefile et ses dépendances pour fonctionner. L'approche exposé dans ce document visant la frugalité (Ukify apporte surtout une configuration épurée mais l'aspect fonctionnel est identique), le strict nécessaire sera installé.

apt install --no-install-recommends systemd-boot-efi binutils gawk sbsigntool

Détails des paquets installés :

  • systemd-boot-efi : fournit le stub /usr/lib/systemd/boot/efi/linuxx64.efi.stub
  • binutils : fournit le programme de construction du binaire PE objdump
  • gawk : fournit la version GNU de l'interpréteur de langage AWK afin de ne pas avoir l'erreur de fonction non définie function strtonum never defined du awk de base
  • sbsigntool : permet de signer l'UKI en vu d'une validation Secure Boot (optionnel)

Script de génération

La génération d'une UKI nécessitant la concaténation d'une multitude d'éléments, il n'est pas envisageable de renseigner l'ensemble des paramètres manuellement. Je vous propose un script permettant la création d'une image ainsi que sa rotation sur trois versions différentes (l'une écrase la précédente à chaque nouvelle exécution).

vim /mnt/usr/local/sbin/gen-uki
#!/bin/bash

# Récupération de la version du noyau en cours d'utilisation
if [[ -n "${1}" ]]; then
	version_noyau="${1}"
else
	version_noyau="$(uname -r)"
fi

# Chemin vers l'initrd de travail
if [[ -n "${2}" ]]; then
	chemin_initrd="${2}"
else
	chemin_initrd="/boot/initrd.img-${version_noyau}"
fi

# Chemin vers le noyau de travail
if [[ -n "${2}" ]]; then
	chemin_noyau="/boot/vmlinuz-${1}"
else
	chemin_noyau="/boot/vmlinuz-${version_noyau}"
fi

# Définition des variables du script
chemin_lsb="/usr/lib/os-release"
chemin_stub="/usr/lib/systemd/boot/efi/linuxx64.efi.stub"
chemin_cmdline="/etc/kernel/cmdline"
chemin_splash="/dev/null"
nom_uki1="1-debian.efi"
nom_uki2="2-debian.efi"
nom_uki3="3-debian.efi"
chemin_base="/boot/efi/EFI/Linux/"
chemin_dst_uki="${chemin_base}/${nom_uki1}"
chemin_sb_clef="/usr/local/lib/secureboot/mok/MOK.priv"
chemin_sb_cert="/usr/local/lib/secureboot/mok/MOK.pem"

# Création de l'arborescence des UKI
mkdir -p "${chemin_base}"

# Préparation de l'UKI
align="$(objdump -p ${chemin_stub} | awk '{ if ($1 == "SectionAlignment"){print $2} }')"
align=$((16#$align))
osrel_offs="$(objdump -h "${chemin_stub}" | awk 'NF==7 {size=strtonum("0x"$3); offset=strtonum("0x"$4)} END {print size + offset}')"
osrel_offs=$((osrel_offs + "$align" - osrel_offs % "$align"))
cmdline_offs=$((osrel_offs + $(stat -Lc%s "${chemin_lsb}")))
cmdline_offs=$((cmdline_offs + "$align" - cmdline_offs % "$align"))
splash_offs=$((cmdline_offs + $(stat -Lc%s "${chemin_cmdline}")))
splash_offs=$((splash_offs + "$align" - splash_offs % "$align"))
initrd_offs=$((splash_offs + $(stat -Lc%s "${chemin_splash}")))
initrd_offs=$((initrd_offs + "$align" - initrd_offs % "$align"))
linux_offs=$((initrd_offs + $(stat -Lc%s "${chemin_initrd}")))
linux_offs=$((linux_offs + "$align" - linux_offs % "$align"))

# Roulement, si elle existe et n'est pas vide, de l'UKI 2 en 3
if [[ -s "${chemin_base}${nom_uki2}" ]]; then
	echo "Copie de l'UKI 2 en 3..."
	cp -v "${chemin_base}${nom_uki2}" "${chemin_base}${nom_uki3}"
fi

# Roulement, si elle existe et n'est pas vide, de l'UKI 1 en 2
if [[ -s "${chemin_dst_uki}" ]]; then
	echo "Copie de l'UKI 1 en 2..."
	cp -v "${chemin_dst_uki}" "${chemin_base}${nom_uki2}"
fi

echo "Génération de l'image noyau combinée (UKI) numéro 1..."
objcopy \
    --add-section .osrel="${chemin_lsb}" --change-section-vma .osrel=$(printf 0x%x $osrel_offs) \
    --add-section .cmdline="${chemin_cmdline}" --change-section-vma .cmdline=$(printf 0x%x $cmdline_offs) \
    --add-section .splash="${chemin_splash}" --change-section-vma .splash=$(printf 0x%x $splash_offs) \
    --add-section .initrd="${chemin_initrd}" --change-section-vma .initrd=$(printf 0x%x $initrd_offs) \
    --add-section .linux="${chemin_noyau}" --change-section-vma .linux=$(printf 0x%x $linux_offs) \
    "${chemin_stub}" "${chemin_dst_uki}"

# Signature de l'UKI pour la vérification Secure Boot seulement si la clé et son certificat son présents
if [[ -r "${chemin_sb_clef}" && -r "${chemin_sb_cert}" ]]; then
	sbsign --key "${chemin_sb_clef}" --cert "${chemin_sb_cert}" --output "${chemin_dst_uki}" "${chemin_dst_uki}"
	sbverify --cert "${chemin_sb_cert}" "${chemin_dst_uki}"
fi

Les paramètres du noyau (de la variable $chemin_cmdline) doivent contenir les informations sur votre système de fichier racine afin que Linux puisse l'amorcer. Vous pouvez vous appuyer sur l'exemple ci-dessous qui comprend une partition chiffrée contenant un sous-volume BTRFS.

echo "root=UUID=${uuid_racine} cryptdevice=UUID=${uuid_luks} ro rootflags=subvol=@rootfs console=tty0 console=ttyS0,115200n8" > /mnt/etc/kernel/cmdline

Pensez bien entendu à remplacer les variables par vos propres valeurs...

Vous pouvez également vous appuyer sur le /proc/cmdline pour visualiser les paramètres utilisés par votre propre système afin de vous en inspirer. La liste des paramètres possible est visualisable dans la documentation du noyau.

chmod u+x /usr/local/sbin/gen-uki

Automatisation

Le script ainsi créé peut être lancé manuellement mais il est préférable qu'il soit appelé à chaque mise à jour de noyau ou d'initrd afin de garantir un démarrage sur la dernière version disponible. Cette dépendance se créée en l'exposant dans les répertoire de post-construction de nos deux composants d'intérêts.

mkdir -p /etc/initramfs/post-update.d
ln -s /usr/local/sbin/gen-uki /etc/initramfs/post-update.d/zz-gen-uki
ln -s /usr/local/sbin/gen-uki /etc/kernel/postinst.d/zz-gen-uki
chmod u+x /etc/initramfs/post-update.d/zz-gen-uki /etc/kernel/postinst.d/zz-gen-uki

INFORMATION

Le répertoire /etc/initramfs/post-update.d n'est pas créé par défaut dans GNU/Linux Debian mais est bien parcouru lors de la génération d'une nouvelle archive d'init.

Création des entrées de démarrage

Dans l'optique d'optimiser au mieux le démarrage de notre machine, nous n'utiliserons aucun chargeur d'amorçage. Nous créerons alors des entrées de démarrage dans notre UEFI pointant directement sur nos UKI (son menu nous permettra de sélectionner la version à lancer). Pour se faire, il est possible de passer par l'interface de celui-ci (lorsque l'option est disponible) ou via l'outil Efibootmgr (universel). Voici un exemple pour les trois générées par le script (il est possible de créer les entrées avant les images) :

disque_systeme=/dev/nvme0n1

efibootmgr -c -d "${disque_systeme}" -p 1 -l '\EFI\Linux\1-debian.efi' -L "Debian UKI 1"
efibootmgr -c -d "${disque_systeme}" -p 1 -l '\EFI\Linux\2-debian.efi' -L "Debian UKI 2"
efibootmgr -c -d "${disque_systeme}" -p 1 -l '\EFI\Linux\3-debian.efi' -L "Debian UKI 3"

Ainsi, dans le cas malencontreux où une mise à jour de noyau ou une reconstruction d'initrd occasionne une impossibilité à démarrer, les versions précédentes de votre image vous permettront de vous en sortir.

Sources de la section

Secure Boot

INFORMATION

Cette section est facultative.

Secure Boot (chapitre 27 des spécification UEFI) est une fonctionnalité apparue avec la version 2.3.1 de l'UEFI permettant de borner l'amorçage aux seuls systèmes dont la signature cryptographique est reconnue par sa base interne. Son intérêt principal est d'empêcher l'exécution de code ayant été ajouté à l'UEFI PE à l'insu de l'administrateur de la machine mais il peut également servir, entre-autre, à empêcher tout autre système (amorçage USB par exemple) de démarrer.

Afin que cette fonction ai un sens, il est préférable de n'autoriser que vos propres clés. Ceci n'est malheureusement pas possible sur la plupart des PC du marcher. En effet, les UEFI des machines grand publique embarquent celles de Microsoft pour le démarrage de Windows (obligatoire pour bénéficier de la certification idoine) et elles ne proposent pas (dans toutes celles que j'ai vu) de fonction pour gérer la base interne de clés (elles ne proposent bien souvent même pas d'administrer les entrées de démarrage...). Il est peut-être possible de contourner cette limitation avec l'outil mokutil mais je n'ai pas creusé cette piste... La problématique des clés Microsoft est bien entendu que leur présence rend caduc l'exclusivité de la validité de vos signatures. Qui peut utiliser leur clé ? L'administration de ce paramètre semble donc réservée aux ordinateurs professionnels (les DELL XPS le permettent) et aux plateformes serveurs.

Il convient en outre de s'assurer qu'aucun accès physique non autorisé à la machine n'a lieu car les gros malins qui ont rédigés les spécifications de l'UEFI n'ont pas crus bon d'imposer le stockage de sa configuration en mémoire morte... L'utilisation d'une mémoire vive maintenue par une pile comme sur l'antique BIOS ayant retenu leur préférence, le débranchement de cette dernière engendrera une réinitialisation complète des paramètres et de la base de clés Secure Boot (permettant ainsi l'introduction de toute forme de charges utiles au démarrage). Ceci aura donc pour effet de ruiner la mince barrière de sécurité que nous venons de mettre en place...

Enfin, un mot de passe d'accès aux paramètres de l'UEFI reste tout de même une pratique à suivre afin de vous assurer qu'aucune injection de clé non autorisée n'ai lieu. Dans le cas d'une réinitialisation par débranchement de la pile, vous saurez au moins qu'une action physique a été opérée sur la machine et pourrez entreprendre les mesures qui s'imposent (à commencer par ne surtout pas taper votre phrase de passe LUKS !). Vous pourrez par la suite vérifier la signature de votre UKI via un autre système pour s'assurer de son intégrité (pensez également à vérifier la présence d'enregistreurs de frappes matériels).

Glossaire

La base de clés de Secure Boot comporte cinq sections :

  • MOK (Machine Owner Key) : base de données contenant les clés de l'utilisateur - inutile pour cette procédure
  • PK (Plateform Key) : base de données contenant les clés de la machine - utile pour cette procédure
  • DB (Data Base) : base de données des clés autorisées (liste blanche) - utile pour cette procédure
  • KEK (Key Exchange Keys) : aucune idée - inutile pour cette procédure
  • DBX : (Data Base <introuvable dans les specs mais probablement Exclusion>)  : base de données des clés interdites (liste noire) - inutile pour cette procédure

Génération de clés

Création du répertoire de travail

mkdir -p /usr/local/lib/secureboot/mok

Génération d'une clé privée RSA 4096 bits (le maximum possible avec Shim - que nous n'utilisons pas - est, aux dernières nouvelles (21/06/2022) 2048 bits) et d'un certificat public associé

openssl req -nodes -new -x509 -newkey rsa:4096 -keyout /usr/local/lib/secureboot/mok/MOK.priv -outform DER -out /usr/local/lib/secureboot/mok/MOK.der -days 36500 -subj "/CN=Mon Nom/"

Conversion du certificat en PEM afin d'être utilisable par sbsign

openssl x509 -inform der -in /usr/local/lib/secureboot/mok/MOK.der -out /usr/local/lib/secureboot/mok/MOK.pem

Empêcher la lecture de ces fichiers par un autre utilisateur que root

chmod 400 /usr/local/lib/secureboot/mok/{MOK.priv,MOK.der,MOK.pem}

Les étapes qui suivent sont réalisées automatiquement par le script créé plus haut et sont notées à titre indicatif.

Le certificat au format PEM est utilisé par les commandes du paquet sbsigntool alors que la DER est utilisée par les UEFI (pourquoi utiliser le même format hein ?). il doit être renseignée deux fois dans les menus de gestion des clés de votre carte mère :

  • une fois dans Plateform Key (PK)
  • l'autre dans DB (Authorized Signatures)

Le certificat DER doit être accessible à l'UEFI pour injection. Vous pouvez le mettre dans toute partition FAT comme votre ESP /boot/efi/EFI/ ou sur une clé USB afin de l'y sélectionner depuis le menu dédié (peut être supprimé à l'issue).

Signature de l'image

Dans la commande qui suit, la signature de l'image va réécrir par dessus l'originale (sa somme de contrôle va changer). Ce comportement est volontaire mais peut être changé en modifiant la valeur de --output

sbsign --key /usr/local/lib/secureboot/mok/MOK.priv --cert /usr/local/lib/secureboot/mok/MOK.pem --output /boot/efi/EFI/Linux/1-debian.efi /boot/efi/EFI/Linux/1-debian.efi

Vérification de la signature

sbverify --cert /usr/local/lib/secureboot/mok/MOK.pem /boot/efi/EFI/Linux/1-debian.efi

Une fois Secure Boot activé et votre système démarré, vous pouvez vérifier l'effectivité de cette fonctionnalité via la commande suivante du paquet mokutil

mokutil --sb-state

ASTUCE

La signature d'un module noyau complilé par vous même peut se faire via la commande suivante (exemple pour le pilote ixgbe) : /usr/lib/linux-kbuild-6.1/scripts/sign-file sha512 /usr/local/lib/secureboot/mok/MOK.{priv,der} /lib/modules/6.1.0-17-amd64/updates/drivers/net/ethernet/intel/ixgbe/ixgbe.ko /tmp/ixgbe.ko.

Sources de la section