UEFI固件漏洞学习
工作需要,进行一波学习
前置知识
UEFI
UEFI是描述开发固件(特别是BIOS)标准接口集的规范。该固件是启动期间在CPU上执行的第一批内容,负责初始化硬件并进行设置,从而让操作系统可以启动。该固件存储在计算机SPI Flash中。攻击者如果成功攻击这个硬件,就可以在硬盘以外的其他位置实现持久性。
SMM
系统管理模式(System Management mode)(以下简称SMM)是Intel在80386SL之后引入x86体系结构的一种CPU的执行模式。系统管理模式只能通过系统管理中断(System Management Interrupt, SMI)进入,并只能通过执行RSM指令退出。SMM模式对操作系统透明,换句话说,操作系统根本不知道系统何时进入SMM模式,也无法感知SMM模式曾经执行过。为了实现SMM,Intel在其CPU上新增了一个引脚SMI# Pin,当这个引脚上为高电平的时候,CPU会进入该模式。在SMM模式下一切被都屏蔽,包括所有的中断。SMM模式下的执行的程序被称作SMM处理程序,所有的SMM处理程序只能在称作系统管理内存(System Management RAM,SMRAM)的空间内运行。可以通过设置SMBASE的寄存器来设置SMRAM的空间。SMM处理程序只能由系统固件(如BIOS或UEFI)实现。
SMM拥有自己的存储空间,称为SMRAM,可以防止其他模式对其进行访问。SMM可以被看作是一个“安全的世界”,与ARM上的Trust Zone类似。但是,其最初的目标并非是提供安全功能,而是处理计算机的特定要求,例如高级电源管理(APM,已由ACPI替代)。如今,它还用于保护对包含UEFI代码的SPI Flash的写访问。
- 要进入
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
SMI是进入SMM的唯一途径,SMI可以由处理器的SMI#管脚有效或者是APIC(Advanced Program Interface Controller)总线的SMI信息。 它是不可屏蔽的中断,独立于其它形式的中断。
SMI的中断优先级是3, 而debug breakpoint是4, NMI是5, 最高的是Reset, 它的中断优先级是1。
SMRAM
SMM的处理程序只能在SMRAM里面运行,所以了解SMRAM的结构非常重要。这当然是出于安全的考虑,毕竟SMM有最高的优先级,如果在哪儿都可以运行,那么其它的程序改动了内存里的一点东西,也会影响到SMM,如果这个改动是恶意的,那后果就不堪设想了。
SMRAM的大小不是无限大的,它最初只有64KB,其起始地址是SMBASE(这个值保存在一个专门的寄存器中),在SMBASE+0x8000H处开始存放的是SMI的中断处理程序。而在SMRAM的高地址处存放着处理器进入SMM时的状态信息,这些信息在处理器退出SMM时会被恢复到处理器中。
SMBASE开机时的默认值是0x30000H,一般BIOS会把它重定位到0xA0000H处。
漏洞类型
SMM
SMM Callout(系统管理模式调出漏洞)
每当SMM代码调用SMRAM边界外的函数时会出现这种漏洞。最常见的场景是SMI处理程序。它试图调用作为其操作一部分的UEFI启动服务或者运行时服务,拥有操作系统权限的攻击者可以再触发SMI前修改这些服务所在的物理页面,从而在这些被影响的服务被调用的时候劫持然后特权执行流程
碎碎念
感觉和内核的劫持tty的opertions里的函数差不多。通过一系列操作和高权限修改对应的函数的代码让其变成恶意代码,然后调用原函数就会执行我们布置的恶意代码。更重要已经是特权执行了,因为本身执行SMI处理程序时就是高权限的。SMI可以简单当成中断处理,虽然它是比正常的中断处理等级高的,而且其他中断在SMM代码执行时是屏蔽掉的。SMRAM边界可以回头看一眼SMRAM的结构,只有64kb的一个部分。
低址SMRAM损坏
正常情况下,用于向SMI处理器传递参数的通信缓冲区不能与SMRAM重叠。不然每次SMI处理程序将数据写到通信缓冲区的时候都会覆盖SMRAM的某些部分。
**SmmIsBufferOutsideSmmValid()**函数实现了对SMI通信缓冲区和SMRAM区域的检查,但是comm buffer的大小用户可控。
SmmIsBufferOutsideSmmValid只会检查comm buffer是否和SMRAM重叠,不会检查写入comm buffer的内容是否溢出comm buffer。举个例子就是假设comm buffer定义时长度10byte,但是输入的30byte。SmmIsBufferOutsideSmmValid函数会检查comm buffer到comm buffer+10byte的区域和SMRAM有无重叠,但是不会检查comm buffer到comm buffer+30byte的区域。所以SMI处理处理不当的话依然会造成低地址的SMRAM损坏。
缓解措施就是检查comm buffer输入的长度是否会大于等于其定义的长度。
任意SMRAM损坏
没使用SmmIsBufferOutsideSmmValid函数对多级指针指向的地址空间做检查,如果SMI处理程序中多级指针指向SMRAM内的地址空间,就可以造成任意SMRAM损坏。比较好理解的一个漏洞。
例子如下:
此处通过判断CommBuffer的地址处的字节来进行switch,如果当前字节为0,2,3以外的值就执行default里的代码。default的代码中将一个错误代码写入了CommBuffer+1地址为首地址的指针 所指向的值。有点绕。具体可以看下面的图。假设Comm Buffer的结构如下,第一个byte是4,第二个qword是Address,程序执行流程就是判断第一位是4然后进入default,再把错误代码写入Address指向的地址处。我们指定这个address是在SMRAM的范围里,就可以造成一个任意SMRAM的损坏。
TOCTOU attacks
有时即使在嵌套指针上调用了SmmIsBufferOutsideSmmValid也不足以保护SMI处理程序的安全,原因是SMM设计的时候没有考虑到并发性。没有锁,也把其他中断屏蔽了。因此受到一些固有的竞态条件漏洞的影响。最突出的是针对通信缓冲区的TOCTOU攻击
time-of-check-time-of-use——我还以为是什么不认识的单词原来是首字母嗯拼(碎碎念)
因为通信缓冲区本身在SMRAM之外,所以它的内容可以在SMI处理程序执行的时候发生改变,意味着两次取这个值返回的值不一定相同。
为了解决这个问题,多进程环境的SMM使用了SMI会合技术,一旦某个cpu进入SMM,一个专门的软件序言就会向系统中其他所有处理器发送一个处理器间中断(IPI),将使他们也进入SMM,并在那里等待SMI的完成。然后第一个进入SMM的处理器才安全调用处理函数来服务SMI。
攻击原理如下
comm buffer驻留在缓冲区以外,进入SMI处理程序后,comm buffer中的值可以被SMI处理程序修改,以及被外设通过DMA的方式进行修改。
所以说如果SmmIsBufferOutsideSmmValid检查完多级指针后,如果再对指针指向的值进行修改,然后多级指针再次被引用,那就可以绕过SmmIsBufferOutsideSmmValid的一次检查。
如前面所说,两次取这个值不一定相同,因为在执行SmmIsBufferOutsideSmmValid到调用CopyMem前是有一个代码执行的空窗的,在这期间内我们如果可以使用dma将对应的smm_field_18的内存进行修改,就可以做到让指针指向SMRAM的任意地址达到破坏的效果。
SetVariable信息泄露
UEFI有俩API实现对NVRAM的读和写,GetVariable和SetVariable。对这俩API使用不当会产生一些漏洞模式
更改NVRAM变量,需要先用GetVariable将nvram变量的值读入一个局部变量,然后对缓冲区的局部变量进行修改,然后用SetVariable将保存在局部变量的值写入nvram。
1 | Status = gRT->GetVariable ( |
1 | Status = gRT->SetVariable ( |
这俩API的第四个参数是读取和写入的nvram变量的长度,如果SetVariable的第四个参数大于修改后的局部变量的长度,会将nvram的值写入nvram变量造成信息泄露。
具体流程如下:
- 分配一个堆栈缓冲区来保存与变量相关的数据。
- 使用GetVariable()服务将变量的内容读入堆栈缓冲区。
- 对堆栈缓冲区执行所有必要的修改。
- 使用SetVariable()服务将修改后的堆栈缓冲区写回 NVRAM。
假设如下情况,SetVariable硬编码一个固定size
我们如果GetVariable时使用了短于这个硬编码的长度(我们可以通过操作系统来修改部分NVRAM变量如具有EFI_VARIABLE_RUNTIME_ACCESS属性的变量)。由于SetVariable的size长度硬编码,我们就可以泄露堆栈的未初始化的变量。
攻击流程如下:
首先调用操作系统提供的API函数来截断变量(如SetFirmwareEnvironmentVariable),然后触发SMI处理程序,处理程序将:
分配基于堆栈的缓冲区,默认未初始化,这意味着它保存了SMM中发生的之前函数调用的剩余内容
调用GetVariable函数将变量的内容读入缓冲区。因为攻击者截断了NVRAM的变量,所以缓冲区肯定比变量的长度长。意味着它在GetVariable返回的时候仍然会带有一些未初始化的byte
修改栈内的局部变量
调用SetVariable来把修改后的栈里的buffer写到NVRAM中。这样就可以带出一些未初始化的byte,写到了NVRAM的变量中。造成了一波信息泄露。
Double GetVariable
GetVariable第一次从nvram取值写入栈里的时候,datasize的长度会被改写成对应nvram变量的长度,第二次调用GetVariable函数时,如果对datasize未做初始化,就有可能造成溢出。
1 | bool CheckBatterySafety() |
也是可以造成一次信息泄露的漏洞
题目训练
发现有点多了还是丢隔壁吧
参考
https://zh.wikipedia.org/wiki/%E7%B3%BB%E7%BB%9F%E7%AE%A1%E7%90%86%E6%A8%A1%E5%BC%8F