Thursday, October 24, 2024

Tales from the Call-Gate: An SMM Supervisor Vulnerability

  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:

  1. Retrieves the Trusted SMI Entry Code

  2. Installs an SMM Interface for PiSmmCpuDxeSmm

  3. It sends the DRTMInfo message to the PSP

  4. It reserves the required memory to hold information per core such as the Syscall Entry Point, SMM Base Address, GDT and GDTR


This post is only going to cover steps 1 and 2, if you're interested in a full overview of the flow please reference our presentation on it.

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.


; 16-bit - Real Mode
00 : 2E 66 8B 3E 88 82 mov edi, dword ptr cs:[0x8288] ; Load GDTR
06 : 3E 66 67 0F 01 17 lgdt ds:[edi]
0c : BB 47 80 mov bx, 0x8047
0f : B8 08 00 mov ax, 8
12 : 2E 89 47 FE mov word ptr cs:[bx - 2], ax
16 : 66 B9 11 01 01 C0 mov ecx, 0xc0010111
1c : 0F 32 rdmsr
1e : 66 89 C7 mov edi, eax
21 : 66 67 8D 87 47 80 00 00 lea eax, [edi + 0x8047]
29 : 2E 66 89 47 FA mov dword ptr cs:[bx - 6], eax
2e : 0F 20 C3 mov ebx, cr0
31 : 66 81 E3 F3 FF FA 9F and ebx, 0x9ffafff3
38 : 66 83 CB 23 or ebx, 0x23 ; Configuration bits for cr0
3c : 0F 22 C3 mov cr0, ebx ; Enter Protected Mode
; 32-bit - Protected Mode
45 : 00 00 add BYTE PTR [eax],al
47 : 66 b8 20 00 mov ax,0x20
4b : 66 8e d8 mov ds,ax
4e : 66 8e c0 mov es,ax
51 : 66 8e e0 mov fs,ax
54 : 66 8e e8 mov gs,ax
57 : 66 8e d0 mov ss,ax
; Load Stack Pointer
5a : 8b a7 90 82 00 00 mov esp,DWORD PTR [edi+0x8290]
60 : 8b 14 24 mov edx,DWORD PTR [esp]
63 : 8b a7 98 82 00 00 mov esp,DWORD PTR [edi+0x8298]
69 : eb 00 jmp 0x6b
6b : be 8c 82 00 00 mov esi,0x828c ; 0x8000 + 0x28C
70 : 48 dec eax
71 : 01 fe add esi,edi
73 : 8b 06 mov eax,DWORD PTR [esi]
75 : 0f 22 d8 mov cr3,eax
78 : b8 68 06 00 00 mov eax,0x668
7d : 0f 22 e0 mov cr4,eax
80 : 83 ec 08 sub esp,0x8
83 : 0f 01 04 24 sgdtd [esp]
87 : 8b 44 24 02 mov eax,DWORD PTR [esp+0x2]
8b : 83 c4 08 add esp,0x8
8e : b1 89 mov cl,0x89
90 : 38 88 85 00 00 00 cmp BYTE PTR [eax+0x85],cl
96 : 74 06 je 0x9e
98 : 88 88 85 00 00 00 mov BYTE PTR [eax+0x85],cl
9e : b8 80 00 00 00 mov eax,0x80
a3 : 0f 00 d8 ltr ax
a6 : b9 80 00 00 c0 mov ecx,0xc0000080
ab : 52 push edx
ac : 0f 32 rdmsr
ae : 66 0d 00 08 or ax,0x800
b2 : 0f 30 wrmsr
b4 : 5a pop edx
b5 : 6a 38 push 0x38
b7 : e8 00 00 00 00 call 0xbc
bc : 83 04 24 21 add DWORD PTR [esp],0x21
c0 : 52 push edx
c1 : b9 80 00 00 c0 mov ecx,0xc0000080
c6 : 0f 32 rdmsr
c8 : 80 cc 01 or ah,0x1
cb : 0c 01 or al,0x1
cd : 0f 30 wrmsr
cf : 5a pop edx
d0 : 0f 20 c3 mov ebx,cr0
; enables paging and more
d3 : 81 cb 23 00 01 80 or ebx,0x80010023
d9 : 0f 22 c3 mov cr0,ebx
dc : cb retf ; Transition into 64-bit mode
; 64-bit
; Load IdtSize
dd : 48 8B 87 74 82 00 00 mov rax,QWORD PTR [rdi+0x8274]
; Load IDT for 64-bit mode
e4 : 0F 01 18 lidt [rax]
e7 : 66 B8 20 00 mov ax,0x20
eb : 8E D8 mov ds,ax
ed : 8E C0 mov es,ax
ef : 8E E0 mov fs,ax
f1 : 8E E8 mov gs,ax
f3 : 8E D0 mov ss,ax
f5 : 48 83 EC 08 sub rsp,0x8
f9 : 48 81 EC 00 02 00 00 sub rsp,0x200
100: 48 0F AE 04 24 fxsave64 [rsp]
105: 52 push rdx
106: 48 81 EC 80 00 00 00 sub rsp,0x80
10d: 48 89 14 24 mov QWORD PTR [rsp],rdx
111: B8 00 00 00 00 mov eax,0x0
116: 8B 87 98 82 00 00 mov eax,DWORD PTR [rdi+0x8298]
11c: 48 89 44 24 08 mov QWORD PTR [rsp+0x8],rax
121: 8B 87 90 82 00 00 mov eax,DWORD PTR [rdi+0x8290]
127: 48 89 44 24 10 mov QWORD PTR [rsp+0x10],rax
12c: 8B 87 9C 82 00 00 mov eax,DWORD PTR [rdi+0x829c]
132: 48 89 44 24 18 mov QWORD PTR [rsp+0x18],rax
137: 48 8D 87 28 82 00 00 lea rax,[rdi+0x8228]
13e: 48 89 44 24 20 mov QWORD PTR [rsp+0x20],rax
143: 48 8D 87 CC 81 00 00 lea rax,[rdi+0x81cc]
14a: 48 89 44 24 28 mov QWORD PTR [rsp+0x28],rax
14f: 48 89 E0 mov rax,rsp
152: 48 05 88 00 00 00 add rax,0x88
158: 48 89 44 24 30 mov QWORD PTR [rsp+0x30],rax
; Load SmiEntry
15d: 48 8B 87 4C 82 00 00 mov rax,QWORD PTR [rdi+0x824c]
164: 48 89 E1 mov rcx,rsp
167: 48 83 EC 48 sub rsp,0x48
16b: FF D0 call rax
16d: 48 83 C4 48 add rsp,0x48
171: 48 81 C4 80 00 00 00 add rsp,0x80
178: 5A pop rdx
179: 48 25 FF FF FF 00 and rax,0xffffff
17e: 48 3D FF FF FF 00 cmp rax,0xffffff
183: 74 14 je 0x19a
185: 3C FF cmp al,0xff
187: 74 00 je 0x18a
189: 4C 8D 3D 4C 00 00 00 lea r15,[rip+0x4c]
; Load SmiRandezvous
190: 48 8B B7 5C 82 00 00 mov rsi,QWORD PTR [rdi+0x825c]
197: FF E6 jmp rsi
19a: 66 B8 53 00 mov ax,0x53
19e: 8E D8 mov ds,ax
1a0: 8E C0 mov es,ax
1a2: 8E E0 mov fs,ax
1a4: 8E E8 mov gs,ax
1a6: B8 00 00 00 00 mov eax,0x0
1ab: 67 8B 87 90 82 00 00 mov eax,DWORD PTR [edi+0x8290]
1b2: 48 8B B7 5C 82 00 00 mov rsi,QWORD PTR [rdi+0x825c]
1b9: 41 BF 63 00 00 00 mov r15d,0x63
1bf: 49 C1 E7 20 shl r15,0x20
1c3: 6A 53 push 0x53
1c5: 50 push rax
1c6: 6A 5B push 0x5b
1c8: 56 push rsi
1c9: 48 CB rex.W retf
; Call-Gate Code
1cc: 48 83 C4 20 add rsp, 0x20
1d0: 66 B8 20 00 mov ax, 0x20
1d4: 8E D8 mov ds, ax
1d6: 8E C0 mov es, ax
1d8: 8E E0 mov fs, ax
1da: 8E E8 mov gs, ax
1dc: 8E D0 mov ss, ax
1de: 48 89 D9 mov rcx, rbx
; RDI assumed to be SMM_BASE+0x8000 but is attacker controlled
1e1: 48 8B 87 54 82 00 00 mov rax, QWORD PTR [rdi+0x8254]
; Call the attacker controlled function
1e8: FF D0 call rax
1ea: 48 0F AE 0C 24 fxrstor64 [rsp]
1ef: 48 81 C4 00 02 00 00 add rsp, 0x200
1f6: B8 10 00 00 00 mov eax, 0x10
1fb: E8 07 00 00 00 call 0x202
200: F3 90 pause
202: 0F AE E8 lfence
204: EB F9 jmp 0x1ff
206: E8 07 00 00 00 call 0x20e
20b: F3 90 pause
20d: 0F AE E8 lfence
20f: EB F9 jmp 0x20b
211: 48 FF C8 dec rax
214: 75 E3 jne 0x1fa
216: 48 81 C4 00 01 00 00 add rsp, 0x100
21d: 0F AA rsm
21f: 21 02 and DWORD PTR [rdx], eax
221: 90 nop
222: 90 nop
223: 90 nop
224: 90 nop
225: 90 nop

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.


; Call-Gate Code
1cc: 48 83 C4 20 add rsp, 0x20
1d0: 66 B8 20 00 mov ax, 0x20
1d4: 8E D8 mov ds, ax
1d6: 8E C0 mov es, ax
1d8: 8E E0 mov fs, ax
1da: 8E E8 mov gs, ax
1dc: 8E D0 mov ss, ax
1de: 48 89 D9 mov rcx, rbx
; RDI assumed to be SMM_BASE+0x8000 but is attacker controlled
1e1: 48 8B 87 54 82 00 00 mov rax, QWORD PTR [rdi+0x8254]
; Call the attacker controlled function
1e8: FF D0 call rax


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

These issues were reported to the AMD PSIRT team on 5/9/2023. The AMD PSIRT team issued CVE-2023-20596 and released the following AMD-SB-7011 bulletin on 11/14/2023.