Еще пара слов об устройстве NVRAM в UEFI-совместимых прошивках (про Dell DVAR)
Здравствуй, читатель. С моих прошлых статей про NVRAM прошло некоторое количество времени (за эти почти 10 лет мало что изменилось, и все эти форматы до сих пор с нами практически без модификаций), а моя работа на одну фруктовую компанию не позволяла мне писать статьи, тесты и посты без одобрения кучей непонятных людей, но теперь эта работа осталась в прошлом, а желание писать так и не пропало.
Эта статья - практическая реализация этого желания, а поговорим мы в ней о формате Dell DVAR, и немного о декларативном языке для написания парсеров Kaitai Struct, на котором я недавно переписал парсеры всех известных UEFITool NE форматов NVRAM.
Идея придумать свой собственный формат для хранения переменных, нужных только для функционирования самой прошивки, вообще говоря, здравая, потому что и сам формат можно сделать проще, и нет необходимости тащить с собой груз совместимости с интерфейсом GetVariable/SetVariable, а главное, станет сильно сложнее перепутать "системные" переменные с "пользовательскими" при разработке прошивки.
В итоге Dell где-то приблизительно в 2018 году (самый ранний дамп, в котором я видел эти новые переменные был от середины 2019, но скорее всего сам формат разработали немного раньше) решила, что надо бы последовать лучшим практикам, и придумала удивительный, в каком-то смысле, формат Dell DVAR (названный мной так по сигнатуре).
Самое раннее упоминание в сети о существовании парсеров этого формата, которые я смог найти - вот оно. Если его перевести с китайского автоматическим переводчиком (прости, читатель, за мое незнание китайского языка), получим примерно следующее:
Индекс (NameId:NamespaceId) | Название переменной | Описание переменной |
---|---|---|
492:1 | "PPID" | Серийный номер материнской платы |
429:1 | "FanCtrlOvrd" | Передача управления вентиляторами от EC драйверу FanControlSmm |
42A:1 | "ChassisPolicy" | Выбор между типами шасси, использующими одну и ту же прошивку |
2618:1 | "Service Tag" | Сервис-тег |
617:1 | "Asset Tag" | Ассет-тег |
E30:1 | "ProductName" | Имя продукта |
E31:1 | "Sku" | Номер SKU |
E78:1 | "System Map" | Карта файла BIOS |
2503:3 | "FirstPowerOnDate" | Дата первого включения |
2502:3 | "MfgDate" | Дата производства |
62B:1 | "May Man Mode" | Возможно, признак заводского режима |
1:2 | "May Man Mode1" | Возможно, другой признак заводского режима |
445:1 | "AcPwrRcvry" | Стратегия после пропадания питания |
478:1 | "WakeOnLan5" | Конфигурация Wake on Lan |
Код самого парсера там тоже есть, но он получен как результат "some data experience and guesswork", и потому хоть и работает в некоторых редких случаях, но все равно никуда не годен.
После еще одного раунда поисков оказалось, что намного более функциональный и полноценный парсер есть внутри утилиты UefiBiosEditor (новые версии которой автор загружает вот сюда), но у нее есть два фатальных недостатка - она проприетарная и для Windows. Зато ее можно использовать для кросс-чека с моим собственным кодом, который я добавил в UEFITool NE A71.
Обратная разработка формата Dell DVAR
На первый взгляд из hex-редактора, область с переменными DVAR выглядит вот так:
Кроме хорошо заметной сигнатуры, давшей название формату, все остальное выглядит неприятно - никаких очевидных полей, вроде размера хранилища, флагов, типов данных и т.п. невооруженным взглядом не видно, зато хорошо заметен повторяющийся паттерн AA Fp Fq Fr Fs, где p,q,r,s - шестнадцатеричные цифры, близкие к F.
Если немного помедитировать над этой картиной (и поиграться с уже разобранными переменными в UefiBiosEditor), то внезапно приходит понимание, что инженеры Dell, помня о том, что на NOR flash можно "бесплатно" установить любой бит в 0, но чтобы установить уже установленный в 0 бит обратно в 1, нужно стереть и записать весь блок целиком (а он бывает и 4кб, в хорошем случае, и 64кб, в не очень хорошем), придумали хранить все метаданные (заголовки, флаги, и т.п.) в формате "0 - это 1, а 1 - это 0", т.е. AA FD FB F8 FE - это, на самом деле, 55 02 04 07 01, что уже намного больше похоже на набор флагов, идентификаторов, и размеров данных.
После того, как главный трюк становится понятен, все остальное - не слишком сложная после многих лет опыта работа по реверс-инженирингу бинарного формата, который не пытались обфусцировать специально. Зато с опытом также пришло понимание, что не обязательно все делать вручную (даже если хочется иногда угореть по хардкору, как в старые добрые времена), и для этого теперь есть хорошие инструменты, а именно - декларативный язык описания форматов Kaitai Struct, и его Web IDE.
Загружаем туда наш дамп, и пишем примерно следующее:
meta:
id: dell_dvar
title: Dell DVAR Storage
application: Dell UEFI firmware
file-extension: dvar
tags:
- firmware
license: CC0-1.0
ks-version: 0.9
endian: le
seq:
- id: signature
size: 4
- id: len_store_c
type: u4
- id: flags_c
type: u1
instances:
len_store:
value: 0xFFFFFFFF - len_store_c
flags:
value: 0xFF - flags_c
Сначала в области meta у нас описание самого формата, которое на парсинг влияет мало, но нужно будет позже. Выше на скриншоте редактора видно, что повторяющиеся записи АА Fp Fq Fr Fs начинаются через 5 байт после сигнатуры, поэтому логично предположить, что 4 из них - это размер хранилища, а оставшийся - какие-то флаги или что-то подобное. Так и запишем, не забыв, что реальные значения у нас отличаются от того, что в файле записано, и понадобятся потом именно они. В итоге доброе IDE показывает нам все, что на текущий момент уже распарсилось:
Дальше надо разобрать формат отдельной переменной, с учетом того, что переменные бывают разные, и в каких-то хранится больше метаданных и данных, чем в других.
После пары часов активной любви вприсядку получается примерно следующее:
seq:
- id: signature
size: 4
- id: len_store_c
type: u4
- id: flags_c
type: u1
- id: entries
type: dvar_entry
repeat: until
repeat-until: _.state_c == 0xFF
instances:
len_store:
value: 0xFFFFFFFF - len_store_c
flags:
value: 0xFF - flags_c
types:
dvar_entry:
seq:
- id: state_c
type: u1
- id: flags_c
type: u1
if: state_c != 0xFF
- id: types_c
type: u1
if: state_c != 0xFF
- id: attributes_c
type: u1
if: state_c != 0xFF
- id: namespace_id_c
type: u1
if: state_c != 0xFF and (flags == 2 or flags == 6)
- id: namespace_guid
size: 16
if: state_c != 0xFF and flags == 6
- id: name_id_8_c
type: u1
if: state_c != 0xFF and types == 0
- id: name_id_16_c
type: u2
if: state_c != 0xFF and (types == 4 or types == 5)
- id: len_data_8_c
type: u1
if: state_c != 0xFF and (types == 0 or types == 4)
- id: len_data_16_c
type: u2
if: state_c != 0xFF and types == 5
- id: data_8
size: len_data_8
if: state_c != 0xFF and (types == 0 or types == 4)
- id: data_16
size: len_data_16
if: state_c != 0xFF and types == 5
instances:
state:
value: 0xFF - state_c
flags:
value: 0xFF - flags_c
types:
value: 0xFF - types_c
attributes:
value: 0xFF - attributes_c
namespace_id:
value: 0xFF - namespace_id_c
name_id_8:
value: 0xFF - name_id_8_c
name_id_16:
value: 0xFFFF - name_id_16_c
len_data_8:
value: 0xFF - len_data_8_c
len_data_16:
value: 0xFFFF - len_data_16_c
Переменная DVAR состоит из заголовка (который присутствует у всех переменных), опциональных полей (только у некоторых), и собственно данных:
typedef struct _DVAR_ENTRY_HEADER {
UINT8 StateC;
UINT8 FlagsC;
UINT8 TypeC;
UINT8 AttributesC;
UINT8 NamespaceIdC;
// Наличие или отстутствие нижеследующего зависит от Flags и Type
// EFI_GUID NamespaceGuid;
// UINT8 | UINT16 NameId;
// UINT8 | UINT16 DataSize;
// UINT8 Data[DataSize];
} DVAR_ENTRY_HEADER;
#define DVAR_ENTRY_STATE_STORING 0x01 // Запись в переменную начата
#define DVAR_ENTRY_STATE_STORED 0x05 // Запись закончена, переменная валидна
#define DVAR_ENTRY_STATE_DELETING 0x15 // Удаление переменной начато
#define DVAR_ENTRY_STATE_DELETED 0x55 // Удаление переменной закончено
#define DVAR_ENTRY_FLAG_NAME_ID 0x02 // Переменная с NameId
#define DVAR_ENTRY_FLAG_NAMESPACE_GUID 0x04 // Переменная с NamespaceGuid
#define DVAR_ENTRY_TYPE_NAME_ID_8_DATA_SIZE_8 0x00
#define DVAR_ENTRY_TYPE_NAME_ID_16_DATA_SIZE_8 0x04
#define DVAR_ENTRY_TYPE_NAME_ID_16_DATA_SIZE_16 0x05
Итого, конкретная переменная однозначно идентифицируется парой NamespaceId (идентификатор области видимости) и NameId (собственно идентификатор переменной), и областей видимости может быть до 255, а уникальных переменных внутри одной области - до 65535. Некоторые переменные (обычно это первое вхождение переменной с не встречавшимся до этого NamespaceId) также хранят в метаданных GUID для их NamespaceId. Если переменная с NamespaceGuid помечена удаленной, заново этого GUID не сохраняют, оставляя его в этой "удаленной" переменной.
В теории, у переменных DVAR так же может быть и отдельное имя в формате UTF8, по пока что ни одного дампа с такими переменными я не видел, и потому будем считать, что этих единорогов пока что не существует.
Все переменные, состояние которых не 0x05, прошивка игнорирует. При установке нового значения старая запись помечается как удаленная (состояние 0x55), а в конце хранилища создается новое. Парсинг заканчивается при нахождении свободной области после последней переменной. В нашем случае переменных в хранилище оказалось аж 1532 штуки, из которых удаленных более 90%. Если хранилище заполнится до отказа, прошивка произведет сборку мусора, для чего у нее есть вторая копия хранилища, на которую можно затем переключиться, изменив флаги в его заголовке.
Самое замечательное в использовании Kaitai Struct для разбора бинарных форматов в том, что по декларативному описанию формата его компилятор может сгенерировать готовый парсер на многих популярных ЯП, в том числе на C++, на котором написан UEFITool. Остается только красиво вывести результаты парсинга в окно Structure, и можно считать, что дело в шляпе.
Вместо заключения
На Хабре уже было несколько статей про Kaitai Struct, мне запомнились вот эти две, если вам интересны другие примеры его применения - их там есть.
Оказалось также, что на самых новых на данный момент машинах Dell (на 2025 год) отказались от использования этого формата, и теперь снова валят все в одно "стандартное" хранилище в формате VSS2, вот так:
Будем считать, что формат уже стал древним легаси, и больше мы его на новых машинах не увидим. Скатертью по жопе, в общем то, не очень то и хотелось.
Спасибо за внимание, читатель, будут вопросы - с удовольствием отвечу в комментариях, если вдруг найдется файл с DVAR, который не парсится - issue-tracker есть на GitHub.