И снова здравствуйте, уважаемые читатели.
Начатый в предыдущих трех частях разговор о форматах хранилищ 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; // Атрибуты записи
};

Он же на скриншоте:

На вид пока все очень просто, сначала правильная сигнатура — 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:


По значениям атрибутов видно, как формат развивался с течением времени. До стандарта 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
};

Он же на скриншоте:

Итого, размер расширенного заголовка — 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 работает вот так на 95% десктопов и серверов последних 5 лет. Посмотрите, уважаемые читатели, до чего доводит экономия на байтах и обвешивание старого формата новыми костылями в отчаянной попытке не переписывать драйвер NVRAM заново, и не делайте так, пожалуйста.

Заключение


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

Комментарии (13)


  1. Garrett
    18.04.2016 23:56
    +1

    Нда, кислые ягодки.
    Спасибо за описание формата!
    Теперь понятно откуда некоторые особенности и баги UEFI растут.
    Столько возможностей для багов и эксплоитов =\


    1. CodeRush
      19.04.2016 00:06

      На здоровье. Формат, конечно, полный песец. Ничего не мешало использовать формат VSS, который хоть и занимает побольше, зато прямой как палка и парсить его — одно удовольствие, но нет, если стандарт позволяет свободу в реализации, ей нужно пользовать именно таким образом.
      Забыл сказать спасибо ребятам из проекта CHIPSEC за референсную реализацию парсига, на которую я частенько оглядывался, и тов. xvilka, который помогал с разбором NVAR и образами, и советами, и даже кодом.


      1. AVX
        19.04.2016 08:40
        +1

        Немного не в тему, но если уж форматы nvram более-менее известны, то почему-то на сайте subzero.io нет пока поддержки NVRAM:
        «UEFI Varibles (NVRAM)
        Coming Soon»


        1. CodeRush
          19.04.2016 08:45

          Потому что для Subzero.io используется обрезанная версия uefi-firmware-parser, в которую разбор NVRAM еще не портирован из UEFITool или CHIPSEC. Либо Тедди допилит UFP, либо мне хватит сил на libffsparser, но поддержка эта рано или поздно добавится.


  1. JerleShannara
    19.04.2016 10:17
    +3

    > Зачем так сделано — еще одна великая тайна, я не могу придумать уважительной причины такому поведению.
    Железячник во мне говорит что это такой своеобразный wear leveling, и в AMI таким образом попытались продлить жизнь SPI флешке. В итоге имеем всю область nvram с одинаковым износом.
    Да, есть у меня одна плата которая умерла какраз по износу флеша — верификация обрамывается ровно на одном блоке, причем постоянно, я думаю не надо говорить, что в дампе прошивки находилось на этом месте. Машинка перезагружалась раз 6 за день, прожила два года, подарила мне нелюбовь к macronix.


    1. CodeRush
      19.04.2016 10:33
      +1

      Может быть, не исключено, но можно придумать и менее хитрые способы, при которых хотя бы не нужно проходить по односвязным спискам до конца, чтобы до данных добраться.
      Плат с «уставшим» SPI-чипом я тоже перевидал изрядно, причем симтомы там почти каждый раз разные, и сразу не поймешь, почему система сначала 5 лет работала в станке без сбоев, а потом начала на загрузке виснуть, на выключении виснуть, или вообще просто виснуть, без внешних причин. Никогда на SPI-чип не подумал бы, если бы его замена не решала бы проблему.


    1. CodeRush
      19.04.2016 10:40
      +1

      С другой строны, в качестве wear leveling'а отлично работает стратегия VSS, когда с предыдущей структуры снимается бит Valid, и в конец записывается следующая, не важно, большего там размера данные, меньшего или такого же. Дошли до конца — делаем сборку мусора. Подход AMI в данном случае лучше тем, что не нужно копировать имя и GUID, т.е. ресурс флеша все-таки экономится. Заодно становится понятно, зачем поле Next обновляют не в первой записи, а в последней — пара месяцев, и первую запись переменной MonitonicCounter или MemScrambleSeed затерло бы иначе просто до дыр, а так — все в порядке. В общем, тут все же больше экономия места на флеше, чем управление износом, на мой взгляд.


      1. JerleShannara
        19.04.2016 14:13
        +2

        Ну как говорится модель Велосипеды-Баги-Костыли используется крупными мировыми разработчиками ПО. А с экономией я скажу так — корбутовская прошивка влезает в 1мб и спокойно работает и не жужжит (ну правда тоже гадит в флеш, но это решаемо) без особых проблем — система грузится, плюшек и фишек нету, имеем на выходе интерфейс старого доброго биоса на новых платах. С другой стороны если насовать в UEFI кучу «унЕкальных прЕлажений», добавить 3D моделей и прочих свистелок и перделок(привет асусу/асроку, не забудьте добавить функцию заказа пиццы в свои материнки, т.к. пока до вашей техподдержки из uefi достучишься очень хочется кушать, т.к. реализация vpn тормозит у вас) — можно и в 8Мб не влезть, экономия 5кб при таких объемах других модулей мне кажется просто смешной.


        1. CodeRush
          19.04.2016 14:28
          +1

          Можно и 64 кб впихнуть, если стараться, но это все никому не нужно.
          Сейчас вот в недавней презентации новой линейки Atom под кодовым именем Apollo Lake на слайде двинули интересную во всех отношениях идею — отказ от SPI-чипа для хранения прошивки в пользу EMMC. Там и автоматический WL, и регистр EXT_CSD, в котором результаты WL и прогноз живучести можно наблюдать, и разделы специальные под загрузчики с защитой от записи и тайминг-атак, да и вообще плюсы сплошные. С другой же стороны, получив возможность иметь в прошивке не 16 МБ, а 16Гб, у некоторых производителей от такого обилия моментально снесет крышу, и в прошивку засунут не просто 3D-моделей и заказ пиццы, а вообще целиковый эмулятор андроида (привет, AMI DuOS), какой-нибудь еще линукс для административных задач, и парочку своих велосипедов на паровой тяге размером в полгигабайта.
          Я устал уже разным людям из индустрии говорить, что прошивки вообще и UEFI в частности «созданы, чтобы умирать», как PHP в свое время, и не нужно делать ОС общего назначения из UEFI Shell, там кишки наружу все и любое переполнение буфера — это компрометация всей системы. Все равно об стену горохом, мусора в прошивке с каждым годом все больше, а толку от нее все меньше.


          1. JerleShannara
            19.04.2016 15:06
            +2

            Идея насчёт eMMC хороша в свете последней моды с этим самым NVRAM вечно пишущимся, а насчёт 16Гб — ну асус в своё время пихал в платы БИОСной эпохи ExpressGate, работало неплохо. Другое дело, что в случае eMMC задачу разделить код прошивки и код перделок сделают через одно место, в эпоху P3-P4 я любил подпихивать через ROMOS во все платы, что через меня проходили самый обычный memtest86, получалось удобно, но безопасностью и не пахло даже. И я даже гарантию дам, что асус таки засунет всякой лабуды. Впрочем получить такую машинку с таким DOMом я бы не отказался, можно браузилку вконтактика засунуть, или банк-клиент на RO носитель, который всегда в материнке. Но, не вешать её на UEFI ничем, кроме загрузчика, нафиг нафиг нафиг мне голая опа выставленная в окно с надписью «засуньте сюда что-нить»
            П.С. Идею спёрли из микроконтроллерного/микропроцессорного мира — в тех-же армах давно уже на чипе микробутлоадер, который по конфигурации пинов грузит основной код с eMMC/SD/USART/чтотамзахрень


            1. CodeRush
              19.04.2016 15:14
              +1

              Идею адаптировали еще на Bay Trail, только там загрузка была двухстадийная и PEI все равно был только на флешке, а теперь вот удалось интегрировать контролер eMMC достаточно глубоко, чтобы грузится с него. Безопасность там обеспечат каким-нибудь аналогом BootGuard, при котором прошивку можно будет хранить хоть где — пока коллизии SHA256 не научились находить за полчаса, такой защиты хватит. Посмотрим, я не буду загадывать. Производители для индустрии у пользователя возможность добавить собственный код в прошивку не отбирали и не будут отбирать, а до всяких Asus'ов мне теперь уже дела почти нет, пусть хоть на голове танцуют.


          1. en1gma
            19.04.2016 21:30

            так ли отказ? там sata и pcie есть, там что можно собрать систему и без emmc… я б сказал, что добавили возможность и, вероятно, загрузка задаётся hw bootstrap, а ля любой embed.
            да и к чему такое удивление? что наконец в мир х86 пришла возможность «мультибута»? с помощью разной степени проприетарности загрузчиков что c голого nand что с emmc разный embed грузится как бы не с десяток лет.
            интересно, как будет защищена область с fw. тут а ля embed не выйдет, тут ос с парадигмой «пользователь — бог» крутиться будут. fw должна будет разруливать доступ к секторам с собой, или, например, заниматься эмуляцией блочного устроства, скрывая физический накопитель или, например, изменяя нумерацию секторов.


            1. CodeRush
              19.04.2016 21:47
              +1

              Я же так и написал, что это идея, хочешь используй eMMC в качестве хранилища прошивки, хочешь — продолжай использовать SPI. Да и не удивляюсь я ничему, выше уже писал, что на Bay Trail было почти то же самое. Вы меня как-то неправильно поняли, мне кажется.
              Защищена область будет очень просто — начиная со стандарта eMMC 4.4 на них имеется специальный RPMB-раздел размером до 32 Мб, на котором можно хранить ключи шифрования и прочую чувствительную информацию, и два раздела Boot0 и Boot1 (оба до 32 Мб), запись на которые можно запретить до перезагрузки во время инициализации прошивки. В итоге ничего разруливать не нужно — контролер eMMC разрулит все за нас.