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

    И снова здравствуйте, уважаемые читатели.
    Начатый в предыдущих трех частях разговор о форматах хранилищ 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
      Нда, кислые ягодки.
      Спасибо за описание формата!
      Теперь понятно откуда некоторые особенности и баги UEFI растут.
      Столько возможностей для багов и эксплоитов =\
      • 0
        На здоровье. Формат, конечно, полный песец. Ничего не мешало использовать формат VSS, который хоть и занимает побольше, зато прямой как палка и парсить его — одно удовольствие, но нет, если стандарт позволяет свободу в реализации, ей нужно пользовать именно таким образом.
        Забыл сказать спасибо ребятам из проекта CHIPSEC за референсную реализацию парсига, на которую я частенько оглядывался, и тов. xvilka, который помогал с разбором NVAR и образами, и советами, и даже кодом.
        • +1
          Немного не в тему, но если уж форматы nvram более-менее известны, то почему-то на сайте subzero.io нет пока поддержки NVRAM:
          «UEFI Varibles (NVRAM)
          Coming Soon»

          • 0
            Потому что для Subzero.io используется обрезанная версия uefi-firmware-parser, в которую разбор NVRAM еще не портирован из UEFITool или CHIPSEC. Либо Тедди допилит UFP, либо мне хватит сил на libffsparser, но поддержка эта рано или поздно добавится.
      • +3
        > Зачем так сделано — еще одна великая тайна, я не могу придумать уважительной причины такому поведению.
        Железячник во мне говорит что это такой своеобразный wear leveling, и в AMI таким образом попытались продлить жизнь SPI флешке. В итоге имеем всю область nvram с одинаковым износом.
        Да, есть у меня одна плата которая умерла какраз по износу флеша — верификация обрамывается ровно на одном блоке, причем постоянно, я думаю не надо говорить, что в дампе прошивки находилось на этом месте. Машинка перезагружалась раз 6 за день, прожила два года, подарила мне нелюбовь к macronix.
        • +1
          Может быть, не исключено, но можно придумать и менее хитрые способы, при которых хотя бы не нужно проходить по односвязным спискам до конца, чтобы до данных добраться.
          Плат с «уставшим» SPI-чипом я тоже перевидал изрядно, причем симтомы там почти каждый раз разные, и сразу не поймешь, почему система сначала 5 лет работала в станке без сбоев, а потом начала на загрузке виснуть, на выключении виснуть, или вообще просто виснуть, без внешних причин. Никогда на SPI-чип не подумал бы, если бы его замена не решала бы проблему.
          • +1
            С другой строны, в качестве wear leveling'а отлично работает стратегия VSS, когда с предыдущей структуры снимается бит Valid, и в конец записывается следующая, не важно, большего там размера данные, меньшего или такого же. Дошли до конца — делаем сборку мусора. Подход AMI в данном случае лучше тем, что не нужно копировать имя и GUID, т.е. ресурс флеша все-таки экономится. Заодно становится понятно, зачем поле Next обновляют не в первой записи, а в последней — пара месяцев, и первую запись переменной MonitonicCounter или MemScrambleSeed затерло бы иначе просто до дыр, а так — все в порядке. В общем, тут все же больше экономия места на флеше, чем управление износом, на мой взгляд.
            • +2
              Ну как говорится модель Велосипеды-Баги-Костыли используется крупными мировыми разработчиками ПО. А с экономией я скажу так — корбутовская прошивка влезает в 1мб и спокойно работает и не жужжит (ну правда тоже гадит в флеш, но это решаемо) без особых проблем — система грузится, плюшек и фишек нету, имеем на выходе интерфейс старого доброго биоса на новых платах. С другой стороны если насовать в UEFI кучу «унЕкальных прЕлажений», добавить 3D моделей и прочих свистелок и перделок(привет асусу/асроку, не забудьте добавить функцию заказа пиццы в свои материнки, т.к. пока до вашей техподдержки из uefi достучишься очень хочется кушать, т.к. реализация vpn тормозит у вас) — можно и в 8Мб не влезть, экономия 5кб при таких объемах других модулей мне кажется просто смешной.
              • +1
                Можно и 64 кб впихнуть, если стараться, но это все никому не нужно.
                Сейчас вот в недавней презентации новой линейки Atom под кодовым именем Apollo Lake на слайде двинули интересную во всех отношениях идею — отказ от SPI-чипа для хранения прошивки в пользу EMMC. Там и автоматический WL, и регистр EXT_CSD, в котором результаты WL и прогноз живучести можно наблюдать, и разделы специальные под загрузчики с защитой от записи и тайминг-атак, да и вообще плюсы сплошные. С другой же стороны, получив возможность иметь в прошивке не 16 МБ, а 16Гб, у некоторых производителей от такого обилия моментально снесет крышу, и в прошивку засунут не просто 3D-моделей и заказ пиццы, а вообще целиковый эмулятор андроида (привет, AMI DuOS), какой-нибудь еще линукс для административных задач, и парочку своих велосипедов на паровой тяге размером в полгигабайта.
                Я устал уже разным людям из индустрии говорить, что прошивки вообще и UEFI в частности «созданы, чтобы умирать», как PHP в свое время, и не нужно делать ОС общего назначения из UEFI Shell, там кишки наружу все и любое переполнение буфера — это компрометация всей системы. Все равно об стену горохом, мусора в прошивке с каждым годом все больше, а толку от нее все меньше.
                • +2
                  Идея насчёт eMMC хороша в свете последней моды с этим самым NVRAM вечно пишущимся, а насчёт 16Гб — ну асус в своё время пихал в платы БИОСной эпохи ExpressGate, работало неплохо. Другое дело, что в случае eMMC задачу разделить код прошивки и код перделок сделают через одно место, в эпоху P3-P4 я любил подпихивать через ROMOS во все платы, что через меня проходили самый обычный memtest86, получалось удобно, но безопасностью и не пахло даже. И я даже гарантию дам, что асус таки засунет всякой лабуды. Впрочем получить такую машинку с таким DOMом я бы не отказался, можно браузилку вконтактика засунуть, или банк-клиент на RO носитель, который всегда в материнке. Но, не вешать её на UEFI ничем, кроме загрузчика, нафиг нафиг нафиг мне голая опа выставленная в окно с надписью «засуньте сюда что-нить»
                  П.С. Идею спёрли из микроконтроллерного/микропроцессорного мира — в тех-же армах давно уже на чипе микробутлоадер, который по конфигурации пинов грузит основной код с eMMC/SD/USART/чтотамзахрень
                  • +1
                    Идею адаптировали еще на Bay Trail, только там загрузка была двухстадийная и PEI все равно был только на флешке, а теперь вот удалось интегрировать контролер eMMC достаточно глубоко, чтобы грузится с него. Безопасность там обеспечат каким-нибудь аналогом BootGuard, при котором прошивку можно будет хранить хоть где — пока коллизии SHA256 не научились находить за полчаса, такой защиты хватит. Посмотрим, я не буду загадывать. Производители для индустрии у пользователя возможность добавить собственный код в прошивку не отбирали и не будут отбирать, а до всяких Asus'ов мне теперь уже дела почти нет, пусть хоть на голове танцуют.
                  • 0
                    так ли отказ? там sata и pcie есть, там что можно собрать систему и без emmc… я б сказал, что добавили возможность и, вероятно, загрузка задаётся hw bootstrap, а ля любой embed.
                    да и к чему такое удивление? что наконец в мир х86 пришла возможность «мультибута»? с помощью разной степени проприетарности загрузчиков что c голого nand что с emmc разный embed грузится как бы не с десяток лет.
                    интересно, как будет защищена область с fw. тут а ля embed не выйдет, тут ос с парадигмой «пользователь — бог» крутиться будут. fw должна будет разруливать доступ к секторам с собой, или, например, заниматься эмуляцией блочного устроства, скрывая физический накопитель или, например, изменяя нумерацию секторов.
                    • +1
                      Я же так и написал, что это идея, хочешь используй eMMC в качестве хранилища прошивки, хочешь — продолжай использовать SPI. Да и не удивляюсь я ничему, выше уже писал, что на Bay Trail было почти то же самое. Вы меня как-то неправильно поняли, мне кажется.
                      Защищена область будет очень просто — начиная со стандарта eMMC 4.4 на них имеется специальный RPMB-раздел размером до 32 Мб, на котором можно хранить ключи шифрования и прочую чувствительную информацию, и два раздела Boot0 и Boot1 (оба до 32 Мб), запись на которые можно запретить до перезагрузки во время инициализации прошивки. В итоге ничего разруливать не нужно — контролер eMMC разрулит все за нас.

            Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

            Самое читаемое