Skip to main content
This vulnerability consists of a stack-based buffer overflow in the SMI handler of a Lenovo UEFI firmware. The relevant code of the affected component is as follows:
MACRO_EFI Callback04(UINTN CpuIndex)
{
  PARAM_BUFFER *Param;
  UINTN Index;
  PARAM_BUFFER_VARIABLE *Var;
  CHAR16 *Name;
  UINT32 NameSize;
  UINT8 *VariableData;
  UINT32 RdiReg;
  UINT32 RsiReg;
  CHAR16 VariableName[128];
  UINT32 RaxReg;
  UINT32 RbxReg;
  UINT32 RcxReg;

  RaxReg = 0;
  RbxReg = 0;
  RcxReg = 0;
  RdiReg = 0;
  RsiReg = 0;
  gEfiSmmCpuProtocol->ReadSaveState(gEfiSmmCpuProtocol, 4, EFI_SMM_SAVE_STATE_REGISTER_RAX, CpuIndex, &RaxReg);
  gEfiSmmCpuProtocol->ReadSaveState(gEfiSmmCpuProtocol, 4, EFI_SMM_SAVE_STATE_REGISTER_RBX, CpuIndex, &RbxReg);
  gEfiSmmCpuProtocol->ReadSaveState(gEfiSmmCpuProtocol, 4, EFI_SMM_SAVE_STATE_REGISTER_RCX, CpuIndex, &RcxReg);
  gEfiSmmCpuProtocol->ReadSaveState(gEfiSmmCpuProtocol, 4, EFI_SMM_SAVE_STATE_REGISTER_RDI, CpuIndex, &RdiReg);
  gEfiSmmCpuProtocol->ReadSaveState(gEfiSmmCpuProtocol, 4, EFI_SMM_SAVE_STATE_REGISTER_RSI, CpuIndex, &RsiReg);

  // ...

  Param = RdiReg;
  Index = 0;

  // Attacker-controlled pointer
  Var = (RdiReg + 0x38);

  if ( Param->Count )
  {
    do
    {
      ZeroMem(VariableName, 200);
      Name = &Var->VariableName;
      NameSize = Var->VariableNameSize;
      if ( NameSize )
      {
        if ( VariableName != Name )
        {
          // VariableNameSize is not validated -> overflow of VariableName stack buffer
          CopyMem(VariableName, Name, Var->VariableNameSize);
          NameSize = Var->VariableNameSize;
        }
      }
      VariableData = &Var->VariableName + NameSize;
      // ...
      ++Index;
      Var = &VariableData[Var->VariableDataSize];
    }
    while ( Index < Param->Count );
  }
  // ...
  return 0;
}
The fourth call to gEfiSmmCpuProtocol->ReadSaveState initializes the RdiReg variable with the pointer value specified in EFI_SMM_SAVE_STATE_REGISTER_RDI. Further in the code, the call to CopyMem uses fields from a structure pointed to by RdiReg + 0x38 as its SourceBuffer and Length parameters. For reference, the CopyMem prototype is as follows:
void *CopyMem(void *DestinationBuffer, const void *SourceBuffer, UINTN Length);
Name is attacker-controlled and Var->VariableNameSize is not validated before the call to CopyMem, thus allowing an attacker to overflow the stack-allocated buffer with attacker-controllable data.

Finding the vulnerable function

An easy approach is to match these five calls to gEfiSmmCpuProtocol->ReadSaveState in a row and later retrieve the address of the fourth one. The following disassembled code represents this part (dynamic operand values are masked with ..):
4C 8B C9                mov     r9, rcx                                             ; instruction #1
83 64 24 .. ..          and     [rsp+140h+RsiReg], 0
8B D3                   mov     edx, ebx
48 89 44 24 ..          mov     [rsp+140h+Buffer], rax
44 8D 43 ..             lea     r8d, [rbx+22h]
48 8B 05 .. .. .. ..    mov     rax, cs:gEfiSmmCpuProtocol
48 8B C8                mov     rcx, rax
FF 10                   call    qword ptr [rax+EFI_SMM_CPU_PROTOCOL.ReadSaveState]  ; 1st call
48 8D 45 ..             lea     rax, [rbp+40h+RbxReg]
4D 8B CC                mov     r9, r12                                             ; instruction #10
48 89 44 24 ..          mov     [rsp+140h+Buffer], rax
44 8D 43 ..             lea     r8d, [rbx+23h]
48 8B 05 .. .. .. ..    mov     rax, cs:gEfiSmmCpuProtocol
8B D3                   mov     edx, ebx
48 8B C8                mov     rcx, rax
FF 10                   call    qword ptr [rax+EFI_SMM_CPU_PROTOCOL.ReadSaveState]  ; 2nd call
48 8D 45 ..             lea     rax, [rbp+40h+RcxReg]
4D 8B CC                mov     r9, r12
48 89 44 24 ..          mov     [rsp+140h+Buffer], rax
44 8D 43 ..             lea     r8d, [rbx+24h]                                      ; instruction #20
48 8B 05 .. .. .. ..    mov     rax, cs:gEfiSmmCpuProtocol
8B D3                   mov     edx, ebx
48 8B C8                mov     rcx, rax
FF 10                   call    qword ptr [rax+EFI_SMM_CPU_PROTOCOL.ReadSaveState]  ; 3rd call
48 8D 44 24 ..          lea     rax, [rsp+140h+RdiReg]
4D 8B CC                mov     r9, r12
48 89 44 24 ..          mov     [rsp+140h+Buffer], rax
44 8D 43 ..             lea     r8d, [rbx+29h]
48 8B 05 .. .. .. ..    mov     rax, cs:gEfiSmmCpuProtocol
8B D3                   mov     edx, ebx                                            ; instruction #30
48 8B C8                mov     rcx, rax
FF 10                   call    qword ptr [rax+EFI_SMM_CPU_PROTOCOL.ReadSaveState]  ; 4th call
48 8D 44 24 ..          lea     rax, [rsp+140h+RsiReg]
4D 8B CC                mov     r9, r12
48 89 44 24 ..          mov     [rsp+140h+Buffer], rax
44 8D 43 ..             lea     r8d, [rbx+28h]
48 8B 05 .. .. .. ..    mov     rax, cs:gEfiSmmCpuProtocol
8B D3                   mov     edx, ebx
48 8B C8                mov     rcx, rax
FF 10                   call    qword ptr [rax+EFI_SMM_CPU_PROTOCOL.ReadSaveState]  ; instruction #40 / 5th call
81 7D 58 20 04 4D 53    cmp     [rbp+40h+RaxReg], 534D0420h
The pattern below, especially when combined with the rest of the rule logic, is generic enough to catch variations of the function of interest while remaining strict enough to avoid false positives. As always, there is a trade-off.
local read_save_state_calls = project:search_code(
    "4c8bc9836424....8bd348894424..448d43..488b05........488bc8ff10488d45..4d8bcc48894424..448d43..488b05........8bd3488bc8ff10488d45..4d8bcc48894424..448d43..488b05........8bd3488bc8ff10488d4424..4d8bcc48894424..448d43..488b05........8bd3488bc8ff10488d4424..4d8bcc48894424..448d43..488b05........8bd3488bc8ff10817d")

if not read_save_state_calls then return end
-- keep working with `read_save_state_calls`
The two-dot sequence (..) matches any single byte. It is also possible to match nibbles using a single dot; for example, .f matches any byte whose low nibble is f (e.g., 0f, 1f, …, ff).

Finding the call to CopyMem

Here is the disassembled code around the call to CopyMem:
44 8B 46 ..             mov     r8d, [rsi+4]                  ; instruction #1
48 8D 56 ..             lea     rdx, [rsi+1Ch]
41 8B C0                mov     eax, r8d
4D 85 C0                test    r8, r8
74 ..                   jz      short loc_1614
48 8D 4C 24 ..          lea     rcx, [rsp+140h+VariableName]
48 3B CA                cmp     rcx, rdx
74 ..                   jz      short loc_1614
48 8D 4C 24 ..          lea     rcx, [rsp+140h+VariableName]
E8 .. .. .. ..          call    CopyMem                       ; instruction #10
8B 46 ..                mov     eax, [rsi+4]
And here’s a pattern to locate this call:
local copymem_call = project:search_code(
    "448b46..488d56..418bc04d85c074..488d4c24..483bca74..488d4c24..e8........8b46")

if not copymem_call then return end
-- keep working with `copymem_call`

Annotating and returning evidence

The final step is to annotate the appropriate addresses and return evidence. The complete check function is as follows:
local function check(project)
    local read_save_state_calls = project:search_code(
        "4c8bc9836424....8bd348894424..448d43..488b05........488bc8ff10488d45..4d8bcc48894424..448d43..488b05........8bd3488bc8ff10488d45..4d8bcc48894424..448d43..488b05........8bd3488bc8ff10488d4424..4d8bcc48894424..448d43..488b05........8bd3488bc8ff10488d4424..4d8bcc48894424..448d43..488b05........8bd3488bc8ff10817d")

    if not read_save_state_calls then return end

    local copymem_call = project:search_code(
        "448b46..488d56..418bc04d85c074..488d4c24..483bca74..488d4c24..e8........8b46")

    if not copymem_call then return end

    -- get the address of the function to which the found code belongs
    local function_address = read_save_state_calls.function_address

    -- get the 32nd instruction from the pattern matched by `read_save_state_calls`;
    -- this is the 4th call to `gEfiSmmCpuProtocol->ReadSaveState`
    local read_save_state = read_save_state_calls.insns[32]

    -- get the 10th instruction from the pattern matched by `copymem_call`;
    -- this is the call to `CopyMem`
    local copymem = copymem_call.insns[10]

    return result:high{
        name = "CVE-2025-4425",
        description = "Stack-based buffer overflow vulnerability in the SMI handler on a Lenovo device",
        evidence = {
            functions = {
                [function_address] = {
                    annotate:at{
                        location = read_save_state.address,
                        message = "This call to `gEfiSmmCpuProtocol->ReadSaveState` initializes\n`BufferRdi` (the last parameter) with the pointer value specified in\n`EFI_SMM_SAVE_STATE_REGISTER_RDI`"
                    }, annotate:at{
                        location = copymem.address,
                        message = "This call to `CopyMem` uses unvalidated data from `EFI_SMM_SAVE_STATE_REGISTER_RDI`\nas the `SourceBuffer` and `Length` parameters, allowing a potential attacker to overflow\nthe stack-allocated `Destination` buffer"
                    }
                }
            }
        }
    }
end