Устройство NVRAM в UEFI-совместимых прошивках, часть четвертая

в 20:15, , рубрики: firmware, NVRAM, UEFI, UEFITool, реверс-инжиниринг, системное программирование, Форматы данных, хаб firmware || GTFO!

Устройство NVRAM в UEFI-совместимых прошивках, часть четвертая - 1И снова здравствуйте, уважаемые читатели.
Начатый в предыдущих трех частях разговор о форматах хранилищ NVRAM, используемых различными реализациями UEFI, подходит к своему логическому концу. Нерассмотренным остался только один формат — NVAR, который используется в прошивках на основе кодовой базы AMI Aptio. Компания AMI в свое время смогла «оседлать» практически весь рынок прошивок для десктопных и серверных материнских плат, поэтому формат NVAR оказался чуть ли не распространённее, чем оригинальный и «стандартный» VSS.
Если вам интересно, чем хорош и чем плох формат хранилища NVRAM от AMI — добро пожаловать под кат.

Отказ от ответственности №4

Повторение — мать заикания основа запоминания, поэтому автор не оставляет попыток убедить читателя в том, что ковыряние в прошивке — дело опасное, и до любых изменений следует сделать резервную копию на программаторе, чтобы потом не было мучительно больно за бесцельно потраченные на восстановление работоспособности системы пару дней (или недель). Автор по-прежнему не несет ответственности ни за что, кроме очепяток, сведения о которых можно присылать в Л/С, вы используете эти полученные реверс-инженирингом знания на свой страх и риск.

AMI NVAR

Ну вот, наконец удалось добраться до последнего в моем списке формата хранилища NVRAM, которого я буду называть NVAR по используемой в его в заголовке сигнатуре. В отличие от всех остальных форматов, описанных в предыдущих частях, данные в формате NVAR хранятся не в томе с GUID FFF12B8D-7696-4C8B-A985-2747075B4F50 (EFI_SYSTEM_NV_DATA_FV_GUID), а в обычном FFS-файле с GUID CEF5B9A3-476D-497F-9FDC-E98143E0422C (NVAR_STORE_FILE_GUID) либо 9221315B-30BB-46B5-813E-1B1BF4712BD3 (NVAR_EXTERNAL_DEFAULTS_FILE_GUID).
Файл с первым GUID хранится в отдельном томе, специально предназначенном для NVRAM, чаще всего таких томов два — основной и резервный, и если с данными или форматом основного что-то происходит, и драйвер NVRAM может определить это, то он переключается на использование резервного хранилища. Иногда резервное хранилище заполняется еще на этапе сборки прошивки, но чаще под него просто оставляется место, и оно создается при первом запуске (поэтому первый запуск после обновления прошивки может быть довольно долгим). Второй файл хранится в томе DXE, имеет несколько другой, зависящий от конкретной платформы, формат и используется для восстановления «умолчаний» некоторых переменных в случае, если и основное, и дополнительно хранилища повреждены невосстановимо.

Так как данные в формате NVAR хранятся внутри файла, информация о максимальном размере хранилища и о том, где его найти, уже доступна прошивке благодаря сервисам UEFI FFS, поэтому каких либо дополнительных заголовков разработчики AMI выдумывать не стали, и сразу же после заголовка, с максимальной упаковкой и без выравнивания, начинаются записи NVAR.

Заголовок такой записи выглядит так:

struct NVAR_ENTRY_HEADER {
    UINT32 Signature;      // Сигнатура NVAR
    UINT16 Size;           // Размер записи вместе с заголовком
    UINT32 Next : 24;      // Смещение следующего элемента в списке,
                           // либо специально значение (0 либо 0xFFFFFF в зависимости ErasePolarity)
    UINT32 Attributes : 8; // Атрибуты записи
};

Он же на скриншоте:
Устройство NVRAM в UEFI-совместимых прошивках, часть четвертая - 2
На вид пока все очень просто, сначала правильная сигнатура — NVAR, затем размер записи — 0x5D3, пустое поле Next, атрибуты — 0x83, непонятное восьмибитное поле — 0x00 и имя переменной в кодировке ASCII — StdDefaults.

Оказывается, что формат данных данных сильно зависит от битов поля Attributes, которое можно представить в таком виде:

enum NVAR_ENTRY_ATTRIBUTES {
    RuntimeVariable = 0x01, // Переменная, которая хранится в этой (или одной из следующих за ней по списку) записи, имеет атрибут RT
    AsciiName = 0x02,       // Имя переменной хранится в ASCII вместо UCS2
    LocalGuid = 0x04,       // GUID переменной хранится в самой записи, иначе в ней хранится только индекс в базе данных GUIDов
    DataOnly = 0x08,        // В записи хранятся только данные, такая запись не может быть первой в списке
    ExtendedHeader = 0x10,  // Присутствует расширенный заголовок, который находится в конце записи
    HwErrorRecord = 0x20,   // Переменная, которая хранится в этой (или одной из следующих за ней по списку) записи, имеет атрибут HW
    AuthWrite = 0x40,       // Переменная, которая хранится в этой (или одной из следующих за ней по списку) записи, 
                            // имеет атрибут AV и/или TA
    EntryValid = 0x80       // Запись валидна, если этот бит не установлен, запись должна быть пропущена
};

Таким образом, наши атрибуты 0x83 — это на самом деле EntryValid + AsciiName + RuntimeVariable, а непонятное до этого восьмибитное поле — это индекс в базе данных GUID'ов. Замечу также, что длина имени нигде не хранится, и для того, чтобы найти начало данных, нужно каждый раз вызывать strlen(). Если бы был установлен атрибут LocalGuid, вместо индекса на 1 байт присутствовал бы весь GUID на 16. Получается, что в базе данных GUIDов (открою секрет, она находится в самом конце файла и растет вверх, т.е. наш нулевой GUID — последние 16 байт файла с хранилищем NVRAM, первый — предпоследние 16 байт и так далее) может храниться не более 256 различных GUIDов, но этого достаточно для любых возможных применений NVRAM на данный момент, а места экономит прилично.

То же самое из окна UEFITool NE:
Устройство NVRAM в UEFI-совместимых прошивках, часть четвертая - 3

По значениям атрибутов видно, как формат развивался с течением времени. До стандарта UEFI 2.1 у переменных NVRAM было всего 3 возможных атрибута: NV, BS, RT. Атрибут NV хранить бессмысленно, т.к. только такие переменные в хранилище NVRAM и попадают, а BS и RT не являются взаимоисключающими и у «здоровой» переменой могут быть либо только BS, либо BS + RT, поэтому для этих атрибутов использовался только один бит — RuntimeVariable. Отлично, получилось сэкономить целых 24 бита на переменную.
Затем оказалось, что физический уровень NVRAM не всегда надежен, и надо бы считать контрольную сумму от данных, чтобы отличать поврежденные переменные от нормальных, поэтому завели бит ExtendedHeader, а контрольную сумму стали хранить в самом конце записи, после данных.
Прошло немного времени, и под давлением Microsoft в UEFI 2.1 был добавлен еще один атрибут — HW, используемый для переменных WHEA. Ладно, под него завели бит HwErrorRecord, надо так надо.
Потом в UEFI 2.3.1C неожиданно добавили SecureBoot вместе с двумя новыми атрибутами для переменных — AV и AW. К счастью, хранить последний не очень нужно (т.к. такая переменная всего одна, dbx), а под первый пришлось выделить последний свободный бит AuthWrite.
Радоваться получилось совсем недолго, уже в UEFI 2.4 добавили еще один атрибут — TA, который, внезапно, оказалось некуда совать, т.к. в свое время сэкономили целых 24 бита. В итоге пришлось заводить дополнительное поле в расширенном заголовке, который хранится после данных. Там же пришлось хранить временную метку и хэш для AV/TA-переменных.

После всех этих доработок, расширенный заголовок получился вот таким:

struct NVAR_EXTENDED_HEADER {
    UINT8 ExtendedAttributes; // Атрибуты расширенного заголовка
    // UINT64 TimeStamp;      // Присутствует, если ExtendedAttributes | ExtTimeBased (0x20)
    // UINT8  Sha256Hash[32]; // Присутствует, если ExtendedAttributes | ExtAuthWrite (0x10) 
                              // или ExtendedAttributes | ExtTimeBased (0x20)
    // UINT8  Checksum;       // Присутствует, если ExtendedAttributes | ExtChecksum (0x01)
    UINT16 ExtendedDataSize;  // Размер заголовка без поля ExtendedAttributes
};

Он же на скриншоте:
Устройство NVRAM в UEFI-совместимых прошивках, часть четвертая - 4
Итого, размер расширенного заголовка — 0x2C, контрольная сумма — 0x10, нулевой хэш, временная метка — 0x5537BB5D и атрибуты — 0x21 (ExtChecksum + ExtTimeBased).

Получается так, что чтобы получить значение атрибутов для какой-либо переменной, её нужно разбирать всю целиком, вычисляя смещения динамически и собирая значения из нескольких разных мест в файле. И все это ровно потому, что когда-то давно сэкономили целых 24 байта. Будете разрабатывать свой формат — не экономьте на спичках, сделайте одолжение самому себе из будущего!

Но и это еще не все, ведь у нас остались не рассмотренными атрибут DataOnly и поле Next в заголовке. Используются они для того, чтобы сэкономить на GUID, имени и атрибутах, если переменная, в которую осуществляется запись, уже существует. Вместо того, чтобы снять со старой записи атрибут EntryValid и записать новую целиком, в заголовке старой записи заполняется поле Next, а в свободном месте файла создается запись с атрибутом DataOnly, на которую этот самый Next и ссылается, причем там уже нет ни GUID'а, ни имени, но зато присутствует расширенный заголовок. Более того, когда значение переменной переписывается в следующий раз, поле Next исправляется не в первой записи в этом своеобразном односвязном списке, а в последней, удлиняя список. А т.к. существуют переменные, которые обновляются при каждой перезагрузке (да тот же MonotonicCounter), очень скоро NVRAM наполняется копиями данных этой переменной до краев, а доступ к ней замедляется с каждой перезагрузкой, пока не окажется, что места нет вообще, и драйверу NVRAM нужно выполнять сборку мусора. Зачем так сделано — еще одна великая тайна, я не могу придумать уважительной причины такому поведению.

В UEFITool NE пришлось добавить действие Go to data, которое работает на переменных типа Link (т.е. таких, у которых поле Next не пустое) и выбирает последний элемент в односвязном списке, в котором хранятся нынешние данные переменной, а не те, что были там черт знает когда до этого:
Устройство NVRAM в UEFI-совместимых прошивках, часть четвертая - 5

Доступ к переменным NVRAM работает вот так на 95% десктопов и серверов последних 5 лет. Посмотрите, уважаемые читатели, до чего доводит экономия на байтах и обвешивание старого формата новыми костылями в отчаянной попытке не переписывать драйвер NVRAM заново, и не делайте так, пожалуйста.

Заключение

Я не знаю, что цензурного сказать о формате NVAR. В погоне за компактностью AMI умудрились пожертвовать всем остальным, и если поначалу казалось, что жертва эта была небольшой и незаметной, с развитием спецификации UEFI формат превратился в местный аналог Abomination'а, собранного из кусков непонятно чего, сшитых непонятно как. Нам всем повезло, что драйвер NVRAM у AMI достаточно хорош, чтобы вовремя и незаметно убирать в хранилище мусор, переключаться на резервное хранилище при повреждении основного, стартовать с разрушенным NVRAM, переживать запись «под самую крышку» и т.п., но достигнуто это все скорее не благодаря, а вопреки.
История с форматами NVRAM, надеюсь, подошла к концу, теперь вы знаете о них почти столько же, сколько и я сам. Спасибо большое за внимание, удачных вам прошивок, чипов и NVRAM'ов.

Автор: CodeRush

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js