Пристрій 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.2 у змінних 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.1 C несподівано додали 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 + ExtAuthWrite + 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'ів.

Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.