Documentation Index
Fetch the complete documentation index at: https://vulhunt-docs.binarly.io/llms.txt
Use this file to discover all available pages before exploring further.
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