自从我们发布UEFI系列文章的最后一篇以来,已经过去了整整一年了。在此期间,固件安全社区比以往任何时候都更加活跃,并发表了一些高质量的出版物。值得注意的样本包括发现新的UEFI植入物,如MoonBounce和ESPecter,以及最近由Binarly披露的不少于23个高危BIOS漏洞。
在过去的一年里,我们也在努力寻找和利用SMM漏洞。在花了几个月的时间后,我们注意到了SMM代码中一些重复出现的反模式,并对漏洞的潜在可利用性形成了相当好的直觉。最终,在披露了13个这样的漏洞后,我们成功地结束了2021年的工作,这些漏洞影响了行业中大多数知名的OEM厂商。此外,还有几个漏洞仍在走负责任的披露流程,应该很快就会公开。
在这篇文章中,我们将为读者分享与SMM漏洞相关的知识、工具和方法。我们希望,当大家读完这篇文章时,自己也能挖掘这种固件漏洞。请注意,本文假设读者已经熟悉SMM术语和内部结构,所以,如果您对这些内容还不够熟悉的话,我们强烈建议大家先阅读参考文献部分列出的资料。现在,让我们开始吧。
SMM漏洞的分类
虽然在理论上,SMM代码是与外界相互隔离的,但在现实中的某些情况下,非SMM代码可以触发甚至影响在SMM内部运行的代码。因为SMM的架构非常复杂,里面有很多“活动部件”,所以,因此攻击面非常大,其中涉及通信缓冲区、NVRAM变量、支持DMA的设备等传递的数据,等等。
在下一节中,我们将介绍一些较常见的SMM安全漏洞。对于每种漏洞类型,我们将提供简短的描述,相应的缓解措施以及检测策略。请注意,该漏洞清单并不详尽,只包含SMM环境中特有的漏洞。因此,其中并没有提及某些非常常见的安全漏洞,如堆栈溢出和重复释放(double-frees)等漏洞。
系统管理模式调出漏洞(SMM Callout)
最基本的SMM漏洞类型被称为“SMM调出”。每当SMM代码调用位于SMRAM边界之外的函数时(如SMRR所定义),就会出现这种漏洞。最常见的调出场景是SMI处理程序:它试图调用作为其操作的一部分的UEFI启动服务或运行时服务。拥有操作系统级权限的攻击者可以在触发SMI之前修改这些服务所在的物理页面,从而在受影响的服务被调用后劫持特权执行流程。
图1 SMM调出漏洞示意图
缓解措施
最明显的方法,就是防止写出这种有问题的代码;此外,我们也可以在硬件层面提供相应的缓解措施。从第四代酷睿微架构(Haswell)开始,英特尔CPU支持一个名为SMM_Code_Chk_En的安全功能。如果这个安全功能被打开,一旦进入SMM,CPU将被禁止执行位于SMRAM区域之外的任何代码。您可以将这个功能视为Supervisor Mode Execution Prevention(SMEP)的SMM等价物。
通过执行CHIPSEC的smm_code_chk模块,可以查询这种缓解措施的工作状态。
图2 使用chipsec查询针对SMM调出漏洞的硬件缓解措施
检测方法
针对SMM调出漏洞的静态检测方法其实是非常简单的。在分析给定的SMM二进制文件时,要仔细查找导致调用UEFI启动或运行时服务的执行流程的SMI处理程序。这样一来,寻找SMM调出漏洞的问题就被简化为搜索调用图中某些路径的问题。幸运的是,我们根本不需要手动完成这项工作,因为efiXplorer IDA插件已经实现了这种启发式方法。
正如我们在本系列的前几篇文章中提到的,efiXplorer能够提供一站式服务,它已经成为了用IDA分析UEFI二进制文件的事实上的标准方法。它提供了下列功能:
定位和重命名已知的UEFI GUID;
定位和重命名SMI处理程序;
定位和重命名UEFI启动/运行时服务;
efiXplorer的最新版本使用Hex-Rays反编译器来改进分析过程。其中一个特点是能够为传递给LocateProtocol()或其SMM对应的SmmLocateProtocol()等方法的接口指针分配正确的类型。
给Ghidra用户的一点提示:我们还想补充的是,Ghidra插件efiSeek负责上述列表中的所有变更。但是,它不包括efiXplorer所提供的协议窗口和漏洞检测功能等用户界面元素。
在完成对输入文件的分析后,efiXplorer将继续检查由SMI处理程序执行的所有调用,从而得到潜在调出的精选清单:
图3 efiXplorer发现的调出
图4 sub_7F8可以从SMI处理程序访问,但仍然调用位于SMRAM之外的启动服务
在大多数情况下,这个启发式方法工作得很好,但是我们遇到了几种边缘情况,这时可能会产生误报。其中,最常见的误报是由于使用EFI_SMM_RUNTIME_SERVICES_TABLE所造成的。这是一个UEFI配置表,它暴露了与标准EFI_RUNTIME_SERVICES_TABLE完全相同的功能,唯一显著的区别是,与它的“标准”对应物不同,它驻留在SMRAM中,因此适合被SMI处理程序所使用。许多SMM二进制文件在完成一些模板式的初始化任务后,经常将全局RuntimeServices指针重新映射到SMM特定的实现:
图5 将全局RuntimeService指针重新映射到与SMM兼容的实现上
通过重新映射的指针调用运行时服务,会产生一种乍看之下似乎是调出的情况,尽管仔细检查会发现并非如此。为了克服这个问题,分析人员应该始终在SMM二进制文件中搜索标识为EFI_SMM_RUNTIME_SERVICES_TABLE的GUID。如果找到这种GUID,则大部分涉及UEFI运行时服务的调出都有可能是误报。但这并不适用于涉及启动服务的调出。
图6 通过重新映射的RuntimeService指针调用GetVariable()引起的误报
另一个潜在的误报来源是各种“双模式”包装器函数,这意味着它们可以从SMM和非SMM上下文中进行调用。在内部,如果调用方在SMM中运行,这些函数会派发对SMM服务的调用,否则会派发对等效的启动/运行时服务的调用。我们在野外看到的最常见的样本是EDK2的FreePool(),如果要释放的缓冲区位于SMRAM中,它就调用gSmst->SmmFreePool(),否则就调用gBs->FreePool()。
图7 EDK2的FreePool()实用函数是一个常见的误报来源
正如这个例子所展示的,漏洞分析人员应该意识到,静态代码分析技术很难确定某些代码路径在实践中不会被执行,因此,很可能将其标记为调出。在编译后的二进制文件中识别这种函数的相关技巧和窍门,将在后文中加以介绍。
低址SMRAM损坏
类型说明
在正常情况下,用于向SMI处理器传递参数的通信缓冲区不得与SMRAM重叠。这个限制的理由很简单:如果不是这样,那么每次SMI处理程序将某些数据写入通信缓冲区的时候——例如,为了向调用方返回状态代码时,都会“顺带”修改SMRAM的某些部分,这是不可取的。
图8 不应该出现的情况
在EDK2中,负责检查给定缓冲区是否与SMRAM重叠的函数被称为SmmIsBufferOutsideSmmValid()。这个函数在每次SMI调用时都会在通信缓冲区上被调用,以便执行这一限制。
图9 EDK2禁止通信缓冲区与SMRAM重叠
唉,由于通信缓冲区的大小也在攻击者的控制之下,这种检查本身并不足以保证全面的保护,因此,一些额外的责任落在了固件开发者的肩上。我们很快就会看到,许多SMI处理程序在这里出问题了,并留下了一个漏洞,攻击者可以利用这个漏洞来绕过这个限制,进而破坏SMRAM的低址部分。为了了解为何会出现这种情况,让我们仔细考察一个具体的例子。
图10 一个存在安全漏洞的SMI处理程序
上面是现实生活中非常简单的SMI处理程序。我们可以将其操作分为4步:
检查参数;
将MSR_IDT_MCR5寄存器的值读入局部变量;
从中计算一个64位值,然后将结果写回通信缓冲区;
返回到调用方。
精明的读者可能知道这样一个事实,即在第3步中,一个8字节的值被写入通信缓冲区,但在第1步中,代码并没有检查缓冲区至少8字节长这一先决条件。由于缺乏这项检查,攻击者就可以通过以下方式发动攻击:
将通信缓冲器放到尽可能靠近SMRAM基址的位置(例如SMRAM-1);
将通信缓冲区的大小设置为足够小的整数值,例如1字节;
触发易受攻击的SMI。从原理上讲,内存布局如下所示:
图11 调用SMI时的内存布局
就SmmEntryPoint而言,通信缓冲区只有1个字节长,与SMRAM并不重叠。正因为如此,SmmIsBufferOutsideSmmValid()将成功执行,实际的SMI处理程序将被调用。在第3步中,处理程序将盲目地将一个QWORD值写入通信缓冲区,这样做会无意中也对SMRAM的低7个字节也执行了写入操作。
图12 损坏发生时的内存布局
根据EDK2,TSEG(SMRAM的事实上的标准位置)的底部包含一个SMM_S3_RESUME_STATE类型的结构体,其工作是控制从S3睡眠状态的恢复过程。正如下面所看到的,这个结构体包含了大量的成员和函数指针,它们的损坏会使攻击者受益。
图13 SMM_S3_RESUME_STATE对象的定义
缓解措施
为了缓解这类漏洞,SMI处理程序必须显式检查提供的通信缓冲区和bailout的大小,以防实际大小与预期大小不同。这可以通过下列方式实现:
取消对所提供的CommBufferSize参数的引用,然后将其与预期的大小进行比较。该方法之所以有效,是因为我们已经看到SmmEntryPoint调用了SmmIsBufferOutsideSmmValid(CommBuffer, *CommBufferSize),以保证缓冲区的*CommBufferSize字节位于SMRAM之外。
图14 可以通过检查CommBufferSize参数来缓解低址SMRAM损坏漏洞
再次调用通信缓冲区上的SmmIsBufferOutsideSmmValid(),这一次使用处理程序所期望的大小。
检测方法
为了检测这类漏洞,我们应该寻找那些没有正确检查通信缓冲区大小的SMI处理程序。这表明该处理程序没有执行以下任何一项:
取消对CommBufferSize参数的引用。
在通信缓冲区上调用SmmIsBufferOutsideSmmValid()。
条件1很容易检测,因为efiXplorer已经能够定位SMI处理程序并为它们分配正确的函数原型。条件2也很容易验证,但关键是:由于SmmIsBufferOutsideSmmValid()是静态链接的代码,所以,我们必须能够在编译后的二进制文件中识别它,相关的技巧和窍门将在后面加以介绍。
任意SMRAM损坏
类型说明
虽然在我们对SMM漏洞的分析中肯定是向前迈进了一大步,但之前的漏洞类别仍然存在几个重要的限制,使得它们在现实生活场景中难以利用。一个更好、更强大的利用原语将允许我们破坏SMRAM中的任意位置,而不仅仅是那些毗邻底部的位置。
这样的利用原语通常可以在其通信缓冲区包含嵌套指针的SMI处理程序中找到。由于通信缓冲区的内部布局事先并不知道,所以,SMI处理程序本身需要对其进行正确的解析和净化处理,这通常归结为对嵌套的指针调用SmmIsBufferOutsideSmmValid(),如果其中一个指针恰好与SMRAM重叠,就跳出。我们就可以在EDK2的SmmLockBox驱动中可以找到一个正确检查这些条件的教科书式的例子。
图15 SmmLockBoxSave的子处理程序,用于对嵌套指针进行净化处理
为了向操作系统报告SMM已经实现了哪些最佳实践,现代UEFI固件通常会创建并填充一个ACPI表,称为Windows SMM Mitigations Table,或简称WSMT。除其他事项外,WSMT还维护一个名为COMM_BUFFER_NESTED_PTR_PROTECTION的标志,如果存在该标志,则表明SMI处理程序不会在未经事先净化的情况下使用嵌套指针。这个表可以使用chipsec模块common.wsmt进行转储和解析。
图16 使用CHIPSEC来转储和解析WSMT表的内容
不幸的是,实践表明,大部分情况下报告的缓解措施和现实之间的相关性是很小的。即使存在WSMT,并且报告指出所有支持的缓解措施都是有效的,也经常发现SMM驱动程序完全忘了对通信缓冲区进行净化处理。利用这一点,攻击者可以用一个指向SMRAM内存的嵌套指针来触发有漏洞的SMI。根据特定处理程序的性质,这可能导致指定地址的损坏或从该地址读取的敏感信息。下面,让我们来看一个具体的例子。
图17 SMI处理程序没有对嵌套指针进行净化处理,使其容易受到内存损坏的攻击
在上面的片段中,有一个SMI处理程序,它通过通信缓冲区获得一些参数。根据反编译的伪代码,我们可以推断出,缓冲区的第一个字节被解释为一个OpCode字段,指示处理程序接下来应该做什么(1)。我们可以看出(2),这个字段的有效值是0、2或3。如果实际值与这些值不同,将执行默认子句(3)。在这个子句中,一个错误代码被写到通讯缓冲区第2个字段所指向的内存位置。由于这个字段和通信缓冲区的全部内容都在攻击者的控制之下,因此,他们可以在触发SMI之前对其进行如下设置。
图18 导致SMRAM损坏的通信缓冲区内容
随着处理程序的执行,OpCode字段的值将迫使它退回到缺省子句,而地址字段将由攻击者根据他或她想要破坏的SMRAM的确切部分提前选择。
缓解措施
若要缓解此类漏洞,SMI处理程序必须在使用通信缓冲区之前对其传递的任何指针值进行净化。指针验证可以通过以下两种方式之一执行:
调用SmmIsBufferOutsideSmmValid函数:正如前面提到的,SmmIsBufferOutsideSmmValid()是EDK2提供的一个实用函数,用于检查给定的缓冲区是否与SMRAM重叠。净化外部输入指针时,这是首先方法。
另外,一些基于AMI代码库的UEFI实现并没有使用SmmIsBufferOutsideSmmValid(),而是通过一个名为AMI_SMM_BUFFER_VALIDATION_PROTORT的专用协议提供了类似的功能。除了调用函数和使用UEFI协议的语义差异之外,这两种方法的工作方式大致相同。在下一节,我们将为读者介绍如何将该协议的定义正确导入IDA。
图19 AMI_SMM_BUFFER_VALIDATION_PROTOCOL是一种操作
检测这类漏洞的基本思路是寻找不调用SmmIsBufferOutsideSmmValid()或利用等价的AMI_SMM_BUFFER_VALIDATION_PROTOCOL的SMI处理程序。然而,一些边缘情况也必须被考虑到。如果不这样做,可能会引入不必要的假阳性或假阴性。
对通信缓冲区本身调用SmmIsBufferOutsideSmmValid()函数:这只能保证通信缓冲区不与SMRAM重叠(详见低址SMRAM损坏漏洞),但它对嵌套的指针没有效果。因此,当试图评估处理程序对rouge指针值的鲁棒性时,这些情况不应该被考虑在内。
完全不使用嵌套指针。一些SMI处理程序可能不会调用SmmIsBufferOutsideSmmValid(),仅仅是因为通信缓冲区没有保存任何嵌套指针,而是保存了其他数据类型,如整数、布尔标志等。为了区分这种良性的情况和易受攻击的情况,我们必须清楚了解通信缓冲区的内部布局。
虽然这可以作为逆向工程过程的一部分手动完成,但幸运的是,如今自动类型重构已经绝非科幻小说,相反,已经存在相应的工具,并且都可以作为现成的解决方案随时使用。对于这种类型的漏洞来说,两个最突出和最成功的IDA插件是HexRaysPyTools和HexRaysCodeXplorer。使用这些工具中的任何一个,都可以对原始指针访问表示法进行相应的转换,例如:
图20 使用原始CommBuffer的SMI处理器
变成更友好和更容易理解的点到成员表示法:
图21 使用重构CommBuffer的SMI处理器
更重要的是,这些插件可以跟踪各个字段的访问方式。基于访问模式,它们完全有能力重构包含结构体的布局,并推断出成员的数量、大小、类型、属性,等等。当应用于通信缓冲区时,这种方法可以帮助我们快速查找其中是否含有嵌套指针。
图22 由HexRaysCodeXplorer推断出的重建后的CommBuffer。注意,这个结构体的两个成员都是嵌套指针。
小结
在这篇文章中,我们为读者分享了与SMM漏洞相关的知识、工具和方法。由于篇幅较长,我们分为两篇进行发表,更多精彩内容,敬请期待!
(未完待续)
本文翻译自:https://www.sentinelone.com/labs/zen-and-the-art-of-smm-bug-hunting-finding-mitigating-and-detecting-uefi-vulnerabilities/如若转载,请注明原文地址