Skip to content

Boot Guard TOCTOU Vulnerability Mitigation

Michael Kubacki edited this page Nov 23, 2019 · 1 revision

Boot Guard TOCTOU Vulnerability Mitigation

A security vulnerability was discovered (CVE-2019-11098 - BZ 1614) in EDK II firmware that allows an attacker with physical access to achieve code execution after the Intel® Boot Guard ACM computes and validates the hash of the Initial Boot Block (IBB) and firmware measurements have been extended into the TPM PCR0. This means the firmware will be considered valid and PCR0 will have expected values even though unsigned code has executed. By using targeted SPI transactions to modify IBB code after the IBB image is verified in CPU LLC, the modified code may later be fetched from SPI flash and executed.

This attack requires physical access to the SPI flash.

Background

Intel® Boot Guard is a technology which establishes a strong component based Static Root of Trust for verification and measurement (S-CRTV/S-CRTM). Protection is provided against malicious modification of the Initial Boot Block (IBB). The IBB in this context refers to the initial "BIOS" boot block code provided by the platform manufacturer that is verified by the ACM. The ACM places the CPU into Non-Eviction Mode (NEM) and reads the IBB into the CPU LLC. The verification and/or measurement of the IBB will occur on the image in the CPU LLC. Eventually, IBB execution begins when ACM jumps to the IBB entry point.

In terms of Platform Initialization (PI) specification terminology, IBB execution begins in the SEC phase and eventually reaches the PEI phase where permanent memory (i.e. DRAM) is initialized. All of the code executed up to and including the memory initialization code is from the CPU LLC operating in NEM. Throughout the boot execution flow in NEM, pointers are typically stored in global data structures (such as the HOB list or PPI database) or behind architecturally defined interfaces such as the Global Descriptor Table (GDT) or Interrupt Descriptor Table (IDT).

The addresses stored in these global areas during NEM execution refer to content verified and/or measured by ACM or modified by a trusted code component in IBB. After permanent memory is initialized, the IBB will read subsequent portions of firmware code into permanent memory and verify and/or measure the code according to boot policy to continue a chain of trust. However, in order to use the cache in a normal hierarchical caching mode, NEM must be disabled and the cache will be flushed in the process. Global software structures such as the HOB list and PPI database that were maintained in the heap in Temporary RAM are migrated by the PEI Foundation to permanent memory before the general module dispatch process begins in permanent memory. However, the code in the PEI Foundation today will only "migrate" (i.e. convert pointers from a Temporary RAM location to the corresponding permanent memory location) pointers for known code structures (e.g. defined in the UEFI specification or PI specification) in the Temporary RAM heap and Temporary RAM stack. There's a large set of pointers to locations that were in Cache-As-RAM (CAR) whose pointers are not updated. If the system firmware flash ROM is stored on a SPI flash device, the addresses map 1:1 to the original contents in cache via SPI flash MMIO. This means the code is accessible for functionality but it will result in a read to SPI flash whereas the verified copy in CPU LLC should be used instead.

In order to mitigate this vulnerability, global pointers into CAR addresses must not exist (and certainly not dereferenced) after NEM is disabled. Those resources must be moved from CAR to permanent memory and any global pointer references must be updated. In this mitigation, the process of performing these actions is referred to as "migration".

Platform Guidance for the Mitigation Changes Provided

The changes described in this mitigation are intended to simply integrate into firmware solutions. For the changes to function as intended, the platform firmware implementation should follow these guidelines.

The changes are currently being staged in the following EDK II fork for additional validation before being sent to the EDK II mailing list: https://github.com/makubacki/edk2/tree/btg_toctou_mitigation_staging

The changes should not be considered final or production ready until they are reviewed and pushed onto edk2/master.

  1. Always ensure PcdShadowPeimOnBoot and PcdShadowPeimOnS3Boot (if platform supports S3) are set to TRUE if Boot Guard is enabled and V=1 or M=1.
  2. Always ensure PcdMigrateTemporaryRamFirmwareVolumes is set to TRUE.
  3. Ensure that all PEIMs are relocatable. Relocation tables should not be stripped.
  4. If an Intel® Firmware Support Package (FSP) binary solution is used, the binary must have these mitigation changes integrated.
  5. Avoid maintaining pointers to pre-memory addresses inside embedded structures or other non-standard structures that the automatic migration code introduced in this change cannot identify.
  6. Migrate the FIT table based on platform requirements for FIT access in post-memory.

Very Important

  1. Enable paging after memory initialization and mark the IBB range as Not Present (NP).

    This will cause a page fault on access to the IBB region. This CR2 register can be used to identify the address accessed and the IP.

High-Level Migration Required

Resources that must be migrated can be categorized as code or data.

Code Migration

Code pointers are typically registered during pre-memory execution so they are available for invocation later in the boot flow such as PPIs or callbacks that will be invoked upon some future event (e.g. PPI notification).

The following code pointers are addressed in the current mitigation posting:

  • PEIM-to-PEIM Interfaces (PPIs)
  • PEI notifications
  • Reset callback handlers
  • Status code callback handlers

PEI Convert Pointer PPI

A new PPI is introduced in these mitigation changes that is published by the PEI Foundation to provide a pointer conversion service for pointers to firmware volumes in pre-memory that are not migrated by the PEI Foundation. This PPI is only published when PcdMigrateTemporaryRamFirmwareVolumes is set to TRUE.

PEIM-to-PEIM Interfaces (PPIs) and PEI Notifications

Modules that produce PPIs can register for shadow using the PEI Services which make the PEI Foundation call into the module's entry point after permanent memory is initialized. Once relocated in permanent memory, the module can use the ReinstallPpi () API defined in the Platform Initialization (PI) specification to reinstall the PPI with a GUID and interface structure in permanent memory. In practice, this is tedious and error prone so a generic PPI migration solution is introduced in this mitigation to automatically migrate PPIs installed under conditions that allow migration. The migration process occurs during PEI Core shadow, before permanent memory module dispatch beings. A similar reinstall service does not exist for PEI notifications, however must be migrated and can be handled in a similar way to PPIs.

If the PPI descriptor address is not within a PEIM that is in an installed FV (in the PEI Foundation's FV list) or the Temporary RAM heap/stack, this mitigation solution will not be able to automatically migrate the PPI. In this case, the PEIM that installed the PPI must re-install the PPI. Furthermore, if the PPI interface structure is stored in a dynamically allocated memory buffer, this mitigation solution will not be able to automatically migrate the pointers in the structure.

The pointers known to the PEI Foundation (e.g. defined in MdePkg/Include/Pi/PiPeiCis.h) are updated during migration because the offset from the Temporary RAM heap/stack and permanent memory heap/stack are known to the PEI Foundation. Similarly, the offset from the Temporary RAM FV and corresponding permanent memory FV is known to the PEI Foundation. Other pointers (such as static function pointers in a PPI interface structure) rely upon the PE32/COFF address fixup done using a relocation table during the FV migration support introduced in this mitigation change. If the addresses are not statically calculated in the image, such as those placed in a buffer allocated with AllocatePool (), the module must manually update the pointers to the permanent memory location.

Reset Callbacks and Status Code Callbacks

Reset callback and status code callback functions can be registered by PEIMs throughout the boot. These callbacks are registered outside the PEI Foundation so the PEIMs that provide the registration service use the PEI Convert Pointer PPI service to convert the function addresses to permanent memory addresses.

Data Migration

Global data pointers are inherent in the system architecture in addition to firmware implementation.

Pointers to the following data structures are addressed in the current mitigation posting:

  • Interrupt Descriptor Table (IDT)
    • Migrated in CpuMpPei
  • Global Descriptor Table (GDT)
    • Migrated in SecCore (SecTemporaryRamDone ())
  • PEI Services table pointer
    • Migrated in PeiCore
  • Temporary RAM (T-RAM) heap and stack
    • Migrated in PeiCore (or TemporaryRamMigration ())
  • PPI list pointer
    • Migrated in PeiCore
  • Hand Off Block (HOB) list pointer
    • Migrated in PeiCore
  • Memory allocation HOB entries
    • Migrated in PeiCore
  • Firmware Volume (FV) HOB entries
    • Migrated in PeiCore

Verifying Migration

Several mechanisms are introduced to simplify checking the migration of code and data resources.

Catching Invalid Accesses

In order to catch an access to a verified IBB range after NEM is disabled, a page table may be set up in post-memory PEI that marks the IBB range as Not Present. This causes a page fault which allows the user to quickly identify the address that was accessed and caused the issue by inspecting the value in the CR2 register.

In the example exception handler print message, code execution was attempted at the invalid address 0xFFF75630. An address map may be used to map that address to a specific function.

!!!! IA32 Exception Type - 0E(#PF - Page-Fault)  CPU Apic ID - 00000000 !!!!
ExceptionData - 00000000  I:0 R:0 U:0 W:0 P:0 PK:0 SS:0 SGX:0
EIP  - FFF75630, CS  - 00000010, EFLAGS - 00010046
EAX  - ACA54C9C, ECX - 3BC1F6DE, EDX - ACA54CA0, EBX - 00000000
ESP  - ACA4F930, EBP - ACA4FA14, ESI - ACA4FA10, EDI - 00000040
DS   - 00000018, ES  - 00000018, FS  - 00000018, GS  - 00000018, SS - 00000018
CR0  - 80000013, CR2 - FFF75630, CR3 - BDFF3000, CR4 - 00000620
DR0  - 00000000, DR1 - 00000000, DR2 - 00000000, DR3 - 00000000
DR6  - FFFF0FF0, DR7 - 00000400
GDTR - ACA5B4B0 0000003F, IDTR - BE1FF004 0000010F
LDTR - 00000000, TR - 00000000
FXSAVE_STATE - ACA4F670

Debug Print Messages

In a DEBUG build, messages will be printed to the DEBUG output that show before and after migration details. Some examples of those output are shown below to aid in finding the messages in DEBUG output.

Many of the debug print messages are defined as DEBUG_VERBOSE. To see these messages, set BIT22 ("Detailed debug message") in the PCDs PcdFixedDebugPrintErrorLevel / PcdDebugPrintErrorLevel. This should specifically be set for PeiCore, CpuMpPei, and the module that FirmwareInterfaceTableLib is linked against (if used).

PPI and PEI Notification Migration Messages

  1. PEI Callback Notifications
PPI lists before Temporary RAM FV migration:
CallbackNotify[ 0] {49EDB1C1-BF21-4761-BB12-EB0031AABB39} at 0xFFECCFE4 (CAR)
CallbackNotify[ 1] {EA7CA24B-DED5-4DAD-A389-BF827E8F9B38} at 0xFFECCFF0 (CAR)
CallbackNotify[ 2] {0ECC666B-4662-47F9-9DD5-D096FF7DA49E} at 0x869D5000 (Post-Memory)
CallbackNotify[ 3] {605EA650-C65C-42E1-BA80-91A52AB618C6} at 0xFFED1150 (CAR)
CallbackNotify[ 4] {F894643D-C449-42D1-8EA8-85BDD8C65BDE} at 0xFFEE72A4 (CAR)
.   .   .
PPI lists after Temporary RAM FV migration:
CallbackNotify[ 0] {49EDB1C1-BF21-4761-BB12-EB0031AABB39} at 0x8A1B2EC4 (Post-Memory)
CallbackNotify[ 1] {EA7CA24B-DED5-4DAD-A389-BF827E8F9B38} at 0x8A1B2ED0 (Post-Memory)
CallbackNotify[ 2] {0ECC666B-4662-47F9-9DD5-D096FF7DA49E} at 0x869D5000 (Post-Memory)
CallbackNotify[ 3] {605EA650-C65C-42E1-BA80-91A52AB618C6} at 0x8A077150 (Post-Memory)
CallbackNotify[ 4] {F894643D-C449-42D1-8EA8-85BDD8C65BDE} at 0x8A08D2A4 (Post-Memory)
.   .   .
  1. PEI Notify Notifications
Lists before EvacuateTempRam ( ):
DispatchNotify[ 0] {DCD0BE23-9586-40F4-B643-06522CED4EDE} at 0xFFECD038 (CAR)
.   .   .
Lists after EvacuateTempRam ( ):
DispatchNotify[ 0] {DCD0BE23-9586-40F4-B643-06522CED4EDE} at 0x8A1B2F18 (Post-Memory)
.   .   .
  1. PPIs
Lists before EvacuateTempRam ( ):
PPI[ 0] {8C8CE578-8A3D-4F1C-9935-896185C32DD3} at 0x8A1B2F0C (Post-Memory)
PPI[ 1] {5473C07A-3DCB-4DCA-BD6F-1E9689E7349A} at 0x8A1B2E88 (Post-Memory)
PPI[ 2] {B9E0ABFE-5979-4914-977F-6DEE78C278A6} at 0x8A1B2F88 (Post-Memory)
PPI[ 3] {CEAB683C-EC56-4A2D-A906-4053FA4E9C16} at 0x869D500C (Post-Memory)
PPI[ 4] {6F8C2B35-FEF4-448D-8256-E11B19D61077} at 0x869D5018 (Post-Memory)
PPI[ 5] {2F3962B2-57C5-44EC-9EFC-A69FD302032B} at 0x869D5024 (Post-Memory)
PPI[ 6] {0ECC666B-4662-47F9-9DD5-D096FF7DA49E} at 0x869D5030 (Post-Memory)
PPI[ 7] {06E81C58-4AD7-44BC-8390-F10265F72480} at 0xFFED115C (CAR)
PPI[ 8] {01F34D25-4DE2-23AD-3FF3-36353FF323F1} at 0xFFED1168 (CAR)
PPI[ 9] {4D8B155B-C059-4C8F-8926-06FD4331DB8A} at 0xFFED1138 (CAR)
PPI[10] {A60C6B59-E459-425D-9C69-0BCC9CB27D81} at 0xFFED1144 (CAR)
.   .   .
Lists after EvacuateTempRam ( ):
PPI[ 0] {8C8CE578-8A3D-4F1C-9935-896185C32DD3} at 0x8A1B2F0C (Post-Memory)
PPI[ 1] {5473C07A-3DCB-4DCA-BD6F-1E9689E7349A} at 0x8A1B2E88 (Post-Memory)
PPI[ 2] {B9E0ABFE-5979-4914-977F-6DEE78C278A6} at 0x8A1B2F88 (Post-Memory)
PPI[ 3] {CEAB683C-EC56-4A2D-A906-4053FA4E9C16} at 0x869D500C (Post-Memory)
PPI[ 4] {6F8C2B35-FEF4-448D-8256-E11B19D61077} at 0x869D5018 (Post-Memory)
PPI[ 5] {2F3962B2-57C5-44EC-9EFC-A69FD302032B} at 0x869D5024 (Post-Memory)
PPI[ 6] {0ECC666B-4662-47F9-9DD5-D096FF7DA49E} at 0x869D5030 (Post-Memory)
PPI[ 7] {06E81C58-4AD7-44BC-8390-F10265F72480} at 0x8A07715C (Post-Memory)
PPI[ 8] {01F34D25-4DE2-23AD-3FF3-36353FF323F1} at 0x8A077168 (Post-Memory)
PPI[ 9] {4D8B155B-C059-4C8F-8926-06FD4331DB8A} at 0x8A077138 (Post-Memory)
PPI[10] {A60C6B59-E459-425D-9C69-0BCC9CB27D81} at 0x8A077144 (Post-Memory)
.   .   .

FV and FV HOB Migration Messages

  1. FV and PEIM Migration
FV[01] at 0xFF8A0000.
  Migrating FV[1] from 0xFF8A0000 to 0x89FF6000
  FV buffer range from 0x89FF6000 to 0x8A066000
    Child FV[02] is being migrated.
    Child FV offset = 0x180.
    Child migrated FV header at 0x89FF6180.
    Migrating FileHandle  0 PlatformVTdInfoSamplePei
    Migrating FileHandle  1 IntelVTdPmrPei
    .   .   .
  1. FV HOB Migration All FV HOB Base Addresses (BA) should be in permanent memory ranges.
  Found FV HOB.
     BA=0000000089FF6180  L=0000000000010000
     BA=0000000089FF6180  L=0000000000010000
     BA=0000000089FF6180  L=0000000000010000
     BA=0000000089F960A0  L=0000000000000078
  .   .   .

GDT, IDT, and FIT Migration Messages

  1. GDT Migration Address 0xFFFFDB10 is in the CAR address range.
Dumping GDT (before migration):
GDT at 0xFFFFDB10. Entries: 8. Size: 0x40

  Entry[0000]
    Base = 0x0
    Limit  = 0x0
    Access Bytes:
      Type: 0x0
        Accessed             : 0x0
        RW                   : 0x0
        Direction/Conforming : 0x0
        Executable           : 0x0
      Descriptor Type (S)    : 0x0
      Privilege (DPL)        : 0x0
      Present (P)            : 0x0
      Flags:
        AVL                  : 0x0
        L                    : 0x0
        DB                   : 0x0
        G                    : 0x0
  Entry[0001]
    Base = 0x0
    Limit  = 0xFFFFF
    Access Bytes:
      Type: 0x2
        Accessed             : 0x0
        RW                   : 0x1
        Direction/Conforming : 0x0
        Executable           : 0x0
      Descriptor Type (S)    : 0x1
      Privilege (DPL)        : 0x0
      Present (P)            : 0x1
      Flags:
        AVL                  : 0x0
        L                    : 0x0
        DB                   : 0x1
        G                    : 0x1
.   .   .

After migration, GDT is at 0x869DBAF0, an address in DRAM.

Dumping GDT (after migration):
GDT at 0x869DBAF0. Entries: 8. Size: 0x40

  Entry[0000]
    Base = 0x0
    Limit  = 0x0
    Access Bytes:
      Type: 0x0
        Accessed             : 0x0
        RW                   : 0x0
        Direction/Conforming : 0x0
        Executable           : 0x0
      Descriptor Type (S)    : 0x0
      Privilege (DPL)        : 0x0
      Present (P)            : 0x0
      Flags:
        AVL                  : 0x0
        L                    : 0x0
        DB                   : 0x0
        G                    : 0x0
  Entry[0001]
    Base = 0x0
    Limit  = 0xFFFFF
    Access Bytes:
      Type: 0x2
        Accessed             : 0x0
        RW                   : 0x1
        Direction/Conforming : 0x0
        Executable           : 0x0
      Descriptor Type (S)    : 0x1
      Privilege (DPL)        : 0x0
      Present (P)            : 0x1
      Flags:
        AVL                  : 0x0
        L                    : 0x0
        DB                   : 0x1
        G                    : 0x1
.   .   .
  1. IDT Migration Address 0x8A1B4004 is already in permanent memory. However, the interrupt handler entries are in CAR address ranges.
Dumping IDT:
IDT at 0x8A1B4004. Entries: 34. Size: 0x110

  Entry[000]
    Offset      = 0xFFFF9890
    Selector    = 0x10
    Gate Type   = Interrupt (32-bit) (0x8E)
  Entry[001]
    Offset      = 0xFFFF989A
    Selector    = 0x10
    Gate Type   = Interrupt (32-bit) (0x8E)
  Entry[002]
    Offset      = 0xFFFF98A4
    Selector    = 0x10
    Gate Type   = Interrupt (32-bit) (0x8E)

After migration, all entries are now in DRAM.

Dumping IDT:
IDT at 0x8A1B4004. Entries: 34. Size: 0x110

  Entry[000]
    Offset      = 0x869DC250
    Selector    = 0x10
    Gate Type   = Interrupt (32-bit) (0x8E)
  Entry[001]
    Offset      = 0x89A1B6AA
    Selector    = 0x10
    Gate Type   = Interrupt (32-bit) (0x8E)
  Entry[002]
    Offset      = 0x89A1B6B4
    Selector    = 0x10
    Gate Type   = Interrupt (32-bit) (0x8E)
Clone this wiki locally
You can’t perform that action at this time.