by Joseph Tartaro and Enrique Nissim
Introduction
A few years ago we started analyzing the platform security of AMD systems. This research led to a number of blog posts and presentations at several technical security conferences. The presentations covered issues from SMM modules, the AMD SMM Supervisor and even a decades old CPU bug. The theme of the research was dubbed "Back to the Future", this was tongue in cheek due to the types of vulnerabilities that we were finding for AMD systems that have not affected Intel platforms for many years, such as failures to lock down SPI flash. Another bug that inspired the concept was what we reported regarding the SMM Supervisor, which involved x86 Call-Gates, something that you would never see related to modern exploitation in today's world. Although the reported issue received a CVE, we realized we never made the details public, hence this blog post.Overview
The SMM Supervisor is essentially a new operating system within SMM, with the purpose of deprivileging SMI handlers. This is a new security boundary to help mitigate constant SMM vulnerabilities in various vendor custom SMI handlers. If you're ever reverse engineering a modern SMM module you may see a pattern like the following, this is checking the CPL and if it is running under Ring-3 then it knows the SMM Supervisor is initialized and will call into the new interface, else it will operate normally for legacy purposes or if no supervisor is running.
The details in this blog post are specifically regarding the AMD SMM Supervisor implementation, if you have an interest in the Intel implementation detailed blog posts and presentations have been given by Satoshi Tanda which you can review.
The SMM Supervisor implementation lives in the SmmSupervisorBinRelease module.
During its initialization, the module does essentially four things:
Retrieves the Trusted SMI Entry Code
Installs an SMM Interface for PiSmmCpuDxeSmm
It sends the DRTMInfo message to the PSP
It reserves the required memory to hold information per core such as the Syscall Entry Point, SMM Base Address, GDT and GDTR
The Trusted SMI Entry Code is the code that is going to be executed upon the System Management Interrupt (SMI). This code is part of a Raw Freeform file that comes in the BIOS image and has a guid of 83E1F409-21A3-491D-A415-B163A153776D.
The Supervisor has functions to parse this blob of data and extract the specific sections as required. The beginning of the file starts with a PSP header (0x10 bytes long), which at offset 0x08 indicates the number of sections that follow (5 in this case).
Each section is represented with the following struct:
Struct policy_section {
int type;
unsigned int size;
unsigned int offset;
int unused;
};
The Trusted SMI Entry Code is type 0xC1, it has a size of 0x2A4 and starts at offset 0x600. The following image shows the beginning of the supervisor code.
The code puts the content the section 0xC1 and its size into global variables, followed by the installation of an SMM Protocol Interface with the GUID SmmSupervisorInterfaceProtocolGuid (1738B3B1-762F-454B-9779432877A062F6).
This interface exposes the size of the Trusted SMI Entry Code, and four functions:
Using UEFITool to search for the GUID across the entire BIOS image, there is a single module that consumes this interface, which is PiSmmCpuDxeSmm.
In EDK-II, it is PiSmmCpuDxeSmm the module that prepares SMRAM and installs the first set of SMM Protocols that are used for SmmCore. In this case, the module invokes the first exported function in the Supervisor interface, which returns the Trusted SMI Entry Code that is going to be copied into SMM_BASE+0x8000, which is the SMI Handler Entry Point.
The purpose of the Supervisor SmmCpuFeaturesInstallSmiHandler function is to patch the Trusted SMI Entry Code with the arguments received from PiSmmCpuDxeSmm (e.g. IDTR, Cr3, SmiRandezvous function pointer) and return the modified version back so it gets copied into the proper SMRAM location.
Analysis of SmmCpuFeaturesInstallSmiHandler
This function starts by allocating memory for the GDT and initializing it with predefined values. The predefined values are the following:
These values translate to the following:
This new GDT is where the new call-gates are configured. An x86 call-gate is a mechanism in the x86 architecture used to facilitate controlled transitions between different privilege levels in the processor. It allows code running in a lower-privileged ring (ring-3) to call a into a higher-privileged ring (ring-0). If you need a refresher on x86 internals we recommend you review the OST2 Architecture 2001: x86-64 OS Internals course.
The function that initializes the GDT also receives an argument that is used to patch the entry 0x60 (the first call-gate), but SmmCpuFeaturesInstallSmiHandler sets this argument to NULL.
The predefined values have the two call-gates set with a DPL of 3 (Ring-3) and the selector points to 0x38, which is a 64-bit Code Segment with DPL 0 (Ring-0).
Following the initialization of the GDT, the code parses the memory content of the 0xC1 Section (the Trusted SMI Entry) and gets a pointer to the end of the section minus 4.
The code performs a calculation using this value to get a pointer to a table that is located right after the code ends:
pTable = pC1Section + C1SectionSize - 4 - 0x78
C1SectionSize = 0x2A4
pTable = pC1Section + 0x228
EDI holds the pointer to the table located at +0x228.
The code uses the table bytes to get offsets into locations that will be used to store the arguments passed to the function and function pointers like the high-level SmiEntry and SmiExit.
The table at the end looks like this:
The Trusted SMI Entry Code references these offsets taking into account the SmmBase and the SmiEntry Point (0x8000). Note the various comments regarding when GDTR, SmiEntry, SmiExit, Smi Randezvous, etc. are referenced.
At offset +0x15D the SmiEntry function pointer is retrieved from the table and then is invoked at +0x16B. The code builds a structure in the stack of 0x80 bytes in size and passes it through RCX.
At offset 0x28, this structure holds a pointer to the Call-Gate offset. This offset is passed into the InitializeGDT function as part of the SmiEntry execution:
After the SmiEntry code finishes, the code follows and executes the SmiRandezvous, which will be executed with Ring-3 privileges (Usermode) and will invoke the proper OEM’s SMI Handlers.
The problem here is that the GDT set by the Supervisor contains a Call-Gate with DPL3 that can be abused by a malicious (or compromised) SMI Handler to elevate privileges.
Because the Call-Gate code is also part of the SMI Entry, it expects that RDI will always contain the value of SmmBase. Nevertheless, a malicious SMI Handler can control the value of RDI. This allows for the attacker to escalate to Ring-0 privileges.
In addition to the Call-Gate issue, you can see at offset +0x78, the code initializes CR0 to 0x668. This means SMEP is not enabled in this configuration, which means that an attacker can point RDI to a function in Ring-3 and take control of Ring-0 by abusing the Call-Gate. Also, UMIP is not enabled in this context. This translates into Supervisor pointer leakages from Ring-3 via the instructions Store IDT (sidt) and Store GDT (sgdt). These additional misconfigurations make it easier for an attacker to exploit the Call-Gate vulnerability.
Timeline