x86架构BIOS攻击面梳理与分析
之前的一份学习笔记,主要整理了一下x86架构下BIOS的一些攻击面,BootKit部分还没有搬上来。
可能有一些理解存在疏漏的地方,还请看官老爷斧正。
调研目标
一、梳理安全启动的基本流程
- 经历的过程
- 软硬件层面需要完成的工作
二、梳理攻击面和UEFI
的保护机制
三、找出攻击面对应的具体漏洞案例
四、BootKit
简介
安全启动
UEFI
UEFI
规范中定义的一项功能。它保证只有有效的第 3 方固件代码才能在原始设备制造商 (OEM) 固件环境中运行。 在安全启动中,只有原始设备制造商提供的固件是有效的,任何第 3 方固件代码均不受信任。
UEFI Secure Boot
包括两部分——启动映像的验证和映像安全数据库更新的验证。
UEFI规范
UEFI
启动规范中划分的启动过程如图所示。对于固件而言,不同指令集架构,不同厂商的实现可能略有不同,但是整体思路应该都遵循UEFI
规范。
SEC
主要工作:前期验证
功能:
- 接受并处理系统启动和重启信号
- 系统加电信号
- 系统重启信号
- 系统运行过程中的严重异常信号
- 处理器从实模式切换到保护模式
- 初始化临时的存储区域:使用
CAR-Cache-as-RAM
来配置CPU
缓存以作为临时RAM
- 作为可信系统的根
- 传递参数给
pei
- 系统当前状态,方便
pei
去检查系统健康 - 可启动固件的大小与地址
- 临时ram区域的地址与大小
- 栈的地址与大小
- 找到
PEI
加载程序并从spi
中开始运行
PEI
主要工作:为dxe
准备执行环境,将信息组成hob
列表给dex
(在UEFI
中,HOB(Hand-Off Block)
数据是一种用于传递系统初始化信息的机制。在UEFI
系统运行期间,由于硬件设备、启动程序和操作系统内核的不同,需要对系统进行一系列初始化处理,以确保系统正常运行。而HOB
数据就是用来携带这些初始化信息的数据结构。)
功能:
pei foundation
pei
基础服务和流程peimodule
peim
,并根据peim
之间的依赖关系顺序执行peim
DXE
主要工作:系统初始化功能:
- 根据
hob
初始化系统服务 - 调度
driver
,初始化SMM
的运行时环境 - 打开
EFI_BDS_ARCH_PROTOCOL
(PROTOCOL
是DEX
和驱动之间的通信方式) - 打开
entry
->bds
BDS
主要工作:执行启动策略功能:
- 初始化控制台设备
- 加载必要的设备驱动
- 根据系统设置加载和执行启动项
TSL
主要工作:加载OS_loader
的第一阶段,系统控制权仍然由UEFI
内核掌控
RT
主要工作:runtime
,系统控制权由UEFI
移交OS_loader
AL
系统遇到灾难性错误,系统需要提供错误处理和灾难恢复机制,这种机制运行在AL
阶段。
安全启动流程
1.从信任根(PI
固件和PK
)验证下一启动阶段的固件签名,UEFI
规范中的SEC
,PEI
和DEX
都属于PI
固件
2.由可信根验证KEK
3.由KEK
验证db
和dbx
完整性
4.安全启动从可执行文件定位并读取签名证书,并与计算的哈希值比较
5.启动已验证成功的DEX
驱动,UEFI app
,Option ROM
控制权转交给OS loader
6.OS loader
加载操作系统
保护机制
软件层面
1.对操作系统引导加载程序,系统镜像以及内核初始化后加载的驱动进行签名,所有的签名保存在签名数据库中,签名数据库位于spi
闪存或者nvram
中
2.内存管理层面,对物理页表和虚拟页表的保护
3.在EDK2
中,负责检查给定缓冲区是否与SMRAM
重叠的函数被称为SmmIsBufferOutsideSmmValid()
。这个函数在每次SMI
调用时都会在通信缓冲区上被调用,用来检查用于向SMI
处理器传递参数的通信缓冲区是否与SMRAM
重叠。
4.为了向操作系统报告哪些缓解措施已在 SMM
UEFI
Windows SMM
WSMT
)的 ACPI
表。除其他外,WSMT
COMM_BUFFER_NESTED_PTR_PROTECTION
SMI
处理程序在没有事先清理的情况下不会使用嵌套指针。可以使用chipsec
common.wsmt
转储和解析此表。
5.HVCI
:windows hypervisor code integrity checking
,这项技术不是保护固件的,而是基于固件对内核文件进行校验。HVCI
利用Windows
虚拟化技术,运行在Windows
操作系统顶层的一个虚拟化层,将系统内核保护在这个虚拟化层内部。在启用HVCI
的情况下,系统会在每次启动时验证内核文件的完整性,并在内核加载时创建一个受保护的内核代码区域,只有被授权的代码才能够访问该区域。
硬件层面
1.Intel Boot Guard
:通过固件签名的方式防止Bootrom
被恶意烧写和篡改;
2.对spi
闪存区设置硬件读写位的保护,防止软硬件层面对spi
进行篡改
BIOS
控制寄存器
BIOSWE(bit 0)
:当设置时,对BIOS空间的访问将允许读写操作,否则仅允许读取操作。BLE(bit 1)
:设置后,BIOSWE
SMM
0
设置为1
。任何从非SMM
代码设置BIOSWE
SMI
。这为OEM
SMI
处理程序的机会,通过将其设置回0
来保护BIOSWE
位SMM_BWP(bit 5)
:设置后,BIOS 区域不可写,除非所有处理器都处于SMM
BIOSWE
为 1。设置此位可解决 Speed Racer 竞争条件漏洞
PR0-PR4
保护范围寄存器:每个寄存器为特定范围的SPI
BIOS
HSFS
) 寄存器中的闪存配置锁定 (FLOCKDN
) 位未设置时,才能设置它们。
这个FLOCKDN
FLOCKDN
被设置,只有在下一次硬件复位后它才会被清除。这意味着受保护范围寄存器保护的内存范围在被锁定后不能被任何运行时代码(包括SMM
代码)修改。即使是合法的固件更新也必须在 PR 寄存器被锁定之前执行。
PR0
寄存器:控制SPI
控制器的输入时序和输出时序,包括时钟极性、相位和时钟分频等参数。PR1
寄存器:控制SPI
控制器的中断和DMA
操作,包括中断使能、中断标志和DMA
使能等参数。PR2
寄存器:控制SPI
控制器的FIFO缓冲区,包括FIFO深度、读写指针和数据长度等参数。PR3
寄存器:控制SPI
控制器的错误检测和纠正,包括传输错误标志、FIFO
溢出标志和校验和错误标志等参数。PR4
寄存器:控制SPI
控制器的安全保护,包括写保护、读保护和访问权限等参数。
3.建立硬件可信根,通过根密钥对后续加载的镜像和签名数据库进行逐级的校验,根密钥安全,则所加载的镜像可信
4.SMM_Code_Chk_En
MSR_SMM_FEATURE_CONTROL
SMRR
SMM
MCE
。因此,启用和锁定此位是缓解 SMM
调出漏洞的重要一步。
5.smm_dma
可以避免SMRAM
被DMA
设备进行修改。
6.通过完全删除EFI_VARIABLE_RUNTIME_ACCESS
属性或使用EDKII_VARIABLE_LOCK_PROTOCOL
等协议来实现对NVRAM
变量的保护,使变量成为只读。
攻击面梳理
硬件层面
侧信道
SMM
推测执行攻击
本地无权限的攻击者利用无序执行CPU的微架构的能力来泄漏受保护的内存中的内容。研究人员扩展了该方法来访问高权限的系统管理模式(System Management Mode,SMM
)内存。
故障注入
一些通过故障注入硬件攻击的手段,改变Bootrom
执行过程中的逻辑门(比如寄存器或者内存的值),导致启动阶段跳转到非预期的分支执行,从而绕过安全启动。
一些通过故障注入绕过安全启动的案例有:switch
的硬件破解,esp32
的安全启动绕过等等。
https://raelize.com/blog/espressif-systems-esp32-bypassing-sb-using-emfi/
https://limitedresults.com/2019/09/pwn-the-esp32-secure-boot/
PEI
如上图所示,如果PCD
数据以及NVRAM
数据不受读写保护的话,通过修改此类数据,在设备启动过程中,存在针对PEI
阶段的攻击面。
binarly
团队通过PEI
早期存在的漏洞,实现了禁用intel PPAM
保护的方法。
SMM
系统管理模式(System Management mode)
SMM
是 Intel 处理器的一种相对模糊的模式,用于底层硬件的控制,作为处理系统范围内诸如电源管理,系统硬件控制以及 OEM (Original Equipment Manufacturer)专有设计代码等功能的专用操作模式。该模式仅供系统固件使用,不支持应用软件或系统软件。SMM
只能运行在特定的管理内存中(SMRAM
)
- 要进入
SMM
模式,只能通过SMI
(System Manager Interrupt,系统管理中断),SMI
可以通过SMI
#引脚和APIC
中断来产生,且SMI
是不可屏蔽的中断; - 要退出
SMM
,只能通过RSM
指令(RSM
只能在SMM
中用): - 进入
SMM
之后,普通的中断都被屏蔽掉了; - 进入
SMM
模式之后执行环境就到了实模式下,分页也被Disable掉了(CR0.PE=0,CR0.PG=0
),此时内存访问最大到4G
; SMM
是不可重入的,意思是当你在SMM
模式之后就不会再接收SMI
了,直到RSM
指令退出RSM
;
SMI (System Management Interrupt)
名为系统管理中断, 是进入**SMM (System Management Mode)**
,即系统管理模式的方式。
SMRAM
SMRAM
是指系统管理模式下的随机存取存储器(System Management RAM
),它是计算机中的一种特殊内存,位于CPU和主板芯片组之间。SMRAM
通常用于保存BIOS固件中使用的特定代码、数据和配置信息,并且只有在系统处于Intel CPU的安全模式时才能访问。SMRAM
也可以被恶意软件利用来获取系统中的敏感信息,因此需要采取一些安全措施来保护SMRAM
的安全。
系统管理模式下,除了常见的溢出等漏洞,还有一些特定的只存在于SMM
的漏洞模式。
系统管理模式调出漏洞(SMM Callout
)
最基本的SMM
漏洞类型被称为“SMM
调出”。每当SMM
代码调用位于SMRAM
边界之外的函数时(如SMRR所定义),就会出现这种漏洞。最常见的调出场景是SMI
处理程序:它试图调用作为其操作的一部分的UEFI
启动服务或运行时服务。拥有操作系统级权限的攻击者可以在触发SMI
之前修改这些服务所在的物理页面,从而在受影响的服务被调用后劫持特权执行流程。
低址SMRAM
损坏
在正常情况下,用于向SMI
处理器传递参数的通信缓冲区不得与SMRAM
重叠。这个限制的理由很简单:如果不是这样,那么每次SMI
处理程序将某些数据写入通信缓冲区的时候——例如,为了向调用方返回状态代码时,都会“顺带”修改SMRAM
的某些部分,这是不可取的。
SmmIsBufferOutsideSmmValid()
函数实现了对SMI
通信缓冲区和SMRAM
区域的检查。
但是由于comm Buffer
的大小是用户可控的,SmmIsBufferOutsideSmmValid()
只会检查Comm Buffer
是否和SMRAM
重叠,而不会检查写入Comm Buffer
的内容是否溢出Comm Buffer
,所以如果SMI
处理程序处理不当的话,依然会造成低址SMRAM
损坏。
缓解措施,是要明确检查Comm Buffer
的大小是否符合预期(大于等于写入Comm Buffer
的长度)。
任意SMRAM
损坏
没有使用SmmIsBufferOutsideSmmValid()
对多级指针指向的地址空间做检查。如果SMI
处理程序中的多级指针指向SMRAM
内的地址空间,就有可能造成任意SMRAM
损坏。
TOCTOU attacks
time-of-check-time-of-use
Comm Buffer
是驻留在缓冲区以外的,进入SMI
处理程序之后,Comm Buffer
中的值是可以被SMI
处理程序修改,以及被外设通过DMA
的方式进行修改的。
前面提到使用SmmIsBufferOutsideSmmValid()
对多级指针进行验证,如果验证之后又可以进行修改,然后多级指针又被引用,那相当于没有进行验证。
SetVariable
信息泄露
UEFI
中有两个API
实现对NVRAM
的读和写:GetVariable
和SetVariable
。对这两个API
运用不当会产生一些漏洞模式。
更改NVRAM
变量,需要先用GetVariable
将nvram
变量的值读入一个局部变量,对缓冲区中的局部变量进行修改,然后用SetVariable
将保存在局部变量的值写入nvram
。
Status = gRT->GetVariable (
L"ExampleConfiguration", // VariableName
&gEfiExampleConfigurationVariableGuid, // VendorGuid
&Attributes, // Attributes
&DataSize, // DataSize
&ExampleConfiguration // Data
);
Status = gRT->SetVariable (
L"ExampleConfiguration", // VariableName
&gEfiExampleConfigurationVariableGuid, // VendorGuid
EFI_VARIABLE_NON_VOLATILE |
EFI_VARIABLE_BOOTSERVICE_ACCESS |
EFI_VARIABLE_RUNTIME_ACCESS,
// Attributes
sizeof (EXAMPLE_CONFIGURATION), // DataSize
&ExampleConfiguration // Data
);
GetVariable
和SetVariable
第四个参数是读取和写入的nvram
变量的长度,如果SetVariable
的第四个参数的长度大于修改后的局部变量的长度,就会将SMRAM
内存中的值写入nvram
变量造成信息泄露。
Double GetVariable
GetVariable
在第一次从nvram
取值写入栈中时,datasize
的长度会被改写为对应nvram
变量的长度。第二次调用GetVariable
函数时,如果对datasize
未做初始化,就有可能造成溢出。
bool CheckBatterySafety()
{
EFI_GUID VendorGuid;
char Buffer;
UINTN DataSize;
VendorGuid.Data1 = 0xFB3B9ECE;
*&VendorGuid.Data2 = 0x49334ABA;
*VendorGuid.Data4 = 0xD6B49DB4;
*&VendorGuid.Data4[4] = 0x5123897D;
Buffer = 0;
DataSize = 1i64;
return (!gRT_157B30->GetVariable(L"BatterySafetyModeStatus", &VendorGuid, 0i64, &DataSize, &Buffer)
|| !gRT_157B30->GetVariable(L"BatterySafetyMode", &VendorGuid, 0i64, &DataSize, &Buffer))
&& Buffer == 1;
}
capsule
固件更新
capsule固件更新:基于capsule的固件更新是一种用于在UEFI
固件中更新代码的方法。使用此方法,新的固件代码被放置在一个称为"capsule"的数据包中,并且该数据包具有签名以验证其完整性和来源。然后,系统将capsule存储在非易失性存储器(如固态硬盘)中,并在下一次启动时执行更新。与传统的基于刷写的BIOS更新不同,基于capsule的更新是一种更安全、更可靠的更新方式,因为它可以提供以下优点:
- 可以验证固件更新的完整性和来源,从而防止恶意软件或未经授权的更新。
- 不需要进入操作系统来执行更新,降低了风险并提高了更新的成功率。
- 即使更新失败,系统也可以回滚到之前的稳定状态,避免了可能带来的损失。
capsule
固件更新过程
1.开始发送一个固件更新Capsule的处理过程,操作系统将获取这个Capsule并将其分割成多个片段,分布在地址空间中进行处理。
2.操作系统通过SetFirmwareEnvironmentVariable
函数创建CapsuleUpdateDate
这个EFI
变量,通过这个EFI
变量确定描述碎片化的固件胶囊在内存中的地址。这一步应该是判断capsule Firmware
是否有固件更新。
然后发生 "warm reset",将控制权转回到固件上。
3.控制权移交到固件之后,固件会将内存中的Capsule
内存块重新合并打包到capsule firmware
中。
4.UEFI
OEM
签名
5.经过签名校验之后,将capsule
固件写入spi
闪存
capsule
固件更新攻击面
- 合并阶段
- 解析
capsule
封装的过程 - 解析
capsule
未签名封装块的过程
合并阶段
在合并capsule
的阶段,对要写入内存的大小和capsule
内存大小校验不当,就可能导致整数溢出,继而堆栈溢出。
S3 Boot Script
UEFI
启动脚本表是一种数据结构,用于在ACPI S3
睡眠期间保存平台状态,当大多数平台组件关闭电源时。通常,该结构位于特殊的非易失性存储(NVS
)内存区域中。UEFI
代码在正常引导期间构建引导脚本表,并在S3
恢复期间解释其条目,此时平台正在从睡眠中唤醒。攻击者可以在操作系统内核模式下修改当前引导脚本表内容并触发S3
挂起-恢复周期,从而在安全功能未初始化或未锁定的早期平台初始化阶段实现任意代码执行。
hook boot script
中的函数
可以通过读取ACPI
全局变量中保存的boot脚本表格,解析其中的函数调用指令,并替换为跳转到自定义代码的指令。然后将原有指令备份,并将自定义代码插入到被hook的函数中。这样,在boot流程执行到相应的位置时,就会跳转到自定义代码执行,并完成攻击或劫持目标操作系统的控制权。
因此,要hook boot脚本表格中的函数,需要进行如下步骤:
- 读取
ACPI
全局变量,获取boot script
表格地址。 - 解析boot脚本表格,找到需要hook的固件函数对应的地址。
- 对该函数地址处的指令进行patch,将其替换为跳转到自定义代码的指令,并将原有指令备份。
- 将自定义代码插入到被hook的函数中,并在其执行前后实现攻击或劫持的目的。
UEFI
中,可以使用Virtual Address Map (VAM)
协议来获取操作系统中的虚拟地址和UEFI
中的物理地址之间的映射关系,并且可以通过Memory Allocation Services协议等API
来向系统申请可读写的内存空间。基于这些API
和协议,可以在UEFI
环境中加载hook代码,并修改Boot Script Table中固件函数的入口指针,使其指向hook代码。也可以通过写入物理内存的方式实现hook
。
攻击PR0-PR4
保护寄存器
SPI
闪存保存着加载引导程序的镜像,PR4
对SPI
的读写进行了保护,硬件排序闪存状态寄存器(HSFS
)位于SPI
主机接口寄存器区域开始的偏移量0x04
处,它有FLOCKDN
位,可以防止PR0-PR4
寄存器的值被修改。
当FLOCKDN
位被设置时,在完全复位之前不可能改变PR0-PR4
寄存器的值,所以,PRx
闪存写保护的最明显的弱点是平台固件在启动过程中到底如何设置它们。
除了正常的启动路径,现代ACPI
兼容的计算机固件还为S3
恢复实现了一个单独的启动路径,当计算机从S3
睡眠中醒来时使用,这是一个特殊的电源状态,当大多数平台组件关闭时(S4
和S5
恢复是在正常启动路径中实现的,S1
和S2
恢复在大多数Intel平台上没有使用)。UEFI
固件有一个特殊的数据结构,叫做UEFI
启动脚本表,它可以在S3
休眠状态下存活,用于在正常启动路径中保存平台寄存器的值,并在S3
恢复时恢复它们。启动脚本表存储在内存中,因此,如果攻击者能够从运行的操作系统中修改它并触发S3
暂停-恢复,他将有可能覆盖负责平台安全的某些系统寄存器的值。一些供应商的固件在执行Boot Script Table
之前没有锁定这些特定的寄存器。
为了保护平台免受此类攻击,UEFI
规范引入了一种特殊的机制,称为SMM LockBox
,它用于在系统管理RAM
(SMRAM
)中存储启动脚本表--SMM
可以实现访问,但是操作系统无法访问。
GURB
GURB(GBand Unified Bootloader)
统一引导加载程序
针对GURB
的攻击,一方面是实现更高权限的任意代码执行,另一方面是绕过安全启动,破坏信任链,这块具体的一些漏洞模式还待整理,相关的漏洞案例可见后文CVE
梳理部分。
CVE
梳理
SMM
相关漏洞
CVE-2021-26943
漏洞描述
华硕 UX360CA BIOS 303
SMM
本地权限提升漏洞
非物理接触,ring 0
特权级向ring -2
特权级攻击,实现本地权限提升。
漏洞成因
易受攻击的模块和相应的 SMI
UsbRt (0x31)
、SdioSmm (0x40)
NvmeSmm (0x42)
。所有这些 SMI
0x40E
SMRAM
中的地址。
EFI_STATUS
EFIAPI
SdioSmm_SwSmi_40h(
EFI_HANDLE DispatchHandle,
CONST VOID *Context,
VOID *CommBuffer,
UINTN *CommBufferSize
)
{
// ...
struct_v0 *userControlled = *(0x10 * MEMORY[0x40E] + 0x104);
if ( EFI_ERROR(ValidateBufferIsOutsideSmram(userControlled, sizeof(struct_v0)))
|| userControlled->Offset0_FunctionCode >= 4 )
{
userControlled->Offset2 = 7;
}
else
{
// ...
}
return EFI_SUCCESS;
}
漏洞利用
- 确保物理地址
0x40e
为零 - 在物理地址
0x104
SMRAM
的地址,比如0x88400000
- 发出
SMI 0x40
0x88400000+2
0x7
。- 通过以下步骤在
SMRAM
SMST
) 的地址:
- 从
HKLM\HARDWARE\RESOURCEMAP\System Resources\Loader Reserved .Raw
值的内容中获取UEFI
运行时代码的物理地址范围 - 通过扫描地址范围中的“
smmc
”签名找到SMM_CORE_PRIVATE_DATA
的地址 SMM_CORE_PRIVATE_DATA
在偏移量30h
处具有指向SMST
的指针
- 使用上述写原语覆盖
SMST
d0h
SmmLocateProtocol
函数的指针。该值更新为0x07070707
。 - 在物理内存
0x07070707
写入shellcode
- 触发另一个调用
Smst->SmmLocateProtocol
的SMI
,比如SMI 0xdf
。0x07070707
处的shellcode
在SMM
中执行
CVE-2021-3452 SMM callout
漏洞
联想ThinkPad
中存在的一个漏洞,这个漏洞是一个SMM callout
漏洞。
这里需要一些前置的知识:CPU
在切换到SMM
时需要保存寄存器状态(就像用户态切换到内核态也会保存寄存器状态),在SMRAM
就有一块区域用于存放保存的寄存器的值,称为save state
。
typedef struct _ssa_normal_reg {
UINT64 r15; // start at SMBASE + 0xFF1C
UINT64 r14; // 0xFF24
UINT64 r13; // 0xFF2C
UINT64 r12; // 0xFF34
UINT64 r11; // 0xFF3C
UINT64 r10; // 0xFF44
UINT64 r9; // 0xFF4C
UINT64 r8; // 0xFF54
UINT64 rax; // 0xFF5C
UINT64 rcx; // 0xFF64
UINT64 rdx; // 0xFF6C
UINT64 rbx; // 0xFF74
UINT64 rsp; // 0xFF7C
UINT64 rbp; // 0xFF84
UINT64 rsi; // 0xFF8C
UINT64 rdi; // 0xFF94
} ssa_normal_reg_t;
这块内存就是用户可控的。在实际的漏洞利用的过程中,这块内存可以用来写入shellcode
来绕过SMM_Code_Chk_En
的保护。
关于漏洞利用,我的理解是:在进入SMM
前,提前控制rax
寄存器的值为0x5380
,rbx
寄存器的值为0x7003
,同时通过控制寄存器在save state
写入恶意代码。通过ReadSaveState
来读取之前保存的寄存器的状态,将参数传递给SwSmiHandler_48C
函数,这时候gRT_1B00->ResetSystem
对应的应该是save state
区域提前布置好的恶意代码。这里没有看明白他是如何替换gRT_1800->ResetSystem
的函数指针。
.text:000000000000048C SwSmiHandler_48C proc near ; DATA XREF: sub_314+FA↑o
...
.text:000000000000048C
.text:000000000000048C mov [rsp-28h+arg_0], rbx
.text:0000000000000491 mov [rsp-28h+arg_8], rsi
.text:0000000000000496 mov [rsp-28h+arg_10], rdi
.text:000000000000049B push rbp
.text:000000000000049C push r12
.text:000000000000049E push r13
...
...
.text:0000000000000BAC ; ---------------------------------------------------------------------------
.text:0000000000000BAC
.text:0000000000000BAC loc_BAC: ; CODE XREF: SwSmiHandler_48C+6C2↑j
.text:0000000000000BAC mov rax, cs:gRT_1B00
.text:0000000000000BB3 xor edx, edx ; ResetStatus
.text:0000000000000BB5 xor r9d, r9d ; ResetData
.text:0000000000000BB8 lea ecx, [rdx+2] ; ResetType
.text:0000000000000BBB xor r8d, r8d ; DataSize
.text:0000000000000BBE call [rax+EFI_RUNTIME_SERVICES.ResetSystem] ; gRT->ResetSystem()
...
.text:0000000000000314 sub_314 proc near ; CODE XREF: _ModuleEntryPoint+7B↑p
...
.text:0000000000000340 call [rax+_EFI_SMM_SYSTEM_TABLE2.SmmLocateProtocol] ; gSmst->SmmLocateProtocol
.text:0000000000000346 mov rax, cs:gSmst_1AF8
.text:000000000000034D lea r8, [rsp+28h+EFI_SMM_SW_DISPATCH2_PROTOCOL_IF] ; Interface
.text:0000000000000352 lea rcx, EFI_SMM_SW_DISPATCH2_PROTOCOL_GUID_19F0 ; Protocol
.text:0000000000000359 xor edx, edx ; Registration
...
.text:00000000000003F5 lea r9, [rsp+28h+DispatchHandle] ; DispatchHandle
.text:00000000000003FA mov rcx, [rax]
.text:00000000000003FD mov rax, [rsp+28h+EFI_SMM_SW_DISPATCH2_PROTOCOL_IF]
.text:0000000000000402 lea r8, [rsp+28h+RegisterContext] ; RegisterContext
.text:0000000000000407 mov cs:qword_1C28, rcx
.text:000000000000040E lea rdx, SwSmiHandler_48C ; DispatchFunction
.text:0000000000000415 mov rcx, rax ; This
.text:0000000000000418 mov [rsp+28h+RegisterContext.SwSmiInputValue], 80h ; '€'
.text:0000000000000421 call [rax+EFI_SMM_SW_DISPATCH2_PROTOCOL.Register]
__int64 __fastcall SwSmiHandler_48C():
case 0x7003u:
gRT_1B00->ResetSystem(EfiResetShutdown, 0i64, 0i64, 0i64);
goto LABEL_132;
CVE-2021-21555 Double_GetVariable漏洞
// mEraseRecordShare - buffer is allocated on heap.
// AepErrorLog - NVRAM variable is controlled by the attacker.
Tries = 3;
DataSize = 0;
...
do
{
// overflow occurs after the second try because variable length exceeds 964 bytes
Status = gRT->GetVariable(L"AepErrorLog", &VendorGuid, 0, &DataSize, mEraseRacordShare);
--Tries;
v5 = Status < 0;
}
while ( Status < 0 && Tries );
...
第一次读取AepErrorLog
变量的值之后,未对DataSize
做初始化,如果AepErrorLog
大于946
字节的话,在下一次调用gRT->GetVariable
时,会导致堆溢出。
CVE-2021-21556 memset误用导致\x00覆写返回地址
在UEFI
编程中,GetVariable
和SetVariable
函数用于读取和写入存储在NVRAM
(非易失性随机存储器)中的变量。这些函数的返回值如下:
GetVariable
函数会返回一个符合EFI_STATUS
数据类型的状态码。如果操作成功,则返回EFI_SUCCESS
。如果变量不存在,则返回EFI_NOT_FOUND
。如果缓冲区太小以容纳整个变量,则返回EFI_BUFFER_TOO_SMALL
。如果发生其他错误,则返回适当的错误码。SetVariable
函数同样会返回一个符合EFI_STATUS
数据类型的状态码。如果操作成功,则返回EFI_SUCCESS
。如果变量为只读,或者变量名非法,则返回EFI_INVALID_PARAMETER
。如果尝试删除不存在的变量,则返回EFI_NOT_FOUND
。如果NVRAM
空间不足,则返回EFI_OUT_OF_RESOURCES
。如果发生其他错误,则返回适当的错误码。
如果`NVRAM`变量`MirrorRequest`长度大于`5`字节,则会用`\x00`覆写局部变量`_MirrorRequest`。如果覆写长度大于`0xD1`,则会导致覆写返回地址。
CVE-2021-39298 SMM callout
漏洞
在SMM
中调用了SMRAM
以外的`EFI_RUNTIME_SERVICES
中的函数GetVariable
。
EFI_STATUS __fastcall SwSmiHandler_1574(EFI_HANDLE DispatchHandle)
{
EFI_STATUS result;
UINT32 Attributes;
UINTN DataSize;
EFI_SMM_SW_DISPATCH2_PROTOCOL *EfiSmmSwDispatch2;
__int64 Data[6];
Attributes = 6;
DataSize = 32i64;
if ( (gRT_20F8->GetVariable(L"AmdMemContextData", &VendorGuid, &Attributes, &DataSize, Data) & 0x8000000000000000ui64) == 0i64 )
x_AmdMemContextHandler(Data);
result = gSmst_20F0->SmmLocateProtocol(&EFI_SMM_SW_DISPATCH_PROTOCOL_GUID_2050, 0i64, &EfiSmmSwDispatch2);
if ( (result & 0x8000000000000000ui64) == 0i64 )
return (EfiSmmSwDispatch2->UnRegister)(EfiSmmSwDispatch2, DispatchHandle);
return result;
}
CVE-2021-39299 Double GetVariable
漏洞
...
_GetCoolControlData:
DataSize = 8i64;
vStatus = gRT_23B0->GetVariable(
(CHAR16 *)L"CoolControlData",
&gVendorGuid,
(UINT32 *)&Attributes,
&DataSize,
vCoolControlDataValue);
// If the length of the CoolControlData variable is greater than 8, then at the moment vStatus = EFI_BUFFER_TOO_SMALL
// (this means that the GetVariable call will occur a second time)
if ( !vStatus
|| (vStatus = gRT_23B0->GetVariable(
(CHAR16 *)L"CoolControlData",
&gVendorGuid,
(UINT32 *)&Attributes,
&DataSize,
vCoolControlDataValue)) == 0 )
{
LOBYTE(v19) = vCoolControlDataValue[1];
LOBYTE(v20) = 1;
sub_1A78(v20, v19);
}
return vStatus;
}
CVE-2022-23924
堆溢出漏洞
和SMM
通信缓冲区相关的漏洞,漏洞模式比较有意思:通过控制通信缓冲区控制源地址和源地址中字符串长度,通过nvram
变量控制目的地址对应的堆空间的大小,从而实现溢出。
EFI_STATUS __fastcall SmiHandler_2158(
EFI_HANDLE DispatchHandle,
const void *Context,
_QWORD *CommBuffer,
UINTN *CommBufferSize)
{
// [COLLAPSED LOCAL DECLARATIONS. PRESS KEYPAD CTRL-"+" TO EXPAND]
Num = 4;
if ( CommBuffer && CommBufferSize && (-(__int64)(*CommBufferSize != 24) & EFI_INVALID_PARAMETER) == 0 )
{
Size = GetStrSize((_WORD *)CommBuffer[1]);
CommBuffer1 = CommBuffer[1];
Size1 = Size;
Res = 0;
if ( CommBuffer1 && Size1 )
Res = SmmIsBufferOutsideSmmValid(CommBuffer1, Size1);
if ( (-(__int64)(Res == 0) & EFI_INVALID_PARAMETER) == 0 )
{
if ( *(_BYTE *)CommBuffer )
{
if ( *(_BYTE *)CommBuffer == 1 )
{
if ( GetWideStrSize((_WORD *)CommBuffer[1]) )
{
Res1 = FactoryConfigHandler((_WORD *)CommBuffer[1], (__int64)&Num, 1);// possible heap overflow
goto _Exit;
}
}
else if ( *(_BYTE *)CommBuffer == 2 )
{
Res1 = BuildIdHandler((_WORD *)CommBuffer[1], (__int64)&Num, 1);// possible heap overflow
_Exit:
CommBuffer[2] = Res1;
return 0;
}
}
else if ( GetWideStrSize((_WORD *)CommBuffer[1]) )
{
CommBuffer[2] = Validate((_WORD *)CommBuffer[1]) ? 0 : EFI_CRC_ERROR;
return 0;
}
CommBuffer[2] = EFI_INVALID_PARAMETER;
}
}
return 0;
}
如果来自 CommBuffer
CommBuffer + 8
处,则将调用函数FactoryConfigHandler
(偏移量:0x1A60
)
如果来自CommBuffer
CommBuffer + 8
处,则将调用函数BuildIdHandler
(偏移量:0x1C88
)
FactoryConfigHandler
函数中的代码如下:
Status = SmmGetVariable(L"FactoryConfig", &VariableValue);
if ( Stautus )
goto _Exit;
if ( GetWideStrSize(StringFromCommBufferPtr) )
{
StrSize = GetStrSize(StringFromCommBufferPtr);
Value = VariableValue;
if ( VariableValue && StrSize )
{
if ( StrSize > 500 )
StrSize = 500;
if ( VariableValue != StringFromCommBufferPtr )
CopyMem(VariableValue, StringFromCommBufferPtr, StrSize);
}
...
}
SmmGetVariable
会获取FactoryConfig
NVRAM
变量的大小,然后会申请对应大小的堆空间,将堆地址保存在VariableValue
变量中。
这样在CopyMem
地址处就可以同时控制目的地址和源地址,以及StrSize
,然后造成堆溢出,BuildIdHandler
原理一样,StrSize
对应的大小不同。
CVE-2022-23925
任意SMRAM
地址损坏漏洞
控制SMM
通信缓冲区未经验证的多级指针,实现任意SMRAM
地址写。
EFI_STATUS __fastcall SmiHandler_246C(
EFI_HANDLE DispatchHandle,
const void *Context,
CommBuffer *CommBuffer,
UINTN *CommBufferSize)
{
// [COLLAPSED LOCAL DECLARATIONS. PRESS KEYPAD CTRL-"+" TO EXPAND]
if ( CommBuffer && CommBufferSize && (-(*CommBufferSize != 64) & EFI_INVALID_PARAMETER) == 0 )
{
Header = 'UBES';
Error = -(CommBuffer->Header != 'UBES') & EFI_INVALID_PARAMETER;
if ( CommBuffer->Header == 'UBES' )
{
if ( !CommBuffer->EnableChecks
|| !sub_40E8('UBES', Context)
|| CommBuffer->Case == 2
|| sub_324C()
|| sub_3088(CommBuffer->field_30, CommBuffer->field_28, 0i64) )
{
Error = 0i64;
}
else
{
Error = EFI_ACCESS_DENIED;
}
}
if ( Error )
goto _Exit;
Case = CommBuffer->Case;
if ( Case > 5 )
{
Case1 = Case - 6;
if ( Case1 )
{
Case2 = Case1 - 1;
if ( Case2 )
{
Case3 = Case2 - 1;
if ( Case3 )
{
if ( Case3 != 1 )
{
_Exit1:
Error = EFI_INVALID_PARAMETER;
goto _Exit;
}
// Case3 = CommBuffer->Case - 8 = 1
// CommBuffer->Case = 9
Res = ToCopyMemS(CommBuffer->Offset, CommBuffer->Dst, CommBuffer->DstSize);
}
...
}
...
}
...
}
...
}
return 0;
}
CommBuffer->Header
应等于0x55424553
( UBES
)。如果我们设置CommBuffer->EnableChecks
为0x00
和CommBuffer->Case
比0x09
我们将触发以下代码:
Res = ToCopyMemS(CommBuffer->Offset, CommBuffer->Dst, CommBuffer->DstSize);
__int64 __fastcall ToCopyMemS(__int64 Offset, void *DstBuffer, unsigned int DstBufferSize)
{
// [COLLAPSED LOCAL DECLARATIONS. PRESS KEYPAD CTRL-"+" TO EXPAND]
Status = 0i64;
if ( !DstBuffer || !DstBufferSize )
return EFI_INVALID_PARAMETER;
if ( gProprietaryprotocol1Located ) // Should be zero
return (*(gProprietaryprotocol_1 + 2))(gArgs[0] + Offset);
Func = *(gProprietaryprotocol_2 + 2);
if ( !Func ) // Should be zero
{
CopyMemS(DstBuffer, DstBufferSize, (Offset + gArgs[0]), DstBufferSize);
return Status;
}
return Func(gArgs[0] + Offset);
}
CVE-2022-23930
堆越界写漏洞
EFI_STATUS __fastcall SmiHandler_19A0(
EFI_HANDLE DispatchHandle,
const void *Context,
void *CommBuffer,
UINTN *CommBufferSize)
{
// [COLLAPSED LOCAL DECLARATIONS. PRESS KEYPAD CTRL-"+" TO EXPAND]
...
// size not validated, we can allocate small buffer, then write out of bounds
CommBufferCopy = (unsigned __int64 *)GetCopy(*CommBufferSize, CommBuffer);
if ( CommBufferCopy )
{
...
*((_QWORD *)CommBufferCopy + 2) = Status; // OOB write
CopyMemS(CommBuffer, *CommBufferSize, CommBufferCopy, *CommBufferSize);
Clear(&CommBufferCopy1, *CommBufferSize);
}
return 0;
}
void *__fastcall GetCopy(UINTN Size, const void *Buffer)
{
// [COLLAPSED LOCAL DECLARATIONS. PRESS KEYPAD CTRL-"+" TO EXPAND]
result = AllocatePool(Size); // gSmst->SmmAllocatePool wrapper
if ( result && Size && result != Buffer )
return CopyMem(result, Buffer, Size);
return result;
}
GetCopy
函数会为Buffer
在堆中创建一个大小为Size
的副本。它这里提到size的大小小于24会造成越界写。具体UEFI
中堆分配机制我还不太了解。
*((_QWORD *)CommBufferCopy + 2) = Status; // OOB write
DXE
相关
CVE-2022-037
内存信息泄露
__int64 sub_1D38()
{
// ...
DataSize = 139;
result = (gRT->GetVariable)(L"LenovoSecurityConfig", &LENOVO_SECURITY_CONFIG_VARIABLE_GUID, &DataSize, LenovoSecurityConfigValue);
if ( (result & 0x8000000000000000) == 0 )
{
if...
if ( gLenovoRuntimeConfigFlag2 | gLenovoRuntimeConfigFlag1 )
{
GetVariable = gRT->GetVariable;
DataSize = 49;
(GetVariable)(L"LenovoFunctionConfig", &gVariableGuid, &Attributes, &DataSize, LenovoFunctionConfigValue);
LenovoFunctionConfigValue[0] = 0;
(gRT->SetVariable)(L"LenovoFunctionConfig", &gVariableGuid, Attributes, DataSize, LenovoFunctionConfigValue);
}
return 0;
}
return result;
}
GetVariable
之后,SetVariable
之前未作初始化。
CVE-2022-4432
内存信息泄漏
__int64 __fastcall EventNotifier()
{
__int64 result;
EFI_SET_VARIABLE SetVariable;
EFI_GET_VARIABLE GetVariable;
EFI_SET_VARIABLE SetVariable_1;
__int64 DataSize;
unsigned int Attributes;
__int64 UnknownProtocolInterface;
__int16 LenovoAbtStatusValue[7];
char LenovoSecurityConfig[139];
result = LocateProtocol(&gProtocolGuid, &gDataSegmentBase);
if ( (result & 0x8000000000000000) == 0 )
{
DataSize = 0x8B;
result = (gRT->GetVariable)(
L"LenovoSecurityConfig",
&LENOVO_SECURITY_CONFIG_VARIABLE_GUID,
&Attributes,
&DataSize,
LenovoSecurityConfig);
if ( (result & 0x8000000000000000) == 0 )
{
SetVariable = gRT->SetVariable;
LenovoSecurityConfig[38] = 1;
if ( ((SetVariable)(
L"LenovoSecurityConfig",
&LENOVO_SECURITY_CONFIG_VARIABLE_GUID,
Attributes,
DataSize,
LenovoSecurityConfig) & 0x8000000000000000) == 0 )
(*(UnknownProtocolInterface + 0x10))(); // aa5978d6-4fba-4827-b776-9a11fc8cce4c
GetVariable = gRT->GetVariable;
DataSize = 14;
if ( ((GetVariable)(L"LenovoAbtStatus", &unk_4030, &Attributes, &DataSize, LenovoAbtStatusValue) & 0x8000000000000000) != 0 )
{
sub_28A8(LenovoAbtStatusValue, 14);
LenovoAbtStatusValue[2] = 0x100;
LenovoAbtStatusValue[0] = 1;
sub_28A8(&LenovoAbtStatusValue[3], 7);
}
SetVariable_1 = gRT->SetVariable;
LOBYTE(LenovoAbtStatusValue[2]) = 1;
return (SetVariable_1)(L"LenovoAbtStatus", &unk_4030, Attributes, DataSize, LenovoAbtStatusValue);
}
}
return result;
}
这里可以控制后面GetVariable
读取后,Datasize
变量的长度,如果设置的NVRAM
变量长度x
大于14
,就会泄露&LenovoAbtStatusValue+14
后面的内存地址。
((SetVariable)(
L"LenovoSecurityConfig",
&LENOVO_SECURITY_CONFIG_VARIABLE_GUID,
Attributes,
DataSize,
LenovoSecurityConfig) & 0x8000000000000000) == 0 )
return (SetVariable_1)(L"LenovoAbtStatus", &unk_4030, Attributes, DataSize, LenovoAbtStatusValue);
CVE-2022-40517
栈溢出
...
AsciiStrToUnicodeStr(&Name, VariableName);
StrCatS(VariableName, 0x80, L".Type");
Flag = 0;
if ( !(gRT->GetVariable)(VariableName, &gVariableGuid, 0, &DataSize, VariableValue) )
{
Flag = 1;
dword_1E630 = VariableValue[0];
}
AsciiStrToUnicodeStr(&Name, VariableName);
StrCatS(VariableName, 0x80, L".FwName");
if ( !(gRT->GetVariable)(VariableName, &gVariableGuid, 0, &DataSize, VariableValue) )
{
sub_9B38(word_1E634, 0x1F, VariableValue, 0x1F);
Flag = 1;
}
AsciiStrToUnicodeStr(&Name, VariableName);
StrCatS(VariableName, 0x80, L".PartiLabel");
if ( !(gRT->GetVariable)(VariableName, &gVariableGuid, 0, &DataSize, VariableValue) )
{
sub_9B38(word_1E674, 0x1F, VariableValue, 0x1F);
Flag = 1;
}
AsciiStrToUnicodeStr(&Name, VariableName);
StrCatS(VariableName, 0x80, L".PartiRootGuid");
if ( !(gRT->GetVariable)(VariableName, &gVariableGuid, 0, &DataSize, VariableValue) )
{
sub_8E98(&byte_1E6B4, VariableValue);
Flag = 1;
}
AsciiStrToUnicodeStr(&Name, VariableName);
StrCatS(VariableName, 0x80, L".PartiGuid");
if ( !(gRT->GetVariable)(VariableName, &gVariableGuid, 0, &DataSize, VariableValue) )
{
sub_8E98(&byte_1E6C4, VariableValue);
Flag = 1;
}
AsciiStrToUnicodeStr(&Name, VariableName);
StrCatS(VariableName, 0x80, L".ImagePath");
if ( !(gRT->GetVariable)(VariableName, &gVariableGuid, 0, &DataSize, VariableValue) )
{
sub_9B38(word_1E6D4, 0x1F, VariableValue, 0x1F);
Flag = 1;
}
...
每次调用GetVariable
之前,没有对DataSize
做初始化,有可能导致栈溢出。
GURB
相关
CVE-2020-14372
漏洞描述
通过CVE-2020-14372
,在Linux
内核被加载后,一个攻击者可能会利用RUB 2
的漏洞绕过安全引导(Secure Boot
)机制。当一个系统重启后,攻击者可以让 grub(通过它的配置文件)加载一个自定义高级配置和电源接口(ACPI
)表来禁用内核的锁定机制,从而进一步允许将未经签名的代码加载到内核空间中,从而可能会损害系统的数据完整性、机密性和可用性。
漏洞成因
GRUB 2引导加载程序支持多个模块和命令,可用于修改引导加载程序的行为或扩展其功能。当使用安全引导技术引导系统时,所有加载的模块都应遵守相关的限制并拒绝用户加载未经签名的代码。此安全漏洞可以使GRUB 2 ACPI
命令选项不遵守安全引导的限制。因此,恶意的代理可以加载一个精心制作的ACPI
表,该表会修改环境,从而导致安全引导的机制被破坏。
CVE-2020-10713
漏洞描述
Eclypsium
研究人员在多数Linux系统使用的GRUB2
引导程序中发现了一个漏洞将其命名为“BootHole
”(CVE-2020-10713
),即使启用了Secure Boot
,也可在启动进程中执行任意代码。攻击者可利用该漏洞安装持久且隐秘的bootkit
或恶意引导程序来控制设备。
漏洞成因
BootHole
漏洞是解析grub.cfg
文件时在GRUB2
中发生的缓冲区溢出。此配置文件是通常位于EFI
系统分区中的外部文件,因此可以由具有管理员特权的攻击者修改,而无需更改已签名供应商shim
和GRUB2 bootloader
可执行文件的完整性。缓冲区溢出使攻击者可以在UEFI
执行环境中获得任意代码执行权限,该代码可以用于运行恶意软件,更改启动过程,直接修补OS内核或执行恶意代码。
该漏洞的根因,是GURB2
和flex
解释器设计模式不匹配造成的。
flex生成的解析器引擎将此定义包含为令牌处理代码的一部分:
在这个宏中,生成的代码检测到它遇到的令牌太大而无法放入flex的内部解析缓冲区并调用YY_FATAL_ERROR()
,这是使用flex生成的解析器的软件提供的帮助函数。
但是,YY_FATAL_ERROR()GRUB2
软件包中提供的实现是:
GRUB2
中只是将错误输出到控制台,然后返回调用函数,并没有exit
。这时候YY_flex_strncpy
被调用,造成堆溢出。