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

5e4fcf19fa5b4ef3a555a604ff82ee89.jpg Здравствуйте, уважаемые читатели. Когда-то очень давно, почти 3 года назад, я написалпару статей о форматах данных, используемых в UEFI-совместимых прошивках. С тех пор в этих форматах мало что изменилось, поэтому писать про них снова я не буду. Тем не менее, в тех статьях был достаточно серьезный пробел — отсутствовали какие-либо упоминания об NVRAM и используемых для её хранения форматах, т.к. тогда разбор NVRAM мне был попросту неинтересен, ибо те же данные можно получить из UEFI Shell на работающей системе буквально одной командой dmpstore.
По прошествии трех лет выяснилось, что хранилище NVRAM умеет разваливаться по различным причинам, и чаще всего это событие приводит к «кирпичу», т.е. воспользоваться вышеупомянутой командой уже не получится, а данные (или то, что от них осталось) надо доставать. Собрав пару развалившихся NVRAM’ов вручную в Hex-редакторе, я сказал »хватит это терпеть! », добавил поддержку разбора форматов NVRAM в UEFITool NE, и решил написать цикл статей об этих форматах по горячим следам и свежей памяти.
В первой части поговорим о том, что вообще такое этот NVRAM, и рассмотрим формат VSS и его вариации. Если интересно — добро пожаловать под кат.

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


Не смея нарушать уже устоявшуюся традицию, вынужден написать, что все сведения, которые вы тут можете почерпнуть, могут привести при неосторожном или неправильном использовании к разрушению ваших NVRAM, прошивки, системы и веры в человечество. Автор не несет ответственности вообще ни за что, используйте полученные знания на свой страх и риск, делайте зарядку, хорошо кушайте и больше спите.

Все сведения, описываемые в этой статье, получены при помощи реверс-инжиниринга множества различных образов UEFI-совместимых прошивок, и потому не претендуют на стопроцентную полноту или истинность. Если найдете ошибку — сообщите о ней в комментариях, буду рад исправить.

Введение


Начнем с того, что вообще такое эта NVRAM и зачем она вдруг понадобилась авторам спецификации UEFI, с учетом того, что до этого все спокойно пользовались для хранения своих настроек CMOS SRAM на батарейке и не жужжали. О «логическом» уровне NVRAM я уже рассказывал немного, а здесь постараюсь рассказать подробнее о «физическом».
Итак, NVRAM — это такая специальная область данных, в которой хранятся те UEFI-переменные, у которых установлен атрибут Non-Volatile. Самые популярные переменные такого рода — это Setup, в которой хранится большая часть текущих настроек из BIOS Setup, BootXXXX/BootOrder/BootNext, управляющие порядком загрузки, PK/KEK/db/dbx/dbt, отвечающие за работу SecureBoot, MonotonicCounter, защищающий от replay-атак на предыдущую пятёрку, и множество других, конкретный список зависит от вендора, модели платы и версии её прошивки.

Чаще всего NVRAM располагают на том же SPI-чипе, что и исполняемый код прошивки, по одной простой и банальной причине — это практически бесплатно (ибо 100–200 Кб на чипе емкостью в 8 Мб можно найти почти всегда, а отдельная микросхема CMOS SRAM на 128 Кб стоит весьма ощутимых денег). Бесплатность эта приводит к нескольким весьма серьезным рискам:

  1. Если в драйвере NVRAM есть ошибка, то он может разрушить не только свои данные, но и данные соседей, в том числе и том, в котором хранится код, тогда после перезагрузки машина встанет колом, и восстановить её из такого состояния будет весьма непросто.
  2. Каждая запись в NVRAM (а их обычно делают несколько при каждом включении и каждой перезагрузке) снижает ресурс SPI-чипа, и при некоторых условиях (к примеру, при постоянно высокой температуре, что не редкость для промышленных ПК) уже через 3–5 лет ресурс этот полностью вырабатывается и система начинает вести себя очень странно. При этом никаких аналогов SMART, EXT_CSD или автоматического wear-out leveling’а производители SPI-чипов 25-ой серии не предоставляют, и я уже пару раз видел системы, на которых чип просто «устал» до полной неработоспособности и его пришлось менять.
  3. Невозможно сбросить разрушенный или неправильный NVRAM перемычкой или выниманием батарейки, нужно стирание при помощи внешнего по отношению к хранилищу SPI-устройства. Некоторое производители имитируют поведение привычного пользователям джампера CLEAR_CMOS при помощи специального DXE-драйвера, храня в CMOS SRAM (которая до сих пор есть, но теперь она значительно меньше, т.к. хранятся в ней только часы и пара флагов) флаг NVRAM_IS_VALID. Если при следующей загрузке флаг этот оказывается сброшен, то выполняется восстановление значений по умолчанию для переменных вроде Setup. К сожалению, очень часто это не помогает, т.к. до загрузки этого драйвера была целая фаза PEI, в которой тоже были модули с запросами к NVRAM, и если запросы удовлетворить не получилось — то и восстановить ничего не выйдет, ибо загрузка прекратится раньше.


Требования к NVRAM


При реализации «физического» уровня NVRAM производителям прошивок пришлось решать множество вопросов: как обеспечить быстрый доступ к переменным на чтение (читаются они во время загрузки достаточно активно), как снизить нагрузку на флеш-память при записи, как хранить переменные таким образом, чтобы не дублировать общие для нескольких переменных данные (vendor GUID’ы, к примеру), как восстановить хотя бы часть данных после сбоя, и так далее. При этом, предложенный Intel при выпуске стандарта EFI 1.10 формат хранилища данных NVRAM оказался хоть и простым, но удовлетворяющим далеко не всем вышеперечисленным требованиям, плюс его формат не был описан в спецификации UEFI PI, т.е. выбор реализации NVRAM оставили конечным вендорам.

В результате вместо одного формата FFSv2, который хоть и получил потом расширенный заголовок и пару спорных полей в ZeroVector, но остался именно стандартом, для NVRAM вендоры умудрились реализовать три принципиально различных формата, что делает её разбор весьма увлекательным занятием.

Какие бывают форматы


Прежде, чем говорить о форматах, поговорим немного об их названиях. Каждый вендор, следуя давней традиции назвать свою страну «страной» или «землей», а её народ — «людьми», называет свой формат «форматом хранения NVRAM», что несколько мешает их различать. Но нам повезло: т.к. NVRAM обычно хранится внутри специального тома с относительно произвольной структурой, то у заголовков хранилищ имеются сигнатуры, и эти сигнатуры у каждого формата оказались разными. Вот по сигнатурам я их и буду называть, хотя эта терминология еще не устоялась.

Первым исторически и по распространенности оказался предложенный Intel на заре развития EFI формат VSS, который в стандарте UEFI 2.3.1C был расширен для поддержки защищенных переменных, используемых для реализации SecureBoot, а также получил пару расширений от компании Apple, используемых только в их прошивках. Рядом с данными в формате VSS может храниться блок FTW, данные из которого помогают восстановить NVRAM в случае аварийно неоконченной записи (помните, что «питание компьютера можно отключить» в любую секунду). После внедрения SecureBoot понадобилось хранить значения по умолчанию для его переменных, для чего некоторые вендоры добавили к тому же формату блок FDC (тоже названный по сигнатуре), где эти «умолчания» и хранятся.

Почти сразу оказалось, что хранить NVRAM исключительно формате VSS вовсе не обязательно, поэтому кто-то из вендоров (не знаю точно, кто был первым, по моему это был Phoenix) реализовал ему на замену формат EVSA, в котором появилась дедупликация GUID’ов и имен переменных, зато пропали возможности FTW. Формат это не получил особого распространения, но иногда все же нет-нет, да встречается в старых прошивках времен UEFI 2.1. Для своих хранилищ EVSA используют те же самые основной и дополнительный тома NVRAM, что и VSS, поэтому разбор структуры этих томов, как я уже говорил, занятие весьма увлекательное.

В Apple пошли еще дальше, и добавили в те же многострадальные тома еще два блока данных — SVS, формат которого совпадает с обычным VSS с точностью до сигнатуры, и Fsys, формат которого в Apple придумали с нуля.

Последний в нашем списке формат — NVAR, разработан компанией AMI и используется ими с самых первых реализаций Aptio4, пережил с тех пор два обновления, одно из которых добавило контрольную сумму для данных, хранящихся в переменной, а второе — поддержку защищенных переменных SecureBoot. Сам формат достаточно интересный, использует дедупликацию GUID’в, оптимизирует размер символа в именах переменных (которые, по спецификации, в кодировке UCS2), если все они помещаются в однобайтовую кодировку, относительно устойчив к сбоям, но нуждается в периодической «сборке мусора». К сожалению, обновления повлияли на него не самым лучшим образом, и его разбор после них сильно усложнился, а вместе с ним увеличилась и вероятность ошибок, поэтому непонятно, выиграли ли AMI что-либо от решения не использовать VSS или нет.

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

Формат VSS и его вариации


Данные NVRAM во всех виденных мной UEFI-совместимых прошивках, кроме основанных на коде AMI (о которых я расскажу в части, посвященной формату NVAR), хранятся в одном или нескольких томах с GUID FFF12B8D-7696–4C8B-A985–2747075B4F50 (он же EFI_SYSTEM_NV_DATA_FV_GUID, я его называю «основным»), либо с GUID 00504624–8A59–4EEB-BD0F-6B36E96128E0 (его я называю «дополнительным»).
Оба тома имеют разреженную структуру, поэтому приходится просматривать их байт за байтом в поисках сигнатур хранилищ и блоков. Заголовок хранилища VSS выглядит следующим образом:

struct VSS_VARIABLE_STORE_HEADER {
    UINT32  Signature; // Сигнатура
    UINT32  Size;      // Полный размер хранилища вместе с заголовком
    UINT8   Format;    // Байт, указывающий на то, что с форматом хранилища все хорошо (0x5A)
    UINT8   State;     // Байт, указывающий на то, что с данными в хранилище все хорошо (0xFE)
    UINT16  Unknown;   // Неизвестное поле, используется только в заголовках Apple SVS
    UINT32  : 32;      // Зарезервированное поле
};


Не все пока еще умеют разбирать структуры языка C на лету, поэтому есть смысл показать ту же самую структуру на скриншоте:
458d1c15b126417986e4e56a529d50b4.png
Легко видно, что перед нами заголовок хранилища VSS с соответствующей сигнатурой, общим размером 0xFFB8 байт, правильно отформатированное и с верными данными.
Apple иногда использует такой же заголовок, но с другой сигнатурой — $SVS. Зачем так сделано — не знаю, think different, видимо.
Сразу после заголовка хранилища начинаются хранящиеся в нем переменные. Располагаются они друг за другом, и на всех архитектурах, кроме IA64 (она же Itanium), для которой упоминается требование выравнивания начала переменных по восьмибайтовой границе, но у меня просто нет образов прошивок для этой архитектуры, чтобы проверить это утверждение.

Форматов переменных за десятилетнюю историю VSS накопилось три штуки: старый, использовавшийся до UEFI 2.3.1C, его расширение от Apple с дополнительным полем для CRC32, и новый, внедрение которого потребовалось для поддержки SecureBoot. Возможно, есть еще какие-то другие, но найти образы с ними мне пока не удалось, может быть у читателей получится.

Standard Этот формат широко использовался практически всеми производителями UEFI-совместимых прошивок, кроме AMI, в течение лет примерно семи, пока не потребовалось внедрение SecureBoot. Заголовок «стандартной» переменной выглядит так:

struct VSS_VARIABLE_HEADER {
    UINT16    StartId;    // Маркер начала переменной (0xAA 0x55)
    UINT8     State;      // Состояние переменной
    UINT8     : 8;        // Зарезервированное поле
    UINT32    Attributes; // Аттрибуты переменной
    UINT32    NameSize;   // Размер имени переменной, которое хранится как 0-терминированная строка в UCS2
    UINT32    DataSize;   // Размер данных, хранящихся в переменной
    EFI_GUID  VendorGuid; // GUID переменной
};


На этот раз на скриншноте можно показать сразу несколько переменных:
d36c1532ab6a46c48001cfe50e96f8af.png
Точнее говоря, полторы: PchInit и часть Setup. Они имеют состояние 0×7F (VARIABLE_HEADER_VALID), атрибуты 0×07 (NV+BS+RT), длину имени 0×10 и 0×0C, длину данных 0×04 и 0×2B0, и GUID E6C2F70A-B604–4877–85BA-DEEC89E117EB и 4DFBBAAB-1392–4FDE-ABB8-C41CC5AD7D5D соответственно.

Если вручную разбирать ничего не хочется, можно воспользоваться последней альфа-версией UEFITool NE, из него том NVRAM со скриншотов выше выглядит так:
1560d9ddfe234f1c92785f4949d60805.png

Apple CRC Примерно пару лет назад в Apple решили, что их переменным не хватает контрольной суммы, и поэтому добавили к заголовку выше еще одно поле, в котором хранится CRC32-контрольная сумма блока данных переменной. Этот формат Apple использует по сей день, и, скорее всего, продолжит использовать в будущем. Заголовок его выглядит вот так:

struct VSS_APPLE_VARIABLE_HEADER {
    UINT16    StartId;    // Маркер начала переменной (0xAA 0x55)
    UINT8     State;      // Состояние переменной
    UINT8     : 8;        // Зарезервированное поле
    UINT32    Attributes; // Атрибуты переменной
    UINT32    NameSize;   // Размер имени переменной, которое хранится как 0-терминированная строка в UCS2
    UINT32    DataSize;   // Размер данных, хранящихся в переменной
    EFI_GUID  VendorGuid; // GUID переменной
    UINT32    DataCrc32;  // CRC32-контрольная сумма данных
};


Скриншоты прикладывать не буду, там все совершенно по аналогии, скажу только, что Apple использует дополнительный атрибут 0×80000000 (CRC_USED), чтобы отличать свой заголовок от стандартного.Authenticated После того, как UEFI Forum принял решение использовать NVRAM для хранения ключей, используемых технологией SecureBoot, понадобилась доработка формата. Новые переменные получили заголовок следующего формата:

struct VSS_AUTH_VARIABLE_HEADER {
    UINT16    StartId;          // Маркер начала переменной (0xAA 0x55)
    UINT8     State;            // Состояние переменной
    UINT8     : 8;              // Зарезервированное поле
    UINT32    Attributes;       // Атрибуты переменной
    UINT64    MonotonicCounter; // Счетчик, защищающий от replay-атак
    EFI_TIME  Timestamp;        // Временная метка, еще одна защита от replay-атак
    UINT32    PubKeyIndex;      // Индекс в БД публичных ключей, или 0, если такая БД не используется
    UINT32    NameSize;         // Размер имени переменной, которое хранится как 0-терминированная строка в UCS2
    UINT32    DataSize;         // Размер данных, хранящихся в переменной
    EFI_GUID  VendorGuid;       // GUID переменной
};


На скриншоте такая переменная выглядит примерно так:
75354ca57f604acabf50372c7ab4d335.png
Маркер тот же, что и у обычных переменных, состояние в данном случае 0×3F (VARIABLE_ADDED), атрибуты — 0×27 (BS+NV+RT+TA), счетчик не задействован, зато задействована временная метка в формате EFI_TIME, индекс в БД публичных ключей также не задействован, размер имени — 0×08, размер данных — 0×64D, GUID — D719B2CB-3D3A-4596-A3BC-DAD00E67656F, а зовут эту переменную dbx.

В UEFITool эта же переменная выглядит вот так:
1a64f46ea3914c2cb24ac30235e6bbbe.png

Заключение

Ну вот, с форматами VSS более или менее разобрались, в следующий раз поговорим о форматах Fsys, EVSA и NVAR, а также о различных блоках данных, которых можно найти рядом с основной NVRAM.
Надеюсь, что первая часть вам понравилась, большое спасибо за внимание и до встречи во второй части.

© Habrahabr.ru