- mostly: usecase -> use case - continously -> continuously - single typos in docs/FILE_DESCRIPTOR_STORE.md
307 lines
11 KiB
C
307 lines
11 KiB
C
/* SPDX-License-Identifier: LGPL-2.1-or-later */
|
|
|
|
#include <stdalign.h>
|
|
|
|
#include "bcd.h"
|
|
#include "efi-string.h"
|
|
|
|
enum {
|
|
SIG_BASE_BLOCK = 1718052210, /* regf */
|
|
SIG_KEY = 27502, /* nk */
|
|
SIG_SUBKEY_FAST = 26220, /* lf */
|
|
SIG_KEY_VALUE = 27510, /* vk */
|
|
};
|
|
|
|
enum {
|
|
REG_SZ = 1,
|
|
REG_MULTI_SZ = 7,
|
|
};
|
|
|
|
/* These structs contain a lot more members than we care for. They have all
|
|
* been squashed into _padN for our convenience. */
|
|
|
|
typedef struct {
|
|
uint32_t sig;
|
|
uint32_t primary_seqnum;
|
|
uint32_t secondary_seqnum;
|
|
uint64_t _pad1;
|
|
uint32_t version_major;
|
|
uint32_t version_minor;
|
|
uint32_t type;
|
|
uint32_t _pad2;
|
|
uint32_t root_cell_offset;
|
|
uint64_t _pad3[507];
|
|
} _packed_ BaseBlock;
|
|
assert_cc(sizeof(BaseBlock) == 4096);
|
|
assert_cc(offsetof(BaseBlock, sig) == 0);
|
|
assert_cc(offsetof(BaseBlock, primary_seqnum) == 4);
|
|
assert_cc(offsetof(BaseBlock, secondary_seqnum) == 8);
|
|
assert_cc(offsetof(BaseBlock, version_major) == 20);
|
|
assert_cc(offsetof(BaseBlock, version_minor) == 24);
|
|
assert_cc(offsetof(BaseBlock, type) == 28);
|
|
assert_cc(offsetof(BaseBlock, root_cell_offset) == 36);
|
|
|
|
/* All offsets are relative to the base block and technically point to a hive
|
|
* cell struct. But for our use case we don't need to bother about that one,
|
|
* so skip over the cell_size uint32_t. */
|
|
#define HIVE_CELL_OFFSET (sizeof(BaseBlock) + 4)
|
|
|
|
typedef struct {
|
|
uint16_t sig;
|
|
uint16_t _pad1[13];
|
|
uint32_t subkeys_offset;
|
|
uint32_t _pad2;
|
|
uint32_t n_key_values;
|
|
uint32_t key_values_offset;
|
|
uint32_t _pad3[7];
|
|
uint16_t key_name_len;
|
|
uint16_t _pad4;
|
|
char key_name[];
|
|
} _packed_ Key;
|
|
assert_cc(offsetof(Key, sig) == 0);
|
|
assert_cc(offsetof(Key, subkeys_offset) == 28);
|
|
assert_cc(offsetof(Key, n_key_values) == 36);
|
|
assert_cc(offsetof(Key, key_values_offset) == 40);
|
|
assert_cc(offsetof(Key, key_name_len) == 72);
|
|
assert_cc(offsetof(Key, key_name) == 76);
|
|
|
|
typedef struct {
|
|
uint16_t sig;
|
|
uint16_t n_entries;
|
|
struct SubkeyFastEntry {
|
|
uint32_t key_offset;
|
|
char name_hint[4];
|
|
} _packed_ entries[];
|
|
} _packed_ SubkeyFast;
|
|
assert_cc(offsetof(SubkeyFast, sig) == 0);
|
|
assert_cc(offsetof(SubkeyFast, n_entries) == 2);
|
|
assert_cc(offsetof(SubkeyFast, entries) == 4);
|
|
|
|
typedef struct {
|
|
uint16_t sig;
|
|
uint16_t name_len;
|
|
uint32_t data_size;
|
|
uint32_t data_offset;
|
|
uint32_t data_type;
|
|
uint32_t _pad;
|
|
char name[];
|
|
} _packed_ KeyValue;
|
|
assert_cc(offsetof(KeyValue, sig) == 0);
|
|
assert_cc(offsetof(KeyValue, name_len) == 2);
|
|
assert_cc(offsetof(KeyValue, data_size) == 4);
|
|
assert_cc(offsetof(KeyValue, data_offset) == 8);
|
|
assert_cc(offsetof(KeyValue, data_type) == 12);
|
|
assert_cc(offsetof(KeyValue, name) == 20);
|
|
|
|
#define BAD_OFFSET(offset, len, max) \
|
|
((uint64_t) (offset) + (len) >= (max))
|
|
|
|
#define BAD_STRUCT(type, offset, max) \
|
|
((uint64_t) (offset) + sizeof(type) >= (max))
|
|
|
|
#define BAD_ARRAY(type, array, offset, array_len, max) \
|
|
((uint64_t) (offset) + offsetof(type, array) + \
|
|
sizeof((type){}.array[0]) * (uint64_t) (array_len) >= (max))
|
|
|
|
static const Key *get_key(const uint8_t *bcd, uint32_t bcd_len, uint32_t offset, const char *name);
|
|
|
|
static const Key *get_subkey(const uint8_t *bcd, uint32_t bcd_len, uint32_t offset, const char *name) {
|
|
assert(bcd);
|
|
assert(name);
|
|
|
|
if (BAD_STRUCT(SubkeyFast, offset, bcd_len))
|
|
return NULL;
|
|
|
|
const SubkeyFast *subkey = (const SubkeyFast *) (bcd + offset);
|
|
if (subkey->sig != SIG_SUBKEY_FAST)
|
|
return NULL;
|
|
|
|
if (BAD_ARRAY(SubkeyFast, entries, offset, subkey->n_entries, bcd_len))
|
|
return NULL;
|
|
|
|
for (uint16_t i = 0; i < subkey->n_entries; i++) {
|
|
if (!strncaseeq8(name, subkey->entries[i].name_hint, sizeof(subkey->entries[i].name_hint)))
|
|
continue;
|
|
|
|
const Key *key = get_key(bcd, bcd_len, subkey->entries[i].key_offset, name);
|
|
if (key)
|
|
return key;
|
|
}
|
|
|
|
return NULL;
|
|
}
|
|
|
|
/* We use NUL as registry path separators for convenience. To start from the root, begin
|
|
* name with a NUL. Name must end with two NUL. The lookup depth is not restricted, so
|
|
* name must be properly validated before calling get_key(). */
|
|
static const Key *get_key(const uint8_t *bcd, uint32_t bcd_len, uint32_t offset, const char *name) {
|
|
assert(bcd);
|
|
assert(name);
|
|
|
|
if (BAD_STRUCT(Key, offset, bcd_len))
|
|
return NULL;
|
|
|
|
const Key *key = (const Key *) (bcd + offset);
|
|
if (key->sig != SIG_KEY)
|
|
return NULL;
|
|
|
|
if (BAD_ARRAY(Key, key_name, offset, key->key_name_len, bcd_len))
|
|
return NULL;
|
|
|
|
if (*name) {
|
|
if (strncaseeq8(name, key->key_name, key->key_name_len) && strlen8(name) == key->key_name_len)
|
|
name += key->key_name_len;
|
|
else
|
|
return NULL;
|
|
}
|
|
|
|
name++;
|
|
return *name ? get_subkey(bcd, bcd_len, key->subkeys_offset, name) : key;
|
|
}
|
|
|
|
static const KeyValue *get_key_value(const uint8_t *bcd, uint32_t bcd_len, const Key *key, const char *name) {
|
|
assert(bcd);
|
|
assert(key);
|
|
assert(name);
|
|
|
|
if (key->n_key_values == 0)
|
|
return NULL;
|
|
|
|
if (BAD_OFFSET(key->key_values_offset, sizeof(uint32_t) * (uint64_t) key->n_key_values, bcd_len) ||
|
|
(uintptr_t) (bcd + key->key_values_offset) % alignof(uint32_t) != 0)
|
|
return NULL;
|
|
|
|
const uint32_t *key_value_list = (const uint32_t *) (bcd + key->key_values_offset);
|
|
for (uint32_t i = 0; i < key->n_key_values; i++) {
|
|
uint32_t offset = *(key_value_list + i);
|
|
|
|
if (BAD_STRUCT(KeyValue, offset, bcd_len))
|
|
continue;
|
|
|
|
const KeyValue *kv = (const KeyValue *) (bcd + offset);
|
|
if (kv->sig != SIG_KEY_VALUE)
|
|
continue;
|
|
|
|
if (BAD_ARRAY(KeyValue, name, offset, kv->name_len, bcd_len))
|
|
continue;
|
|
|
|
/* If most significant bit is set, data is stored in data_offset itself, but
|
|
* we are only interested in UTF16 strings. The only strings that could fit
|
|
* would have just one char in it, so let's not bother with this. */
|
|
if (FLAGS_SET(kv->data_size, UINT32_C(1) << 31))
|
|
continue;
|
|
|
|
if (BAD_OFFSET(kv->data_offset, kv->data_size, bcd_len))
|
|
continue;
|
|
|
|
if (strncaseeq8(name, kv->name, kv->name_len) && strlen8(name) == kv->name_len)
|
|
return kv;
|
|
}
|
|
|
|
return NULL;
|
|
}
|
|
|
|
/* The BCD store is really just a regular windows registry hive with a rather cryptic internal
|
|
* key structure. On a running system it gets mounted to HKEY_LOCAL_MACHINE\BCD00000000.
|
|
*
|
|
* Of interest to us are these two keys:
|
|
* - \Objects\{bootmgr}\Elements\24000001
|
|
* This key is the "displayorder" property and contains a value of type REG_MULTI_SZ
|
|
* with the name "Element" that holds a {GUID} list (UTF16, NUL-separated).
|
|
* - \Objects\{GUID}\Elements\12000004
|
|
* This key is the "description" property and contains a value of type REG_SZ with the
|
|
* name "Element" that holds a NUL-terminated UTF16 string.
|
|
*
|
|
* The GUIDs and properties are as reported by "bcdedit.exe /v".
|
|
*
|
|
* To get a title for the BCD store we first look at the displayorder property of {bootmgr}
|
|
* (it always has the GUID 9dea862c-5cdd-4e70-acc1-f32b344d4795). If it contains more than
|
|
* one GUID, the BCD is multi-boot and we stop looking. Otherwise we take that GUID, look it
|
|
* up, and return its description property. */
|
|
char16_t *get_bcd_title(uint8_t *bcd, size_t bcd_len) {
|
|
assert(bcd);
|
|
|
|
if (HIVE_CELL_OFFSET >= bcd_len)
|
|
return NULL;
|
|
|
|
BaseBlock *base_block = (BaseBlock *) bcd;
|
|
if (base_block->sig != SIG_BASE_BLOCK ||
|
|
base_block->version_major != 1 ||
|
|
base_block->version_minor != 3 ||
|
|
base_block->type != 0 ||
|
|
base_block->primary_seqnum != base_block->secondary_seqnum)
|
|
return NULL;
|
|
|
|
bcd += HIVE_CELL_OFFSET;
|
|
bcd_len -= HIVE_CELL_OFFSET;
|
|
|
|
const Key *objects_key = get_key(bcd, bcd_len, base_block->root_cell_offset, "\0Objects\0");
|
|
if (!objects_key)
|
|
return NULL;
|
|
|
|
const Key *displayorder_key = get_subkey(
|
|
bcd,
|
|
bcd_len,
|
|
objects_key->subkeys_offset,
|
|
"{9dea862c-5cdd-4e70-acc1-f32b344d4795}\0Elements\00024000001\0");
|
|
if (!displayorder_key)
|
|
return NULL;
|
|
|
|
const KeyValue *displayorder_value = get_key_value(bcd, bcd_len, displayorder_key, "Element");
|
|
if (!displayorder_value)
|
|
return NULL;
|
|
|
|
char order_guid[sizeof("{00000000-0000-0000-0000-000000000000}\0")];
|
|
if (displayorder_value->data_type != REG_MULTI_SZ ||
|
|
displayorder_value->data_size != sizeof(char16_t[sizeof(order_guid)]) ||
|
|
(uintptr_t) (bcd + displayorder_value->data_offset) % alignof(char16_t) != 0)
|
|
/* BCD is multi-boot. */
|
|
return NULL;
|
|
|
|
/* Keys are stored as ASCII in registry hives if the data fits (and GUIDS always should). */
|
|
char16_t *order_guid_utf16 = (char16_t *) (bcd + displayorder_value->data_offset);
|
|
for (size_t i = 0; i < sizeof(order_guid) - 2; i++) {
|
|
char16_t c = order_guid_utf16[i];
|
|
switch (c) {
|
|
case '-':
|
|
case '{':
|
|
case '}':
|
|
case '0' ... '9':
|
|
case 'a' ... 'f':
|
|
case 'A' ... 'F':
|
|
order_guid[i] = c;
|
|
break;
|
|
default:
|
|
/* Not a valid GUID. */
|
|
return NULL;
|
|
}
|
|
}
|
|
/* Our functions expect the lookup key to be double-derminated. */
|
|
order_guid[sizeof(order_guid) - 2] = '\0';
|
|
order_guid[sizeof(order_guid) - 1] = '\0';
|
|
|
|
const Key *default_key = get_subkey(bcd, bcd_len, objects_key->subkeys_offset, order_guid);
|
|
if (!default_key)
|
|
return NULL;
|
|
|
|
const Key *description_key = get_subkey(
|
|
bcd, bcd_len, default_key->subkeys_offset, "Elements\00012000004\0");
|
|
if (!description_key)
|
|
return NULL;
|
|
|
|
const KeyValue *description_value = get_key_value(bcd, bcd_len, description_key, "Element");
|
|
if (!description_value)
|
|
return NULL;
|
|
|
|
if (description_value->data_type != REG_SZ ||
|
|
description_value->data_size < sizeof(char16_t) ||
|
|
description_value->data_size % sizeof(char16_t) != 0 ||
|
|
(uintptr_t) (bcd + description_value->data_offset) % alignof(char16_t))
|
|
return NULL;
|
|
|
|
/* The data should already be NUL-terminated. */
|
|
char16_t *title = (char16_t *) (bcd + description_value->data_offset);
|
|
title[description_value->data_size / sizeof(char16_t) - 1] = '\0';
|
|
return title;
|
|
}
|