bitlk: implement validation of FVE metadata

This commit implements FVE metadata block validation based on:
* CRC-32 (to detect random corruption);
* AES-CCM-encrypted SHA-256 (to detect malicious manipulations).

The hash-based validation requires us to decrypt the VMK first, so
it's only performed when obtaining the volume key.

This allows us to detect corrupted/altered FVE metadata blocks and
pick the valid one (before this commit: the first FVE metadata block
is always selected).

Fixes: #953

tests: add BitLocker image with corrupted headers

The image contains 2 manually corrupted metadata blocks (out of 3),
the library should use the third one to correctly load the volume.

Signed-off-by: Maxim Suhanov <dfirblog@gmail.com>
This commit is contained in:
Maxim Suhanov
2025-08-04 13:07:35 +03:00
committed by Milan Broz
parent 9cfdd6ba06
commit 68d4749d8a
3 changed files with 205 additions and 14 deletions

View File

@@ -111,6 +111,7 @@ struct bitlk_superblock {
struct bitlk_fve_metadata {
/* FVE metadata block header */
uint8_t signature[8];
/* size of this block (in 16-byte units) */
uint16_t fve_size;
uint16_t fve_version;
uint16_t curr_state;
@@ -132,6 +133,32 @@ struct bitlk_fve_metadata {
uint64_t creation_time;
} __attribute__ ((packed));
struct bitlk_validation_hash {
uint16_t size;
uint16_t role;
uint16_t type;
uint16_t flags;
/* likely a hash type code, anything other than 0x2005 isn't supported */
uint16_t hash_type;
uint16_t unknown1;
/* SHA-256 */
uint8_t hash[32];
} __attribute__ ((packed));
struct bitlk_fve_metadata_validation {
/* FVE metadata validation block header */
uint16_t validation_size;
uint16_t validation_version;
uint32_t fve_crc32;
/* this is a single nested structure's header defined here for simplicity */
uint16_t nested_struct_size;
uint16_t nested_struct_role;
uint16_t nested_struct_type;
uint16_t nested_struct_flags;
/* datum containing a similar nested structure (encrypted using VMK) with hash (SHA256) */
uint8_t nested_struct_data[BITLK_VALIDATION_VMK_DATA_SIZE];
} __attribute__ ((packed));
struct bitlk_entry_header_block {
uint64_t offset;
uint64_t size;
@@ -361,6 +388,54 @@ static int parse_vmk_entry(struct crypt_device *cd, uint8_t *data, int start, in
return 0;
}
static bool check_fve_metadata(struct bitlk_fve_metadata *fve)
{
if (memcmp(fve->signature, BITLK_SIGNATURE, sizeof(fve->signature)) || le16_to_cpu(fve->fve_version) != 2 ||
(fve->fve_size << 4) > BITLK_FVE_METADATA_SIZE)
return false;
return true;
}
static bool check_fve_metadata_validation(struct bitlk_fve_metadata_validation *validation)
{
/* only check if there is room for CRC-32, the actual size must be larger */
if (le16_to_cpu(validation->validation_size) < 8 || le16_to_cpu(validation->validation_version > 2))
return false;
return true;
}
static bool parse_fve_metadata_validation(struct bitlk_metadata *params, struct bitlk_fve_metadata_validation *validation)
{
/* extra checks for a nested structure (MAC) and BITLK FVE metadata */
if (le16_to_cpu(validation->validation_size) < sizeof(struct bitlk_fve_metadata_validation))
return false;
if (le16_to_cpu(validation->nested_struct_size != BITLK_VALIDATION_VMK_HEADER_SIZE + BITLK_VALIDATION_VMK_DATA_SIZE) ||
le16_to_cpu(validation->nested_struct_role) != 0 ||
le16_to_cpu(validation->nested_struct_type) != 5)
return false;
/* nonce */
memcpy(params->validation->nonce,
validation->nested_struct_data,
BITLK_NONCE_SIZE);
/* MAC tag */
memcpy(params->validation->mac_tag,
validation->nested_struct_data + BITLK_NONCE_SIZE,
BITLK_VMK_MAC_TAG_SIZE);
/* AES-CCM encrypted datum with SHA256 hash */
memcpy(params->validation->enc_datum,
validation->nested_struct_data + BITLK_NONCE_SIZE + BITLK_VMK_MAC_TAG_SIZE,
BITLK_VALIDATION_VMK_DATA_SIZE - BITLK_NONCE_SIZE - BITLK_VMK_MAC_TAG_SIZE);
return true;
}
void BITLK_bitlk_fvek_free(struct bitlk_fvek *fvek)
{
if (!fvek)
@@ -391,6 +466,7 @@ void BITLK_bitlk_metadata_free(struct bitlk_metadata *metadata)
free(metadata->guid);
free(metadata->description);
free(metadata->validation);
BITLK_bitlk_vmk_free(metadata->vmks);
BITLK_bitlk_fvek_free(metadata->fvek);
}
@@ -402,20 +478,25 @@ int BITLK_read_sb(struct crypt_device *cd, struct bitlk_metadata *params)
struct bitlk_signature sig = {};
struct bitlk_superblock sb = {};
struct bitlk_fve_metadata fve = {};
struct bitlk_fve_metadata_validation validation = {};
struct bitlk_entry_vmk entry_vmk = {};
uint8_t *fve_entries = NULL;
uint8_t *fve_validated_block = NULL;
size_t fve_entries_size = 0;
uint32_t fve_metadata_size = 0;
uint32_t fve_size_real = 0;
int fve_offset = 0;
char guid_buf[UUID_STR_LEN] = {0};
uint16_t entry_size = 0;
uint16_t entry_type = 0;
int i = 0;
int r = 0;
int valid_fve_metadata_idx = -1;
int start = 0;
size_t key_size = 0;
const char *key = NULL;
char *description = NULL;
struct crypt_hash *hash;
struct bitlk_vmk *vmk = NULL;
struct bitlk_vmk *vmk_p = params->vmks;
@@ -490,15 +571,80 @@ int BITLK_read_sb(struct crypt_device *cd, struct bitlk_metadata *params)
for (i = 0; i < 3; i++)
params->metadata_offset[i] = le64_to_cpu(sb.fve_offset[i]);
log_dbg(cd, "Reading BITLK FVE metadata of size %zu on device %s, offset %" PRIu64 ".",
sizeof(fve), device_path(device), params->metadata_offset[0]);
fve_validated_block = malloc(BITLK_FVE_METADATA_SIZE);
if (fve_validated_block == NULL) {
r = -ENOMEM;
goto out;
}
/* read FVE metadata from the first metadata area */
if (read_lseek_blockwise(devfd, device_block_size(cd, device),
device_alignment(device), &fve, sizeof(fve), params->metadata_offset[0]) != sizeof(fve) ||
memcmp(fve.signature, BITLK_SIGNATURE, sizeof(fve.signature)) ||
le16_to_cpu(fve.fve_version) != 2) {
log_err(cd, _("Failed to read BITLK FVE metadata from %s."), device_path(device));
for (i = 0; i < 3; i++) {
/* iterate over FVE metadata copies and pick the valid one */
log_dbg(cd, "Reading BITLK FVE metadata copy #%d of size %zu on device %s, offset %" PRIu64 ".",
i, sizeof(fve), device_path(device), params->metadata_offset[i]);
if (read_lseek_blockwise(devfd, device_block_size(cd, device),
device_alignment(device), &fve, sizeof(fve), params->metadata_offset[i]) != sizeof(fve) ||
!check_fve_metadata(&fve) ||
(fve_size_real = le16_to_cpu(fve.fve_size) << 4, read_lseek_blockwise(devfd, device_block_size(cd, device),
device_alignment(device), &validation, sizeof(validation), params->metadata_offset[i] + fve_size_real) != sizeof(validation)) ||
!check_fve_metadata_validation(&validation) ||
/* double-fetch is here, but we aren't validating MAC */
read_lseek_blockwise(devfd, device_block_size(cd, device), device_alignment(device), fve_validated_block, fve_size_real,
params->metadata_offset[i]) != fve_size_real ||
(crypt_crc32(~0, fve_validated_block, fve_size_real) ^ ~0) != le32_to_cpu(validation.fve_crc32)) {
/* found an invalid FVE metadata copy, log and skip */
log_dbg(cd, _("Failed to read or validate BITLK FVE metadata copy #%d from %s."), i, device_path(device));
} else {
/* found a valid FVE metadata copy, use it */
valid_fve_metadata_idx = i;
break;
}
}
if (valid_fve_metadata_idx < 0) {
/* all FVE metadata copies are invalid, fail */
log_err(cd, _("Failed to read and validate BITLK FVE metadata from %s."), device_path(device));
r = -EINVAL;
goto out;
}
/* check that a valid FVE metadata block is in its expected location */
if (params->metadata_offset[valid_fve_metadata_idx] != le64_to_cpu(fve.fve_offset[valid_fve_metadata_idx])) {
log_err(cd, _("Failed to validate the location of BITLK FVE metadata from %s."), device_path(device));
r = -EINVAL;
goto out;
}
/* update offsets from a valid FVE metadata copy */
for (i = 0; i < 3; i++)
params->metadata_offset[i] = le64_to_cpu(fve.fve_offset[i]);
/* check that the FVE metadata hasn't changed between reads, because we are preparing for the MAC check */
if (memcmp(&fve, fve_validated_block, sizeof(fve)) != 0) {
log_err(cd, _("BITLK FVE metadata changed between reads from %s."), device_path(device));
r = -EINVAL;
goto out;
}
crypt_backend_memzero(&params->sha256_fve, 32);
if (crypt_hash_init(&hash, "sha256")) {
log_err(cd, _("Failed to hash BITLK FVE metadata read from %s."), device_path(device));
r = -EINVAL;
goto out;
}
crypt_hash_write(hash, (const char *)fve_validated_block, fve_size_real);
crypt_hash_final(hash, (char *)&params->sha256_fve, 32);
crypt_hash_destroy(hash);
/* do some extended checks against FVE metadata, but not including MAC verification */
params->validation = malloc(sizeof(struct bitlk_validation));
if (!params->validation) {
r = -ENOMEM;
goto out;
}
if (!parse_fve_metadata_validation(params, &validation)) {
log_err(cd, _("Failed to parse BITLK FVE validation metadata from %s."), device_path(device));
r = -EINVAL;
goto out;
}
@@ -583,17 +729,18 @@ int BITLK_read_sb(struct crypt_device *cd, struct bitlk_metadata *params)
}
memset(fve_entries, 0, fve_entries_size);
log_dbg(cd, "Reading BITLK FVE metadata entries of size %zu on device %s, offset %" PRIu64 ".",
fve_entries_size, device_path(device), params->metadata_offset[0] + BITLK_FVE_METADATA_HEADERS_LEN);
log_dbg(cd, "Getting BITLK FVE metadata entries of size %zu on device %s, offset %" PRIu64 ".",
fve_entries_size, device_path(device), params->metadata_offset[valid_fve_metadata_idx] + BITLK_FVE_METADATA_HEADERS_LEN);
if (read_lseek_blockwise(devfd, device_block_size(cd, device),
device_alignment(device), fve_entries, fve_entries_size,
params->metadata_offset[0] + BITLK_FVE_METADATA_HEADERS_LEN) != (ssize_t)fve_entries_size) {
log_err(cd, _("Failed to read BITLK metadata entries from %s."), device_path(device));
if (BITLK_FVE_METADATA_HEADERS_LEN + fve_entries_size > fve_size_real) {
log_err(cd, _("Failed to check BITLK metadata entries previously read from %s."), device_path(device));
r = -EINVAL;
goto out;
}
/* fetch these entries from validated buffer to avoid double-fetch */
memcpy(fve_entries, fve_validated_block + BITLK_FVE_METADATA_HEADERS_LEN, fve_entries_size);
while ((fve_entries_size - start) >= (sizeof(entry_size) + sizeof(entry_type))) {
/* size of this entry */
@@ -716,6 +863,8 @@ int BITLK_read_sb(struct crypt_device *cd, struct bitlk_metadata *params)
}
out:
free(fve_entries);
free(fve_validated_block);
return r;
}
@@ -1110,6 +1259,7 @@ int BITLK_get_volume_key(struct crypt_device *cd,
struct volume_key *open_vmk_key = NULL;
struct volume_key *vmk_dec_key = NULL;
struct volume_key *recovery_key = NULL;
struct bitlk_validation_hash dec_hash = {};
const struct bitlk_vmk *next_vmk = NULL;
next_vmk = params->vmks;
@@ -1172,6 +1322,36 @@ int BITLK_get_volume_key(struct crypt_device *cd,
}
crypt_free_volume_key(vmk_dec_key);
log_dbg(cd, "Trying to decrypt validation metadata using VMK.");
r = crypt_bitlk_decrypt_key(crypt_volume_key_get_key(open_vmk_key),
crypt_volume_key_length(open_vmk_key),
(const char*)params->validation->enc_datum,
(char *)&dec_hash,
BITLK_VALIDATION_VMK_DATA_SIZE - BITLK_NONCE_SIZE - BITLK_VMK_MAC_TAG_SIZE,
(const char*)params->validation->nonce, BITLK_NONCE_SIZE,
(const char*)params->validation->mac_tag, BITLK_VMK_MAC_TAG_SIZE);
if (r < 0) {
log_dbg(cd, "Failed to decrypt validation metadata using VMK.");
crypt_free_volume_key(open_vmk_key);
if (r == -ENOTSUP)
return r;
break;
}
/* now, do the MAC validation */
if (le16_to_cpu(dec_hash.role) != 0 ||le16_to_cpu(dec_hash.type) != 1 ||
(le16_to_cpu(dec_hash.hash_type) != 0x2005)) {
log_dbg(cd, "Failed to parse decrypted validation metadata.");
crypt_free_volume_key(open_vmk_key);
return -ENOTSUP;
}
if (memcmp(dec_hash.hash, params->sha256_fve, sizeof(dec_hash.hash)) != 0) {
log_dbg(cd, "Failed MAC validation of BITLK FVE metadata.");
crypt_free_volume_key(open_vmk_key);
return -EINVAL;
}
r = decrypt_key(cd, open_fvek_key, params->fvek->vk, open_vmk_key,
params->fvek->mac_tag, BITLK_VMK_MAC_TAG_SIZE,
params->fvek->nonce, BITLK_NONCE_SIZE, true);

View File

@@ -21,6 +21,8 @@ struct volume_key;
#define BITLK_NONCE_SIZE 12
#define BITLK_SALT_SIZE 16
#define BITLK_VMK_MAC_TAG_SIZE 16
#define BITLK_VALIDATION_VMK_HEADER_SIZE 8
#define BITLK_VALIDATION_VMK_DATA_SIZE 72
#define BITLK_STATE_NORMAL 0x0004
@@ -85,6 +87,13 @@ struct bitlk_fvek {
struct volume_key *vk;
};
struct bitlk_validation {
uint8_t mac_tag[BITLK_VMK_MAC_TAG_SIZE];
uint8_t nonce[BITLK_NONCE_SIZE];
/* technically, this is not "VMK", but some sources call it this way */
uint8_t enc_datum[BITLK_VALIDATION_VMK_DATA_SIZE];
};
struct bitlk_metadata {
uint16_t sector_size;
uint64_t volume_size;
@@ -101,8 +110,10 @@ struct bitlk_metadata {
uint32_t metadata_version;
uint64_t volume_header_offset;
uint64_t volume_header_size;
const char *sha256_fve[32];
struct bitlk_vmk *vmks;
struct bitlk_fvek *fvek;
struct bitlk_validation *validation;
};
int BITLK_read_sb(struct crypt_device *cd, struct bitlk_metadata *params);