TrustZone Break-in Vulnerabilities in Ampere UEFI MM Drivers (Arbitrary Out-of-Bounds Write)
ARM Ampere管理模式下的PCIe驱动存在多个任意的Out-of-Bounds空字节写漏洞,源于初始化锁未设置和未对NS-EL1提供的缓冲区大小进行充分验证。攻击者可利用这些漏洞在S-EL0驱动地址空间中任意位置写入空字节,并通过HotPlug接口覆盖全局数据结构。 2026-1-5 23:59:30 Author: github.com(查看原文) 阅读量:0 收藏

Summary

Multiple arbitrary Out-of-Bounds (OOB) '\0' byte write vulnerabilities affecting the ARM Ampere Management Mode (MM) PCIe driver were discovered. This code is bundled into the ARM Unified Extensible Firmware Interface (UEFI) firmware and runs in the Secure world at Exception Level 0 (S-EL0). The PCIe driver is used to initialize the root complex, underlying controllers, logging facilities, and perform self tests during UEFI Driver Execution Environment (DXE) phase. After initialization a lock is used to limit the available interfaces to a few functions, which are accessible from Non-Secure EL1 (NS-EL1) using the Secure Monitor Call (SMC) instruction with a predefined shared buffer for message passing.

It was found that the post initialization lock was never set leaving per-initialization interfaces available and multiple handlers use a UIN64 size from an NS-EL1 supplied buffer without proper bounds checking to index an array and writing a '\0' byte.

Ampere has addressed the vulnerability and posted a security bulletin.

Analysis

NS-EL1 software interacts with the MM PCIe driver using the Firmware Framework for A-Profile (FFA) specification through the SMC instruction. The SMC instruction is used to switch to and from the Non-Secure and Secure worlds and FFA is used for dispatching to specific drivers and services. A shared buffer (initialized during the UEFI DXE phase as mNsCommBufferMemRegion in ArmPkg/Drivers/MmCommunicationDxe/MmCommunication.c) is used to communicate messages between NS-EL1 and the S-EL0 PCIe driver.

The handler for the PCIe MM driver can be reached using the following GUID using the FFA specification. PCIeMmHeader and PCIeMmBuffer show the general layout of the communication buffer. Before the PCIeMmHandler dispatch routine is called the MM subsystem copies the shared message buffer to a private memory region to prevent Time-of-Check to Time-of-Use (TOCTOU) issues. As drivers uniquely define the underlying structures in the communicated messages each must perform validation before using the supplied data.

The code snippets included were created through analysis with Ghidra using names contained in string references from the UEFI image. Known functions and variables from the EDK2 repository were also applied.

#define PCIE_MM_GUID \
    {0xe49f1b7a, 0xd3c9, 0x44f4, { 0x9b, 0xc4, 0xd3, 0xb2, 0x9a, 0xcb, 0xb3, 0x20 }};

#define MAX_PCIE_MM_MAX_SIZE 0x10000

typedef struct {
  UINT64  FuncId;
  UINT8   data[1];
} PcieMmHeader;

// ...

typedef struct {
  UINT64  unknown_0;
  UINT64  size;
  UINT64  unknown_1;
  UINT8   data[1];
} PcieMmBuffer;

The PCIeMmBuffer structure includes a FuncId field for dispatching to specific sub-handlers and both 103 and 104 , shown in the code snippet below as case 2 and case 3, include a UINT64 size to represent the size of a NS-EL1 supplied buffer.

EFI_STATUS PcieMmHandler(int DispatchHandle,void *RegisterContext,void *CommBuffer, uint64_t *CommBufferSize) {
  // ...
  PcieMmHeader *header;
  PcieMmBuffer *buffer;

  ASSERT(CommBuffer);
  ASSERT(CommBufferSize);

  if ((DebugLevel() & 0xff) != 0) {
    Debug(0x40,"%a \n","PcieMmHandler");
    Debug(0x400000, "PcieMm Handler: CommBuffer - 0x%p, CommBufferSize - 0x%x\n", CommBuffer, *CommBufferSize);
    Debug(0x400000, "PcieMm Handler: FuncId - %d\n", CommBuffer->FuncId);
  }
  header = (PCIeMmHeader *)CommBuffer;
  FuncId = header->FuncId;
  buffer = header->data;
  if (pcie_mm_lock == '\0') {
    if (106 < function) {
      //...
      goto switchD_000077a8_caseD_4;
    }
    if (FuncId < 100) goto LAB_00007760;
    if (FuncId - 101 < 6) {
      switch(FuncId - 101 & 0xffffffff) {
      // ...
      case 1: // initialization lock function
        pcie_mm_lock = '\x01';
        break;
      case 2:
        if (CommBuffer->size == 0) {
          data = (char *)0x0;
        }
        else {
          data = &CommBuffer->data;
          i = CommBuffer->size + -1;
          if (data[i] != '\0') {
            data[i] = '\0';
          }
        }
        // ...
      case 3:
        if (CommBuffer->size == 0) {
          data = (char *)0x0;
        }
        else {
          data = &CommBuffer->data;
          i = CommBuffer->size + -1;
          if (data[i] != '\0') {
            data[i] = '\0';
          }
        }
        // ...
  // ...

When PCIeMmHandler is called some initial checks are performed on the CommBuffer and CommBufferSize. FuncId extracted from the CommBuffer is used for further dispatching with case values greater than 99 only being allowed before pcie_mm_lock is set, which is meant to perform during initialization in the UEFI DXE boot phase. Initialization code is responsible for calling FuncId 102 once completed. After this only the FuncIds with a value less than or equal to 99 are allowed.

Each dispatch case is independently responsible for validating the PCIeMmBuffer because the underlying structure is unique to the specific handler. For function 103 and 104 no validation is performed. When either of these routines are called they check buffer->size and if not zero proceed to index data with size and assign a '\0' byte to the dereferenced location. As size is a UINT64 this provides the ability to write a '\0' byte to any location in the S-EL0 driver's address space.

Additionally, it was found that with pcie_mm_lock not being set the HotPlug MM driver left additional interfaces exposed to NS-EL1 at runtime. The HotPlugMmHandler could be used to call function 107 with an attacker controlled buffer. The data structures specific to the HotPlugMmHandler are excluded because the conversion is similar to the PCIeMmHandler above.

#define NUM_TABLE_ENTRIES 25

HotPlugTableEntry TableEntries[NUM_TABLE_ENTRIES];
// ...
UINT32 TableIndex = 0;
// ...

EFI_STATUS HotPlugMmHandler(int DispatchHandle,void *RegisterContext,void *CommBuffer, uint64_t *CommBufferSize) {
  // ...
  HotPlugHeader *header;
  HotPlugBuffer *buffer;

  header = (HotPlugHeader *)CommBuffer;
  function = header->function;
  index = header->index;
  data header->data;

  // ...
  if (function == 107) {
    // ...
    if (index < 25) {
      ZeroMem(&TableEntries[TableIndex], 8);
      CopyMem(&TableEntries[TableIndex], data, 8);
      //...
    }
    else {
      if (index != 255) goto LAB_000068f4;
      ZeroMem(&TableEntries[TableIndex], 8);
      CopyMem(&TableEntries[TableIndex], data, 8);
     // ...
  }
  // ...
}

The buffer is copied into a global table as long as an attacker supplied UINT32 index is less than 25 or if the index equals 255. TableIndex is never checked to ensure it’s within the bounds of the table resulting in other global data structures to be overwritten when called enough times.

Timeline

Date reported: 2025-09-19
Date fixed: 2025-12-18
Date disclosed: 2025-12-18


文章来源: https://github.com/google/security-research/security/advisories/GHSA-jxxm-gxxf-64mg
如有侵权请联系:admin#unsafe.sh