Как стать автором
Обновить

Еще пара слов об устройстве NVRAM в UEFI-совместимых прошивках (про Dell DVAR)

Уровень сложностиСредний
Время на прочтение7 мин
Количество просмотров2.4K

Здравствуй, читатель. С моих прошлых статей про 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 выглядит вот так:

Сырой DVAR
Сырой 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 показывает нам все, что на текущий момент уже распарсилось:

Заголовок хранилища DVAR, hex
Заголовок хранилища DVAR, hex

Дальше надо разобрать формат отдельной переменной, с учетом того, что переменные бывают разные, и в каких-то хранится больше метаданных и данных, чем в других.

После пары часов активной любви вприсядку получается примерно следующее:

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, по пока что ни одного дампа с такими переменными я не видел, и потому будем считать, что этих единорогов пока что не существует.

Первая переменная DVAR, с NamespaceGuid, помечена удаленной
Первая переменная DVAR, с NamespaceGuid, помечена удаленной
Следующая переменная DVAR, обычная, помечена удаленной
Следующая переменная DVAR, обычная, помечена удаленной
Первая переменная (40F:1), которая не помечена удаленной, хранит 0
Первая переменная (40F:1), которая не помечена удаленной, хранит 0

Все переменные, состояние которых не 0x05, прошивка игнорирует. При установке нового значения старая запись помечается как удаленная (состояние 0x55), а в конце хранилища создается новое. Парсинг заканчивается при нахождении свободной области после последней переменной. В нашем случае переменных в хранилище оказалось аж 1532 штуки, из которых удаленных более 90%. Если хранилище заполнится до отказа, прошивка произведет сборку мусора, для чего у нее есть вторая копия хранилища, на которую можно затем переключиться, изменив флаги в его заголовке.

Самое замечательное в использовании Kaitai Struct для разбора бинарных форматов в том, что по декларативному описанию формата его компилятор может сгенерировать готовый парсер на многих популярных ЯП, в том числе на C++, на котором написан UEFITool. Остается только красиво вывести результаты парсинга в окно Structure, и можно считать, что дело в шляпе.

Результат разбора хранилища DVAR в UEFITool NE
Результат разбора хранилища DVAR в UEFITool NE
Переменная 40F:1 со значением 0 теперь выглядит вот так
Переменная 40F:1 со значением 0 теперь выглядит вот так

Вместо заключения

На Хабре уже было несколько статей про Kaitai Struct, мне запомнились вот эти две, если вам интересны другие примеры его применения - их там есть.

Оказалось также, что на самых новых на данный момент машинах Dell (на 2025 год) отказались от использования этого формата, и теперь снова валят все в одно "стандартное" хранилище в формате VSS2, вот так:

Ба, старые знакомые, 40F:1, ты ли это?
Ба, старые знакомые, 40F:1, ты ли это?

Будем считать, что формат уже стал древним легаси, и больше мы его на новых машинах не увидим. Скатертью по жопе, в общем то, не очень то и хотелось.

Спасибо за внимание, читатель, будут вопросы - с удовольствием отвечу в комментариях, если вдруг найдется файл с DVAR, который не парсится - issue-tracker есть на GitHub.

Теги:
Хабы:
+44
Комментарии0

Публикации

Ближайшие события