x86架构BIOS攻击面梳理与分析

   之前的一份学习笔记,主要整理了一下x86架构下BIOS的一些攻击面,BootKit部分还没有搬上来。

   可能有一些理解存在疏漏的地方,还请看官老爷斧正。

调研目标

一、梳理安全启动的基本流程

  • 经历的过程
  • 软硬件层面需要完成的工作

二、梳理攻击面和UEFI的保护机制

三、找出攻击面对应的具体漏洞案例

四、BootKit简介

安全启动

  UEFIUEFI 规范中定义的一项功能。它保证只有有效的第 3 方固件代码才能在原始设备制造商 (OEM) 固件环境中运行。 在安全启动中,只有原始设备制造商提供的固件是有效的,任何第 3 方固件代码均不受信任。

UEFI Secure Boot 包括两部分——启动映像的验证和映像安全数据库更新的验证。

UEFI规范

 

在bios里更改com口 bios设置com口打开_初始化

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 foundationpei基础服务和流程
  • peimodulepeim,并根据peim之间的依赖关系顺序执行peim

DXE

主要工作:系统初始化功能:

  • 根据hob初始化系统服务
  • 调度driver,初始化SMM的运行时环境
  • 打开EFI_BDS_ARCH_PROTOCOLPROTOCOLDEX和驱动之间的通信方式)
  • 打开entry->bds

BDS

主要工作:执行启动策略功能:

  • 初始化控制台设备
  • 加载必要的设备驱动
  • 根据系统设置加载和执行启动项

TSL

主要工作:加载OS_loader的第一阶段,系统控制权仍然由UEFI内核掌控

RT

主要工作:runtime,系统控制权由UEFI移交OS_loader

AL

系统遇到灾难性错误,系统需要提供错误处理和灾难恢复机制,这种机制运行在AL阶段。

安全启动流程

1.从信任根(PI固件和PK)验证下一启动阶段的固件签名,UEFI规范中的SECPEIDEX都属于PI固件

2.由可信根验证KEK

3.由KEK验证dbdbx完整性

4.安全启动从可执行文件定位并读取签名证书,并与计算的哈希值比较

5.启动已验证成功的DEX驱动,UEFI appOption ROM控制权转交给OS loader

6.OS loader加载操作系统

在bios里更改com口 bios设置com口打开_初始化_02

在bios里更改com口 bios设置com口打开_固件_03

保护机制

软件层面

1.对操作系统引导加载程序,系统镜像以及内核初始化后加载的驱动进行签名,所有的签名保存在签名数据库中,签名数据库位于spi闪存或者nvram

2.内存管理层面,对物理页表和虚拟页表的保护

3.在EDK2中,负责检查给定缓冲区是否与SMRAM重叠的函数被称为SmmIsBufferOutsideSmmValid()。这个函数在每次SMI调用时都会在通信缓冲区上被调用,用来检查用于向SMI处理器传递参数的通信缓冲区是否与SMRAM重叠。

4.为了向操作系统报告哪些缓解措施已在 SMMUEFIWindows SMMWSMT)的 ACPI表。除其他外,WSMTCOMM_BUFFER_NESTED_PTR_PROTECTIONSMI处理程序在没有事先清理的情况下不会使用嵌套指针。可以使用chipseccommon.wsmt转储和解析此表。

5.HVCIwindows hypervisor code integrity checking,这项技术不是保护固件的,而是基于固件对内核文件进行校验。HVCI利用Windows虚拟化技术,运行在Windows操作系统顶层的一个虚拟化层,将系统内核保护在这个虚拟化层内部。在启用HVCI的情况下,系统会在每次启动时验证内核文件的完整性,并在内核加载时创建一个受保护的内核代码区域,只有被授权的代码才能够访问该区域。

硬件层面

1.Intel Boot Guard:通过固件签名的方式防止Bootrom被恶意烧写和篡改;

2.对spi闪存区设置硬件读写位的保护,防止软硬件层面对spi进行篡改

  • BIOS控制寄存器
  • BIOSWE(bit 0):当设置时,对BIOS空间的访问将允许读写操作,否则仅允许读取操作。
  • BLE(bit 1):设置后,BIOSWESMM0设置为 1。任何从非SMM代码设置 BIOSWESMI。这为 OEMSMI处理程序的机会,通过将其设置回 0来保护BIOSWE
  • SMM_BWP(bit 5):设置后,BIOS 区域不可写,除非所有处理器都处于 SMMBIOSWE为 1。设置此位可解决 Speed Racer 竞争条件漏洞
  • PR0-PR4保护范围寄存器:每个寄存器为特定范围的 SPIBIOSHSFS) 寄存器中的闪存配置锁定 (FLOCKDN) 位未设置时,才能设置它们。
    这个 FLOCKDNFLOCKDN被设置,只有在下一次硬件复位后它才会被清除。这意味着受保护范围寄存器保护的内存范围在被锁定后不能被任何运行时代码(包括 SMM 代码)修改。即使是合法的固件更新也必须在 PR 寄存器被锁定之前执行。
  • PR0寄存器:控制SPI控制器的输入时序和输出时序,包括时钟极性、相位和时钟分频等参数。
  • PR1寄存器:控制SPI控制器的中断和DMA操作,包括中断使能、中断标志和DMA使能等参数。
  • PR2寄存器:控制SPI控制器的FIFO缓冲区,包括FIFO深度、读写指针和数据长度等参数。
  • PR3寄存器:控制SPI控制器的错误检测和纠正,包括传输错误标志、FIFO溢出标志和校验和错误标志等参数。
  • PR4寄存器:控制SPI控制器的安全保护,包括写保护、读保护和访问权限等参数。

3.建立硬件可信根,通过根密钥对后续加载的镜像和签名数据库进行逐级的校验,根密钥安全,则所加载的镜像可信

4.SMM_Code_Chk_EnMSR_SMM_FEATURE_CONTROLSMRRSMMMCE。因此,启用和锁定此位是缓解 SMM调出漏洞的重要一步。

5.smm_dma可以避免SMRAMDMA设备进行修改。

6.通过完全删除EFI_VARIABLE_RUNTIME_ACCESS属性或使用EDKII_VARIABLE_LOCK_PROTOCOL等协议来实现对NVRAM变量的保护,使变量成为只读。

攻击面梳理

在bios里更改com口 bios设置com口打开_初始化_04

在bios里更改com口 bios设置com口打开_初始化_05

硬件层面

侧信道

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之前修改这些服务所在的物理页面,从而在受影响的服务被调用后劫持特权执行流程。

在bios里更改com口 bios设置com口打开_固件_06

低址SMRAM损坏

在正常情况下,用于向SMI处理器传递参数的通信缓冲区不得与SMRAM重叠。这个限制的理由很简单:如果不是这样,那么每次SMI处理程序将某些数据写入通信缓冲区的时候——例如,为了向调用方返回状态代码时,都会“顺带”修改SMRAM的某些部分,这是不可取的。

在bios里更改com口 bios设置com口打开_在bios里更改com口_07

SmmIsBufferOutsideSmmValid()函数实现了对SMI通信缓冲区和SMRAM区域的检查。

但是由于comm Buffer的大小是用户可控的,SmmIsBufferOutsideSmmValid()只会检查Comm Buffer是否和SMRAM重叠,而不会检查写入Comm Buffer的内容是否溢出Comm Buffer,所以如果SMI处理程序处理不当的话,依然会造成低址SMRAM损坏。

在bios里更改com口 bios设置com口打开_固件_08

缓解措施,是要明确检查Comm Buffer的大小是否符合预期(大于等于写入Comm Buffer的长度)。

任意SMRAM损坏

没有使用SmmIsBufferOutsideSmmValid()对多级指针指向的地址空间做检查。如果SMI处理程序中的多级指针指向SMRAM内的地址空间,就有可能造成任意SMRAM损坏。

在bios里更改com口 bios设置com口打开_在bios里更改com口_09

TOCTOU attacks

time-of-check-time-of-use

Comm Buffer是驻留在缓冲区以外的,进入SMI处理程序之后,Comm Buffer中的值是可以被SMI处理程序修改,以及被外设通过DMA的方式进行修改的。

前面提到使用SmmIsBufferOutsideSmmValid()对多级指针进行验证,如果验证之后又可以进行修改,然后多级指针又被引用,那相当于没有进行验证。

在bios里更改com口 bios设置com口打开_初始化_10

在bios里更改com口 bios设置com口打开_初始化_11

在bios里更改com口 bios设置com口打开_在bios里更改com口_12

SetVariable信息泄露

UEFI中有两个API实现对NVRAM的读和写:GetVariableSetVariable。对这两个API运用不当会产生一些漏洞模式。

更改NVRAM变量,需要先用GetVariablenvram变量的值读入一个局部变量,对缓冲区中的局部变量进行修改,然后用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
                );

GetVariableSetVariable第四个参数是读取和写入的nvram变量的长度,如果SetVariable的第四个参数的长度大于修改后的局部变量的长度,就会将SMRAM内存中的值写入nvram变量造成信息泄露。

在bios里更改com口 bios设置com口打开_固件_13

 

在bios里更改com口 bios设置com口打开_在bios里更改com口_14

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的更新是一种更安全、更可靠的更新方式,因为它可以提供以下优点:

  1. 可以验证固件更新的完整性和来源,从而防止恶意软件或未经授权的更新。
  2. 不需要进入操作系统来执行更新,降低了风险并提高了更新的成功率。
  3. 即使更新失败,系统也可以回滚到之前的稳定状态,避免了可能带来的损失。

capsule固件更新过程

1.开始发送一个固件更新Capsule的处理过程,操作系统将获取这个Capsule并将其分割成多个片段,分布在地址空间中进行处理。

 

在bios里更改com口 bios设置com口打开_初始化_15

2.操作系统通过SetFirmwareEnvironmentVariable函数创建CapsuleUpdateDate这个EFI变量,通过这个EFI变量确定描述碎片化的固件胶囊在内存中的地址。这一步应该是判断capsule Firmware是否有固件更新。

然后发生 "warm reset",将控制权转回到固件上。

在bios里更改com口 bios设置com口打开_初始化_16

3.控制权移交到固件之后,固件会将内存中的Capsule内存块重新合并打包到capsule firmware中。

在bios里更改com口 bios设置com口打开_固件_17

4.UEFIOEM签名

在bios里更改com口 bios设置com口打开_固件_18

5.经过签名校验之后,将capsule固件写入spi闪存

在bios里更改com口 bios设置com口打开_寄存器_19

capsule固件更新攻击面

  1. 合并阶段
  2. 解析capsule封装的过程
  3. 解析capsule未签名封装块的过程

合并阶段

在合并capsule的阶段,对要写入内存的大小和capsule内存大小校验不当,就可能导致整数溢出,继而堆栈溢出。

在bios里更改com口 bios设置com口打开_在bios里更改com口_20

S3 Boot Script

UEFI启动脚本表是一种数据结构,用于在ACPI S3睡眠期间保存平台状态,当大多数平台组件关闭电源时。通常,该结构位于特殊的非易失性存储(NVS)内存区域中。UEFI代码在正常引导期间构建引导脚本表,并在S3恢复期间解释其条目,此时平台正在从睡眠中唤醒。攻击者可以在操作系统内核模式下修改当前引导脚本表内容并触发S3挂起-恢复周期,从而在安全功能未初始化或未锁定的早期平台初始化阶段实现任意代码执行。

在bios里更改com口 bios设置com口打开_初始化_21

hook boot script中的函数

可以通过读取ACPI全局变量中保存的boot脚本表格,解析其中的函数调用指令,并替换为跳转到自定义代码的指令。然后将原有指令备份,并将自定义代码插入到被hook的函数中。这样,在boot流程执行到相应的位置时,就会跳转到自定义代码执行,并完成攻击或劫持目标操作系统的控制权。

因此,要hook boot脚本表格中的函数,需要进行如下步骤:

  1. 读取ACPI全局变量,获取boot script表格地址。
  2. 解析boot脚本表格,找到需要hook的固件函数对应的地址。
  3. 对该函数地址处的指令进行patch,将其替换为跳转到自定义代码的指令,并将原有指令备份。
  4. 将自定义代码插入到被hook的函数中,并在其执行前后实现攻击或劫持的目的。

UEFI中,可以使用Virtual Address Map (VAM)协议来获取操作系统中的虚拟地址和UEFI中的物理地址之间的映射关系,并且可以通过Memory Allocation Services协议等API来向系统申请可读写的内存空间。基于这些API和协议,可以在UEFI环境中加载hook代码,并修改Boot Script Table中固件函数的入口指针,使其指向hook代码。也可以通过写入物理内存的方式实现hook

攻击PR0-PR4保护寄存器

SPI闪存保存着加载引导程序的镜像,PR4SPI的读写进行了保护,硬件排序闪存状态寄存器(HSFS)位于SPI主机接口寄存器区域开始的偏移量0x04处,它有FLOCKDN位,可以防止PR0-PR4寄存器的值被修改。

FLOCKDN位被设置时,在完全复位之前不可能改变PR0-PR4寄存器的值,所以,PRx闪存写保护的最明显的弱点是平台固件在启动过程中到底如何设置它们。

除了正常的启动路径,现代ACPI兼容的计算机固件还为S3恢复实现了一个单独的启动路径,当计算机从S3睡眠中醒来时使用,这是一个特殊的电源状态,当大多数平台组件关闭时(S4S5恢复是在正常启动路径中实现的,S1S2恢复在大多数Intel平台上没有使用)。UEFI固件有一个特殊的数据结构,叫做UEFI启动脚本表,它可以在S3休眠状态下存活,用于在正常启动路径中保存平台寄存器的值,并在S3恢复时恢复它们。启动脚本表存储在内存中,因此,如果攻击者能够从运行的操作系统中修改它并触发S3暂停-恢复,他将有可能覆盖负责平台安全的某些系统寄存器的值。一些供应商的固件在执行Boot Script Table之前没有锁定这些特定的寄存器。

为了保护平台免受此类攻击,UEFI规范引入了一种特殊的机制,称为SMM LockBox,它用于在系统管理RAMSMRAM)中存储启动脚本表--SMM可以实现访问,但是操作系统无法访问。

GURB

GURB(GBand Unified Bootloader)统一引导加载程序

  针对GURB的攻击,一方面是实现更高权限的任意代码执行,另一方面是绕过安全启动,破坏信任链,这块具体的一些漏洞模式还待整理,相关的漏洞案例可见后文CVE梳理部分。

CVE梳理

SMM相关漏洞

CVE-2021-26943

漏洞描述

华硕 UX360CA BIOS 303SMM本地权限提升漏洞

非物理接触,ring 0特权级向ring -2特权级攻击,实现本地权限提升。

漏洞成因

易受攻击的模块和相应的 SMIUsbRt (0x31)SdioSmm (0x40)NvmeSmm (0x42)。所有这些 SMI0x40ESMRAM中的地址。

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 为零
  • 在物理地址 0x104SMRAM的地址,比如0x88400000
  • 发出 SMI 0x40
  • 0x88400000+20x7
  • 通过以下步骤在 SMRAMSMST) 的地址:
  • HKLM\HARDWARE\RESOURCEMAP\System Resources\Loader Reserved .Raw值的内容中获取UEFI运行时代码的物理地址范围
  • 通过扫描地址范围中的“smmc”签名找到SMM_CORE_PRIVATE_DATA 的地址
  • SMM_CORE_PRIVATE_DATA在偏移量 30h处具有指向 SMST的指针
  • 使用上述写原语覆盖 SMSTd0hSmmLocateProtocol函数的指针。该值更新为0x07070707
  • 在物理内存 0x07070707写入 shellcode
  • 触发另一个调用Smst->SmmLocateProtocolSMI,比如SMI 0xdf0x07070707处的shellcodeSMM中执行

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寄存器的值为0x5380rbx寄存器的值为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编程中,GetVariableSetVariable函数用于读取和写入存储在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。如果发生其他错误,则返回适当的错误码。

 

在bios里更改com口 bios设置com口打开_固件_22

如果`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;
}

如果来自 CommBufferCommBuffer + 8处,则将调用函数FactoryConfigHandler(偏移量:0x1A60

如果来自CommBufferCommBuffer + 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会获取FactoryConfigNVRAM变量的大小,然后会申请对应大小的堆空间,将堆地址保存在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->EnableChecks0x00CommBuffer->Case0x09我们将触发以下代码:

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系统分区中的外部文件,因此可以由具有管理员特权的攻击者修改,而无需更改已签名供应商shimGRUB2 bootloader可执行文件的完整性。缓冲区溢出使攻击者可以在UEFI执行环境中获得任意代码执行权限,该代码可以用于运行恶意软件,更改启动过程,直接修补OS内核或执行恶意代码。

该漏洞的根因,是GURB2flex解释器设计模式不匹配造成的。

flex生成的解析器引擎将此定义包含为令牌处理代码的一部分:

在bios里更改com口 bios设置com口打开_寄存器_23

在这个宏中,生成的代码检测到它遇到的令牌太大而无法放入flex的内部解析缓冲区并调用YY_FATAL_ERROR(),这是使用flex生成的解析器的软件提供的帮助函数。

但是,YY_FATAL_ERROR()GRUB2软件包中提供的实现是:

在bios里更改com口 bios设置com口打开_固件_24

GRUB2中只是将错误输出到控制台,然后返回调用函数,并没有exit。这时候YY_flex_strncpy被调用,造成堆溢出。