从关于UEFI的常识性知识转向倾销UEFI固件

2020-08-17 01:18:43

这是为研究人员提供的关于如何仿真、调试和模糊UEFI模块的一系列帖子中的第一篇,我们从如何转储SPI闪存的复习内容开始。

在撰写本文时,UEFI规范已经达到了2.8版。作为一个有近20年历史的标准(该规范的1.1版现已废弃,于2002年底发布),可以肯定地认为,大多数在信息安全行业工作的人在他们的职业生涯中至少听说过一次UEFI这个术语。一个稍微大胆,但仍然相对安全的赌注是,押注于这样一种断言,即大多数安全专业人士除了只知道UEFI的存在外,对UEFI是什么以及它试图实现的目标也有一些模糊的了解。在我们的一些同事的帮助下,我们进行了一次不太科学的快速调查,他们中的大多数人都知道UEFI与固件打交道,它本质上是IBM PC时代遗留BIOS的替代品,并且它是一个基础,在此基础上可以实现几个与安全相关的功能,如安全引导(Secure Boot)。

然而,当被问及他们中有多少人有实际的UEFI实践经验时,举起指向空中的手指数量急剧下降。这实际上有一个相当好的理由:固件安全,特别是UEFI安全,仍然被认为是小众话题,远远超出了主流流行的范围。近年来,这方面的情况变得越来越好,网站、书籍甚至培训班几乎完全致力于这一主题。但是,与其他更易访问的领域(如网络安全或操作系统安全)通常从社区获得的关注度相比,固件安全得到的关注度只是很小的一部分。因此,一般的安全研究人员从未窥视过他或她机器上的UEFI固件,也从未试图对其进行反向工程,更不用说对其进行模糊处理,这一点也就不足为奇了。

尽管可以理解,但目前的情况仍远未达到最佳状态。这样做的原因有两个。首先,UEFI无处不在。它无处不在,因为兼容UEFI的固件几乎随处可见,从低端Raspberry Pi SOC开始,遍及所有主流笔记本电脑和台式电脑,最后发展到超高端服务器。UEFI安全性重要的第二个原因与现代计算机体系结构的层级性质有关,在这种体系结构中,堆栈的每一层都与其下面的层一样安全。由于固件“位于”该堆栈的底部(通常位于硬件之上),固件级别的威胁有可能危及整个系统的安全,同时绕过许多传统的基于内核甚至基于虚拟机管理程序的缓解措施。

这篇博客是我们将试图阐明这个主题的一系列帖子中的第一篇,目的是帮助社区中更多的研究人员登上UEFI的“火车”。在这篇文章中,我们将主要集中于提供获取UEFI固件转储所需的理论背景和实践知识,这些固件通常位于SPI芯片上。未来的帖子将在这篇帖子的结尾处继续,并将讨论反向、调试和模糊各个UEFI驱动程序的方法。虽然旅途并不特别舒适,而且有时会颠簸不平,但我们相信,从“UEFI山”的山顶上看到的景色是非常值得这趟旅行的。

在发生任何反转或模糊之前,我们首先需要找出UEFI固件存储在哪里,以及如何获取它,即将它的离线版本转储到磁盘以供进一步分析。根据我们的判断,描述这个过程几乎不可能不参考ESET关于名为LoJax的恶意软件的这份出色的白皮书。简而言之,LoJax是一个Bootkit,在2018年之前和2018年期间,它成功地利用某个硬件错误配置作为感染受害者UEFI固件的手段。由于感染的低级别性质,LoJax具有相当独特的持久性:它可以经受住操作系统重新安装、硬盘更换以及IT人员通常用来清理受感染计算机的大多数其他技术。

为了能够执行感染,LoJax首先必须转储UEFI固件的内容,用其恶意有效负载对其进行修补,然后将其闪回。基于这一描述,很明显,我们可以简单地按照为我们描绘的LoJax路径来获得我们自己的固件。以下是白皮书第4部分的相关摘录,概述了这一过程:

“工具的…。任务是检索SPI闪存上的BIOS区域基址及其大小。此信息包含在SPI主机接口寄存器“BIOS闪存主要区域”中。所有SPI主机接口寄存器都在Roo中进行内存映射

不幸的是,对于不熟悉UEFI世界中流行的一些术语和缩略语的人来说,这段话没有多大意义。因此,为了让我们的消化系统变得更容易,我们应该在接下来的几个部分专门将其分解,解开术语,并确保这一过程得到很好的确立。我们将特别关注SPI闪存以及PCI标准的某些方面。

串行外设接口(简称SPI)是用于将设备连接到处理器的全双工同步串行接口。其中,这些设备可以包括存储器IC、传感器,甚至其他处理器。在我们的案例中,我们主要对焊接到主板并通过SPI连接到处理器的特定闪存芯片感兴趣。该芯片的典型存储容量为16MB,现代系统通常配备一对,总存储容量为32MB。我们对SPI芯片特别感兴趣,因为它通常存储UEFI固件映像以及其他一些重要的系统固件,如千兆位以太网固件或英特尔管理引擎固件。

虽然与SPI协议相关的硬件细节本身很有趣,但它们超出了本文的范围。为了便于我们的讨论,我们将仅限于SPI控制器公开的软件接口。SPI控制器本身就是一个PCI设备,因此需要一些PCI拓扑的初步知识。

外围组件互连,简称PCI,是一种规范,它试图在由不同供应商制造并在不同协议上运行的不同硬件外围设备的广大西部地区强加一些顺序。有点过于简单化了,我们可以说PCI使用两种不同的机制来实现这一目标:一种是专用地址空间,另一种是标准化的每个设备的配置数据。同样,与SPI芯片的情况一样,这里不会特别关注硬件细节。我们将只介绍与软件相关的细节的最小子集,这将有助于我们继续前进。

根据PCI规范,每个PCI兼容设备都有一个所谓的PCI地址。该地址由3个不同的字段组成:总线标识符、设备标识符和功能标识符。在技术文献中,常用的符号是将这些地址称为B.D.F三元组。还值得指出的是,在大多数系统上,所有PCI设备最终都连接到一条总线,因此出于简明考虑,有时会将其省略。

除了它自己的地址空间,PCI规范还规定每个PCI兼容设备应该公开256字节长的缓冲区,通常称为“配置空间”。配置空间可以为我们提供过多的关于设备的信息,例如其设备ID、其供应商ID以及该设备的MMIO范围的位置。

关于x86体系结构的一个众所周知的事实是,它支持的不是一种而是两种不同风格的I/O操作:

基于端口的I/O:具有单独的16位地址空间,并使用两个专用的机器指令从设备读取数据或向设备写入数据(分别为IN和OUT)。

内存映射I/O:保留一定范围的物理地址并将其映射到设备寄存器,而不是DRAM。由于CPU几乎总是使用虚拟地址而不是物理地址来引用内存,为了利用MMIO,操作系统内核必须公开一个API来生成到给定物理地址的有效虚拟映射。例如,在Windows中,这正是MmMapIoSpace API的目的。

PCI设备的I/O可视为基于端口的I/O和内存映射I/O之间的某种混合方法。该过程由3个主要步骤组成:

首先,总线、设备和功能标识符以及配置空间的偏移量被破坏成单个的32位值。这通常使用公式来完成:

0x80000000|总线<;<;16|设备<;<;11|函数<;<;n8|偏移量。

接下来,损坏的值被写入I/O端口0xCF8,通常称为PCI_CONFIG_ADDRESS。

最后,可以通过I/O端口0xCFC(也称为PCI_CONFIG_DATA)读取或写入与设备相关的数据。

在CHIPSEC内核模式驱动程序中可以找到PCI读取例程的简单C实现:

如果我们想要动手,我们可以使用优秀的RWEverything工具(也被LoJax bootkit滥用)来对PCI进行一些小规模的实验。安装并运行RWEverything之后,我们会看到以下不太友好的屏幕:

通过单击命令提示符图标,将打开一个小终端窗口。通过它,我们可以代表RWEverything内核模式驱动程序执行低级命令。

作为练习,让我们尝试从PCI设备0.31.3(我机器上的HD Audio Controller,根据需要调整数字以匹配您的系统)读取第一个DWORD。根据上述读取过程,必须写入端口0xCF8的值如下:

0x80000000|0<;<;16|31<;<;11|3<;<;8|0。

其计算结果为0x8000fb00。了解了这一点,我们可以使用以下命令指示RWEverything执行读取:

根据PCI配置空间的结构,我们可以得出结论,返回值0xA1708086实际上由两个16位字组成:供应商ID和设备ID。要将它们从任意位和字节转换为有意义的信息片段,我们可以使用PCI-IDs存储库,这是一个巨大的、由墙壁维护的各种PCI标识符的数据库。

作为手动使用IN和OUT命令的替代方法,我们可以简单地使用rpci32命令。使用它可以省去我们自己评估有效PCI地址的麻烦:

最后,我们可以完全抛弃命令行界面,直接从GUI查看PCI配置空间:

正如现在可能已经注意到的,RWEverything是固件安全研究人员工具箱中的一个非常强大的工具。与任何其他功能强大的工具一样,它有一个相当陡峭的学习曲线,并且通常提供多种方法来完成一项任务。强烈建议您在使用RWEverything时极其谨慎。不负责任的使用可能导致意想不到的行为,崩溃,甚至机器被砖封。

不用说,每次必须访问PCI设备时都要通过I/O端口0xCF8和0xCFC的过程很麻烦,容易出错(如果手动完成),而且从CPU的角度来看效率也不是很高。为了缓解这些缺点,PCI设备通常利用配置空间公开一组多达6个不同的基址寄存器(简称BAR)。这些条通常是指向可能发生MMIO的物理内存区域的指针。

综上所述,PCI使用I/O端口0xCF8和0xCFC来方便对配置空间的读取和写入。一种常见的安排是使用此机制读取一个或多个条,然后使用更为直观和快速的MMIO方法与设备通信并向其发出命令。

如前所述,SPI控制器(负责SPI闪存)本身就是一个PCI设备。有几种方法可以确定其PCI地址,但最终的真相来源无疑是英特尔平台控制器集线器数据手册。请务必选择与您的芯片组版本最匹配的版本,否则结果可能会变得不可预测!

例如,我们的测试机有一台300系列PCH,因此可以在此处找到匹配的数据表。通过仔细浏览4.2.1节-“PCI设备和功能”,我们可以在PCI地址0.31.5处看到这个特定的SPI控制器。

接下来,我们需要找出哪些条对于SPI控制器是活动的。为此,我们只需使用RWEverything查看设备的PCI配置空间:

从截图中我们可以清楚地看到,SPI控制器总共6个,只有一个活动条。因此,从现在开始,我们可以简单地将其称为SPIBAR,而不会有模棱两可的风险。接下来,我们需要找出哪些SPI寄存器映射到SPIBAR所指向的物理地址。再说一次,英特尔数据表在涉及以下具体细节时不会让人失望:

基于软件的转储SPI闪存的方法相当复杂,并且围绕着以明确定义的方式操作这些寄存器。本质上,3个寄存器在该过程中扮演主要角色:

闪存地址寄存器,通常缩写为FADDR。该寄存器仅保留SPI闪存起始的线性32位偏移量。

闪存数据寄存器,通常缩写为FDATAX。这实际上是一个寄存器数组,每个寄存器有4个字节长。一旦读取周期完成,这些寄存器将填充从闪存读取的原始字节。

硬件排序闪存控制寄存器,通常缩写为HSFC。该寄存器用于向SPI控制器发出命令,由多个字段组成。我们特别感兴趣的是:Flash Data Byte Count(闪存数据字节计数)字段,通常缩写为FDBC。我们使用此字段指定要读/写的字节数。由于该字段的长度仅为6位,因此在单个周期中可以处理的最大字节数被限制为64。

闪烁周期字段(FCYCLE)。此2位字段编码我们要执行的操作类型。此字段的有效值为0b00(读取)、0b10(写入)或0b11(块擦除)。

Flash Cycle Go字段,通常缩写为FGO。将此位设置为1指示SPI控制器对SPI闪存执行操作,具体由FDBC和FCYCLE字段确定。

使用这些寄存器转储SPI闪存的确切过程在优秀的RootKits和Bootkit一书中有详细说明,但本质上由以下步骤组成:

LoJax白皮书中以图形方式描述了相同的一组操作,为清楚起见,此处引用了这些操作:

作为实验,我们可以执行上述过程来尝试转储SPI闪存的标头。同样,我们将使用RWEverything命令行界面来实现这一点。你们中那些更喜欢Python方法的人可能想仔细看看@depletionmode的占卜项目,它应该提供大致相等的功能。

我们已经知道我们测试系统的SPIBAR位于物理地址0xFE010000。根据图7的偏移量,我们可以推断出其他相关SPI寄存器的绝对地址:

了解这一点后,我们可以使用以下命令指示SPI控制器读取闪存的前64个字节:

发出读取命令后,我们可以看到FDATA0-FDATA3寄存器简单地用1填充,而FDATA4保存魔术值0x0FF0A55A。在谷歌上快速搜索一下,就会发现这个值是用来指示SPI闪存在通常所说的“描述符模式”下运行的签名(稍后会有更多信息)。这是读取操作确实成功完成的非常令人信服的证据。

虽然我们刚才概述的手动方法工作得非常好,但(不幸的是)它的伸缩性不是很好,并且非常需要更加自动化和健壮的方法。对我们来说幸运的是,事实证明,Chipsec拥有SPI转储能力,最早可以追溯到2014年。对于那些不熟悉chipsec的人,我们强烈建议您查看他们的存储库,并在可能的情况下为该项目做出贡献。简而言之,芯片安全可以用“平台安全评估框架”来形容,这意味着它有能力对一个活的系统运行一个严格的测试套件,寻找常见的固件漏洞和错误配置。

除了它的主要测试套件之外,chipsec在从终端用户那里抽象出现代固件的许多复杂性方面也做得非常好。例如,整个SPI转储过程被整齐地包装为一个简单的Python命令:

非那样做不行!。当然,这个命令不涉及魔法。在引擎盖下,Chipsec将通过上述所有步骤来执行其操作。好奇的读者可能会发现,当转储程序的实现出现在READ_SPI_TO_FILE函数中时,查看它是有益的。

提示:如果您觉得足够勇敢,您可以使用RWEverything驱动程序与芯片安全一起执行固件获取。这省去了您引导进入测试签名模式和自己构建chipsec内核模式驱动程序的麻烦。缺点是,芯片安全认为RWEverything支持是实验性的,所以在执行时遇到BSOD的可能性很小。另外,请记住,虽然RWEverything是一个有价值的实用程序,但它极大地增加了攻击面,因为任何用户模式程序都可以利用它的低级访问原语来执行高特权操作,所以要小心使用它!

然后,您可以为大多数与chipsec相关的命令传递-helper rwehelper标志。结果,固件获取命令简单地变为:

一旦我们将固件映像整齐地打包为文件,下一步将是验证其完整性并解压缩。也许最好的方法是使用开放源码查看器UEFITool,它也建立了很多关于UEFI固件结构的直觉。

提示:在选择要下载的UEFITool资产时,请优先使用标有“NE”的版本。这些构建来自NEW_ENGINE分支,其中包含一个庞大的UEFI GUID数据库,用于将模糊的字节BLOB(如FC510EE7-FFDC-11D4-BD41-0080C73C8881)转换为更友好、更有意义的名称(如AprioriDxe)。

除了仅查看转储的SPI闪存图像外,UEFITool还支持提取单个文件以供进一步分析。在大多数情况下,我们会对可以反汇编或调试的实际可执行映像感兴趣。要转储它,请展开所需的固件文件,直到出现名为“PE32 IMAGE SECTION”的部分。然后,您只需右键单击它并选择“Extract”即可将其解压缩。

.