Malleabilité de l’en-tête LUKS2 et abus du Null-Cipher dans les Confidential VMs

Tip

Apprenez et pratiquez le hacking AWS :HackTricks Training AWS Red Team Expert (ARTE)
Apprenez et pratiquez le hacking GCP : HackTricks Training GCP Red Team Expert (GRTE) Apprenez et pratiquez le hacking Azure : HackTricks Training Azure Red Team Expert (AzRTE)

Soutenir HackTricks

TL;DR

  • De nombreuses Confidential VMs (CVMs) basées sur Linux et exécutées sur AMD SEV-SNP ou Intel TDX utilisent LUKS2 pour le stockage persistant. L’en-tête LUKS2 sur disque est malléable et n’est pas protégé en intégrité contre des attaquants disposant d’un accès en écriture adjacent au stockage.
  • Si le chiffrement du segment de données dans l’en-tête est réglé sur un null cipher (par ex. “cipher_null-ecb”), cryptsetup l’accepte et l’invité lit/écrit en clair de manière transparente tout en croyant que le disque est chiffré.
  • Avant et y compris cryptsetup 2.8.0, les null ciphers pouvaient être utilisés pour les keyslots ; depuis 2.8.1 ils sont rejetés pour les keyslots avec des mots de passe non vides, mais les null ciphers restent autorisés pour les segments de volume.
  • L’attestation à distance mesure généralement le code/la configuration de la VM, pas les en-têtes LUKS externes modifiables ; sans validation/mesure explicite, un attaquant avec accès en écriture au disque peut forcer des E/S en clair.

Contexte : format sur disque de LUKS2 (ce qui compte pour les attaquants)

  • Un périphérique LUKS2 commence par un en-tête suivi de données chiffrées.
  • L’en-tête contient deux copies identiques d’une section binaire et une section de métadonnées JSON, plus un ou plusieurs keyslots.
  • Les métadonnées JSON définissent :
  • les keyslots activés et leur KDF/cipher de wrapping
  • les segments qui décrivent la zone de données (cipher/mode)
  • des digests (par ex. le hash de la volume key pour vérifier les passphrases)
  • Valeurs sécurisées typiques : KDF de keyslot argon2id ; chiffrement du keyslot et du segment de données aes-xts-plain64.

Inspecter rapidement le cipher du segment directement depuis le JSON:

# Read JSON metadata and print the configured data segment cipher
cryptsetup luksDump --type luks2 --dump-json-metadata /dev/VDISK \
| jq -r '.segments["0"].encryption'

Root cause

  • Les en-têtes LUKS2 ne sont pas authentifiés contre la modification du stockage. Un attaquant côté hôte/stockage peut réécrire les métadonnées JSON acceptées par cryptsetup.
  • Depuis cryptsetup 2.8.0, les en-têtes qui définissent le chiffrement d’un segment sur cipher_null-ecb sont acceptés. Le null cipher ignore les clés et renvoie le texte en clair.
  • Jusqu’à la version 2.8.0, les null ciphers pouvaient aussi être utilisés pour les keyslots (le keyslot s’ouvre avec n’importe quelle passphrase). Depuis 2.8.1, les null ciphers sont rejetés pour les keyslots avec des mots de passe non vides, mais restent autorisés pour les segments. Modifier uniquement le cipher du segment donne toujours des E/S en clair après 2.8.1.

Threat model: why attestation didn’t save you by default

  • Les CVM visent à assurer la confidentialité, l’intégrité et l’authenticité dans un hôte non fiable.
  • L’attestation distante mesure habituellement l’image de la VM et la configuration de lancement, pas l’en-tête LUKS mutable qui réside sur un stockage non fiable.
  • Si votre CVM fait confiance à un en-tête sur disque sans validation/mesure robuste, un attaquant sur le stockage peut le modifier en null cipher et votre machine invitée montera un volume en clair sans erreur.

Exploitation (storage write access required)

Préconditions:

  • Accès en écriture au bloc device chiffré LUKS2 de la CVM.
  • La machine invitée utilise l’en-tête LUKS2 présent sur disque sans validation/attestation robuste.

Steps (high level):

  1. Lire le JSON de l’en-tête et identifier la définition du segment de données. Champ cible exemple : segments[“0”].encryption.
  2. Définir le chiffrement du segment de données sur un null cipher, p.ex. cipher_null-ecb. Conserver les paramètres du keyslot et la structure de digest intactes pour que la passphrase habituelle de la machine invitée continue de « fonctionner ».
  3. Mettre à jour les deux copies d’en-tête et les digests associés pour que l’en-tête soit cohérent en interne.
  4. Au prochain boot, la machine invitée exécute cryptsetup, déverrouille avec succès le keyslot existant avec sa passphrase et monte le volume. Comme le cipher du segment est un null cipher, toutes les lectures/écritures sont en clair.

Variant (pre-2.8.1 keyslot abuse): si area.encryption d’un keyslot est un null cipher, il s’ouvre avec n’importe quelle passphrase. Combinez avec un null segment cipher pour un accès en clair transparent sans connaître le secret de la machine invitée.

Robust mitigations (avoid TOCTOU with detached headers)

Traitez toujours les en-têtes LUKS sur disque comme des entrées non fiables. Utilisez le detached-header mode afin que la validation et l’ouverture utilisent les mêmes octets de confiance provenant de la RAM protégée:

# Copy header into protected memory (e.g., tmpfs) and open from there
cryptsetup luksHeaderBackup --header-backup-file /tmp/luks_header /dev/VDISK
cryptsetup open --type luks2 --header /tmp/luks_header /dev/VDISK --key-file=key.txt

Ensuite, appliquer une (ou plusieurs) des mesures suivantes :

  1. MAC the full header
  • Calculer/vérifier un MAC sur l’intégralité de l’en-tête avant utilisation.
  • N’ouvrir le volume que si le MAC est validé.
  • Exemples observés : Flashbots tdx-init et Fortanix Salmiac ont adopté une vérification basée sur MAC.
  1. Strict JSON validation (backward compatible)
  • Exporter les métadonnées JSON et valider une liste blanche stricte des paramètres (KDF, ciphers, segment count/type, flags).
#!/bin/bash
set -e
# Store header in confidential RAM fs
cryptsetup luksHeaderBackup --header-backup-file /tmp/luks_header $BLOCK_DEVICE
# Dump JSON metadata header to a file
cryptsetup luksDump --type luks2 --dump-json-metadata /tmp/luks_header > header.json
# Validate the header
python validate.py header.json
# Open the cryptfs using key.txt
cryptsetup open --type luks2 --header /tmp/luks_header $BLOCK_DEVICE --key-file=key.txt
Exemple de validateur (faire respecter des champs sûrs) ```python from json import load import sys with open(sys.argv[1], "r") as f: header = load(f) if len(header["keyslots"]) != 1: raise ValueError("Expected 1 keyslot") if header["keyslots"]["0"]["type"] != "luks2": raise ValueError("Expected luks2 keyslot") if header["keyslots"]["0"]["area"]["encryption"] != "aes-xts-plain64": raise ValueError("Expected aes-xts-plain64 encryption") if header["keyslots"]["0"]["kdf"]["type"] != "argon2id": raise ValueError("Expected argon2id kdf") if len(header["tokens"]) != 0: raise ValueError("Expected 0 tokens") if len(header["segments"]) != 1: raise ValueError("Expected 1 segment") if header["segments"]["0"]["type"] != "crypt": raise ValueError("Expected crypt segment") if header["segments"]["0"]["encryption"] != "aes-xts-plain64": raise ValueError("Expected aes-xts-plain64 encryption") if "flags" in header["segments"]["0"] and header["segments"]["0"]["flags"]: raise ValueError("Segment contains unexpected flags") ```
  1. Mesurer/attester l’en-tête
  • Supprimez les salts/digests aléatoires et mesurez l’en-tête assaini dans les PCR TPM/TDX/SEV ou dans l’état de politique KMS.
  • Ne libérez les clés de déchiffrement que lorsque l’en-tête mesuré correspond à un profil approuvé et sûr.

Directives opérationnelles :

  • Appliquez detached header + MAC ou une validation stricte ; ne faites jamais confiance aux en-têtes on-disk directement.
  • Les consommateurs d’attestation doivent refuser les versions de framework pré-correctif dans les allow-lists.

Notes sur les versions et la position du mainteneur

  • Les mainteneurs de cryptsetup ont précisé que LUKS2 n’a pas été conçu pour fournir l’intégrité contre la falsification du stockage dans ce contexte ; les null ciphers sont conservés pour la rétrocompatibilité.
  • cryptsetup 2.8.1 (19 oct. 2025) refuse les null ciphers pour les keyslots avec des mots de passe non vides mais autorise toujours les null ciphers pour les segments.

Vérifications rapides et triage

  • Vérifiez si le chiffrement d’un segment est configuré sur un null cipher :
cryptsetup luksDump --type luks2 --dump-json-metadata /dev/VDISK \
| jq -r '.segments | to_entries[] | "segment=" + .key + ", enc=" + .value.encryption'
  • Vérifiez les keyslot and segment algorithms avant d’ouvrir le volume. Si vous ne pouvez pas MAC, appliquez une validation JSON stricte et ouvrez en utilisant le detached header depuis la mémoire protégée.

Références

Tip

Apprenez et pratiquez le hacking AWS :HackTricks Training AWS Red Team Expert (ARTE)
Apprenez et pratiquez le hacking GCP : HackTricks Training GCP Red Team Expert (GRTE) Apprenez et pratiquez le hacking Azure : HackTricks Training Azure Red Team Expert (AzRTE)

Soutenir HackTricks