Lenovo ThinkPad P51s固件SMM驱动逆向及漏洞分析 - 嘶吼 RoarTalk – 网络安全行业综合服务平台,4hou.com

Lenovo ThinkPad P51s固件SMM驱动逆向及漏洞分析

41yf1sh 资讯 2020-01-30 09:30:00
668519
收藏

导语:在本文中,将首先介绍SMM和UEFI的基础概念,随后对SMM驱动程序漏洞进行详细分析。

一、前言

从去年夏天开始,我尝试对自己的Lenove ThinkPad P51s计算机固件进行逆向分析。之所以对这个型号的固件感兴趣,其原因在于独立BIOS供应商(专门从事固件开发的公司)似乎是Phoenix Technologies而不是AMI,因为此前已经研究过大多数固件。尽管大多数固件都使用EDKII,但供应商的不同意味着其中的很多代码都将有所不同。

我们首先开始分析SMM驱动程序,很快就发现了一个漏洞——在一个SWSMI处理程序中调用SMRAM。该漏洞已经由Lenove在8月完成修复,目前我无法在网上找到关于该漏洞的任何信息。

在本文中,将首先介绍SMM和UEFI的基础概念,随后对这一漏洞进行详细分析,这里面的漏洞利用涉及到在此前另一篇文章中发表的技术——SMM中的Code Check(mate)

以下内容已经公开发表,幻灯片可以参考这里

二、SMM和UEFI

UEFI是描述开发固件(特别是BIOS)标准接口集的规范。该固件是启动期间在CPU上执行的第一批内容,负责初始化硬件并进行设置,从而让操作系统可以启动。该固件存储在计算机SPI Flash中。攻击者如果成功攻击这个硬件,就可以在硬盘以外的其他位置实现持久性。

系统管理模式(SMM)是一种Intel CPU模式,因为该模式与内存或系统管理程序相比具有更高的特权,因此通常被称为Ring 2。SMM拥有自己的存储空间,称为SMRAM,可以防止其他模式对其进行访问。SMM可以被看作是一个“安全的世界”,与ARM上的Trust Zone类似。但是,其最初的目标并非是提供安全功能,而是处理计算机的特定要求,例如高级电源管理(APM,已由ACPI替代)。如今,它还用于保护对包含UEFI代码的SPI Flash的写访问。

Inter手册中“处理器工作模式之间的转换”:

1.png

从上面的示意图中可以看出,可以从任何“普通模式”访问SMM。SMM还支持16位、32位和64位,这使其成为所有其他模式的一个备份。当触发系统管理中断(SMI)时,将会在普通模式和SMM之间进行转换。发生这种情况时,处理器会切换到SMM:它首先将CPU的当前状态保存到一个称为“已保存状态(Saved State)”的存储区(后续可以从该位置恢复到已保存的状态),然后更改包括指令指针在内的上下文,以执行SMRAM中的代码。

基本SMRAM映射,SMBASE可能不会随着SMRAM的开始而被使用:

2.png

SMRAM是由UEFI固件保留的物理RAM区域,提供给SMM使用。SMRR可以保护其不被“普通”访问,也可以保护其不备DMA访问。SMBASE是一个必须在特定范围内的地址,用于确定切换到SMM时已保存状态必须存储的位置,以及应将指令指针设置在哪个位置。为避免两个内核同时切换而造成已保存状态的相互重写,每个内核只有一个SMBASE,并且对SMRAM内部的位置没有限制。

目前,存在几种不同的SMI,但特别需要强调的是其中一种SoftWare SMI(SWSMI),往往会被攻击者利用。在ioport 0xb2上写入值后,代码通常会搜索与写入ioport的值相对应的SWSMI处理程序。这些处理程序通常是以64位编写的。

最后,由UEFI固件初始化在SMM中运行的代码(在SMRAM中设置)。特别是,通常在UEFI引导的驱动程序执行环境(DXE)阶段设置SWSMI处理程序。DXE阶段由数百个驱动程序组成,这些驱动程序用于从硬件初始化到网络栈实现的所有过程。

这些驱动程序提供了位于普通模式下的一组服务(特别是EFI_BOOT_SERVICES和EFI_RUNTIME_SERVICES),这些服务提供了一组基本功能,例如分配和访问非易失性(Non-Volatile)变量。

EFI_BOOT_SERVICES还允许注册和访问协议。协议允许驱动程序共享功能,并根据GUID进行标识。实际上,由于在UEFI引导过程中所有的内存访问都是在物理内存中进行的,因此协议只会将GUID与指针相关联。这些协议中,有一些是公开的,并且有文档记录(一些是记录在UEFI规范中,一些是在edk2中),而另外一些则针对每个构造函数。在DXE阶段结束时,固件将锁定SMRAM,阻止对其进行访问,然后尝试启动引导加载程序以过渡到操作系统。

三、漏洞分析

3.1 初始逆向工程

当我开始进行固件逆向时,我首先需要确定驱动程序是使用哪个协议来注册SWSMI处理程序的。经过分析,发现他们使用了edk2(MdePkg/Include/Protocol/SmmSwDispatch2.h)中定义并记录的经典EFI_SMM_SW_DISPATCH2_PROTOCOL。在确定该协议之后,我开始对使用该协议的所有驱动程序进行了简单的二进制搜索,然后开始进行逆向。

其中一个驱动程序名为SmmOEMInt15,它是一个非常小的驱动程序,仅包含21个函数,其中包括一个注册SWSMI的函数:

// [...]
res = gSmst->SmmLocateProtocol(&UnkProtocolGuid, 0i64, &unk_protocol); // (1)
// [...]
swsmi_number = 0xFFFFFFFF;
if ((*unk_protocol)(&swsmi_oemint15_guid, &swsmi_number) < 0) // (2)
    return EFI_UNSUPPORTED;
RegisterContext.SwSmiInputValue = swsmi_number; // (3)
if (EFI_ERROR(EfiSmmSwDispatch2ProtocolInterface->Register( // (4)
          EfiSmmSwDispatch2ProtocolInterface,
          swsmi_handler_unk_func,
          &RegisterContext,
          &DispatchHandle))
    return EFI_UNSUPPORTED;
return EFI_SUCCESS;

上面的代码片段执行了以下操作:

1. 使用GUID ff052503-1af9-4aeb-83c4-c2d4ceb10ca3(UnkProtocolGuid)检索未记录的协议(unk_protocol),这里利用了EFI_SMM_SYTEM_TABLE2(gSmst),其中包含一些SMM服务。

2. 使用新的未知GUID调用第一个函数eee19e05-079a-4d17-8f46-cf811260db26(&swsmi_oemint15_guid),并将其用于检索数字(swsmi_number)。

3. 然后,会在后续使用的上下文中设置在上一步检索到的swsmi_number,以便注册SWSMI处理程序,该值必须卸载IOPort 0xb2上。

4. 最后,EFI_SMM_SW_DISPATCH2_PROTOCOL用于将函数swsmi_handler_unk_func注册为SWSMI处理程序。

这段代码中存在的第一个问题在于,使用了未知协议来获取SWSMI编号。一些(但不是全部)注册SWSMI的驱动程序使用了该协议,因此在执行任何测试之前必须将其进行逆向。

3.2 SystemSwSmiAllocatorSmm

通过搜索未记录协议的GUID(ff052503-1af9-4aeb-83c4-c2d4ceb10ca3),可以轻松找到实现该协议的驱动程序——SystemSwSmiAllocatorSmm。该驱动程序也非常简单,其中函数更少。

该驱动程序的第一步是在正常环境下分配多个缓冲区,其中的一个非常值得关注,因为它已经在GUID 7E791691-5752-4392-B888-EFF9C74F5D77中注册为配置表。所有驱动程序都可以访问配置表,并将配置表关联到GUID,它们通常用于将数据从一个驱动程序传递到另一个驱动程序,而协议用于传递功能,实际上它们都将GUID与指针进行关联。

一旦完成了这些初始步骤和初始化过程,驱动程序就会注册我们想要进行分析的协议,我根据驱动程序的名称将其命名为SystemSwsmiAllocatorProtocol。该协议包含3个函数:get_swsmi_num_and_add2list、get_swsmi_num_from_guid、add_swsmi_to_list_no_check(这些名称同样也是我们进行命名的)。

基本上,该驱动程序允许将SWSMI编号与GUID相关联。可以请求驱动程序找到下一个可用的SWSMI编号(使用第一个函数)或提供编号(使用第三个函数)。第二个函数仅允许从GUID获取SWSMI编号。

这些关联存储在链表中,该链表由开始时注册的配置表引用。这会允许SMM外部的应用程序获得其希望使用的功能的正确SWSMI编号。这样做的目的是为了避免在其他驱动程序与注册SWSMI处理程序的其他组件之间产生SWSMI编号的冲突。

有了所有这些信息,就可以非常轻松地动态检索SWSMI编号。利用UEFI Shell中的chipsec,我们可以将SWSMI编号与GUID匹配起来:

1. 根据GUID 7E791691-5752-4392-B888-EFF9C74F5D77检索配置表ct_swsmi_allocator。

2. 在ct_swsmi_allocator + 0x38处的指针位于双链表的开头(这是一种保护措施,该元素后面没有实际数据)。可以在这个链表上进行迭代,直到再次到达头部为止。

3. 对于列表中的每个元素,都存在一些值得注意的数据:

(1)在elt-0x8是魔术值0x4E415353;

(2)SWSMI编号位于elt+0x10,使用qword形式;

(3)GUID位于elt+0x18。

一旦掌握了GUID和SWSMI编号之间的相关性,就可以触发SWSMI处理程序的代码,接下来我们进行尝试。

3.3 漏洞详细分析

SmmOEMInt15中SWSMI处理程序的第一个操作是从已保存状态中检索RSI寄存器的值。这个操作是通过使用EFI_MM_CPU_PROTOCOL(以前称为EFI_SMM_CPU_PROTOCOL)来实现的,该文件在手册中有所体现,并且属于edk2的一部分(MdePkg/Include/Protocol/MmCpu.h)。该协议将在已保存状态中搜索CPU保存的值,以获取寄存器并返回。对于SWSMI处理程序而言,这是一个非常有趣的开始,因为这个值是实际的用户输入。

更有趣的是,这个值还用作结构上的指针,并且该结构的前两个字节作为调用不同处理程序的开关的枚举。我开始迅速逆向这个处理程序,但实际上没有完成完整的逆向工作,因为在查看处理程序0x3E00时发现了一段非常值得关注的代码。

该处理程序要做的第一件事,是根据结构中的两个字段计算一个值,然后在调用内部函数之前将其设置为全局变量(受控):

base_ptr = 0x10 * rsi_val->local_used; // local_used off. 0x1C (2 bytes)
controlled = (base_ptr + rsi_val->for_global); // for_global off. 0x10 (2 bytes)
v14 = handler_internal_3E00(base_ptr);

handler_internal_3E00函数以两个非常有趣的基本块开始。

启动handler_internal_3E00函数:

3.png

它做的第一件事是检查*(controlled+2)处的值是否为0,如果为0,则会进行一些奇怪的处理后(实际上是在地址0x4处写入0x FFFEFFFE,我们在物理内存中没有任何保护,这不会造成崩溃)调用EFI_BOOT_SERVICES.LocateHandleBuffer函数。

如果从SMM调用这个函数,这里的问题在于,EFI_BOOT_SERVICES是位于普通世界中的服务表。攻击者可以简单地更改EFI_BOOT_SERVICES表中的地址,并获得任意调用。这种类型的漏洞通常称为SMRAM调用,它们基本上等效于从内核调用用户级的代码。

四、漏洞利用

在以前(大概是2017年到2018年期间),SMRAM的调用非常容易利用——在触发SWSMI之前更改代码(或函数指针)。但是,从当时起,SMM_CODE_CHK_EN缓解措施已经开始普遍使用,并且确实已经在我的Lenovo P51s上激活。

SMM_CODE_CHK_EN是SMM中类似于SMEP的功能:如果在SMM中执行来自SMRAM外部的代码(由SMRR定义),则计算机基本上都会崩溃。实际上,SMM_CODE_CHK_EN是在引导过程中由固件初始化的MSR。它可以被锁定,一旦被锁定,就不能被禁用。因为它是一种类似于SMEP的功能,因此一般的内核绕过将可以在这里利用,但是利用内核绕过存在一些缺点:

(1)固件与内核有所不同,内核绕过的一些技巧可能无法移植到这里使用;

(2)从通常情况看,SMM是一个比较大的黑箱,并且数据通信受到限制;

(3)尽管没有ASLR,但地址将取决于计算机和固件版本。

由于上述这些原因,常规的漏洞利用方法可能无法在一个易受攻击的固件上正常利用。

此时,如果我们尝试使用SMRAM调用来触发0x3E00处理程序的代码,则会发生以下情况:

4.png

1. 我们使用正确的编号、RSI和内存中的正确值触发SWSMI,以实现调用。

2. CPU会将当前状态保存到SMRAM中的某个位置。

3. 一些代码将被执行(包括切换到64位),并且会调用我们的SWSMI处理程序。

4. 0x3E00处理程序将搜索内存中的EFI_BOOT_SERVICES.LocateHandleBuffer函数指针。

5. 调用该函数。

6. 随后,将发生崩溃。因为SMM_CODE_CHK_EN已经被激活,因此在正常情况下对代码的调用将永远不会执行,因此未经修改的原始代码甚至无法工作。

现在我们已经知道,我们要尝试的目标是能够以稳定的方式在SMM中执行我们的代码,并希望可以轻松地在具有相同漏洞的两个不同固件之间移植。为此,我利用了我在之前另外一篇文章中详细介绍的技术——SMM中的Code Check(mate)。

我们的基本思路是利用已保存状态,该状态是在CPU切换到SMM时设置的。已保存状态始终位于SMBASE + 0xFC00的位置,并且包含许多通用寄存器,从而(在最理想情况下)让我们可以控制0x80字节的字符:

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;

由于所有地方都使用了物理地址,并且没有启用任何内存保护,因此已保存状态的内容将是可以执行的。而且,0x80字节足以让我们放置Shellcode,这会让我们获得完整的控制权。

在这里,我们绕过CodeChk的思路是:

5.png

1. 首先,我们用Shellcode中寄存器所在的地址,重写EFI_BOOT_SERVICES结构中LocateHandleBuffer的地址。

2. 然后,我们用存储在寄存器中的Shellcode触发SWSMI。我们仍然必须遵守调用处理程序中所必须的所有条件,但这会为我们的Shellcode留出足够的空间。

3. 然后,CPU将状态保存在SMRAM中,从而实现Shellcode的映射。

4. 我们的SWSMI处理程序将被调用,其自身将调用0x3E00处理程序。

5. 获取EFI_BOOT_SERVICES.LocateHandleBuffer的函数指针,检索位于已保存状态的地址。

6. 我们的Shellcode将被调用,并且由于已保存状态位于SMRAM内部,因此不会触发SMM_CODE_CHK_EN。

这个思路非常简单,它让我们可以在SMRAM内映射Shellcode,而不必依赖于固件内的代码。但遗憾的是,这里还存在一个小问题——我们不知道SMBASE用于计算已保存状态的基址。

在一段时间以来,如何获得SMBASE的值一直是利用SMM漏洞的经典问题。通常,有三种主要的检索技术:猜测、暴力破解、读取包含该值的MSR IA32_SMBASE。前两种技术很有可能导致计算机崩溃,因此我们不得不开始研究从SMM中读取IA32_SMBASE寄存器的可能性,这会导致存在鸡与蛋的问题。因此,我开始寻找一种更好的技术,以一种更加可靠的方式获得SMBASE,而无需控制硬件。

SMBASE在PiSmmCpuDxeSMM驱动程序中初始化,该驱动程序是开源的,并且在edk2中可用。在初始化SMBASE时,首先需要计算要保留的大小。因为每个CPU都需要一个SMBASE,因此不足以保留0x10000,但是为了优化RAM空间,驱动程序避免了每个CPU保留那么多的内存。在驱动程序中计算TileSize以确定SMBASE的偏移量,而实际上在驱动程序中进行动态计算,发现其总是偏移0x2000字节。现在,我们知道了SMBASE与其他之间的相对位置,并且知道将会保留0x10000 + TileSize * (number_of_cpu - 1)字节的内存。

为了预留内存空间,驱动程序在SmmAllocatePages函数上使用包装器,并且未指定将内存映射到的特定地址。默认情况下,SmmAllocatePages将首先尝试查找空闲列表,并且将使用最高的可用地址。在启动时,不会释放这么大的内存空间,这意味着我们可以放心地忽略空闲列表。关于SmmAllocatePages的最后一个有趣的地方,是在于它也用于映射SMM驱动程序,并且当完成SMBASE的分配时,我们知道分配的最后一个驱动程序是PiSmmCpuDxeSMM驱动程序。在这里,我们已经掌握了一些内存布局。

SMBASE周围的内存布局:

6.png

这时,我们仍然没有得到SMBASE,但是我们已经了解了其周围的一些布局,并且碰巧PiSmmCpuDxeSMM在普通世界中注册了一个协议:

Status = SystemTable->BootServices->InstallMultipleProtocolInterfaces (
    &gSmmCpuPrivate->SmmCpuHandle,
    &gEfiSmmConfigurationProtocolGuid, &gSmmCpuPrivate->SmmConfiguration,
    NULL
    );

gSmmCpuPrivate->SmmConfiguration位于PiSmmCpuDxeSMM驱动程序内部,由于已经在EFI_BOOT_SERVICES中注册,因此该指针及其关联的GUID(gEfiSmmConfigurationProtocolGuid)将存储在普通环境中。使用EFI_BOOT_SERVICES.LocateProtocol,我们可以检索此指针。看起来很奇怪,这实际上是“故意”制作的。普通世界的驱动程序在引导阶段会使用此协议,而当它们使用该协议时,SMRAM尚未锁定。但是,可以通过在SMRAM的同时卸载此协议来避免此类漏洞。由于该驱动程序是edk2的一部分,因此大多数固件都将其集成在一起,并且该技术基本上可以在不同的构造函数之间移植。

利用这个泄漏,我们可以计算PiSmmCpuDxeSMM的基址(基址=泄漏地址-偏移量),利用它来推导出SMBASE地址(基址-0x10000 - tilesize * (numcpu - 1)),然后从该计算中获取已保存的状态地址。我在利用该技术时遇到的一个问题时CPU(numcpu)的实际数量与真实情况不符,因此我花了一些时间来弄清楚这个错误。实际上,可以使用EfiPiMpServicesProtocol来获取用于计算的实际数字,该数字可以从普通世界中访问。

至此,我们就具备了漏洞利用所需的一切条件。

完整漏洞利用步骤:

7.png

首先,我们需要获取已保存状态的地址:

1. 使用EFI_BOOT_SERVICES.LocateProtocol函数来检索EfiSmmConfigurationProtocol。

2. 通过该协议,我们得到了PiSmmCpuDxeSMM驱动程序中的泄漏。

3. 它允许计算SMBASE,并计算出我们的Shellcode所在的已保存状态的地址。

然后,我们需要触发漏洞利用:

1. 首先,使用刚刚计算出的值重写EFI_BOOT_SERVICES.LocateHandleBuffer函数的地址。

2. 使用存储在寄存器中的Shellcode触发SWSMI。

3. CPU将我们的Shellcode映射到之前计算的地址。

4. 调用SmmOEMInt15的SWSMI,特别是0x3E00处理程序。

5. 尝试获取LocateHandleBuffer地址时,它将检索已将Shellcode映射到的地址。

6. 最后,调用Shellcode,在SMM中实现代码执行。

五、总结

该漏洞已经在2019年8月在Lenove P51s中进行了静默修复。修复的方式非常简单——删除命令0x3E00的处理程序。但是,就像我们在研究过程中所看到的那样,该处理程序的原始代码会导致计算机崩溃,之所以将其删除,可能是因为它不再起作用,或者不再使用该功能。这个漏洞充分说明了应该进行SMM_CODE_CHK_EN加固的重要性,并不断推动BIOS开发人员删除对SMRAM的调用,这种类型的漏洞正在不断消失。

最后,应该特别说明的是,这个漏洞还不足以在SPI Flash上获得持久性。Lenovo P51s固件上使用了Intel Boot Guard(IBG),这是一种最新的机制,允许在引导时执行代码签名和固件代码的完整性检查。如今,SMM漏洞只是攻击者进行漏洞利用的第一步,要获得持久性往往还需要另一个漏洞来绕过IBG。

本文翻译自:https://www.synacktiv.com/posts/exploit/through-the-smm-class-and-a-vulnerability-found-there.html如若转载,请注明原文地址
  • 分享至
取消

感谢您的支持,我会继续努力的!

扫码支持

打开微信扫一扫后点击右上角即可分享哟

发表评论

 
本站4hou.com,所使用的字体和图片文字等素材部分来源于原作者或互联网共享平台。如使用任何字体和图片文字有侵犯其版权所有方的,嘶吼将配合联系原作者核实,并做出删除处理。
©2024 北京嘶吼文化传媒有限公司 京ICP备16063439号-1 本站由 提供云计算服务