LUKS2 Header Malleability and Null-Cipher Abuse in Confidential VMs

Reading time: 7 minutes

tip

Learn & practice AWS Hacking:HackTricks Training AWS Red Team Expert (ARTE)
Learn & practice GCP Hacking: HackTricks Training GCP Red Team Expert (GRTE)
Learn & practice Az Hacking: HackTricks Training Azure Red Team Expert (AzRTE)

Support HackTricks

TL;DR

  • Many Linux-based Confidential VMs (CVMs) running on AMD SEV-SNP or Intel TDX use LUKS2 for persistent storage. The on-disk LUKS2 header is malleable and not integrity-protected against storage-adjacent attackers.
  • If the header’s data segment encryption is set to a null cipher (e.g., "cipher_null-ecb"), cryptsetup accepts it and the guest transparently reads/writes plaintext while believing the disk is encrypted.
  • Prior to and including cryptsetup 2.8.0, null ciphers could be used for keyslots; since 2.8.1 they are rejected for keyslots with non-empty passwords, but null ciphers remain allowed for volume segments.
  • Remote attestation usually measures VM code/config, not mutable external LUKS headers; without explicit validation/measurement, an attacker with disk write access can force plaintext I/O.

Background: LUKS2 on-disk format (what matters for attackers)

  • A LUKS2 device starts with a header followed by encrypted data.
  • The header contains two identical copies of a binary section and a JSON metadata section, plus one or more keyslots.
  • JSON metadata defines:
    • keyslots enabled and their wrapping KDF/cipher
    • segments that describe the data area (cipher/mode)
    • digests (e.g., hash of the volume key to verify passphrases)
  • Typical secure values: keyslot KDF argon2id; keyslot and data segment encryption aes-xts-plain64.

Quickly inspect the segment cipher directly from JSON:

bash
# 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

  • LUKS2 headers are not authenticated against storage tampering. A host/storage attacker can rewrite the JSON metadata accepted by cryptsetup.
  • As of cryptsetup 2.8.0, headers that set a segment’s encryption to cipher_null-ecb are accepted. The null cipher ignores keys and returns plaintext.
  • Up to 2.8.0, null ciphers could also be used for keyslots (keyslot opens with any passphrase). Since 2.8.1, null ciphers are rejected for keyslots with non-empty passwords, but remain allowed for segments. Switching only the segment cipher still yields plaintext I/O post-2.8.1.

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

  • CVMs aim to ensure confidentiality, integrity, and authenticity in an untrusted host.
  • Remote attestation usually measures the VM image and launch configuration, not the mutable LUKS header living on untrusted storage.
  • If your CVM trusts an on-disk header without robust validation/measurement, a storage attacker can alter it to a null cipher and your guest will mount a plaintext volume without error.

Exploitation (storage write access required)

Preconditions:

  • Write access to the CVM’s LUKS2-encrypted block device.
  • The guest uses the on-disk LUKS2 header without robust validation/attestation.

Steps (high level):

  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 guest’s 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 keyslot’s 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.

Robust mitigations (avoid TOCTOU with 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:

bash
# 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

Then enforce one (or more) of:

  1. MAC the full header

    • Compute/verify a MAC over the entire header prior to use.
    • Only open the volume when the MAC verifies.
    • Examples in the wild: Flashbots tdx-init and Fortanix Salmiac adopted MAC-based verification.
  2. Strict JSON validation (backward compatible)

    • Dump JSON metadata and validate a strict allowlist of parameters (KDF, ciphers, segment count/type, flags).
bash
#!/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
Example validator (enforce safe fields)
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. Measure/attest the header
    • Remove random salts/digests and measure the sanitized header into TPM/TDX/SEV PCRs or KMS policy state.
    • Release decryption keys only when the measured header matches an approved, safe profile.

Operational guidance:

  • Enforce detached header + MAC or strict validation; never trust on-disk headers directly.
  • Consumers of attestation should deny pre-patch framework versions in allow-lists.

Notes on versions and maintainer position

  • cryptsetup maintainers clarified that LUKS2 was not designed to provide integrity against storage tampering in this setting; null ciphers are retained for backward compatibility.
  • cryptsetup 2.8.1 (Oct 19, 2025) rejects null ciphers for keyslots with non-empty passwords but still allows null ciphers for segments.

Quick checks and triage

  • Inspect whether any segment encryption is set to a null cipher:
bash
cryptsetup luksDump --type luks2 --dump-json-metadata /dev/VDISK \
  | jq -r '.segments | to_entries[] | "segment=" + .key + ", enc=" + .value.encryption'
  • Verify keyslot and segment algorithms before opening the volume. If you cannot MAC, enforce strict JSON validation and open using the detached header from protected memory.

References

tip

Learn & practice AWS Hacking:HackTricks Training AWS Red Team Expert (ARTE)
Learn & practice GCP Hacking: HackTricks Training GCP Red Team Expert (GRTE)
Learn & practice Az Hacking: HackTricks Training Azure Red Team Expert (AzRTE)

Support HackTricks