brick工具使用方法及检测原理学习。
安装
项目地址:https://github.com/Sentinel-One/brick
git clone --recursive https://github.com/Sentinel-One/brick.git
注意引用的几个项目也要下载下来:
使用python 3.10及以上版本,安装依赖库:py -3 -m pip install -r requirements.txt
安装Bip框架,如果脚本没法自动下载develop.zip,可以手动下载放到根目录,注释掉install.py这几行:
使用IDA PRO 7.7,并修改IDA插件目录:
把guid.json文件复制到插件目录下:
https://github.com/binarly-io/efiXplorer/tree/master/guids
安装:
py -3 install.py
修改idahunt.py中ida根目录:
使用
cd brick
py -3 brick.py <uefi_rom> [-o outdir]
还得给IDA自带的python安装各种依赖包:
C:\programs\IDA_Pro_7.7\python38>python.exe -m pip install gorilla
Collecting gorilla
Using cached gorilla-0.4.0-py2.py3-none-any.whl (9.6 kB)
Installing collected packages: gorilla
Successfully installed gorilla-0.4.0
C:\programs\IDA_Pro_7.7\python38>python.exe -m pip install edk2-pytool-library
Collecting edk2-pytool-library
Using cached edk2_pytool_library-0.11.4-py3-none-any.whl (435 kB)
Installing collected packages: edk2-pytool-library
Successfully installed edk2-pytool-library-0.11.4
分析完成后报告保存在xxx.rom.html中:
Brick检测模块
未过滤的嵌套指针
如果handler的 CommBuffer
包含嵌套指针,则必须通过 SmmIsBufferOutsideSmmValid
等函数对它们进行清理,以确保用户可控制的参数不会与 SMRAM 重叠。 如果不这样做,可能会导致通常所说的混淆代理攻击,其中高特权的 SMI handler被低特权代码欺骗,从而覆盖或泄漏 SMRAM 内容。
Low SMRAM 损坏
一些SMI handler写入CommBuffer
的时候没有先验证大小。攻击者可以通过将CommBuffer
放在below SMRAM的内存中来进行利用,当handler尝试写入CommBuffer
时,它会损坏SMRAM的低位部分。
正常情况下,用于将参数传递给SMI handler的通信缓冲区(communication buffer
)不得与SMRAM重合。这个限制的原理很简单:如果不这样,那么任何时候SMI handler都会将一些数据写入CommBuffer
——比如为了向调用者返回一个状态码,它也会修改一些沿途的SMRAM,这是不可取的。
EDK2中,SmmIsBufferOutsideSmmValid()
负责检查给定缓冲区是否与SMRAM重合。每次调用SMI时都会在通信缓冲区上调用此函数,以强制执行此限制。
由于通信缓冲区的大小也在攻击者的控制之下,攻击者可以利用该漏洞突破限制并破坏SMRAM的低位。如下例:
上例的SMI handler执行以下4步操作:
- 检查参数
- 将
MSR_IDT_MCR5
寄存器的值赋给局部变量 - 从中计算出一个64位的值,然后将结果写回通信缓冲区
- 返回
在第三步中,一个8字节的值被写入CommBuffer
,但在第一步中却没有检查缓冲区至少为8字节长。由于省略了该检查,攻击者可以通过以下方式进行利用:
- 将
CommBuffer
放在尽可能靠近SMRAM底部的内存位置(如 SMRAM-1) - 将
CommBuffer
的大小设置为一个足够小的整数值,如1个字节 - 触发易受攻击的SMI
内存布局如下图:
就SmmEntryPoint
而言,CommBuffer
只有一个字节,而且没有和SMRAM重合。因此,SmmIsBufferOutsideSmmValid()
返回成功并将调用实际的SMI handler。而在第三步中,handler会盲目地将QWORD值写入CommBuffer
,这样就无意中覆盖了SMRAM的低 7个字节。
在EDK2中,TSEG的底部(SMRAM的实际标准位置)包含一个类型结构体SMM_S3_RESUME_STATE
,用于控制从S3睡眠状态恢复,如下所示,这个结构体包含过多成员和函数指针,攻击者可以通过破坏它们来执行利用。
任意SMRAM损坏
在一些通信缓冲区中包含嵌套指针的SMI handler中可以找到更强大的利用点。SMI handler需要正确地解析、清理通信缓冲区,为此SMI handler会在嵌套指针上调用SmmIsBufferOutsideSmmValid()
并在其中一个指针恰好与SMRAM重合时退出。以EDK2的SmmLockBox
驱动的调用为例:
为向OS报告在SMM中实施的某些操作,现代UEFI固件通常会创建并填充名为Windows SMM Mitigations Table
(WSMT
)的ACPI表。WSMT负责维护名为COMM_BUFFER_NESTED_PTR_PROTECTION
的标志,如该标志存在,则SMI handler在没有事先清理的情况下不可以使用嵌套指针。可以用chipsec
模块common.wsmt
来转储和解析该表:
然而,即使WSMT存在,且所有支持的缓解措施都处于活动状态,但仍然很容易发现完全忘记清理通信缓冲区的SMM驱动程序。利用这一点,攻击者可以使用指向SMRAM内存的嵌套指针来触发易受攻击的SMI。根据不同handler的性质,可以导致指定地址损坏或从该地址泄露敏感信息。如下例:
上面的SMI handler通过通信缓冲区获取一些参数。根据反汇编的伪代码,我们可以推断缓冲区的第一个字节被解释为OpCode
字段,该字段指示handler接下来该做什么(1)。从(2)可以看出,该字段的有效值为0、2、3。如果实际值与这些值不同,则执行默认子句(3)。在此子句中,Error code被写入通信缓冲区的第二个字段所指向的内存位置。由于该字段以及通信缓冲区的全部内容都在攻击者的控制之下,因此可以在触发SMI之前进行如下设置:
当handler执行时,OpCode
字段的值将强制它退回到默认子句,而地址字段将由攻击者根据想要破坏的SMRAM地址来任意选择。
TOCTOU 攻击
CommBuffer
本身位于 SMRAM 之外,因此 DMA 攻击可以在 SMI handler执行时更改其内容。 因此,来自 CommBuffer
的双重提取是危险的,因为它们不一定会产生相同的值。
CSEG-only aware handlers
一些 SMI handlers仅根据兼容性 SMRAM 段 (0xA0000-0xBFFFF) 的地址范围检查调用者提供的指针。 由于其他可能处于活动状态的 SMRAM 区域不受这些 SMI 处理程序的保护,因此 Brick 将它们标记为可能存在漏洞。
通过 NVRAM 变量泄漏 SMRAM 内容
常见的漏洞代码模式如下:
基本上,代码使用 GetVariable
服务来检索某些 NVRAM 变量的内容和大小。 然后它在内存中对其进行处理,最后通过调用SetVariable
服务将其写回。 上面这段代码的问题是对 SetVariable
的调用是使用硬编码的大小而不是 GetVariable
检索的实际大小进行的。 如果攻击者能够从操作系统设置此 NVRAM 变量,可以使其比预期的短得多。 因此,对 GetVariable
的调用将使大部分数据缓冲区未初始化。 这些未初始化的字节最终将在处理对 SetVariable
的调用时显示到 NVRAM,在那里它们可以被仅以操作系统级别权限运行的攻击者查询。
在UEFI中,更新NVRAM变量的操作分为下面4步:
- 分配栈缓冲区,用于保存与变量相关的数据
- 使用
GetVariable()
将变量内容写入栈缓冲区 - 对栈缓冲区执行所需的修改
- 使用
SetVariable()
将修改后的栈缓冲区写回NVRAM
调用GetVariable()
时,第四个参数用作输入输出参数,在入口函数时,它表示调用者需要读取的字节数,而在返回时,它表示实际从NVRAM读取的字节数。如果变量的实际大小与预期大小匹配,则两个值应相同。
开发人员默认变量的大小是不可变的,那么就会出现问题。他们完全忽略了GetVariable()
读取的字节数,并在更新NVRAM内容时只将硬编码的大小传递给SetVariable()
:
由于某些NVRAM变量的内容(EFI_VARIABLE_RUNTIME_ACCESS
属性的变量)可以从操作系统中修改,因此它们可以被滥用以触发SMM中的信息泄露,同时可以作为泄露通道。
首先,攻击者会使用OS提供的API来截断变量(如SetFirmwareEnvironmentVariable()
),使其变得与预期的短,然后继续触发易受攻击的SMI handler,SMI handler将执行下面的操作:
- 分配基于栈的缓冲区(stack-based buffer),和其他栈缓冲区一样,该缓冲区默认情况下未初始化,这意味着它保存了之前在SMM中调用函数的剩余部分
- 调用
GetVariable()
服务读取变量内容到栈缓冲区。正常情况,变量大小等于栈缓冲区大小,但由于攻击者截断了NVRAM中的变量,因此导致缓冲区更长。这意味着在GetVariable()
返回后它也会继续保存一些未初始化的字节。
- 修改内存中的栈缓冲区
- 调用
SetVariable()
服务,将修改后的栈缓冲区写回NVRAM。因为调用的时候使用的是硬编码缓冲区大小,所以它还会把未初始化的部分写入NVRAM。
现在攻击者可以使用API函数(如GetFirmwareEnvironmentVariable()
)来完全公开变量的内容,包括未初始化部分的字节。
近期评论