Header bannerHeader banner
February 8, 2024

The Dark Side of UEFI: A technical Deep-Dive into Cross-Silicon Exploitation

Binarly efiXplorer Team

The Binarly research team presents the first public research on Cross-Silicon Exploitation in the UEFI ecosystem: From x86 to ARM.

Traditionally, disclosures of vulnerabilities in UEFI firmware have predominantly focused on the x86 ecosystem, particularly on devices powered by Intel or AMD.  However, with the explosion of the ARM device market, there has been a push towards expansion and unification, leading to the development of UEFI firmware for ARM hardware as well.  This has inevitably led to the first public disclosure in the history of UEFI specification related to the ARM device ecosystem. In January 2023 BINARLY disclosed multiple vulnerabilities affecting Qualcomm reference code and impacting different device vendors and IBVs: Multiple Vulnerabilities in Qualcomm and Lenovo ARM-based Devices

This was the result of research to determine whether the attacks and classes of bugs can be the same on both ARM and x86 devices. Our work included the reconstruction of the boot flow on ARM devices with UEFI firmware, as well as its security features and mitigations in comparison to x86.  We discuss the coverage and shortcomings of the security protections applied, and achieving arbitrary code execution during boot as the result of exploitation of a vulnerability in the DXE phase with the help of ROP technique (to bypass DEP).

Last May at OffensiveCon (Video), the Binarly REsearch team covered various aspects of the unification of firmware development with frameworks like UEFI and the security implications from an attack and defense perspective. In this blog, we provide a deep dive of the major ideas emerging from this research project.

Talk recording:

Slides: https://github.com/binarly-io/Research_Publications/tree/main/OffensiveCon_2023

Overview of the UEFI ecosystem on ARM

AMI described the very first ideas of developing UEFI firmware for ARM based devices at UEFI plugfest in 2016:

https://uefi.org/sites/default/files/resources/UEFI_Plugfest_March_2016_AMI.pdf

Besides moving to a new CPU architecture, the most obvious difference was implementing an isolated execution environment for SMM-like boot time and runtime services inside ARM’s TrustZone. In the end, every device would have a bootROM (not updatable) SoC-specific firmware, which is typical for ARM CPUs. The system firmware would consist of three parts: UEFI firmware for boot time environment, hypervisor and TrustZone firmware usually build on ARM Trusted Firmware (TF) and Trusted Execution Environment (TEE) supplemented by a lot of code to support UEFI management mode (MM) functionality and various features including security.

Difference between Normal and Secure world - diagram of components

Keeping in mind how a perfect concept could differ from reality, we decided to check out a real UEFI firmware for a real ARM-based device.

Playing with the test ARM device (Windows Dev Kit 2023)

As mentioned above, the target device we chose was Microsoft Windows Dev Kit 2023 (aka project “Volterra”) with Qualcomm Snapdragon 8cx Gen 3 CPU. We chose this device because we were sure that the firmware on this device contains the vulnerabilities we are interested in. Moreover, this device is one of the most affordable ones on the market.

The only disadvantage is this device is difficult to be Linux-friendly, so we decided to conduct our experiments from Windows and from UEFIShell (using our own EFI drivers and applications).

Before any experiments, it is best to get a UEFI firmware image for the current device to search for vulnerabilities, examine boot flow, check for static configurations, etc. 

In general, there are 3 ways to do so:

  • Download from OEM support page;
  • Do a software dump;
  • Do a physical (hardware) dump.

We decided to exclude hardware interference where possible and get the firmware dump with a software way. For Microsoft devices we can extract UEFI firmware images from the recovery packages. For example, in the case of the Microsoft Surface Pro X device, we can find Surface_UEFI_*.bin file inside install.swm file:

Result of run UEFI phrase grep on install.swm file - at least 5 files

Unfortunately we were not able to find the UEFI firmware image in the recovery package for Windows Dev Kit 2023. However, after booting into UEFIShell we discovered the following:

  • Firmware is mapped into physical memory (BIOS region only);
  • Some FDs are mounted during the boot time, here is the output of map command in UEFIShell:
output of map command in UEFIShell - FS0, FS2, FS1 is highlighted

Dumping firmware from physical memory

In order to read BIOS region content from physical memory, we only need to obtain the start address and end address. It is possible to get this information with device command from UEFIShell:

the start address and end address taken from result of UEFISHell command run

If we will read 200 bytes by start address using dmem command, we can make sure that the BIOS region is actually located at address 0x9f000000 (since data match the EFI Firmware Volume Header structure):

Result of run dwem on region start address - first three rows highlighted

As we can see from device command output, the size of the BIOS region is 0x3e0000. However, we have managed to dump 0x5d0000 bytes – maximum possible size to read in a row.

Since it is not possible to dump memory contents to a file with standard UEFIShell commands, we implemented a simple tool (similar to dmem but with additional functionality): DumpMem.

After that we dumped firmware with the following command:

DumpMem.efi 0x9f000000 0x5d0000 uefi.bin

Below is the part of the dumped firmware, parsed with the UEFITool):

UEFITool interface with dumbed firmware opened in it.

Reading firmware content from mounted filesystems

As mentioned before, we discovered that some parts of the firmware (specific files/volumes) were mounted to different FS during the boot. Below is a description of what and where it is mounted:

  • fs2: EFI files from the firmware image
EFI files from the firmware image
  • fs0: Decompressed contents of 9E21FD93-9C72-4C15-8C4B-E77F1DB2D792;
  • fs1: Decompressed contents of 8ACF1180-6AF9-436D-A8EE-F7043C19AF18;
  • fs7: Static ACPI tables;
  • fs8: TZAPPS (trusted applications);
  • fs9: Firmware for Qualcomm Hexagon (Audio DSP);
  • Other fs contain ACPI tables, SMBIOS templates and TZAPPS backups.

This way SEC or DXE modules, configuration files, TZ apps and other firmware components can simply be copied to the USB without any memory dumping.

A visual representation of the different FS is shown below:

A visual representation of the different FS

Hence, any UEFI research should be started with booting into UEFIShell.

UEFI Platform Configuration file

Another gift for us was the uefiplat.cfg file. This file contains the following configuration items:

  • Config
  • MemoryMap
MemoryMap config item
  • RegisterMap
  • ConfigParameters
ConfigParameters config item with SecBootEnableFlag highlighted

This file is write protected and it is not possible to change the platform configuration by rewriting this file. However, this file contains a lot of useful information, which simplified our research and helped to reconstruct the boot flow.

Reconstructing The System Design

With this information and some reverse-engineering, we managed to reconstruct the entire trusted boot chain. As mentioned before, powering on the ARM CPU leads to it running the firmware from its BootROM which is a Root of Trust. The exception level is lowest (the privileges are highest) at this point: EL3. Without switching to any other exception level it verifies and runs:

  • Primary Boot Loader (PBL) containing Worlds-switch implementation and Secure Monitor to support Secure Monitor Calls (SMCs) from Normal and Secure Worlds;
  • Extended Boot Loader (XBL), which in turn verifies and runs 
    • CDT image - display support;
    • AOP (Always-On Processor) firmware - low power mode CPU on duty;
    • XBL config - description of further boot steps.
Tree of UEFI capsule with PBL+XBL, CDT, XBL Config and AOP highlighted

Here is the scheme of the given trusted boot chain part:

scheme of the given trusted boot chain part: left to right - ARM SoC chip -> PBL (SBL1) -> XBL (SBL2) -> (CDT Image, XBL config, AOP Firmware)

The boot of the Secure Worlds continues with bringing up of TrustZone (TZ) environment, which means verifying and running:

  • QSEE Dev Config;
  • Qualcomm Universal Peripheral (QUP) - TZ drivers;
  • Qualcomm Secure Execution Environment (QSEE) - TZ kernel:
    • Trustlets - TZ apps.

The Normal World’s goal is to prepare in turn an OS boot environment:

  • UEFI SEC phase;
  • Qualcomm Hypervisor Execution Environment (QHEE);
  • UEFI DXE phase.

The entire Secure World and Normal World trusted boot chain is shown on the following scheme:

Since all disclosed vulnerabilities are related to improper handling of EFI NVRAM variables, we have to also sort out how accessing the EFI variables is implemented on ARM.

On x86 it was quite clear:

Data-Only Attacks Against UEFI BIOS

On ARM, given the architecture specifics, the way to read and write them is different:

This knowledge allows us to prove that at least one of the discovered classes of vulnerabilities is fully portable from x86 to ARM in exploitation terms.

This is quite a new attack vector: OOB read (memory leak) with GetVariable/SetVariable pattern. The vulnerable code snippet in general looks like this:

  • DataSize is updated by gRT->GetVariable() to the actual size of the requested EFI variable;
  • DataSize is not re-initialized prior to gRT->SetVariable();
  • Hence, when the size of the existing NVRAM variable Variable1 is larger than the original DataSize value (N), the (DataSize - N) additional bytes will be written to Variable2 NVRAM variable on the call to gRT->SetVariable().

To exploit such a vulnerability, the attacker simply has to change the first variable (to change the DataSize). The exploitation of such vulnerabilities will work in a similar way on x86 and AArch64, and does not require binary exploitation skills.

A demo of PoC for such class of vulnerabilities is here:

Mitigations applied on ARM-based UEFI firmware in comparison to x86-based firmware

Since we smoothly moved to a discussion of discovered vulnerabilities and how to trigger them (access variables on ARM) it would be also nice to think about the exploitation path and to check what mitigations have been actually enabled on ARM. 

On x86, the security model is described in one of our previous research project:

Data-Only Attacks Against UEFI BIOS

TLDR; a lot of mitigation mechanisms are introduced but almost none of them are enabled in the wild. 

However, the ARM UEFI firmware we looked at contained applied mitigations to prevent stack buffer overflow exploitation. During this research, for the first time we found firmware with stack canary implemented in production UEFI firmware!

Here is the comparison of mitigations applied on x86 and ARM:

Mitigation

x86

ARM

ASLR

-

-

DEP

+/-

+

Stack canary

-

+/-

ASLR

ASLR is not applied on both x86 and ARM (based on our experience). Moreover, ASLR is not supported even in the EDK2 reference code. Work in this direction has been ongoing for a long time:

https://github.com/jyao1/SecurityEx/tree/master/AslrPkg, https://edk2-docs.gitbook.io/a-tour-beyond-bios-mitigate-buffer-overflow-in-ue/address_space_layout_randomization/enable_aslr_for_uefi_in_edkii.

DEP

DEP is partially applied on x86 and fully applied on ARM. We have seen cases on x86 platforms where code execution on the stack/heap was possible and not possible. There are other ways to store shellcode that will survive reboot. For example, via NVRAM. 

Earlier we demonstrated that vulnerabilities in DXE on x86 can be exploited easier than you can imagine. Here is one of the attack scenario that relies on the fact that the code can be executed in MMIO:

  • Write shellcode in NVRAM variable: code
  • Find the address of this variable in the firmware projection in physical memory: code
  • Transfer control there as a result of exploitation (shellcode will survive reboot with the same address).

On our target ARM device, such an attack would not work. Based on our experiments, the code cannot be executed anywhere except the code segments of the UEFI modules. Any attempt to execute code in the NX region causes Synchronous Exception. As well as any access attempt to memory that is protected from reading:

Stack canary

As previously mentioned, we saw stack canary implemented in the target firmware. We think that it is a Qualcomm implementation (since we have seen it on Qualcomm devices from various OEMs). 

Let’s look at how its works:

  • Each module covered with stack canary protection will have specific initialization routine at the beginning of the ModuleEntryPoint function:
  • If Configuration Table with GUID b898d8dc-080a-40f7-99e3-31627b806a5a is exist, gCanary value (pointed to by gCanaryPtr) will be obtained from this table;
  • Otherwise, the value of gCanary will be obtained in the function UpdateCanary() and installed in EFI Configuration Table with GUID b898d8dc-080a-40f7-99e3-31627b806a5a:
    • UpdateCanary() function will call GetRndValue() function, which will return random value obtained via one of SMCs (a kind of RNG service);
  • Then at the beginning of each covered function gCanary value will be saved to local variable (before return address) and at the end of each function it will be checked
    • Stack variables:
    • Canary check:

This implementation seems simple and straightforward. But the question immediately arises: since the value of the canary is updated only when the Configuration Table with GUID b898d8dc-080a-40f7-99e3-31627b806a5a has not yet been created, will the value of the canary be the same for all modules? To check this, we dumped two modules with the canary value initialized. And it turned out that the values are actually the same.

SemmMenu.efi:

DfciMenu.efi:

Another problem we noticed is the coverage. We discovered that not all functions are covered with canary checking (only functions that use unsafe functions from MemLib/PrintLib are covered). E . g. 9/382 function covered in QcomChargerDxeWp DXE module:


We also found that not all modules from the firmware were covered with canary initialization/checking. E.g. MsNetwork DXE module does not contain canary initialization routine at all:

The same coverage problem was described by researchers at Quarkslab in their blog (also on Qualcomm-based device):

https://blog.quarkslab.com/attacking-the-arms-trustzone.html:

Thus, the following conclusions can be drawn in terms of exploitation:

  • The base addresses of drivers, stack and heap are predictable and can be easily obtained from the UEFIShell
  • Even if the canary value is the same for all modules, it still makes it difficult to exploit vulnerabilities such as stack overflow, leaving the following options:
    • Find a vulnerability in a function not covered by stack canary protection
    • Find a vulnerable case where we can obtain code execution by rewriting a local variable in the same function (without rewriting the canary value)
  • DEP works correctly, so with PC control (due to the stack overflows) we have to go with ROP/JOP or jump to a useful primitive with one gadget.

Vulnerabilities

On the Windows Dev Kit 2023 device,  we identified three vulnerabilities that may be interesting in terms of exploitation. The following is a discussion of each of issue.

BRLY-2022-029: GetVariable Stack Overflow (QcomChargerDxeWp)

Vulnerability in QcomChargerDxeWp is a classical double GetVariable() vulnerability when DataSize and variable buffer (in case of heap buffer) is not re-initialized after first call to gRT->GetVariable():

BRLY-2022-029

But as we can see from the vulnerable function pseudocode, in this case 5 values of the variables in a row will be obtained. It means that an attacker can use any pair of variables (10 variants in total). For example, if PrintChargerAppDbgMsg, ChargerPDLogLevel and ChargerPDLogTimer variables are not set, an attacker can exploit a vulnerability via DISABLEBATTERY and ForcePowerTesting variables.

As we can see from the vulnerable function pseudocode, this function is not covered by stack canary mitigation. However, exploitation of this vulnerability is complicated by the fact that the return address is higher on the stack (with less stack offset) than Value, which we can overwrite with a buffer of an arbitrary length:

The left-hand figure shows the location of the return address and the data to be overwritten (we obtained this dump using the hook to gRT->GetVariable()). The right-hand image shows locations of stack variables in the IDA’s stack view for the vulnerable function.

Such cases can still be exploited using the following approaches:

  • Rewrite the values on the stack of the parent function (which are initialized before the vulnerable function is called);
  • Overwrite the return address of the parent function (particular case):
    • The parent function in our case is covered by the canary check.

BRLY-2022-030: GetVariable Stack Overflow (PILDxe)

Vulnerability in PILDxe module is very similar to the previous one:

The attacker can overflow the stack (via Value buffer) through any pair of variables, where VariableName will have the following format: {Section}.{Setting}.

Section – is any section from uefipil.cfg file (ADSPPD, SPSS, ACPI, QUPV3FW, etc):

Possible Setting value enumerated below:

Type, FwName, PartiLabel, PartiRootGuid, PartiGuid, ImagePath, SubsysID, ResvRegionStart, ResvRegionSize, ImageLoadInfo, Unlock, OverrideElfAddr, ProxyGuid, BackupPartiLabel, BackupPartiRootGuid, BackupPartiGuid

The vulnerable function allows to overwrite the PIL configuration during the boot (originally specified in the uefipil.cfg configuration file).

The funny thing is that the developers forgot to initialize the DataSize. Thus, the DataSize value will be filled with garbage value from the stack:

In our case DataSize will be filled with 0x9F3CF100 (some stack pointer). This value is too big for NVRAM variable size, so gRT->GetVariable() will always return EFI_OUT_OF_RESOURCES. However, on other devices or driver versions this value may be small enough to allow this vulnerability to be exploited.

It should be noted that this function is also covered by the canary check.

BRLY-2022-033: GetVariable Stack overflow (UsbConfigDxe)

Vulnerability in the UsbConfigDxe module – yet another double GetVariable() problem. It proved to be the easiest to exploit. The vulnerable pattern is present in the UsbConfigStartUsbLoopback event callback function:

This is how it looks:

The vulnerability again lies in the fact that after the first call to gRT->GetVariable() the DataSize can be rewritten if the actual variable size will be greater than initial DataSize value (In this case the DataSize is initialized with a value of 1).

Then, after the second call to gRT->GetVariable(), the value of the stack variable PortValue can be overflowed.

Writing a proof-of-concept for DOS is fairly straightforward:

  • We need to change values of UsbConfigPrimaryPort and UsbConfigSecondaryPort, and make these values big enough to corrupt the stack
  • We need to signal the UsbConfigStartUsbLoopback event (unfortunately, this event was not signaled by default during the boot as it is a part of the diagnostic feature). To trigger this event we decided to write a simple module. This module will signal UsbConfigStartUsbLoopback event it by Event handle:

However, our goal was to show that we can execute arbitrary code in DXE. So we decided to write a simple ROP chain.

Stack overflow exploitation using a simple ROP chain

We previously showed how to use ROP/JOP in SMM to bypass SMM_Code_Chk_En mitigation:

The advantage of x86 in terms of exploiting vulnerabilities in SMM using ROP/JOP is the rich choice of gadgets:

We haven’t seen the same abundance of gadgets in ARM firmware code. However, in order to execute a simple primitive - a function call with a few arguments - the gadgets were enough.

We decided to display the message on the screen using the following function: gST->ConOut->OutputString(gST->ConOut,    Message).

Where:

  • gST->ConOut – known address (0x9439f320)
  • gST->ConOut->OutputString – known address (0x92fd8cc8)

The ROP chain we built for this is as follows:

Module

Module base

Gadget

Gadget offset

Code

ASN1X509Dxe BS_Code + 0x60d000 Gadget1 0x4ae0 ldr x8, [sp, #8]; cbz x8, #0x4ae0; ldp x29, x30, [sp, #0x10]; add sp, sp, #0x20; ret;
AcpiPlatform BS_Code + 0x6d2000 Gadget2 0x47cc ldr x0, [sp, #0x18]; ldp x29, x30, [sp, #0x30]; add sp, sp, #0x40; ret;
AcpiPlatform BS_Code + 0x6d2000 Gadget3 0xaaf0 add x1, sp, #0x14; blr x8;
AcpiPlatform BS_Code + 0x6d2000 InfiniteLoop 0x10D8 while (1) {};

The code to craft UsbConfigSecondaryPort variable value using this ROP chain show below (link):

After execution of the PoC,  the value of the UsbConfigSecondaryPort variable will look like this:

The final size of the second variable will be 0xD9. So the UsbConfigPrimaryPort variable can take any value of length 0xD9 bytes.

A demo of this PoC is here:

Conclusions

As a result of this research, we came to the following outcomes:

  • Vulnerabilities in UEFI on ARM are harder to exploit, but still not a big deal;
  • UEFI NVRAM API is still misused in many cases;
  • UEFI standard expands the attack surface on ARM TrustZone;
  • Limited usage of mitigations: stack canaries does not apply to all functions, making it possible to use ROP without additional memory leaks;
  • The UEFI on ARM appears to be more secure architecturally than on x86.

Binarly provides FwHunt rules to detect vulnerable devices at scale to help the industry recover from firmware security repeatable failures.

FwHunt Community Scanner: https://github.com/binarly-io/fwhunt-scan

FwHunt detection rules: https://github.com/binarly-io/FwHunt/tree/main/rules

Screenshot of Fwhunt scan results

The Binarly team is constantly working to protect the firmware supply chain and reduce the attack surfaces of our customers industry-wide by delivering innovative technologies to the market. Based on our experience we understand that fixing vulnerabilities for a single vendor is not enough. As a result of the complexity of the firmware supply chain, there are gaps that are difficult to close on the manufacturing end since it involves issues beyond the control of the device vendors.

Are you interested in learning more about Binarly Platform or other solutions? Don't hesitate to contact us at fwhunt@binarly.io.

Check if you are affected by the XZ backdoor