Back to Home

NVRAM device in UEFI-compatible firmware, part four

UEFI · firmware · NVRAM · data formats · UEFITool · hub firmware || GTFO!

NVRAM device in UEFI-compatible firmware, part four

    Hello again, dear readers.
    The talk we started in the previous three parts about the NVRAM storage formats used by various UEFI implementations is coming to its logical end. Only one format remained unexamined - NVAR, which is used in firmware based on the AMI Aptio code base. AMI company at one time was able to "saddle" almost the entire market of firmware for desktop and server motherboards, so the NVAR format was almost more common than the original and "standard" VSS.
    If you are interested in what the AMI NVRAM storage format is good and bad for, welcome to cat.

    Disclaimer No. 4


    Repetition - the mother of stuttering is the basis of memorization, so the author makes no attempts to convince the reader that picking in the firmware is a dangerous thing, and before any changes you should make a backup copy on the programmer so that then it will not be excruciatingly painful for a couple spent aimlessly to restore the system days (or weeks). The author, as before, is not responsible for anything other than slips, information about which can be sent to the L / C, you use this knowledge obtained by reverse engineering at your own peril and risk.

    AMI NVAR


    Well, finally I managed to get to the last in my list of NVRAM storage format, which I will call NVAR by the signature used in its header. Unlike all the other formats described in the previous parts, the data in the NVAR format is not stored in the volume with the GUID FFF12B8D-7696-4C8B-A985-2747075B4F50 (EFI_SYSTEM_NV_DATA_FV_GUID), but in a regular FFS file with the GUID CEF5B9A3F476 -E98143E0422C (NVAR_STORE_FILE_GUID) or 9221315B-30BB-46B5-813E-1B1BF4712BD3 (NVAR_EXTERNAL_DEFAULTS_FILE_GUID).
    The file with the first GUID is stored in a separate volume specially designed for NVRAM, most often there are two such volumes - the primary and backup, and if something happens to the data or the format of the primary, and the NVRAM driver can determine this, it switches to using backup storage . Sometimes the backup storage is full even at the stage of the firmware assembly, but more often space is simply left for it and it is created at the first start (therefore, the first start after updating the firmware can be quite long). The second file is stored in the DXE volume, has a slightly different format, depending on the specific platform, and is used to restore the "defaults" of some variables in the event that both the main and additional storage are irreparably damaged.

    Since the data in the NVAR format is stored inside the file, information about the maximum storage size and where to find it is already available for the firmware thanks to the UEFI FFS services , so the AMI developers did not invent any additional headers, and immediately after the header, with the maximum Packed and without alignment, NVAR recordings begin.

    The title of this entry looks like this:
    struct NVAR_ENTRY_HEADER {
        UINT32 Signature;      // Сигнатура NVAR
        UINT16 Size;           // Размер записи вместе с заголовком
        UINT32 Next : 24;      // Смещение следующего элемента в списке,
                               // либо специально значение (0 либо 0xFFFFFF в зависимости ErasePolarity)
        UINT32 Attributes : 8; // Атрибуты записи
    };
    

    He’s in the screenshot:

    So far, everything is very simple, first the correct signature is NVAR, then the record size is 0x5D3, the Next field is empty, the attributes are 0x83, the incomprehensible eight-bit field is 0x00 and the variable name in ASCII encoding is StdDefaults .

    It turns out that the data data format is highly dependent on the bits of the Attributes field , which can be represented as follows:
    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       // Запись валидна, если этот бит не установлен, запись должна быть пропущена
    };
    

    Thus, our 0x83 attributes are actually EntryValid + AsciiName + RuntimeVariable , and the previously incomprehensible eight-bit field is an index in the GUID database. I also note that the length of the name is not stored anywhere, and in order to find the beginning of the data, you need to call strlen () each time. If the LocalGuid attribute were set, instead of the 1-byte index, the entire GUID would be present at 16. It turns out that the GUID database (I’ll tell you a secret, it is at the very end of the file and grows upwards, i.e. our zero GUID is the last 16 byte of the file with NVRAM storage, the first - the penultimate 16 bytes and so on) can be stored no more than 256 different GUIDs, but this is enough for any possible NVRAM applications at the moment, and it saves a lot of space.

    The same from the UEFITool NE window :


    The attribute values ​​show how the format has evolved over time. Before the UEFI 2.1 standard, NVRAM variables had only 3 possible attributes: NV , BS , RT. The NV attribute is meaningless to store, because only such variables enter the NVRAM storage, and BS and RT are not mutually exclusive, and a “healthy” change can be either only BS or BS + RT, therefore, only one bit was used for these attributes - RuntimeVariable . Great, it turned out to save as much as 24 bits per variable.
    Then it turned out that the physical level of NVRAM is not always reliable, and it would be necessary to read the checksum from the data in order to distinguish the damaged variables from the normal ones, so the ExtendedHeader bit was set , and the checksum was stored at the very end of the record, after the data.
    Some time passed, and under the pressure of Microsoft in UEFI 2.1 another attribute was added - HW , used for variablesWHEA . Okay, the HwErrorRecord bit was brought under it , you need to.
    Then in UEFI 2.3.1C, SecureBoot was unexpectedly added along with two new attributes for variables - AV and AW . Fortunately, it’s not very necessary to store the latter (since there is only one such variable, dbx ), and the last free bit of AuthWrite had to be allocated for the first .
    It was not long to rejoice, already in UEFI 2.4 another attribute was added - TA, which, suddenly, there was nowhere to poke, because at one time saved as many as 24 bits. As a result, I had to create an additional field in the extended header, which is stored after the data. There I had to store a timestamp and a hash for AV / TA variables.

    After all these improvements, the extended header turned out like this:
    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
    };
    

    It is also in the screenshot:

    Total, the size of the extended header is 0x2C, the checksum is 0x10, the hash is zero, the timestamp is 0x5537BB5D and the attributes are 0x21 ( ExtChecksum + ExtTimeBased ).

    It turns out that in order to get the value of the attributes for any variable, it needs to be analyzed as a whole, calculating the displacements dynamically and collecting values ​​from several different places in the file. And all this exactly because once a long time saved as much as 24 bytes. You will develop your own format - do not save on matches, do yourself a favor from the future!

    But this is not all, because we still have not considered the DataOnly attribute and the Next fieldin the title. They are used in order to save on GUID, name and attributes if the variable to which the recording is carried out already exists. Instead of removing the EntryValid attribute from the old record and writing the whole new one, the Next field is filled in the header of the old record, and in the free space of the file, a record with the DataOnly attribute is created , to which this Next also refers, and there is no longer a GUID , not a name, but there is an extended header. Moreover, when the value of the variable is overwritten the next time, the Next fieldcorrected not in the first entry in this peculiar singly linked list, but in the last, by extending the list. And since there are variables that are updated at each reboot (the same MonotonicCounter ), very soon the NVRAM is filled to the brim with copies of the data of this variable, and access to it slows down with each reboot until it turns out that there is no space at all, and the NVRAM driver needs to build garbage. Why is this done - another great secret, I can not come up with a good reason for such behavior.

    In UEFITool NE, I had to add the Go to data action , which works on variables of the Link type (i.e. those with the Next fieldisn’t empty) and selects the last element in a singly linked list, which stores the current data of the variable, and not the ones that were damn there before:


    Access to NVRAM variables works like this on 95% of desktops and servers over the past 5 years. Look, dear readers, what the result of saving on bytes and weighting the old format with new crutches in a desperate attempt not to rewrite the NVRAM driver again, and do not, please.

    Conclusion


    I do not know what censorship has to say about the NVAR format. In pursuit of compactness, AMI managed to sacrifice everything else, and if at first it seemed that this sacrifice was small and inconspicuous, with the development of the UEFI specification, the format turned into a local analogue of Abomination , assembled from pieces of incomprehensible what, sewn incomprehensibly. We are all lucky that the AMI NVRAM driver is good enough to promptly and quietly remove garbage into the storage, switch to the backup storage if the main one is damaged, start with the destroyed NVRAM, survive the recording “right under the cover”, etc., but this is achieved more likely not thanks, but contrary.
    The story with the NVRAM formats, I hope, has come to an end, now you know about them almost as much as I do. Thank you very much for your attention, successful firmware, chips, and NVRAMs.

    Read Next