Начатый в предыдущих трех частях разговор о форматах хранилищ 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)
JerleShannara
19.04.2016 10:17+3> Зачем так сделано — еще одна великая тайна, я не могу придумать уважительной причины такому поведению.
Железячник во мне говорит что это такой своеобразный wear leveling, и в AMI таким образом попытались продлить жизнь SPI флешке. В итоге имеем всю область nvram с одинаковым износом.
Да, есть у меня одна плата которая умерла какраз по износу флеша — верификация обрамывается ровно на одном блоке, причем постоянно, я думаю не надо говорить, что в дампе прошивки находилось на этом месте. Машинка перезагружалась раз 6 за день, прожила два года, подарила мне нелюбовь к macronix.CodeRush
19.04.2016 10:33+1Может быть, не исключено, но можно придумать и менее хитрые способы, при которых хотя бы не нужно проходить по односвязным спискам до конца, чтобы до данных добраться.
Плат с «уставшим» SPI-чипом я тоже перевидал изрядно, причем симтомы там почти каждый раз разные, и сразу не поймешь, почему система сначала 5 лет работала в станке без сбоев, а потом начала на загрузке виснуть, на выключении виснуть, или вообще просто виснуть, без внешних причин. Никогда на SPI-чип не подумал бы, если бы его замена не решала бы проблему.
CodeRush
19.04.2016 10:40+1С другой строны, в качестве wear leveling'а отлично работает стратегия VSS, когда с предыдущей структуры снимается бит Valid, и в конец записывается следующая, не важно, большего там размера данные, меньшего или такого же. Дошли до конца — делаем сборку мусора. Подход AMI в данном случае лучше тем, что не нужно копировать имя и GUID, т.е. ресурс флеша все-таки экономится. Заодно становится понятно, зачем поле Next обновляют не в первой записи, а в последней — пара месяцев, и первую запись переменной MonitonicCounter или MemScrambleSeed затерло бы иначе просто до дыр, а так — все в порядке. В общем, тут все же больше экономия места на флеше, чем управление износом, на мой взгляд.
JerleShannara
19.04.2016 14:13+2Ну как говорится модель Велосипеды-Баги-Костыли используется крупными мировыми разработчиками ПО. А с экономией я скажу так — корбутовская прошивка влезает в 1мб и спокойно работает и не жужжит (ну правда тоже гадит в флеш, но это решаемо) без особых проблем — система грузится, плюшек и фишек нету, имеем на выходе интерфейс старого доброго биоса на новых платах. С другой стороны если насовать в UEFI кучу «унЕкальных прЕлажений», добавить 3D моделей и прочих свистелок и перделок(привет асусу/асроку, не забудьте добавить функцию заказа пиццы в свои материнки, т.к. пока до вашей техподдержки из uefi достучишься очень хочется кушать, т.к. реализация vpn тормозит у вас) — можно и в 8Мб не влезть, экономия 5кб при таких объемах других модулей мне кажется просто смешной.
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, там кишки наружу все и любое переполнение буфера — это компрометация всей системы. Все равно об стену горохом, мусора в прошивке с каждым годом все больше, а толку от нее все меньше.JerleShannara
19.04.2016 15:06+2Идея насчёт eMMC хороша в свете последней моды с этим самым NVRAM вечно пишущимся, а насчёт 16Гб — ну асус в своё время пихал в платы БИОСной эпохи ExpressGate, работало неплохо. Другое дело, что в случае eMMC задачу разделить код прошивки и код перделок сделают через одно место, в эпоху P3-P4 я любил подпихивать через ROMOS во все платы, что через меня проходили самый обычный memtest86, получалось удобно, но безопасностью и не пахло даже. И я даже гарантию дам, что асус таки засунет всякой лабуды. Впрочем получить такую машинку с таким DOMом я бы не отказался, можно браузилку вконтактика засунуть, или банк-клиент на RO носитель, который всегда в материнке. Но, не вешать её на UEFI ничем, кроме загрузчика, нафиг нафиг нафиг мне голая опа выставленная в окно с надписью «засуньте сюда что-нить»
П.С. Идею спёрли из микроконтроллерного/микропроцессорного мира — в тех-же армах давно уже на чипе микробутлоадер, который по конфигурации пинов грузит основной код с eMMC/SD/USART/чтотамзахреньCodeRush
19.04.2016 15:14+1Идею адаптировали еще на Bay Trail, только там загрузка была двухстадийная и PEI все равно был только на флешке, а теперь вот удалось интегрировать контролер eMMC достаточно глубоко, чтобы грузится с него. Безопасность там обеспечат каким-нибудь аналогом BootGuard, при котором прошивку можно будет хранить хоть где — пока коллизии SHA256 не научились находить за полчаса, такой защиты хватит. Посмотрим, я не буду загадывать. Производители для индустрии у пользователя возможность добавить собственный код в прошивку не отбирали и не будут отбирать, а до всяких Asus'ов мне теперь уже дела почти нет, пусть хоть на голове танцуют.
en1gma
19.04.2016 21:30так ли отказ? там sata и pcie есть, там что можно собрать систему и без emmc… я б сказал, что добавили возможность и, вероятно, загрузка задаётся hw bootstrap, а ля любой embed.
да и к чему такое удивление? что наконец в мир х86 пришла возможность «мультибута»? с помощью разной степени проприетарности загрузчиков что c голого nand что с emmc разный embed грузится как бы не с десяток лет.
интересно, как будет защищена область с fw. тут а ля embed не выйдет, тут ос с парадигмой «пользователь — бог» крутиться будут. fw должна будет разруливать доступ к секторам с собой, или, например, заниматься эмуляцией блочного устроства, скрывая физический накопитель или, например, изменяя нумерацию секторов.CodeRush
19.04.2016 21:47+1Я же так и написал, что это идея, хочешь используй eMMC в качестве хранилища прошивки, хочешь — продолжай использовать SPI. Да и не удивляюсь я ничему, выше уже писал, что на Bay Trail было почти то же самое. Вы меня как-то неправильно поняли, мне кажется.
Защищена область будет очень просто — начиная со стандарта eMMC 4.4 на них имеется специальный RPMB-раздел размером до 32 Мб, на котором можно хранить ключи шифрования и прочую чувствительную информацию, и два раздела Boot0 и Boot1 (оба до 32 Мб), запись на которые можно запретить до перезагрузки во время инициализации прошивки. В итоге ничего разруливать не нужно — контролер eMMC разрулит все за нас.
Garrett
Нда, кислые ягодки.
Спасибо за описание формата!
Теперь понятно откуда некоторые особенности и баги UEFI растут.
Столько возможностей для багов и эксплоитов =\
CodeRush
На здоровье. Формат, конечно, полный песец. Ничего не мешало использовать формат VSS, который хоть и занимает побольше, зато прямой как палка и парсить его — одно удовольствие, но нет, если стандарт позволяет свободу в реализации, ей нужно пользовать именно таким образом.
Забыл сказать спасибо ребятам из проекта CHIPSEC за референсную реализацию парсига, на которую я частенько оглядывался, и тов. xvilka, который помогал с разбором NVAR и образами, и советами, и даже кодом.
AVX
Немного не в тему, но если уж форматы nvram более-менее известны, то почему-то на сайте subzero.io нет пока поддержки NVRAM:
«UEFI Varibles (NVRAM)
Coming Soon»
CodeRush
Потому что для Subzero.io используется обрезанная версия uefi-firmware-parser, в которую разбор NVRAM еще не портирован из UEFITool или CHIPSEC. Либо Тедди допилит UFP, либо мне хватит сил на libffsparser, но поддержка эта рано или поздно добавится.