Moving From Manual Reverse Engineering of UEFI Modules To Dynamic Emulation of UEFI Firmware

Introduction

Hello and welcome back to the 2nd part of our blog post series summarizing our research in the fields of UEFI fuzzing and exploitation. In part 1 of the series, aptly titled “Moving from common-sense knowledge about UEFI to actually dumping UEFI firmware”, we gave some highly-condensed yet required background information on the SPI flash memory, and discussed the software-based approach to dump it to disk. We concluded that part by unpacking the firmware image using a myriad of tools.



This part picks up where we left off. We’ll start by giving some more background information on UEFI in general, both from the viewpoint of the boot process itself (what are the different boot phases? How are they related? etc.) as well as from the viewpoint of developers (i.e. what APIs are available to UEFI applications). From there we’ll move on to manually reverse engineer some UEFI modules. Throughout this post, we’ll slowly but surely make our way towards more and more dynamic approaches. If you follow along this post, by the time you finish reading it you’ll have a working environment capable of emulating, tracing and debugging UEFI modules.

2. UEFI Boot Phases

As a painful lesson learned from legacy BIOS, UEFI tries to make the boot process as methodical and organized as possible. For that reason, the UEFI specification divides the boot process into disparate phases, each in charge of setting up specific components crucial for the sound operation of the machine. After a certain phase is done, it should pass control to the next phase in the chain, possibly with some auxiliary data to help it carry out its actions. Graphically, the UEFI boot process is often depicted using hard-to-grasp diagrams, filled with little known acronyms such as SEC, PEI, DXE, BDS, etc.



An accurate and comprehensive review of the boot process, which also includes some attack-surface analysis, was written a while ago by @depletionmode. Here we’ll just give a short overview of each of each of these phases.

SEC phase: Contrary to the popular myth, when booting in UEFI environments the CPU doesn’t magically start executing in 32-bit Protected Mode or 64-bit Long Mode. Rather, the first few instructions executed by the CPU are still legacy, 16-bit Real Mode instructions. Since very little can be done in Real Mode, one of the first jobs of the SEC phase is to switch the processor to Protected Mode. Also, by this time the memory controller in charge of DRAM has not been initialized yet, and so the SEC phase is also in charge of configuring the CPU caches to be used as temporary RAM (a technique known as CAR – Cache-as-RAM).

PEI phase: The Pre-EFI Initialization phase, often shortened to just PEI, usually resides in its own firmware volume (FV) on the SPI flash. It is composed of executable modules which adhere to a file format called TE (Terse Executable), closely related to the well known PE format from Windows.

The PEI phase is in charge of main memory discovery and initialization. After main RAM becomes available, the PEI phase can wind up CAR memory and move on to initialize a bunch of other devices on the motherboard. To pass information down to the DXE phase, PEI modules can create and populate an array of data structures called HOBs (Hand-Off Blocks).

DXE phase: The Driver Execution Environment, or DXE phase for short, is where most of the heavy lifting takes place. Like the PEI phase, the DXE phase also resides in its own FV. The main difference is that this time the executable modules are not TE files, but rather genuine PE32 files. On 64-bit machines, we should expect to find PE32+ files as well, meaning the DXE phase will execute in 64-bit Long Mode.

The DXE phase has a dedicated dispatcher whose job is to enumerate all different DXE modules and execute them one by one. These modules are in charge of setting up System Management Mode (SMM), exposing networking, storage and file system stacks, and basically providing any service a UEFI-based bootloader might need to bring up a kernel. From a security standpoint, the DXE phase is of particular interest because it’s usually where Secure Boot is implemented and enforced.

BDS phase: After the DXE phase is done, control passes on to the BDS (Boot Device Selection) phase. In this phase, the GPT of the disk is parsed and the EFI system partition is searched for. Once it’s found, a boot manager such as bootmgfw.efi can be loaded and executed.

TSL phase: In this phase (Transient System Load), the boot manager will either launch an OS-absent application such as the UEFI shell, or more commonly launch a boot loader. The job of a boot loader such as winload.efi is to prepare the execution environment for the kernel, then load the kernel itself. When it’s done, the boot loader should invoke a UEFI service called ExitBootServices(). By doing so, the boot loader essentially signals the end of the boot process.

RT phase: During runtime phase, the OS kernel should be up and running. It can then proceed to load device drivers, spawn services and background processes, etc.

During the rest of this blog post, unless stated otherwise, we’ll focus exclusively on the DXE phase.

3. Core UEFI Services

In this section, we’ll give a whirlwind tour of some of the most common services available to UEFI applications. While reading it, you should keep in mind that not all the core services will be reviewed. For the full details, please consult the latest version of the UEFI specification at uefi.org.

UEFI services can be broadly divided into two distinct categories: boot services and runtime services. These can be thought of as the basic building blocks upon which the firmware can be constructed, similarly to how a traditional OS exposes a set of well-defined APIs for building applications on top of it.

Boot Services

As the name suggests, boot services are used to facilitate the boot process. They’re available starting from the DXE phase up to the point the OS loader calls ExitBootServices(). After that, all boot services are terminated, the boot service memory is reclaimed and all that remains are the runtime services. The boot services can be further divided into the following sub-categories:

Virtual memory services, which support memory management at the page granularity. The two most prominent services in this category are undoubtedly AllocatePages() and FreePages(). If you come from a Windows background, you may think of these as the UEFI versions of more familiar APIs such as VirtualAlloc() and VirtualFree().

Pool memory services, which support memory management in small chunks that don’t span an entire page. These include AllocatePool() and FreePool(), which can be thought of as the UEFI versions of API pairs such as HeapAlloc()/HeapFree() or the more familiar malloc/free.

Event services, used to synchronize execution flow until a certain event is signaled. This category is composed from services such as CreateEvent(), CreateEventEx(), NotifyEvent(), SignalEvent(), WaitForEvent(), and CloseEvent().

To make a UEFI protocol available to other modules, we can use one of the following services: InstallProtocolInterface(), ReinstallProtocolInterface() or InstallMultipleProtocolInterfaces(). In addition to the GUID identifying the protocol and the pointer to the interface, all these services expect an additional argument of type EFI_HANDLE. This argument is an opaque value representing the caller module (in most cases – its base address). By taking the value of this EFI_HANDLE argument into account, we can differentiate between multiple implementations of the same interface, each offered by a different module.

To consume a UEFI interface, one can either use the LocateProtocol() or OpenProtocol() services. The main difference between the two is that LocateProtocol() simply returns the first protocol instance that matches the given GUID, while OpenProtocol() expects the caller to pass in an additional EFI_HANDLE argument to fully disambiguate which concrete implementation of the protocol is requested.

In addition, the caller can also enumerate all EFI_HANDLES implementing a given protocol by using LocateHandleBuffer(). Thus, a common practice among UEFI developers is to first invoke LocateHandleBuffer() to get an array of all module handles implementing a specific protocol, then iterate over it while calling OpenProtocol() for each entry.

Para el ver artículo original: https://labs.sentinelone.com/moving-from-manual-re-of-uefi-modules-to-dynamic-emulation-of-uefi-firmware/