# LUKS2 Header Malleability and Null-Cipher Abuse in Confidential VMs

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

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

<details>
<summary>Example validator (enforce safe fields)</summary>

```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")
```

</details>

3) 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

- [Vulnerabilities in LUKS2 disk encryption for confidential VMs (Trail of Bits)](https://blog.trailofbits.com/2025/10/30/vulnerabilities-in-luks2-disk-encryption-for-confidential-vms/)
- [cryptsetup issue #954 (null cipher acceptance and integrity considerations)](https://gitlab.com/cryptsetup/cryptsetup/-/issues/954)
- [CVE-2025-59054](https://nvd.nist.gov/vuln/detail/CVE-2025-59054)
- [CVE-2025-58356](https://nvd.nist.gov/vuln/detail/CVE-2025-58356)
- [Related context: CVE-2021-4122 (auto-recovery path silently decrypting disks)](https://www.cve.org/CVERecord?id=CVE-2021-4122)

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