Еще пара слов об устройстве 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 выглядит вот так:

Сырой 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 // Переменная с NamepaceGuid  #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.


ссылка на оригинал статьи https://habr.com/ru/articles/907488/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *