SMM Part 1: An Overview of Intel SMM Functionality and Security in EDK II and Derived IBV Firmware

Table of contents

Introduction

Scope of this article

This post will serve as an introduction to SMM from an exploitation perspective, covering its attack surface, and the mitigations implemented in the latest EDK II and derived independent BIOS vendor firmware. This is Part 1 of a two-part series on SMM. Part 2 covers actual exploitation and turning an arbitrary write into full SMM code execution: SMM Part 2.

Execution model

SMM is a processor operating mode that is distinct from the traditional privilege levels employed by regular control transfer facilities and instruction permission gating. This separate operating mode acts as an isolated world from the rest of the non-SMM code actively executing on the processor, preserving the context of the processor upon entry, and restoring it upon exit, much like a regular interrupt service routine (although context management is implicit in SMM).

Runtime responsibilities

SMM allows the lifetime of firmware code to extend beyond the pre-boot environment and into the runtime environment in an isolated and asynchronous manner independent of the rest of the operating system.

A list of some common use cases:

Basic usage and configuration of SMM with the CPU

The main structure responsible for interaction between the processor and the system management software executive (hereby referred to as the SMM executive) is a region of memory referred to as SMRAM. SMRAM is a configurable region of isolated physical memory controlled by the SMM executive through an internal CPU register referred to as the SMBASE register. By default, the SMBASE is set to physical address 0x30000. The start of the SMBASE region follows a fixed layout that is split into multiple distinct subregions. The SMBASE internal register is per-logical-processor, and each logical-processor must be allocated their own distinct SMBASE region.

                     Layout of the SMRAM region (figure 1).                  
+---------------------------------------------------------------------------+
| +-----------------+                                                       |
| | SMBASE + 0xFFFF |                                                       |
| +-----------------+                                                       |
| |                 |                                                       |
| |                 +-+                                                     |
| +-----------------+ |                         SMRAM                       |
| | SMBASE + 0xFE00 | +-->+-----------------------------------------------+ |
| +-----------------+     |                                               | |
| |                 |     |             Start of State Save Area          | |
| |                 +---->+-----------------------------------------------+ |
| |                       |                                               | |
| |                       |                                               | |
| +-----------------+     |                                               | |
| | SMBASE + 0x8000 |     |                                               | |
| +-----------------+     |                                               | |
| |                 |     |                                               | |
| |                 |     |                                               | |
| |                 |     |             SMI Handler Entry Point           | |
| |                 +---->+-----------------------------------------------+ |
| |                       |              Free Executive Memory            | |
| |                       |                                               | |
| +-----------------+     |                                               | |
| | SMBASE + 0x0000 |     |                                               | |
| +-----------------+     |                                               | |
|                   |     |                                               | |
|                   |     |                                               | |
|                   |     |                                               | |
|                   +---->+-----------------------------------------------+ |
+---------------------------------------------------------------------------+

Note: this is only the minimal decodable region size,
in practice the SMRAM region will typically be much
larger than this fixed sub-region (up to 4GiB).

Free Executive Memory

Free executive memory is a guaranteed span of 0x8000 (32KiB) bytes of SMRAM that is freely usable by the SMM executive (for code, data, etc.), and has no semantic assignment. This span is typically not large enough for the SMM executive of any modern firmware; therefore the firmware/chipset will dedicate a larger region of DRAM referred to as TSEG to be used as SMRAM, and place the SMBASE somewhere within the larger TSEG region.

SMI Handler Entry Point

The only way to switch to the SMM operating mode is through a system-management-interrupt (SMI). Much like the reset vector used by the CPU upon startup/reset, upon a system-management-interrupt (SMI) the processor switches to a mode similar to real-mode (but with address-space/segment limit range increases) and shifts execution to a fixed address within SMRAM (SMBASE + 0x8000). Much like the reset vector, the SMI handler entry point will generally consist of a small routine that handles switching up to the final paging mode and context used by the actual SMM executive code (typically 64-bit mode), before jumping to the actual core SMM executive code to handle dispatch of the SMI. In addition to putting the processor into this pseudo-real-mode state, a handful of processor registers related to operation/execution are updated to a fixed set of values.

+------------------------------+------------------------------------------------------+
|           Register           |      Contents upon execution of the SMI handler      |
+==============================+======================================================+
| General-purpose registers    | Undefined                                            |
+------------------------------+------------------------------------------------------+
| EFLAGS                       | 00000002H                                            |
+------------------------------+------------------------------------------------------+
| EIP                          | 00008000H                                            |
+------------------------------+------------------------------------------------------+
| CS selector                  | SMM Base shifted right 4 bits (default 3000H)        |
+------------------------------+------------------------------------------------------+
| CS base                      | SMM Base (default 30000H)                            |
+------------------------------+------------------------------------------------------+
| DS, ES, FS, GS, SS Selectors | 0000H                                                |
+------------------------------+------------------------------------------------------+
| DS, ES, FS, GS, SS Bases     | 000000000H                                           |
+------------------------------+------------------------------------------------------+
| DS, ES, FS, GS, SS Limits    | 0FFFFFFFFH                                           |
+------------------------------+------------------------------------------------------+
| CR0                          | PE, EM, TS, and PG flags set to 0; others unmodified |
+------------------------------+------------------------------------------------------+
| CR4                          | Cleared to zero                                      |
+------------------------------+------------------------------------------------------+
| DR6                          | Undefined                                            |
+------------------------------+------------------------------------------------------+
| DR7                          | 00000400H                                            |
+------------------------------+------------------------------------------------------+

On top of these default register values, all interrupts (including NMIs and SMIs) are masked upon entry.

State Save Area

The state save area essentially acts like an interrupt frame for the SMI, with the processor automatically storing a snapshot of all relevant processor state to the State Save Area of the SMBASE fixed area associated with this logical-processor. This entire state snapshot will be automatically restored by the processor when leaving SMM via the RSM instruction (similar to IRET but for an SMI).

+------------------+-----------------------------------------------+----------+
|      Offset      |                Register Name                  | Writable |
| SMBASE + 8000H+X |                                               |          |
+==================+===============================================+==========+
| 7FF8H            | CR0                                           | No       |
+------------------+-----------------------------------------------+----------+
| 7FF0H            | CR3                                           | No       |
+------------------+-----------------------------------------------+----------+
| 7FE8H            | RFLAGS                                        | Yes      |
+------------------+-----------------------------------------------+----------+
| 7FE0H            | IA32_EFER                                     | Yes      |
+------------------+-----------------------------------------------+----------+
| 7FD8H            | RIP                                           | Yes      |
+------------------+-----------------------------------------------+----------+
| 7FD0H            | DR6                                           | No       |
+------------------+-----------------------------------------------+----------+
| 7FC8H            | DR7                                           | No       |
+------------------+-----------------------------------------------+----------+
| 7FC4H            | TR SEL                                        | No       |
+------------------+-----------------------------------------------+----------+
| 7FC0H            | LDTR SEL                                      | No       |
+------------------+-----------------------------------------------+----------+
| 7FBCH            | GS SEL                                        | No       |
+------------------+-----------------------------------------------+----------+
| 7FB8H            | FS SEL                                        | No       |
+------------------+-----------------------------------------------+----------+
| 7FB4H            | DS SEL                                        | No       |
+------------------+-----------------------------------------------+----------+
| 7FB0H            | SS SEL                                        | No       |
+------------------+-----------------------------------------------+----------+
| 7FACH            | CS SEL                                        | No       |
+------------------+-----------------------------------------------+----------+
| 7FA8H            | ES SEL                                        | No       |
+------------------+-----------------------------------------------+----------+
| 7FA4H            | IO_MISC                                       | No       |
+------------------+-----------------------------------------------+----------+
| 7F9CH            | IO_MEM_ADDR                                   | No       |
+------------------+-----------------------------------------------+----------+
| 7F94H            | RDI                                           | Yes      |
+------------------+-----------------------------------------------+----------+
| 7F8CH            | RSI                                           | Yes      |
+------------------+-----------------------------------------------+----------+
| 7F84H            | RBP                                           | Yes      |
+------------------+-----------------------------------------------+----------+
| 7F7CH            | RSP                                           | Yes      |
+------------------+-----------------------------------------------+----------+
| 7F74H            | RBX                                           | Yes      |
+------------------+-----------------------------------------------+----------+
| 7F6CH            | RDX                                           | Yes      |
+------------------+-----------------------------------------------+----------+
| 7F64H            | RCX                                           | Yes      |
+------------------+-----------------------------------------------+----------+
| 7F5CH            | RAX                                           | Yes      |
+------------------+-----------------------------------------------+----------+
| 7F54H            | R8                                            | Yes      |
+------------------+-----------------------------------------------+----------+
| 7F4CH            | R9                                            | Yes      |
+------------------+-----------------------------------------------+----------+
| 7F44H            | R10                                           | Yes      |
+------------------+-----------------------------------------------+----------+
| 7F3CH            | R11                                           | Yes      |
+------------------+-----------------------------------------------+----------+
| 7F34H            | R12                                           | Yes      |
+------------------+-----------------------------------------------+----------+
| 7F2CH            | R13                                           | Yes      |
+------------------+-----------------------------------------------+----------+
| 7F24H            | R14                                           | Yes      |
+------------------+-----------------------------------------------+----------+
| 7F1CH            | R15                                           | Yes      |
+------------------+-----------------------------------------------+----------+
| 7F1BH-7F04H      | Reserved                                      | No       |
+------------------+-----------------------------------------------+----------+
| 7F02H            | Auto HALT Restart Field (Word)                | Yes      |
+------------------+-----------------------------------------------+----------+
| 7F00H            | I/O Instruction Restart Field (Word)          | Yes      |
+------------------+-----------------------------------------------+----------+
| 7EFCH            | SMM Revision Identifier Field (Doubleword)    | No       |
+------------------+-----------------------------------------------+----------+
| 7EF8H            | SMBASE Field (Doubleword)                     | Yes      |
+------------------+-----------------------------------------------+----------+
| 7EF7H-7EE4H      | Reserved                                      | No       |
+------------------+-----------------------------------------------+----------+
| 7EE0H            | Setting of "enable EPT" VM-execution control  | No       |
+------------------+-----------------------------------------------+----------+
| 7ED8H            | Value of EPTP VM-execution control field      | No       |
+------------------+-----------------------------------------------+----------+
| 7EC8H            | SSP                                           | Yes      |
+------------------+-----------------------------------------------+----------+
| 7EC7H-7EA0H      | Reserved                                      | No       |
+------------------+-----------------------------------------------+----------+
| 7E9CH            | LDT Base (lower 32 bits)                      | No       |
+------------------+-----------------------------------------------+----------+
| 7E98H            | Reserved                                      | No       |
+------------------+-----------------------------------------------+----------+
| 7E94H            | IDT Base (lower 32 bits)                      | No       |
+------------------+-----------------------------------------------+----------+
| 7E90H            | Reserved                                      | No       |
+------------------+-----------------------------------------------+----------+
| 7E8CH            | GDT Base (lower 32 bits)                      | No       |
+------------------+-----------------------------------------------+----------+
| 7E8BH-7E48H      | Reserved                                      | No       |
+------------------+-----------------------------------------------+----------+
| 7E40H            | CR4 (64 bits)                                 | No       |
+------------------+-----------------------------------------------+----------+
| 7E3FH-7DF0H      | Reserved                                      | No       |
+------------------+-----------------------------------------------+----------+
| 7DE8H            | IO_RIP                                        | Yes      |
+------------------+-----------------------------------------------+----------+
| 7DE7H-7DDCH      | Reserved                                      | No       |
+------------------+-----------------------------------------------+----------+
| 7DD8H            | IDT Base (Upper 32 bits)                      | No       |
+------------------+-----------------------------------------------+----------+
| 7DD4H            | LDT Base (Upper 32 bits)                      | No       |
+------------------+-----------------------------------------------+----------+
| 7DD0H            | GDT Base (Upper 32 bits)                      | No       |
+------------------+-----------------------------------------------+----------+
| 7DCFH-7C00H      | Reserved                                      | No       |
+------------------+-----------------------------------------------+----------+

Relocation/SMBASE modification

As stated previously, the SMBASE internal register defaults to a value of 0x30000, but the actual TSEG region of the chipset (which is typically located underneath stolen IGD memory within the lower main memory address range) is not going to match up with this default base address, not to mention that a separate SMBASE region is needed for each logical-processor. So, simple enough, space must be reserved within TSEG for the fixed SMBASE region size for each logical-processor, but how is the internal SMBASE register actually updated with the corresponding addresses? This is somewhat of a chicken and egg problem. The SMBASE register can only be updated using the SMM Save State Area itself when restored using RSM to return from SMM. Due to this being the sole way of updating the register, a temporary bootstrap SMBASE region must be constructed at the default address. This bootstrap region will serve simply to update the SMBASE field of the save state area for the logical-processor, set up any SMM state for that logical-processor, and then simply RSM. Once the bootstrap SMRAM has been set up, an SMI can then be sent to all other APs one by one to trigger their execution of the bootstrap code, and then finally on the BSP itself to complete the bootstrapping.

System-Management Range Register Interface (SMRR)

The SMRR MSR pair (IA32_SMRR_PHYSBASE and IA32_SMRR_PHYSMASK) allow the BIOS to specify a protected range of SMRAM directly to the processor, and also specify the memory type of SMRAM that will be used when accessing the SMRAM region specified from SMM.

The IA32_SMRR_PHYSBASE register contains the naturally aligned physical base address of SMRAM in bits 31:12, as well as a memory type value to be used in SMM in bits 8:0. This memory type field follows the same encoding/enumeration as the regular MTRR memory type values (UC, WC, WT, WP, WB).

The IA32_SMRR_PHYSMASK register contains a mask in bits 31:12 that specifies the length/range of SMRAM, as well as a valid bit in bit 11 which enables or disables the SMRR register pair correspondingly.

Bit 11 of the IA32_MTRRCAP MSR indicates if the SMRR feature is supported by the processor (but it will always be for modern platforms). Note: the feature may also have to be manually enabled on some older processors through the IA32_FEATURE_CONTROL MSR.

When the SMRR feature is supported and enabled, and the register pair is enabled through the valid bit of the SMRR_PHYSMASK, the processor will behave as such:

This feature only affects regular memory accesses through the processor, and not any external devices. The protection of SMRAM from DMA is chipset-dependent and will be done by the FSP during platform bringup by interacting with the system agent/integrated memory controller (historically the northbridge).

System-Management Mode Feature Control Register (MSR_SMM_FEATURE_CONTROL)

This is an SMM model-specific-register that allows control of certain SMM features and mitigations. The main relevant bit of this register is the SMM_Code_Chk_En feature bit.

When this bit is set to ‘1’ any logical processor in the package that attempts to execute SMM code not within the ranges defined by the SMRR will assert an unrecoverable MCE. In other words, any code that resides outside of SMRAM cannot be executed inside SMM. This is essentially the SMM equivalent of SMEP.

There is a corresponding capability register that indicates which features of MSR_SMM_FEATURE_CONTROL are supported for the processor MSR_SMM_MCA_CAP. The 58th bit of this register must be 1 (indicating support for SMM_Code_Access_Chk) to make use of the SMM code check feature.

Sources of SMIs

There are only two actual ways to signal an SMI to the processor.

There may be multiple chipset or firmware-specific methods of triggering an SMI, but they will boil down to using either of the two methods above to actually signal the SMI to the processor. The main example is the commonly seen chipset-specific I/O port that will trigger an SMI, which exists in the case (mostly historical) that the processor/chipset supports SMM but lacks an LAPIC/IOAPIC, and thus no way to trigger an SMI or enter SMM through software. This I/O port would act as a way for software to assert the SMI# through the chipset (the I/O port being referred to is most likely 0xB2, and additionally allows an index to be passed along with the SMI through the port to invoke a specific SMI sub-handler, usually backing the EFI_SMM_SW_DISPATCH2_PROTOCOL in EDK2).

SMI rendez-vous system

SMIs can be triggered on multiple logical-processors in a system at once concurrently. On modern multiprocessor systems/firmware, the SMM executive is typically designed to execute on one logical-processor with exclusive control of the entire CPU, that is to say, all other logical-processors are quiesced/temporarily halted during the execution of the main SMI handler.

Typically the chipset will be set up to broadcast PCH-source SMIs/SMI# pin assertions to all logical-processors, simplifying the creation of the SMI rendez-vous system. As the logical-processors’ SMI handlers begin to execute, they will first start by making their presence known in the rendez-vous state, informing the other logical-processors that they are present and in SMM. After updating their presence, the execution path will diverge based on if this is going to be the SMM BSP (the logical-processor that is designated to actually service the SMI) or an SMM AP. This process of determining if the current logical-processor is the SMM BSP or an SMM AP is referred to as SMM BSP election. The SMM BSP election process is up to the implementation of the firmware entirely, but it will most typically be a simple check for if this logical-processor is actually the system BSP (for example through the IA32_APIC_BASE MSR), or a first-come-first-serve atomic determination.

Once the logical-processor that the SMI is executing on has been determined to not have been elected as the SMM BSP, it will perform basic bootstrapping, and then enter into a quiesced state, halting execution until the SMM BSP notifies the rest of the logical-processors that it has completed handling of the SMI. As for the SMM BSP path, it will similarly mark its presence and perform bookkeeping, but will then await all SMM APs to be marked as quiesced and waiting at the rendez-vous point in SMM before it will perform execution of the real SMI handler/SMI dispatch.

There are multiple reasons as to why this system exists, with the tradeoff being a system-wide performance loss directly tied to the frequency of SMIs. One benefit of the exclusive SMM system is that it simplifies the implementation of the SMM executive, allows SMM code to never worry about SMRAM data synchronization, and in turn essentially eliminates all standard race conditions that operate on purely SMRAM data. However, despite all logical-processors being quiesced and parked in SMM, it does not eliminate race conditions on standard DRAM or MMIO-backed data that is shared between the SMM world and the regular world (for example: communication buffers), as external devices can still perform DMA or updates to them while the logical-processors are parked!

An optional reduction of code complexity is not the sole reason for this system. This system also acts as a mitigation of certain microarchitectural problems and speculative side channels, most notably ones that stem from microarchitectural state being shared across logical-processors that reside on the same physical core in an SMT system, such as L1 Terminal Fault (L1TF), and Microarchitectural Data Sampling (MDS).

Interaction with the PCH

Despite not being the original purpose of SMM, one of the functionalities of SMM has become to act as a higher privilege level in regards to the PCH/chipset, and in turn, this is what makes it an attractive attack surface. The registers and functionalities of the PCH that are gated by SMM as a privilege level are ever-changing and can differ between chipset/PCH versions. However, for our intents and purposes, we will list a somewhat relevant (but not comprehensive) group of features shared across the majority of modern chipsets.

SMM-access gated SPI flash protections

This section will consist of SPI flash protections that are gated by SMM as a privilege level, covering the previously listed BIOS control/SPI flash access gating protections and giving a more detailed description of their behaviour and interaction, as well as some information about their troubled pasts.

Write Protect Disable (WPD) & Lock Enable (LE)

The first of Intel’s introduced SMM-as-a-privilege SPI flash protections and the one with the most troubled history. When the Lock Enable (LE) bit is set in the PCH’s BIOS_SPI_BC register, any attempts to set the Write Protect Disable (WPD) bit will cause an SMI to be generated, but the bit will still be set!

The intention is that the SMI handler will unset the WPD bit to block the update if it wasn’t desired. However, it is easy to see that this is a fundamentally broken race condition on multiprocessor systems, as there will be a small window between the update of the bit and the signalling of the SMI to all processors where the bit remains set (and also a factor of the SPI controller receiving the actual write command).

It is sufficient for a logical-processor to attempt SPI flash writes in a tight loop while the other logical-processor attempts to set the WPD bit in another tight loop until the race condition is hit (assuming other mitigations/features are not in place).


VOID
SmmWpdRaceProcessor1(
  VOID
  )
{
  UINT64 SpiBaseAddress;

  //
  // Continuously set the WPD on the first logical-processor.
  //
  SpiBaseAddress = SpiPciCfgBase();
  while (Complete == 0) {
    PciSegmentOr8 (
      SpiBaseAddress + R_SPI_CFG_BC,
      B_SPI_CFG_BC_WPD
      );
  }
}

VOID
SmmWpdRaceProcessor2(
  VOID
  )
{
  //
  // Continuously attempt to perform the BIOS SPI flash write.
  //
  while (Complete == 0) {
    SpiWriteBiosRegion(...);
  }
}

Enable InSMM.STS (EISS)

When this bit is set, the BIOS region is not writable until the processor is in SMM mode, the internal InSMM.STS bit is set, and the WPD bit is set in the BIOS_SPI_BC register. We can only imagine that this was introduced as a mitigation for the poorly designed WPD/LE system. This feature eliminates the race-condition mentioned above with the WPD and LE system. Intel’s public PCH documentation simply says that InSMM.STS must be set to 1, but the actual usage is more nuanced and perhaps even chipset specific.

What actually is InSMM.STS? Some versions of the public PCH documentation list InSMM.STS as being a part of an MMIO register at 0xFED30880, bit 0 specifically, but surely this is not just a regular MMIO register that can simply be written to by anyone. It turns out that the usage of the EISS feature is only documented in the private NDA’d Intel Confidential PCH BIOS specifications, but I have managed to find some information about this feature.

The answer can publicly be found in multiple Intel SiliconPkgs published as part of EDK2. For example, the Intel AlderlakeSiliconPkg in EDK2 contains a AlderlakeSiliconPkg/IpBlock/Spi/Smm/Spi.c file which shows exactly how to make use of the feature, and even contains a direct excerpt from the PCH BIOS specification describing the usage.

/**
  Set InSmm.Sts bit
**/
VOID
PchSetInSmmSts (
  VOID
  )
{
  UINT32 Data32;

  //
  // Read memory location FED30880h OR with 00000001h, place the result in EAX,
  // and write data to lower 32 bits of MSR 1FEh (sample code available)
  //
  Data32 = MmioRead32 (R_LT_UCS);
  AsmWriteMsr32 (MSR_SPCL_CHIPSET_USAGE, Data32 | BIT0);

  //
  // Read FED30880h back to ensure the setting went through.
  //
  Data32 = MmioRead32 (R_LT_UCS);
}

EFI_STATUS
EFIAPI
DisableBiosWriteProtect (
  VOID
  )
{
  UINT64 SpiBaseAddress;

  //
  // Write clear BC_SYNC_SS prior to change WPD from 0 to 1.
  //
  SpiBaseAddress = SpiPciCfgBase();
  PciSegmentOr8 (
    SpiBaseAddress + R_SPI_CFG_BC + 1,
    (B_SPI_CFG_BC_SYNC_SS >> 8)
    );

  //
  // Set BIOSWE bit (SPI PCI Offset DCh [0]) = 1b
  // Enable the access to the BIOS space for both read and write cycles
  //
  PciSegmentOr8 (
    SpiBaseAddress + R_SPI_CFG_BC,
    B_SPI_CFG_BC_WPD
    );

  //
  // the BIOS Region can only be updated by following the steps below:
  //  - Once all threads enter SMM
  //  - Read memory location FED30880h OR with 00000001h, place the result in EAX,
  //    and write data to lower 32 bits of MSR 1FEh (sample code available)
  //  - Set BIOSWE bit (SPI PCI Offset DCh [0]) = 1b
  //  - Modify BIOS Region
  //  - Clear BIOSWE bit (SPI PCI Offset DCh [0]) = 0b
  //
  if ((PciSegmentRead8 (SpiBaseAddress + R_SPI_CFG_BC) & B_SPI_CFG_BC_EISS) != 0) {
    PchSetInSmmSts ();
  }

  return EFI_SUCCESS;
}

InSMM.STS is simply a read-only condition that is indirectly updated through a special chipset control MSR 0x1FE also known as MSR_SPCL_CHIPSET_USAGE_ADDR that is only writable from SMM when all of the required conditions have been satisfied. This may differ depending on the chipset, but from what I can gather, this matches up for most. For example, this same code is also found in the CoffeelakeSiliconPkg, EagleStream ServerSiliconPkg, and KabylakeSiliconPkg with minor differences.

PCH SPI flash protections

This section will consist of SPI flash protections implemented by the PCH SPI flash controller that are absolute and apply to anything, including SMM code. Though this is not directly relevant to SMM, it will determine if we will even be able to write to SPI flash from SMM given a successful exploit.

Flash Protected Range Registers (BIOS_FPRx)

The flash protected range registers allow the firmware to specify a variable amount of protected SPI flash regions (usually 1 to 5) to the PCH. Each FPRR consists of a write protection enable bit, a read protection enable bit, and the base and length of the flash region to protect. The base and limits of the range must be aligned to page granularity (0x1000), and are placed naturally aligned into the FPRR (bits 26:12 of the flash linear addresses) into their corresponding bitfields.

These registers are writable by anyone and not SMM-gated, but become read-only once the Flash Configuration Lock-Down (FLOCKDN) bit in the BIOS_HSFSTS_CTL is set (and historically PRR34_LOCKDN for FPRRs 3 and 4), which itself is sticky and can only be cleared by a hardware reset. If a region is protected through this system and FLOCKDN has been set, not even SMM can get around it.

+-----------+--------------------------------------------------------------------------------------------------------+
| Bit Range | Field Information                                                                                      |
+-----------+--------------------------------------------------------------------------------------------------------+
| 31        | Write Protection Enable (WPE):                                                                         |
|           | When set, this bit indicates that the Base and Limit fields in this register                           |
|           | are valid and that writes and erases directed to addresses between them (inclusive)                    |
|           | must be blocked by hardware. The base and limit fields are ignored when this bit is cleared.           |
+-----------+--------------------------------------------------------------------------------------------------------+
| 30:16     | Protected Range Limit (PRL):                                                                           |
|           | This field corresponds to FLA address bits 26:12 and specifies the upper limit of the protected range. |
|           | Address bits 11:0 are assumed to be FFFh for the limit comparison.                                     |
|           | Any address greater than the value programmed in this field is unaffected by this protected range.     |
+-----------+--------------------------------------------------------------------------------------------------------+
| 15        | Read Protection Enable (RPE):                                                                          |
|           | When set, this bit indicates that the Base and Limit fields in this register                           |
|           | are valid and that reads directed to addresses between them (inclusive) must be blocked by hardware.   |
|           | The base and limit fields are ignored when this bit is cleared.                                        |
+-----------+--------------------------------------------------------------------------------------------------------+
| 14:0      | Protected Range Base (PRB):                                                                            |
|           | This field corresponds to FLA address bits 26:12 and specifies the lower base of the protected range.  |
|           | Address bits 11:0 are assumed to be 000h for the base comparison.                                      |
|           | Any address less than the value programmed in this field is unaffected by this protected range.        |
+-----------+--------------------------------------------------------------------------------------------------------+

Both the BIOS_FPR[n] registers and the BIOS_HSFSTS_CTL (containing the sticky FLOCKDN bit) are usually found within the BAR0 MMIO space (SPIBAR/SPI_BAR0) of the SPI/BIOS SPI controller PCIe device (often PCIe B00:D31:F05), but this may differ depending on the PCH.

Global Protected Range Registers (BIOS_GPRx)

The same exact semantics and register format as the BIOS_FPR[n] registers, but these ranges apply globally to all masters / flash requesters, and not just the host/BIOS-side SPI interface. That is to say, GPR can be used to block other flash requesters (such as GbE, CSME, EC) from accessing the protected range. However, unlike the BIOS_FPRRs, these registers are initialized via softstraps, and the MMIO BIOS_GPR[n] register is only a read-only view.

Flash Descriptor

The flash descriptor is a fixed structure of data residing in SPI flash memory, located at the bottom sector of the flash component 0. It is parsed and enforced by the PCH SPI controller during platform initialization.

This structure contains various pieces of information about flash regions, controller region, processor and PCH soft straps, CSME VSCC table containing the JEDEC ID and ME VSCC information of all SPI flash parts, etc.

The flash regions of the flash descriptor are a way to describe the base and limit of each region within the SPI flash memory. Each flash region has a fixed semantic index assigned to it, defined per PCH, typically along the lines of the following:

+----+-------------------------+
| 0  | Flash Descriptor        |
+----+-------------------------+
| 1  | BIOS                    |
+----+-------------------------+
| 2  | CSME                    |
+----+-------------------------+
| 3  | GbE                     |
+----+-------------------------+
| 4  | Platform Data Region    |
+----+-------------------------+
| 8  | Embedded Controller     |
+----+-------------------------+
| 10 | Silicon Security Engine |
+----+-------------------------+

The controller region defines read and write access settings for each region of the SPI0 device. The Controller region recognizes four Controllers: BIOS, Gigabit Ethernet, CSME, and EC. The permissions of which controller can access which region (and with which kinds of operations) are technically dictated by the Region Access Control Table, but there is an explicit matrix of permissions that should be expected for a post manufacturing release.

+----------------------+--------------------------+--------------------------+--------------------------+--------------------------+
| Region               | Processor / BIOS         | CSME                     | GbE                      | EC                       |
+----------------------+--------------------------+--------------------------+--------------------------+--------------------------+
| Flash Descriptor (0) | Read Only                | Read Only                | Not Accessible           | Not Accessible           |
+----------------------+--------------------------+--------------------------+--------------------------+--------------------------+
| BIOS (1)             | Always Read/Write        | Not Accessible           | Not Accessible           | Not Accessible           |
|                      | prior to EOP             |                          |                          |                          |
+----------------------+--------------------------+--------------------------+--------------------------+--------------------------+
| CSME (2)             | Read/Write               | Always Read/Write        | Not Accessible           | Not Accessible           |
|                      | (BIOS only)              |                          |                          |                          |
+----------------------+--------------------------+--------------------------+--------------------------+--------------------------+
| GbE (3)              | Not Accessible           | Read/Write               | Always Read/Write        | Not Accessible           |
+----------------------+--------------------------+--------------------------+--------------------------+--------------------------+
| PDR (4)              | Not Accessible           | Not Accessible           | Not Accessible           | Not Accessible           |
+----------------------+--------------------------+--------------------------+--------------------------+--------------------------+
| EC (8)               | Read/Write               | Not Accessible           | Not Accessible           | Always Read/Write        |
+----------------------+--------------------------+--------------------------+--------------------------+--------------------------+
| CSME Data (15)       | Not Accessible           | Read/Write               | Not Accessible           | Not Accessible           |
+----------------------+--------------------------+--------------------------+--------------------------+--------------------------+

The main thing to take away about this system is that it is typically never a concern for us. On real platforms, BIOS will almost always be writable by the BIOS, especially due to the firmware update implementations used by real-world IBVs.

System Agent/Integrated Memory Controller SPI flash protections

The system agent (or integrated memory controller) is responsible for protecting the SMRAM from non-processor-initiated accesses (such as DMA). The TSEG Memory Base (TSEGMB) register of the host bridge/DRAM controller specifies the base address of the protected TSEG DRAM memory. The TSEG region should begin at or below the stolen graphics memory region of DRAM. Calculation of the TSEG base is typically a function of the Top of Low Usable DRAM (TOLUD) value, and the size of the stolen graphics memory region.

The Top of Low Usable DRAM is the lowest address above both Graphics Stolen memory and TSEG. BIOS determines the base of Graphics Stolen Memory by subtracting the Graphics Stolen Memory Size from TOLUD and further decrements by TSEG size to determine base of TSEG.

An example of the (simplified) code to calculate TSEGMB:

VOID
MrcSetupMemoryMap(
  VOID
  )
{
  //
  // Calculate the TOLUD value.
  //
  MemoryMap->ToludBase = MIN(MemoryMap->TotalPhysicalMemorySize, MEM_4GB - Inputs->MmioSize);

  //
  // Calculate stolen memory region bases.
  //
  GraphicsStolenSize = Outputs->GraphicsStolenSize;
  MemoryMap->BdsmBase = MemoryMap->ToludBase - GraphicsStolenSize - PsmiRegionSize;
  MemoryMap->GttBase = MemoryMap->BdsmBase - Outputs->GraphicsGttSize;

  //
  // Calculate the TSEG base, following the stolen graphics memory area.
  // Example code here does not account for alignment.
  //
  MemoryMap->TsegBase = MemoryMap->GttBase - Inputs->TsegSize;
}

Once the TSEGMB has been programmed, it is essential that the BIOS code locks the register (or overall DRAM configuration), otherwise software could reprogram the TSEGMB register and point it elsewhere, leaving the real SMRAM region unprotected to DMA. On modern platforms the TSEGMB register itself contains a sticky lock bit. For example on all generations of processors that employ the lock bit specifically for the TSEGMB register itself, the layout of the register will typically always match the following:

Once the BIOS decides that all memory setup is complete, all applicable regions will typically be locked.

VOID
MrcLockMemory(
  VOID
  )
{
  //
  // Lock down other registers such as TOLUD, etc.
  //
  PciCf8Or8(PCI_CF8_LIB_ADDRESS(0, 0, 0, TOLUD_REGISTER_OFFSET), (UINT8)BIT0);
  // ...

  //
  // Lock the TSEGMB register, disallow any changes.
  //
  PciCf8Or8(PCI_CF8_LIB_ADDRESS(0, 0, 0, TSEGMB_REGISTER_OFFSET), (UINT8)BIT0);
}

Older platforms may employ an individual per-register LOCK bit, as well as a different system through the SAD_SMRAM register (or only the SAD_SMRAM register), but it is the same concept, the SMM Space Locked (D_LCK) will be set to lock down SMRAM configuration:

// 
// Intel(R) Core Processor Skylake BWG version 0.4.0// 
// 18.6 System Agent Configuration Locking
//  For reliable operation and security, System BIOS must set the following bits:
//  1. For all modern Intel processors, Intel strongly recommends that BIOS should set
//      the D_LCK bit. Set B0:D0:F0.R088h [4] = 1b to lock down SMRAM space.
// BaseAddr values for mSaSecurityRegisters that uses PciExpressBaseAddress will be initialized at
// Runtime inside function CpuPcieInitPolicy().
// 
GLOBAL_REMOVE_IF_UNREFERENCED BOOT_SCRIPT_REGISTER_SETTING mSaSecurityRegisters[] = {
  {0,  R_SA_SMRAMC,  0xFFFFFFFF,  BIT4}
};

//
// This function does SA security lock
//
VOID
SaSecurityLock (
  VOID
  )
{
  UINT8  Index;
  UINT64 BaseAddress;
  UINT32 RegOffset;
  UINT32 Data32And;
  UINT32 Data32Or;

  //
  // 17.2 System Agent Security Lock configuration
  //
  DEBUG ((DEBUG_INFO, "DXE SaSecurityLock\n"));
  for (Index = 0; Index < (sizeof (mSaSecurityRegisters) / sizeof (BOOT_SCRIPT_REGISTER_SETTING)); Index++) {
    BaseAddress = mSaSecurityRegisters[Index].BaseAddr;
    RegOffset   = mSaSecurityRegisters[Index].Offset;
    Data32And   = mSaSecurityRegisters[Index].AndMask;
    Data32Or    = mSaSecurityRegisters[Index].OrMask;
    if (RegOffset == R_SA_SMRAMC) {
      //
      // SMRAMC LOCK must use CF8/CFC access
      //
      PciCf8Or8 (PCI_CF8_LIB_ADDRESS (SA_MC_BUS, SA_MC_DEV, SA_MC_FUN, R_SA_SMRAMC), (UINT8) Data32Or);
      BaseAddress = S3_BOOT_SCRIPT_LIB_PCI_ADDRESS (SA_MC_BUS, SA_MC_DEV, SA_MC_FUN, R_SA_SMRAMC);
      S3BootScriptSavePciCfgReadWrite (
        S3BootScriptWidthUint8,
        (UINTN) BaseAddress,
        &Data32Or,
        &Data32And
        );
    }
  }
}

Historical architectural and platform-level vulnerabilities

A list of historical architectural and platform-level vulnerabilities that compromise SMRAM/execution of SMM. Please note that these are not a concern on modern platforms (even at the time of the research for some of them), save for the LE/WPD race condition and CPU hotplug concerns, which still may be found and exploitable in the wild (especially due to misconfiguration), but is mitigated by proper usage of newer PCH features.

SMRAM Redirection via Graphics Aperture - 2006(?)

Duflot et al, Security Issues Related to Pentium System Management Mode

Chipset memory remapping attack (Q35) - 2008

Rafal Wojtczuk, Joanna Rutkowska, and Alexander Tereshkin, Xen 0wning Trilogy

CPU cache poisoning - 2009

Rafał Wojtczuk and Joanna Rutkowska, “Attacking SMM Memory via Intel CPU Cache Poisoning”

Mitigated by the addition of the SMRR MSR pair system, which treats SMRAM as UC when accessed from non-SMM code when enabled.

LE/WPD SMI suppression - 2014

Corey Kallenberg, Sam Cornwell, Xeno Kovah, and John Butterworth, “Setup For Failure: Defeating Secure Boot”

If SMIs can be suppressed through chipset registers, or inhibited using Intel TXT, the SMI triggered to clear the WPD bit will not fire.

LE/WPD race condition - 2014

Corey Kallenberg and Rafał Wojtczuk, “Speed Racer: Exploiting an Intel Flash Protection Race Condition”

This is the race condition described above in the SMM-access gated SPI flash protections section. Mitigated by introduction of the “Enable InSMM.STS (EISS)” system.

LAPIC relocation/memory sinkhole - 2015

Christopher Domas, “The Memory Sinkhole: Unleashing an x86 Design Flaw Allowing Universal Privilege Escalation”

Relocating IA32_APIC_BASE to SMRAM, and constructing shellcode or a malicious SMBASE structure inside of the LAPIC state. Niche attack that is only a possible issue on certain Intel Core 2 and earlier processors.

Server CPU hotplug and DMA - 2017

Cuauhtemoc Chavez-Corona, Jorge Gonzalez-Diaz, Rene Henriquez-Garcia, Laura Fuentes-Castaneda, and Jan Seidl, “CSW2017 Privilege escalation on high-end servers due to implementation gaps in CPU Hot-Add flow”

Overview of historical attacks and misconfigurations - 2022

Yuriy Bulygin, John Loucaides, Andrew Furtak, Oleksandr Bazhaniuk, and Alexander Matrosov, Summary of Attacks Against BIOS and Secure Boot

EDK2 SMM exploit mitigations

Restricted memory access (PcdCpuSmmRestrictedMemoryAccess)

Restricts access to all non-SMRAM, runtime, reserved, and ACPI NVS memory from SMM after the SmmReadyToLock event has been signalled. UpdateUefiMemMapAttributes will update all non-SMRAM (aside from listed exclusions) as not present if the mitigation is enabled.

  //
  // Set NonMmram to not-present by excluding "RT, Reserved and NVS" memory type when RestrictedMemoryAccess is enabled.
  //
  if (IsRestrictedMemoryAccess ()) {
    if (mUefiMemoryMap != NULL) {
      MemoryMapEntryCount = mUefiMemoryMapSize/mUefiDescriptorSize;
      MemoryMap           = mUefiMemoryMap;
      for (Index = 0; Index < MemoryMapEntryCount; Index++) {
        if (IsUefiPageNotPresent (MemoryMap)) {
          Status = ConvertMemoryPageAttributes (
                     PageTable,
                     mPagingMode,
                     MemoryMap->PhysicalStart,
                     EFI_PAGES_TO_SIZE ((UINTN)MemoryMap->NumberOfPages),
                     EFI_MEMORY_RP,
                     TRUE,
                     NULL
                     );
        }
        MemoryMap = NEXT_MEMORY_DESCRIPTOR (MemoryMap, mUefiDescriptorSize);
      }
    }
  }

Non-executable non-SMM memory

Even if restricted memory access isn’t enabled, on the latest EDK2, all non-SMRAM memory will be marked as non-executable after the SmmReadyToLock event.

  for (Index = 0; Index < mSmmCpuSmramRangeCount; Index++) {
    Base = mSmmCpuSmramRanges[Index].CpuStart;
    if (Base > PreviousAddress) {
      Status = ConvertMemoryPageAttributes (PageTable, mPagingMode, PreviousAddress, Base - PreviousAddress, EFI_MEMORY_XP, TRUE, NULL);
      ASSERT_RETURN_ERROR (Status);
    }
    PreviousAddress = mSmmCpuSmramRanges[Index].CpuStart + mSmmCpuSmramRanges[Index].PhysicalSize;
  }

CommBuffer validation/enforcement

The lower-level SMI dispatcher logic will implicitly validate that the user-provided CommBuffer is completely within valid SMRAM and whitelisted ranges, preventing confused deputy attacks operating on higher privilege memory (SMM or HV). This mitigation still applies when the restricted memory access mitigation is disabled. However, the check only implicitly applies to the top-level CommBuffer, and not any nested pointers inside of it.

    //
    // Check to see if this is a Synchronous SMI sent through the SMM Communication
    // Protocol or an Asynchronous SMI
    //
    CommunicationBuffer = gSmmCorePrivate->CommunicationBuffer;
    BufferSize          = gSmmCorePrivate->BufferSize;
    if (CommunicationBuffer != NULL) {
      //
      // Synchronous SMI for SMM Core or request from Communicate protocol
      //
      IsOverlapped = InternalIsBufferOverlapped (
                       (UINT8 *)CommunicationBuffer,
                       BufferSize,
                       (UINT8 *)gSmmCorePrivate,
                       sizeof (*gSmmCorePrivate)
                       );
      //
      // Check for over or underflows
      //
      if (!SmmIsBufferOutsideSmmValid ((UINTN)CommunicationBuffer, BufferSize) ||
          IsOverlapped || (BufferSize < OFFSET_OF (EFI_SMM_COMMUNICATE_HEADER, Data)))
      {
        //
        // If CommunicationBuffer is not in valid address scope,
        // or there is overlap between gSmmCorePrivate and CommunicationBuffer,
        // or there is over or underflow,
        // return EFI_INVALID_PARAMETER
        //
        gSmmCorePrivate->CommunicationBuffer = NULL;
        gSmmCorePrivate->ReturnStatus        = EFI_ACCESS_DENIED;
      } else {
        // ...
        BufferSize -= CommHeaderSize;
        Status      = SmiManage (
                        CommGuid,
                        NULL,
                        CommData,
                        &BufferSize
                        );
      }
    }

Code access check enable (PcdCpuSmmCodeAccessCheckEnable)

Enables the SMM Code access check enable feature described above in the MSR_SMM_FEATURE_CONTROL section. This essentially acts as SMEP for SMM, preventing execution of any code outside of SMRAM at a processor level, mitigating SMM callout vulnerabilities (a non-SMM pointer call, typically through leftover usage of non-SMM services from inside SMM (BootServices, etc)).

  //
  // Get the current SMM Feature Control MSR value
  //
  SmmFeatureControlMsr = SmmCpuFeaturesGetSmmRegister (CpuIndex, SmmRegFeatureControl);

  //
  // Compute the new SMM Feature Control MSR value
  //
  NewSmmFeatureControlMsr = SmmFeatureControlMsr;
  if (mSmmCodeAccessCheckEnable) {
    NewSmmFeatureControlMsr |= SMM_CODE_CHK_EN_BIT;
    if (FeaturePcdGet (PcdCpuSmmFeatureControlMsrLock)) {
      NewSmmFeatureControlMsr |= SMM_FEATURE_CONTROL_LOCK_BIT;
    }
  }

Always enabled by default in upstream EDK2.

  ## Indicates if SMM Code Access Check is enabled.
  #  If enabled, the SMM handler cannot execute the code outside SMM regions.
  #  This PCD is suggested to TRUE in production image.<BR><BR>
  #   TRUE  - SMM Code Access Check will be enabled.<BR>
  #   FALSE - SMM Code Access Check will be disabled.<BR>
  # @Prompt SMM Code Access Check.
  gUefiCpuPkgTokenSpaceGuid.PcdCpuSmmCodeAccessCheckEnable|TRUE|BOOLEAN|0x60000013

Feature control lock (PcdCpuSmmFeatureControlMsrLock)

This is a sticky lock bit for the MSR_SMM_FEATURE_CONTROL register. Once set, the current value of the register is locked.

Always enabled by default in upstream EDK2.

  ## Indicates if lock SMM Feature Control MSR.<BR><BR>
  #   TRUE  - SMM Feature Control MSR will be locked.<BR>
  #   FALSE - SMM Feature Control MSR will not be locked.<BR>
  # @Prompt Lock SMM Feature Control MSR.
  gUefiCpuPkgTokenSpaceGuid.PcdCpuSmmFeatureControlMsrLock|TRUE|BOOLEAN|0x3213210B

Stack guard (PcdCpuSmmStackGuard)

If enabled, an extra two pages are allocated for each SMI handler stack. One of the extra pages is a guard page at the top of the stack, preventing overflow of the entire stack into arbitrary memory above it.

+----------------------------------------------------+
| Known Good Stack | Guard Page |      SMM Stack     |
+----------------------------------------------------+
|        4K        |     4K     | PcdCpuSmmStackSize |
|<----------------- mSmmStackSize ------------------>|
|                                                    |
|<------------------ Processor n ------------------->|

The second extra page is a “known good stack” page, which is used as the stack pointer for certain interrupt handlers (set through the IST). IST entry 1 in the following code is the known good stack associated with that logical-processor.

  //
  // Additional SMM IDT initialization for SMM stack guard
  //
  if (FeaturePcdGet (PcdCpuSmmStackGuard)) {
    DEBUG ((DEBUG_INFO, "Initialize IDT IST field for SMM Stack Guard\n"));
    InitializeIdtIst (EXCEPT_IA32_PAGE_FAULT, 1);
  }

  //
  // Additional SMM IDT initialization for SMM CET shadow stack
  //
  if ((PcdGet32 (PcdControlFlowEnforcementPropertyMask) != 0) && mCetSupported) {
    DEBUG ((DEBUG_INFO, "Initialize IDT IST field for SMM Shadow Stack\n"));
    InitializeIdtIst (EXCEPT_IA32_PAGE_FAULT, 1);
    InitializeIdtIst (EXCEPT_IA32_MACHINE_CHECK, 1);
  }

Always enabled by default in upstream EDK2.

  ## Indicates if SMM Stack Guard will be enabled.
  #  If enabled, stack overflow in SMM can be caught, preventing chaotic consequences.<BR><BR>
  #   TRUE  - SMM Stack Guard will be enabled.<BR>
  #   FALSE - SMM Stack Guard will be disabled.<BR>
  # @Prompt Enable SMM Stack Guard.
  gUefiCpuPkgTokenSpaceGuid.PcdCpuSmmStackGuard|TRUE|BOOLEAN|0x1000001C

Read-only page tables

All backing page table memory will be marked as read-only once page table setup is complete, unless the debug features SMM heap guard or profiling features are enabled. This will prevent any arbitrary write vulnerabilities from modifying page table entries (for example, flipping the XD bit off on shellcode). Internal memory management code will manually clear the WP bit in CR0 to disable memory write protection when updating page table entries, and then restore it after.

Static page tables (PcdCpuSmmStaticPageTable)

Originally a feature controlled through PcdCpuSmmStaticPageTable, this feature has been deprecated and replaced entirely with restricted memory access (PcdCpuSmmRestrictedMemoryAccess). By default, without this feature enabled, the SMM executive would lazily map any memory accesses that would page-fault due to a non-present PTE.

Used to always be enabled by default, no longer present in upstream EDK2, merged with PcdCpuSmmRestrictedMemoryAccess.

  ## Indicates if SMM uses static page table.
  #  If enabled, SMM will not use on-demand paging. SMM will build static page table for all memory.
  #  This flag only impacts X64 build, because SMM always builds static page table for IA32.
  #  It could not be enabled at the same time with SMM profile feature (PcdCpuSmmProfileEnable).
  #  It could not be enabled also at the same time with heap guard feature for SMM
  #  (PcdHeapGuardPropertyMask in MdeModulePkg).<BR><BR>
  #   TRUE  - SMM uses static page table for all memory.<BR>
  #   FALSE - SMM uses static page table for below 4G memory and use on-demand paging for above 4G memory.<BR>
  # @Prompt Use static page table for all memory in SMM.
  gUefiCpuPkgTokenSpaceGuid.PcdCpuSmmStaticPageTable|TRUE|BOOLEAN|0x3213210D

Read-only IDT and GDT

The backing memory of the IDT and GDT structures are explicitly made read-only after initialization. This is implemented to prevent an arbitrary write from modifying the SMM IDT or GDT. For the case of the IDT, this prevents hijacking SMM execution flow through an interrupt handler using an arbitrary write. As for the case of the GDT, it is not really a target for exploitation in 64-bit mode, but in other modes, the CS segment’s base address may be modified to hijack execution flow.

VOID
PatchGdtIdtMap (
  VOID
  )
{
  EFI_PHYSICAL_ADDRESS BaseAddress;
  UINTN                Size;

  //
  // GDT
  // The range should have been set to RO
  // if it is allocated with EfiRuntimeServicesCode.
  //
  BaseAddress = mGdtBuffer;
  Size        = ALIGN_VALUE (mGdtBufferSize, SIZE_4KB);
  SmmSetMemoryAttributes (
    BaseAddress,
    Size,
    EFI_MEMORY_XP
    );

  //
  // IDT
  // The range should have been set to RO
  // if it is allocated with EfiRuntimeServicesCode.
  //
  BaseAddress = gcSmiIdtr.Base;
  Size        = ALIGN_VALUE (gcSmiIdtr.Limit + 1, SIZE_4KB);
  SmmSetMemoryAttributes (
    BaseAddress,
    Size,
    EFI_MEMORY_XP
    );
}

DEP

EDK2 now attempts to protect all executable image data according to the specified section permissions. Code sections will typically be read-only and executable, as long as they are page-aligned in length. SMM logical-processor stacks and the SMM heap are also read-only and execute-disable.

The SMM save state area is protected to be execute-disable, and the SMM entry point code will be protected to be read-only and execute-disable.

ASLR

Upstream EDK2 currently has no ASLR implementation, but there is a prototype implementation of SMM image base randomization by Jiewen Yao called SecurityEx. Despite having no ASLR, users of the same firmware but with different overall hardware will typically cause base addresses to slightly differ, so address leaks are still useful or required.

Heap guard (PcdHeapGuardPropertyMask)

EDK2 optionally allows heap guard pages in SMM, but it is generally intended as a debugging feature and not recommended to be enabled in production, especially in the case of SMM as it requires other important security features to be disabled, and it consumes a large amount of SMRAM, especially when multiple single-page-granularity allocations occur on the heap. The heap guard works similar to the stack guard, with two extra pages for every allocation, one preceding the main allocation body, and one following it. PcdHeapGuardPoolType and PcdHeapGuardPageType determine the type of memory allocations to apply the heap guard page system to (bitmasks of EFI_MEMORY_TYPE enum values). PcdHeapGuardPropertyMask controls which heap guard features to enable.

Always disabled by default on upstream EDK2.

  ## This mask is to control Heap Guard behavior.
  #
  # Note:
  #   a) Heap Guard is for debug purpose and should not be enabled in product
  #      BIOS.
  #   b) Due to the limit of pool memory implementation and the alignment
  #      requirement of UEFI spec, BIT7 is a try-best setting which cannot
  #      guarantee that the returned pool is exactly adjacent to head guard
  #      page or tail guard page.
  #   c) UEFI freed-memory guard and UEFI pool/page guard cannot be enabled
  #      at the same time.
  #   d) It is not supported to have a guard page at page 0 because this page
  #      may be used for NULL pointer detection or have special meaning if left
  #      mapped. Heap Guard will reject a memory allocation if the head guard
  #      would land on page 0.
  #
  #   BIT0 - Enable UEFI page guard.<BR>
  #   BIT1 - Enable UEFI pool guard.<BR>
  #   BIT2 - Enable SMM page guard.<BR>
  #   BIT3 - Enable SMM pool guard.<BR>
  #   BIT4 - Enable UEFI freed-memory guard (Use-After-Free memory detection).<BR>
  #   BIT6 - Enable non-stop mode.<BR>
  #   BIT7 - The direction of Guard Page for Pool Guard.
  #          0 - The returned pool is near the tail guard page.<BR>
  #          1 - The returned pool is near the head guard page.<BR>
  # @Prompt The Heap Guard feature mask
  gEfiMdeModulePkgTokenSpaceGuid.PcdHeapGuardPropertyMask|0x0|UINT8|0x3000105

Stack canaries

Enabled by default on upstream EDK2 on all supported compilers.

Intel CET shadow stack (PcdControlFlowEnforcementPropertyMask)

Latest EDK2 supports usage of Intel CET shadow-stack hardware-level protection. Intel CET’s shadow-stack feature is designed to stop return-oriented programming. CET-SS works by allocating a second shadow-stack alongside the normal stack, which the processor will use solely for return addresses. When CET-SS is in use, each time a call instruction implicitly pushes a return address, it will be pushed to the shadow stack as well as the regular stack. When a return instruction attempts to pop the address to return to from the stack, it is compared to the value on the shadow stack. If the values on both stacks differ, either the return address was modified or it was never implicitly pushed by a real call. When a return address mismatch happens upon a return, the processor will raise a #CP protection fault.

CET-SS is currently always disabled by default in upstream EDK2.

  ## Indicates the control flow enforcement enabling state.
  #  If enabled, it uses control flow enforcement technology to prevent ROP or JOP.<BR><BR>
  #   BIT0 - SMM CET Shadow Stack is enabled.<BR>
  #   Other - reserved
  # @Prompt Enable control flow enforcement.
  gEfiMdePkgTokenSpaceGuid.PcdControlFlowEnforcementPropertyMask|0x0|UINT32|0x30001017

Manual input pointer validation

Arguably the most important mitigation of them all, all user-provided memory accessed by the SMI handler must be manually validated. This is where the majority of SMM software bugs are introduced. Despite implicit CommBuffer validation, which only accounts for validation of the top-level buffer, many SMI handler commands will contain nested pointers/other addresses, which all must be carefully validated by the SMI handler. On top of that, the majority of IBVs are still making use of the legacy/lower-level EFI_SMM_SW_DISPATCH2_PROTOCOL and manually constructing the top-level buffer address from user registers, effectively skipping the implicit top-level CommBuffer validation. Care must also be taken to not introduce TOCTOU vulnerabilities even when validating all user-input memory. The full datum should be copied to SMRAM before validation and subsequent usage.

EDK2’s SmmMemLib offers SmmIsBufferOutsideSmmValid, which will validate a given user-input address and length for safe SMM communication. SmmMemLib also offers the following helpful wrapper functions which will implicitly validate the arguments, and then perform a memory copy, reducing the steps needed for safe processing of input: SmmCopyMemFromSmram, SmmCopyMemToSmram, SmmCopyMem, SmmSetMem.

Despite SmmMemLib’s existence, most IBVs choose to implement their own validation helper functions. For example, AMI-based firmware uses its own implementation known as AmiBufferValidationLib, the name of which can be discovered from OEM BIOS release notes such as the ones for the Lenovo ThinkCentre M72e.

CHANGES for F1KT68A/F1JT68A
- Adds module BootScriptHide for S3 Boot Script Protection..
- Adds module **AmiBufferValidationLib** and patch code for  Use of Non-Locked BARs in SMI Handlers.

Before introduction of PcdCpuSmmRestrictedMemoryAccess to AMI firmware, the buffer validation system was a simple check against a blacklisted array of SMRAM regions. After integration of restricted memory access, it is essentially equivalent to the upstream EDK2 SmmIsBufferOutsideSmmValid function. Reverse-engineered old AMI SMM input buffer validation function (pre PcdCpuSmmRestrictedMemoryAccess):

EFI_STATUS
AmiValidateMemoryBuffer(
  IN UINT64 BufferStart,
  IN INT64  BufferLength
  )
{
  INT64 i; 
  EFI_PHYSICAL_ADDRESS BufferEnd;
  SMM_AMI_REGION_DEFINITION *Region;
  EFI_PHYSICAL_ADDRESS RegionStart;

  i = 0;
  if ( !BufferStart )
    return EFI_INVALID_PARAMETER;
  if ( !gAmiSmmBlacklistRegionCount )
    return EFI_NOT_FOUND;
  BufferEnd = BufferStart + BufferLength;
  if ( BufferStart + BufferLength < BufferStart )
    return EFI_INVALID_PARAMETER;
  if ( !gAmiSmmBlacklistRegionCount )
    return EFI_SUCCESS;
  for ( Region = gAmiSmmBlacklistRegions; ; Region += 2 )
  {
    RegionStart = Region->Address;
    if ( BufferStart < Region->Address )
      goto CheckBufferEnd;
    if ( BufferStart < RegionStart + Region->Size )
      return EFI_ACCESS_DENIED;
    if ( BufferStart < RegionStart )
    {
CheckBufferEnd:
      if ( BufferEnd > RegionStart )
        break;
    }
    if ( ++i >= gAmiSmmBlacklistRegionCount )
      return EFI_SUCCESS;
  }
  return EFI_ACCESS_DENIED;
}

where gAmiSmmBlacklistRegions is a pool allocation containing all SMRAM regions, obtained using EFI_SMM_ACCESS2_PROTOCOL:

EfiSmmAccess2->GetCapabilities(EfiSmmAccess2, &Size, (VOID*)gAmiSmmBlacklistRegions)
gAmiSmmBlacklistRegionCount = Size >> 5;

Common SMM software vulnerabilities

SMM callout

A historically common vulnerability where SMM attempts to call out to non-SMRAM function pointers. Most occurrences of this have arisen due to misuse of leftover pointers to DXE/BS structures when in SMM mode, due to mixed DXE/SMM drivers (gBS, gRT). Unless the function pointer can be arbitrarily updated by non-SMM code, for example: to point it to a pivot primitive in SMRAM, this kind of bug is largely mitigated by the restricted memory access protection and the SMM code access check feature. It’s also worth mentioning that the runtime glue some newer IBV firmware, particularly AMI-derived implementations, will update the gRT pointer to an SMM-safe wrapper if the driver is running in SMM, so first-glance usage of gRT from SMM may be a red herring.

An example of an SMM callout vulnerability due to usage of a boot services structure from an SMI handler:

  Status = gBS->LocateHandleBuffer (ByProtocol, &EFI_DISK_INFO_PROTOCOL_GUID, NULL, &NoHandles, &HandleBuffer);

On firmware lacking the relevant mitigations, all it takes is updating the LocateHandleBuffer pointer inside gBS to point to a payload outside of SMRAM, and it will be executed in SMM.

Unvalidated input memory

All user input to SMI handlers must be carefully validated to ensure that no user-provided pointers reference memory that is not intended to be used as an SMM communication buffer. Even if the memory is determined to not be in SMRAM, without the restricted memory setting, this is not enough to ensure that the handler isn’t used for a confused deputy attack. For example: a guest of a hypervisor passing along the host physical address of hypervisor memory to a real SMI handler (assuming the hypervisor lets it pass through to host SMM).

An example of an implementation of an SMI handler that is vulnerable due to unvalidated input and TOCTOU issues:

#pragma pack(push, 1)
#define TEST_SMI_INPUT_MAGIC 0x11223344

typedef struct {
  UINT8  Type;
  UINT64 Variable;
} TEST_SMI_SUB_STRUCT;

typedef struct {
  UINT32               Magic;
  UINT64               SubStructSize;
  TEST_SMI_SUB_STRUCT* SubStruct;
  UINT64               Output;
} TEST_SMI_INPUT;
#pragma pack(pop)

//
// Vulnerable implementation 1.
// Vulnerable to confused deputy attacks/unvalidated memory access from SMM, as well as TOCTOU.
//
EFI_STATUS
EFIAPI
TestSmiHandler0(
  IN EFI_HANDLE DispatchHandle,
  IN CONST VOID *Context,
  IN OUT VOID   *CommBuffer,
  IN OUT UINTN  *CommBufferSize
  )
{
  TEST_SMI_INPUT* Input;

  //
  // Access the CommBuffer directly without validation, acceptable due to implicit CommBuffer validation.
  // However, if this variable was retrieved out of band (for example through user registers in the SMM save state),
  // this would no longer be acceptable without validation of the buffer.
  // However, the input CommBufferSize should still be validated before accessing any part of it!
  // The input CommBuffer may also need to be NULL checked depending on the type of protocol that this handler was registered through.
  //
  Input = CommBuffer;
  if (Input->Magic != TEST_SMI_INPUT_MAGIC) {
    return EFI_INVALID_PARAMETER;
  }

  //
  // Even though CommBuffer is implicitly validated, it still points directly to non-SMM accessible memory,
  // so this top-level size check is vulnerable to TOCTOU if used improperly.
  //
  if (Input->SubStructSize != sizeof (TEST_SMI_SUB_STRUCT)) {
    return EFI_INVALID_PARAMETER;
  }

  //
  // Access the sub-region pointed to by the input CommBuffer.
  // This is not safe whatsoever! Only the top-level CommBuffer memory has been validated.
  //
  switch ( Input->SubStruct->Type ) {
  default:
    Input->Output = 0;
    break;
  case 0:
    Input->Output = (Input->SubStruct->Variable * 2);
    break;
  case 1:
    Input->Output = (Input->SubStruct->Variable + 0x1000);
    break;
  }

  return EFI_SUCCESS;
}

TOCTOU

Despite all cores being halted in SMM due to the SMI rendez-vous system, TOCTOU vulnerabilities still exist due to DMA from external devices. Software may enqueue a request to a device that results in DMA to the communication buffer in an attempt to hit the race condition. Storage devices that support DMA are typically the simplest to use as a primitive for this kind of exploitation. For example, a block on disk can simply be used as a staging buffer for the data used to attempt to hit the race condition with. Software can simply enqueue a read request with the output physical address pointing to the communication buffer.

Here is an example of the SMI handler updated to attempt to validate the input SubStruct but remains vulnerable to TOCTOU issues.

//
// Vulnerable implementation 2.
// Vulnerable to TOCTOU attacks despite attempted explicit validation.
//
EFI_STATUS
EFIAPI
TestSmiHandler1(
  IN EFI_HANDLE DispatchHandle,
  IN CONST VOID *Context,
  IN OUT VOID   *CommBuffer,
  IN OUT UINTN  *CommBufferSize
  )
{
  TEST_SMI_INPUT* Input;

  //
  // Access the CommBuffer directly without validation, acceptable due to implicit CommBuffer validation.
  // However, if this variable was retrieved out-of-bound (for example through user registers in the SMM save state),
  // this would no longer be acceptable without validation of the buffer.
  //
  Input = CommBuffer;
  if ((CommBuffer == NULL)
      || (CommBufferSize == NULL)
      || (*CommBufferSize != sizeof (*Input))
      || (Input->Magic != TEST_SMI_INPUT_MAGIC))
  {
    return EFI_INVALID_PARAMETER;
  }

  //
  // Even though CommBuffer is implicitly validated, it still points directly to non-SMM accessible memory,
  // so this top-level size check is vulnerable to TOCTOU if used improperly.
  //
  if (Input->SubStructSize != sizeof (TEST_SMI_SUB_STRUCT)) {
    return EFI_INVALID_PARAMETER;
  }

  //
  // Attempt to validate that the SubStruct points to valid memory.
  // TOCTOU present on Input->SubStruct and Input->SubStructSize here,
  // re-using the live value directly from user memory.
  // A race can be hit that modifies the size to be validated in between the size check above and the size usage here.
  //
  if (!SmmIsBufferOutsideSmmValid (Input->SubStruct, Input->SubStructSize)) {
    return EFI_INVALID_PARAMETER;
  }

  //
  // Access the sub-region pointed to by the input CommBuffer.
  // Looks like the memory is properly validated, but the live pointer for SubStruct is used directly,
  // allowing for a TOCTOU vulnerability when the race condition is hit to update the SubStruct
  // value in between here and the check above, this would lead to an arbitrary read from SMM.
  //
  switch (Input->SubStruct->Type) {
  default:
    Input->Output = 0;
    break;
  case 0:
    Input->Output = (Input->SubStruct->Variable * 2);
    break;
  case 1:
    Input->Output = (Input->SubStruct->Variable + 0x1000);
    break;
  }

  return EFI_SUCCESS;
}

This type of attempted validation combined with a TOCTOU vulnerability remains common in modern real-world firmware.

Corrected TestSmiHandler

This is the final version of the TestSmiHandler routine showcased above, this time with both classes of vulnerabilities corrected. Input data is first copied to local snapshots that are used throughout the lifetime of the function, and input memory ranges are validated.

//
// Secure implementation 3.
//
EFI_STATUS
EFIAPI
TestSmiHandler2(
  IN EFI_HANDLE DispatchHandle,
  IN CONST VOID *Context,
  IN OUT VOID   *CommBuffer,
  IN OUT UINTN  *CommBufferSize
  )
{
  TEST_SMI_INPUT      Input;
  TEST_SMI_SUB_STRUCT SubStruct;

  //
  // Validate and copy the entire input structure to SMRAM, we will operate on this single snapshot throughout the whole function.
  //
  if ((CommBuffer == NULL) || (CommBufferSize == NULL) || (*CommBufferSize != sizeof(Input))) {
    return EFI_INVALID_PARAMETER;
  } else if (EFI_ERROR (SmmCopyMemToSmram (&Input, CommBuffer, sizeof(Input)))) {
    return EFI_INVALID_PARAMETER;
  } else if (Input.Magic != TEST_SMI_INPUT_MAGIC) {
    return EFI_INVALID_PARAMETER;
  }

  //
  // Validate the size of the sub-struct, then validate and make a local snapshot of it.
  //
  if (Input.SubStructSize != sizeof(TEST_SMI_SUB_STRUCT)) {
    return EFI_INVALID_PARAMETER;
  } else if (EFI_ERROR( SmmCopyMemToSmram (&SubStruct, Input.SubStruct, sizeof(TEST_SMI_SUB_STRUCT)))) {
    return EFI_INVALID_PARAMETER;
  }

  //
  // Access our validated local snapshot of the sub-struct pointed to by the validated input CommBuffer snapshot.
  //
  switch (SubStruct.Type) {
  default:
    Input.Output = 0;
    break;
  case 0:
    Input.Output = (SubStruct.Variable * 2);
    break;
  case 1:
    Input.Output = (SubStruct.Variable + 0x1000);
    break;
  }

  //
  // Flush our updated local snapshot back to the real live CommBuffer.
  //
  SmmCopyMemFromSmram (CommBuffer, &Input, sizeof(Input));
  return EFI_SUCCESS;
}

SmmSetVariable confused deputy

SmmSetVariable implicitly operates with higher privileges than the regular runtime services SetVariable, always allowing writes to the target variable. If an arbitrary variable can be written through SMM, it acts as a confused deputy. Although it may seem far-fetched, this really has been found in the wild in a Lenovo Insyde BIOS product (CVE-2025-4424), as well as many instances of AMI firmware. Another thing to note, which can be observed in the same advisory, this can also be used as an arbitrary SMRAM read primitive if an unvalidated address is passed along as the Data argument to SmmSetVariable.

EFI_STATUS
EFIAPI
SmmVariableSetVariable (
  IN CHAR16   *VariableName,
  IN EFI_GUID *VendorGuid,
  IN UINT32   Attributes,
  IN UINTN    DataSize,
  IN VOID     *Data
  )
{
  EFI_STATUS Status;

  //
  // Disable write protection when the calling SetVariable() through EFI_SMM_VARIABLE_PROTOCOL.
  //
  mRequestSource = VarCheckFromTrusted;
  Status         = VariableServiceSetVariable (
                     VariableName,
                     VendorGuid,
                     Attributes,
                     DataSize,
                     Data
                     );
  mRequestSource = VarCheckFromUntrusted;
  return Status;
}

Trust-on-first-use initialization

A common vulnerability-prone pattern that can often be found is providing implicit trust/lack of validation to the first initializer/caller of an SMI handler. Here the EFI firmware variable “HiiDB” (containing a start address and size) is implicitly trusted without proper validation, and cached for later usage. This is a very common pattern to look for when SMI handlers read from firmware variables.

  DataSize = 8;
  if (!gCachedHiiDbData.Address || (HiiDbDataSize = gCachedHiiDbData.Size) == 0) {
    if (EFI_ERROR (gRT->GetVariable (L"HiiDB", &VendorGuid, &Attributes, &DataSize, &gCachedHiiDbData))) {
      return EFI_ACCESS_DENIED;
    }
    HiiDbDataSize = gCachedHiiDbData.Size;
    HiiDbDataPtr = gCachedHiiDbData.Address;
  }
  // ... HiiDbDataPtr used later on as an arbitrary source to copy memory from.

NVS area implicit trust

ACPI, CPU, BiosGuard, etc. NVS memory is often referenced from SMI handlers, and may contain nested unvalidated pointers in some firmware. Aside from nested firmware, the top-level address of the NVS area should be validated as well. Trust-on-first-use of the address of these NVS areas has been observed in real-world firmware (GNVS_PTR firmware variable). Be on the lookout for usage of any EfiACPIMemoryNVS allocated memory from SMM.

SMRAM buffer overflows

Even when the input memory regions are properly validated, there may still be trivial buffer overflows present in SMI handlers. Here is an example of a fully controlled heap overflow found in real firmware:

  PublicAlloc_0x4000 = 0;
  FoundFwStartBasePageAligned = (UINT8*)(FoundFwStartBase - (FoundFwStartBase & 0xFFF));
  if (IsInSmm) {
    gSmst->SmmAllocatePool (EfiRuntimeServicesData, 0x4000, &PublicAlloc_0x4000);
  } else {
    gBS->AllocatePool (EfiBootServicesData, 0x4000, &PublicAlloc_0x4000);
  }
  PublicAlloc_0x4000_Bytes = (UINT8*)PublicAlloc_0x4000;
  COM__MemMove (PublicAlloc_0x4000, FoundFwStartBasePageAligned, 0x4000u);
  ContextOffset = i - (UINT64)FoundFwStartBasePageAligned;
  *(UINT32*)&PublicAlloc_0x4000_Bytes[ContextOffset] = *UserReadOffset;
  *(UINT32*)&PublicAlloc_0x4000_Bytes[ContextOffset + 4] = UserControlledSize;
  // can overflow the 0x4000 buffer, UserControlledSize can be larger!
  COM__MemMove (&PublicAlloc_0x4000_Bytes[ContextOffset + 8], UserRegisterRbx, UserControlledSize);

The same can be found for stack buffers, leading to even easier exploitation in the absence of stack canaries. We won’t cover SMM heap exploitation yet here, as there are usually lower-hanging bugs in a lot of targets.

Uninitialized INOUT variables

Sometimes functions with arguments that are semantically both input and output are treated purely as output. This is especially relevant for the SmmGetVariable function, where a common code pattern is calling it twice, first to query the size of the variable, and second to read the actual data. The intended implementation of this pattern is to initialize the DataSize argument to 0, so that the size validation path in VariableServiceGetVariable always bails out and returns the actual size of the variable and EFI_BUFFER_TOO_SMALL:

  //
  // Get data size
  //
  VarDataSize = DataSizeOfVariable (Variable.CurrPtr);
  ASSERT (VarDataSize != 0);
  if (*DataSize >= VarDataSize) {
    // ... Copy variable to Data argument
    Status = EFI_SUCCESS;
    goto Done;
  } else {
    *DataSize = VarDataSize;
    Status = EFI_BUFFER_TOO_SMALL;
    goto Done;
  }

Here is an example of a broken instance of this pattern using an uninitialized DataSize argument found in real-world firmware:

 UINT8 Data[1280];

 COM__MemSet (Data, 0, sizeof(Data));
 gRT->GetVariable (L"Redacted", &VendorGuid, 0, &DataSize, Data);
 Status = gRT->GetVariable (L"Redacted", &VendorGuid, 0, &DataSize, Data);

There are actually two bugs/exploitable conditions here. The first is purely due to the uninitialized DataSize in/out variable. If the memory on the stack used for the DataSize variable happens to contain a UINTN value that exceeds the size of the “Data” array, the first GetVariable call intended to query the size will succeed and overflow the buffer. The second is the fact that even if the DataSize variable was initialized to 0, it never validates that the updated DataSize fits within the Data variable, leading to a stack buffer overflow with arbitrary data of an arbitrary size. Two critical bugs in two lines.

Error code write to unvalidated memory before bailing out

This falls under the Unvalidated input memory section of the post, but this is a specific kind of pattern that I have observed. Some SMI handlers will unconditionally write out an error code or return status value to the CommBuffer or user data pointer before validating that the memory is in a valid region! Once again it sounds far-fetched, but I have even found this in Intel board reference code.

  UserCommBuffer->ErrorCode = 1;
  // ...
  if (!SmmIsBufferOutsideSmmValid (UserCommBuffer, sizeof (*UserCommBuffer))) {
    return;
  }
· 51 min read