Files
hacktricks-cloud/src/pentesting-cloud/confidential-computing/luks2-header-malleability-null-cipher-abuse.md

8.4 KiB
Raw Blame History

Malleabilità dell'header LUKS2 e abuso del Null-Cipher nelle Confidential VMs

{{#include ../../banners/hacktricks-training.md}}

TL;DR

  • Molte Confidential VMs (CVMs) basate su Linux che girano su AMD SEV-SNP o Intel TDX usano LUKS2 per lo storage persistente. L'header LUKS2 sul disco è malleabile e non è protetto per integrità contro attaccanti adiacenti allo storage.
  • Se la cifratura del segmento dati nell'header è impostata su un null cipher (es., "cipher_null-ecb"), cryptsetup lo accetta e il guest legge/scrive trasparentemente in chiaro pur credendo che il disco sia cifrato.
  • Fino a e incluso cryptsetup 2.8.0, i null cipher potevano essere usati per i keyslots; da 2.8.1 vengono rifiutati per keyslots con password non vuote, ma i null cipher restano ammessi per i segmenti del volume.
  • La remote attestation di solito misura il codice/config del VM, non header LUKS esterni e mutabili; senza validazione/measurements espliciti, un attaccante con accesso in scrittura al disco può forzare I/O in chiaro.

Contesto: formato on-disk di LUKS2 (cosa importa per gli attaccanti)

  • Un device LUKS2 inizia con un header seguito dai dati cifrati.
  • L'header contiene due copie identiche di una sezione binaria e una sezione di metadata JSON, più uno o più keyslots.
  • I metadata JSON definiscono:
    • i keyslot abilitati e la loro wrapping KDF/cipher
    • i segmenti che descrivono l'area dati (cipher/mode)
    • digest (es., hash della chiave del volume per verificare le passphrase)
  • Valori tipicamente sicuri: keyslot KDF argon2id; cifratura del keyslot e dei segmenti dati aes-xts-plain64.

Ispeziona rapidamente il cipher del segmento direttamente dal 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'

Causa principale

  • Gli header LUKS2 non sono autenticati contro manomissioni dello storage. Un host/storage attacker può riscrivere i metadata JSON accettati da cryptsetup.
  • A partire da cryptsetup 2.8.0, sono accettati header che impostano la cifratura di un segmento su cipher_null-ecb. The null cipher ignores keys and returns plaintext.
  • Fino alla 2.8.0, null ciphers potevano anche essere usati per i keyslots (il keyslot si apre con qualsiasi passphrase). Dalla 2.8.1, null ciphers sono respinti per keyslots con password non vuote, ma rimangono consentiti per segments. Cambiare solo il segment cipher continua a produrre plaintext I/O anche dopo la 2.8.1.

Modello di minaccia: perché l'attestazione non ti ha salvato di default

  • I CVM mirano a garantire riservatezza, integrità e autenticità in un host non attendibile.
  • L'attestazione remota di solito misura l'immagine VM e la configurazione di lancio, non l'header LUKS mutabile che risiede su uno storage non attendibile.
  • Se il tuo CVM si fida di un on-disk header senza robusta validazione/misurazione, un storage attacker può alterarlo impostando una null cipher e il tuo guest monterà un volume in plaintext senza errori.

Sfruttamento (accesso in scrittura allo storage richiesto)

Prerequisiti:

  • Accesso in scrittura al block device LUKS2-encrypted del CVM.
  • Il guest usa l'header LUKS2 on-disk senza robusta validazione/attestazione.

Passaggi (ad alto livello):

  1. Read the header JSON and identify the data segment definition. Example target field: segments["0"].encryption.
  2. Set the data segment encryption to a null cipher, e.g., cipher_null-ecb. Keep keyslot parameters and digest structure intact so the guests usual passphrase still “works.”
  3. Update both header copies and associated header digests so the header is self-consistent.
  4. On next boot, the guest runs cryptsetup, successfully unlocks the existing keyslot with its passphrase, and mounts the volume. Because the segment cipher is a null cipher, all reads/writes are plaintext.

Variant (pre-2.8.1 keyslot abuse): if a keyslots area.encryption is a null cipher, it opens with any passphrase. Combine with a null segment cipher for seamless plaintext access without knowing the guest secret.

Mitigazioni robuste (evitare TOCTOU con detached headers)

Always treat on-disk LUKS headers as untrusted input. Use detached-header mode so validation and opening use the same trusted bytes from protected RAM:

# 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

Applicare quindi una (o più) delle seguenti misure:

  1. MAC the full header
  • Calcolare/verificare un MAC sull'intero header prima dell'uso.
  • Aprire il volume solo se il MAC è valido.
  • Esempi reali: Flashbots tdx-init e Fortanix Salmiac hanno adottato la verifica basata su MAC.
  1. Strict JSON validation (backward compatible)
  • Dump dei metadata JSON e validare una allowlist rigorosa di parametri (KDF, ciphers, numero/tipo di segmenti, 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
Esempio di validator (imporre campi sicuri) ```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. Misurare/attestare l'header
  • Rimuovere random salts/digests e misurare l'header sanificato nei PCR di TPM/TDX/SEV o nello stato della policy KMS.
  • Rilasciare le chiavi di decrittazione solo quando l'header misurato corrisponde a un profilo approvato e sicuro.

Indicazioni operative:

  • Applicare detached header + MAC o una validazione rigorosa; non fidarsi mai direttamente degli on-disk headers.
  • I consumatori dell'attestazione dovrebbero negare le versioni del framework pre-patch nelle allow-list.

Note su versioni e posizione dei manutentori

  • I manutentori di cryptsetup hanno chiarito che LUKS2 non è stato progettato per fornire integrità contro la manomissione dello storage in questo contesto; i null ciphers sono mantenuti per compatibilità con le versioni precedenti.
  • cryptsetup 2.8.1 (Oct 19, 2025) rifiuta i null ciphers per i keyslots con password non vuote ma consente ancora i null ciphers per i segments.

Controlli rapidi e triage

  • Verificare se la cifratura di qualche segment è impostata su un null cipher:
cryptsetup luksDump --type luks2 --dump-json-metadata /dev/VDISK \
| jq -r '.segments | to_entries[] | "segment=" + .key + ", enc=" + .value.encryption'
  • Verificare gli algoritmi del keyslot e dei segment prima di aprire il volume. Se non puoi MAC, applica una validazione JSON rigorosa e apri usando il detached header dalla memoria protetta.

Riferimenti

{{#include ../../banners/hacktricks-training.md}}