Здравствуйте, уважаемые читатели. Когда-то очень давно, почти 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 на лету, поэтому есть смысл показать ту же самую структуру на скриншоте:

Легко видно, что перед нами заголовок хранилища 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 переменной
};

На этот раз на скриншноте можно показать сразу несколько переменных:

Точнее говоря, полторы: PchInit и часть Setup. Они имеют состояние 0x7F (VARIABLE_HEADER_VALID), атрибуты 0x07 (NV+BS+RT), длину имени 0x10 и 0x0C, длину данных 0x04 и 0x2B0, и GUID E6C2F70A-B604-4877-85BA-DEEC89E117EB и 4DFBBAAB-1392-4FDE-ABB8-C41CC5AD7D5D соответственно.

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


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 использует дополнительный атрибут 0x80000000 (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 переменной
};

На скриншоте такая переменная выглядит примерно так:

Маркер тот же, что и у обычных переменных, состояние в данном случае 0x3F (VARIABLE_ADDED), атрибуты — 0x27 (BS+NV+RT+TA), счетчик не задействован, зато задействована временная метка в формате EFI_TIME, индекс в БД публичных ключей также не задействован, размер имени — 0x08, размер данных — 0x64D, GUID — D719B2CB-3D3A-4596-A3BC-DAD00E67656F, а зовут эту переменную dbx.

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


Заключение

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

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


  1. CodeRush
    10.04.2016 18:58
    +1

    Давненько я не брал в руки шашек не писал длинных русских текстов, если найдете очепятку — пишите в Л/С, постараюсь исправить.


    1. Ziptar
      11.04.2016 00:14
      +3

      Спасибо за познавательную статью. А очепяток не заметил;)


  1. monah_tuk
    11.04.2016 08:57
    +1

    Кстати, а структуры просто для наглядности или это реальные куски кода? Если реальные, то как обстоят дела с выравниванием и паковкой полей?


    А за статью — спасибо! Всегда интересно почитать про форматы на железе — подучивает нейронную сеть в мозгу, потом помогает при анализе каких-то данных или просто отладки. Но из статьи я не понял: как была построен сам процесс разбора? Т.е. результат вижу: такой-то формат, такие-то данные, а как происходило понимание какие поля чему соответствуют, какие флаги ставятся в полях флагов и так далее.


    1. CodeRush
      11.04.2016 09:36
      +3

      Совершенно реальные, а по суффиксам в именах даже можно догадаться, с каких систем взяты.
      Про выравнивание переменных я упоминал, там его нет во всех случаях, кроме гипотетического IA64, но я реальных образов с таких машине не видел, только сборки QEMU + TianoCore, там выравнивание есть и оно по восьмибайтовой границе. По упаковке — все пакуется наиболее плотно.
      Разбор происходит следующим образом: если есть доступ к машине, то можно получить имена, GUIDы, аттрибуты и данные переменных, просто читая их последовательным вызовом UEFI runtime — функций GetNextVariableName и GetVariable. В итоге половина данных уже есть и их можно затем найти в дампе, остается угадать формат заголовков и значения некоторых аттрибутов, тут помогает открытый код проектов TianoCore, CHIPSEC, плюс некоторые добрый люди в сообществе, вроде тов. xvilka, иногда делятся своими соображениями или даже кодом.
      Если же доступа нет, то либо стоит им разжиться, либо просишь дампы прошивки и вывод команды dmpstore и играешь в угадайку по ним.


  1. extrimov
    11.04.2016 09:36

    Отличная статья.
    P.S. По поводу опечаток заметил в начале:«я написал ПАРУ статей о форматах данных», правильно будет «написал НЕСКОЛЬКО статей», так как ПАРА применяется преимущественно «Две штуки чего-либо», а 3 — уже «несколько».


    1. CodeRush
      11.04.2016 09:37

      Это такой каламбур неудачный про то, что там были части первая, полуторная, и вторая. Я согласен с тем, что «три — это куча, а два — это не куча», но оставлю так.


  1. awoland
    11.04.2016 16:25
    +1

    Ждем продолжения.



  1. bkotov
    11.04.2016 19:55
    +1

    Спасибо. Было время, умудрился пару раз убить материнскую плату своего ноута с прошивкой на основе Insyde. Зато разобрался во всей этой теме, собрал нужный инструментарий и добился желаемого эффекта (разлочки скрытых меню setup utility и изменения таблицы температур задающих скорость вращения кулера в прошивке EC). Кажется был 2009-2010 год и прошивку приходилось потрошить с помощью скриптов на python и slic tool, т.к. нормальный инструментарий отсутствовал в паблике или еще не был написан. Ну и скриншоты из Hex Dump (или как называется этот софт?) навевают ностальгию, тоже визуализировал структуры с помощью цветных областей.


    1. CodeRush
      11.04.2016 20:02

      Всегда пожалуйста.
      У меня тоже мое увлечение (переросшее потом в работу) началось со сломанной материнской платы, прошивка которой просто перестала стартовать после очередного обновления. Пришлось покупать прошитый чип на Ебее, но оказалось, что в этом чипе нет данных SMBIOS, и некоторый достаточно дорогой софт после замены отказался признать систему своей. Пришлось разбираться с восстановлением данных, по результату была написана утилита FD44Editor, потом пришлось соорудить комплект для прошивания (FTK), т.к. стандартные утилиты ASUS меня не устраивали, потом я устал от сложностей с PhoenixToot и сел писать собственный велосипед, и все заверте…
      Софт этот называется HxD, это один из самых простых и маленьких хекс-редакторов для Windows с GUI. Есть гораздо более продвинутые редакторы вроде 010 Editor или WinHex, но мне хватало и этого всегда.