对联想 ThinkPad SMM 管理 UEFI 密码的逆向分析
2020-07-13 12:00:00 Author: www.4hou.com(查看原文) 阅读量:552 收藏

在之前的一篇文章中,我谈到了Lenovo ThinkPad的系统管理模式(SMM)代码中的漏洞。那时,我很好奇如何处理UEFI密码(尤其是用于保护BIOS接口的密码)。密码的处理特定于每个构造函数,这意味着此处说明的代码特定于Lenovo,更确切地说特定于某些ThinkPad(这对于三个不同的ThinkPad版本通常是通用的,因此,大多数内容可能保持不变)。

在此文章中,我先回顾一下之前的漏洞,然后解释我如何查看Lenovo密码。首先,我们将逆向启动方式以及固件中的各种密码,然后再更深入地研究其中两个:开机密码BIOS密码。在这些密码的管理中,尚未发现任何漏洞,但事不宜迟,让我们开始吧。

0x01 SMM和UEFI

UEFI是描述用于开发固件(尤其是BIOS)的标准接口集的规范。该固件是启动时在CPU上执行的第一批操作之一。负责初始化硬件并进行设置,以便操作系统可以启动。该固件存储在计算机中存在的SPI闪存中,攻击者破坏此固件的主要优点是,可以在硬盘以外的其他地方实现持久性。

系统管理模式(SMM)是Intel CPU模式。它通常被称为ring -2, 因为它比内核或系统管理程序更具特权。SMM拥有自己的称为SMRAM的存储空间,该存储空间受到保护,无法通过其他模式访问。SMM可以被视为与ARM上的Trust Zone相似的“安全世界”。但是,其最初目标不是提供安全函数,而是处理计算机的特定要求,例如高级电源管理(APM,已由ACPI代替)。如今,它还用于保护对包含UEFI代码的SPI Flash的写访问。

《英特尔手册》中的“处理器工作模式之间的转换”

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

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

SMRAM是由UEFI固件保留以供SMM使用的物理RAM区域。SMRR可以保护它免受“常规”访问,也可以保护它不受DMA访问等等。SMBASE是一个必须在此范围内的地址,将用于确定在切换到SMM时必须将保存状态存储在哪里以及将指令指针设置在哪个位置。每个内核只有一个SMBASE(为避免两个内核同时切换而彼此重写保存的状态),对SMRAM内部的位置没有限制。

存在几种SMI,但特别是一种软件SMI(SWSMI)对于攻击者来说很有趣。在ioport上写入0xb2值时将触发SWSMI 。进行切换后,代码通常将搜索与ioport上写入的值相对应的SWSMI处理程序,这些处理程序通常以64位编写。

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

这些驱动程序提供了位于正常模式下的一组服务(尤其是 EFI_BOOT_SERVICES和EFI_RUNTIME_SERVICES),这些服务提供了一组基本函数,例如分配和对非易失性变量的访问。

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

0x02 漏洞分析

逆向分析

当我开始逆向固件时,我首先确定驱动程序使用了哪个协议来注册SWSMI处理程序。在这种情况下,他们使用EFI_SMM_SW_DISPATCH2_PROTOCOLedk2(MdePkg/Include/Protocol/SmmSwDispatch2.h)中定义的文档并进行了记录。一旦确定了该协议,我便对使用该协议的所有驱动程序进行了简单的二进制搜索,然后开始逆向。

其中一个驱动程序被命名为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.使用()包含unk_protocolGUID ff052503-1af9-4aeb-83c4-c2d4ceb10ca3(UnkProtocolGuid) 检索未记录的协议(), 其中包含一些SMM服务。

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

3.在swsmi_number在前面的步骤中检索随后处于后来为了注册SWSMI处理程序使用的上下文的设置,这是0xb2必须的IOPORT被写入的值。

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

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

SystemSwSmiAllocatorSmm

通过搜索未公开协议(ff052503-1af9-4aeb-83c4-c2d4ceb10ca3)的GUID,很容易找到实现该协议的驱动程序: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 4,我可以匹配SWSMI号和GUID:

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

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

3.对于elt列表的每个元素,都有一些有趣的数据:

· 在elt-0x8是一种magic 0x4E415353。

· SWSMI号在elt+0x10qword上。

· GUID位于elt+0x18。

一旦检索了GUID和SWSMI编号之间的相关性,就可以触发SWSMI处理程序的代码。

漏洞分析

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

甚至更有趣的是,此值用作结构上的指针,并且此结构的前两个字节用作调用不同处理程序的开关的枚举。我开始快速逆向处理程序,但是由于查看处理程序时发现了一段非常有趣的代码,所以从未真正逆向完成0x3E00。

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

 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函数开始

要做的第一件事是检查at的值*(controlled+2)是否为0,如果是这种情况,它将经过一些奇怪的事情(这的确是0xFFFEFFFE在该地址的写操作,0x4因为我们在物理内存中没有任何保护)这不会造成崩溃,请调用该 EFI_BOOT_SERVICES.LocateHandleBuffer函数。

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

0x03 漏洞利用

大约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的函数,因此通常的内核绕过将起作用,但是使用它们有一些缺点:

· 固件不如内核标准:技巧可能无法移植,

· 从正常的世界来看,SMM是一个很大的黑盒,并且数据通信受到限制,

· 没有ASLR,但地址将取决于计算机和固件版本。

由于所有这些原因,利用漏洞可能无法在另一个易受攻击的固件上正常工作。

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

触发标注

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的想法

1.首先,我们改写的地址LocateHandleBuffer在 EFI_BOOT_SERVICES与在我们的寄存器位于shellcode的地址结构。

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漏洞的经典问题。通常,检索它的主要方法有三种:可以猜测,可以强行使用或读取IA32_SMBASE包含其值的MSR 。前两种技术极有可能使计算机崩溃,遗憾的是IA32_SMBASE 只能从SMM读取寄存器,从而造成鸡和蛋的问题。因此,我开始寻找一种更好的技术,该技术将允许SMBASE可靠地获得硬件控制。

将SMBASE在初始化PiSmmCpuDxeSMM驱动程序,该驱动程序是开源和edk2可用。初始化SMBASE第一件事情时,它计算要保留的大小。因为SMBASE每个CPU 需要一个内存,因此不足以保留0x10000,但是为了优化RAM空间,驱动程序避免了每个CPU保留那么多内存。TileSize在驱动程序中计算A 来确定应移动SMBASE多少,而实际上在驱动程序中进行动态计算时,总是将其偏移0x2000字节。现在我们知道了SMBASE相互比较的位置,并且 0x10000 + TileSize * (number_of_cpu - 1)将保留内存字节。

为了保留内存,驱动程序在SmmAllocatePages 函数上使用包装器,并且未指定将内存映射到的特定地址。默认情况下,SmmAllocatePages将首先尝试查找空闲列表,但没有结果将采用最高的可用地址。在启动时,没有理由释放这么大的内存,这意味着我们可以放心地忽略freelist。关于最后一个有趣的一点 SmmAllocatePages是,它也用于映射SMM驱动程序,并且当完成SMBASE分配时,我们知道最后分配的PiSmmCpuDxeSMM驱动程序是驱动程序。

内存布局如下:

SMBASE周围的内存布局

我们仍然没有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 (base = leak - off)的基地址,用它来推导SMBASE 地址(base - 0x10000 - tilesize * (numcpu - 1)),然后从该计算中获取已保存的状态地址。我在使用此技术时遇到的一个问题是cpu(numcpu)的实际数量与实际情况不符,因此我花了一些时间来弄清该错误。实际上,可以使用EfiPiMpServicesProtocol可从正常世界访问的来获得用于计算的实际数字。

至此,我们具备了漏洞利用所需的一切:

全面开发

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

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中执行代码。

0x04 逆向固件密码

联想ThinkPad固件设置了几种密码,最初我感兴趣的是一种保护BIOS设置界面的密码。固件中的一些驱动程序包含对字符串“ passwords”的引用。我开始看的是LenovoSetupSecurityDxe因为该驱动程序似乎集中了大部分代码,这些代码允许使用用户界面设置和删除密码。

逆向HII

在UEFI中,使用人机界面基础架构(HII)来与用户建立接口:一组允许打印并从用户那里获取value的接口。特别是EFI_HII_STRING_PROTOCOL 允许使用“ a” StringId(简单数字)从“数据库”中检索字符串。基本上,这意味着在逆向时不可能直接在字符串上使用外部参照。在数据库中找到的字符串很容易识别:它们是UTF-16字符串,后跟一个字节,值0x14。此值指示HII数据库的此元素是字符串。在所有这些字符串之前,都有一个header。header遵循的 EFI_HII_STRING_PACKAGE_HDR结构包含StringInfoOffset指示字符串开头的字段。

一旦找到header,就可以找到该数据库的初始化。特别是将调用gEfiHiiDatabaseProtocolInterface->NewPackageList允许使用句柄注册数据库字符串的函数。然后,其余代码将使用此句柄和StringId来检索字符串,通常是通过调用gEfiHiiStringProtocolInterface->GetString。

跟踪字符串使用情况使我们能够确定代码的哪一部分用于哪些活动,并且经过一些逆向操作之后,可以识别出一些有趣的全局变量。

联想密码协议

实际上,用于操作和检查密码的全局变量是协议。这些协议通过以下伪代码获取:

 result = gBootServices->LocateHandleBuffer(ByProtocol, &LenovoPwdGuid, 0, &NoHandles, &ControllerHandle); // (1)
 if (!EFI_ERROR(result))
 {
   for (i = 0; i < NoHandles; ++i)
   {
     if (!EFI_ERROR(gBootServices->OpenProtocol(ControllerHandle[i], &LenovoPwdGuid, &Interface, AgentHandle, 0, 1)) { // (2)
         if (CmpGuid(Interface, Guid1)) { // (3)
             global1 = Interface; // (4)
         }
         // [...] : Same with other Guid and global
     }
 
   }
 }
 // [...]

这段代码不搜索一个协议,而是搜索其中的几个协议,所有协议都具有相同的GUID LenovoPwdGuid(2846b2a8-77c8-4432-86ec-199f205d37ca)(1)。它正在检索每个接口的接口(2),并将接口的开头与硬编码的GUID(Guid1在这种情况下)进行比较(3)。根据GUID,使用相应的全局变量来存储该接口(4)。这样设置了四个不同的全局变量。

这意味着几个不同的协议都安装了相同的GUID,然后通过比较接口结构开始处存在的另一个GUID来区分这些协议。这些接口中的每一个都代表不同类型的密码,并且在该初始GUID之后,提供了用于操纵它们的函数。

密码类型

通过搜索用于比较结构的硬编码GUID,仅发现了一些二进制文件。四个驱动程序都有一个有趣的名字:LenovoPopManagerDxe, LenovoSvpManagerDxe,LenovoHdpManagerDxe和LenovoSmpManagerDxe。查看调试字符串,很容易猜出缩写的含义:

· POP:开机密码

· SVP:SuperVisor密码

· HDP:硬盘密码

· SMP:系统管理密码

还有趣的是,SVP和HDP都具有SMM驱动程序。

通过逆向一个驱动程序的用法和代码,很容易理解GUID之后的第一个函数的用法:

 struct LenovoPasswordManager {
     EFI_GUID    PwdTypeGuid;
     UINT64      unknown;
     EFI_STATUS  (*get_status)(void *this, UINT64 unused, UINT32 *flag_res);
     EFI_STATUS  (*set_pwd)(void *this, char *pwd);
     EFI_STATUS  (*check_pwd)(char *this, char *pwd);
     EFI_STATUS  (*reset_hash)(void *this);
     EFI_STATUS  (*verify_checksum)(void *this);
     void        *unk_func; // not reverse
     void        *unk_func2; // not reverse
 };

除get_status之外,此接口的所有函数都执行一些操作并更改全局共享位字段以指示结果状态:status。该get_status函数允许检索该位字段的值,从而确定用户是否提供了密码。

一旦理解了这一点,我就可以开始研究所有这些如何工作。SMP和SVP密码的工作方式几乎相同,稍后将对其 进行详细说明。HDP已记录在博客文章3中,我对此并不感兴趣。最后,还有开机密码。

0x05 开机密码

POP是用户可以设置的密码,每次计算机启动时都会询问。它由LenovoPopManagerDxe驱动程序处理,该驱动程序公开了前面描述的接口。

密码哈希和PCD

为了查看密码的存储方式,这两个函数set_pwd和 check_pwd是最好的选择。该函数set_pwd首先从0xC参数中给定的指针中获取字节,然后计算散列密码。通过使用驱动程序中73e47354-b0c5-4e00-a714-9d0d5a4fdbfd实现的另一个协议() 计算LenovoCryptService哈希值。该协议的第一个函数允许计算SHA256,并且是用于哈希密码的函数。哈希被加盐,盐通过平台配置数据库(PCD)获取。

PCD是在UEFI PEI和DXE阶段之间传输并在驱动程序之间共享的通用存储系统,PCD协议的实现在edk2中是开源的。PCD允许通过令牌ID定义共享内存缓冲区,该令牌ID在固件编译时自动确定。静态存储由驱动程序加载(一个用于PEI,一个用于DXE),并存在于固件文件系统(FFS)中。可以通过搜索GUID轻松找到该存储,PCD_DATA_BASE_SIGNATURE_GUID但通常与驱动程序位于同一“文件”中。该协议还提供了动态存储,可用于在驱动程序之间共享数据。

如果是盐,则使用动态存储。盐在最近的固件中具有0x20字节大小, 而较旧的固件具有较短的大小(0xA)。只需向PCD协议询问正确的令牌ID,就可以很容易地从UEFI Shell中检索盐。由于令牌ID是在编译时生成的,因此攻击者必须能够自动确定该令牌,或者简单地逆向该特定固件的驱动程序以找到该ID。

需要注意的有趣一点是,0x00在释放该驱动程序中用于存储密码和盐的所有缓冲区之前,都要在其中重置。检索密码的哈希值不像在启动后简单地转储内存那样简单。现在让我们看一下它的存储方式。

存储函数

密码通过允许在存储区中写入字节的函数存储,该函数的代码几乎可以自我说明:

 UINT8 __fastcall write_rtc_storage(UINT8 pos, UINT8 val)
 {
   UINT8 result;
 
   if ( pos >= 0x80u )
   {
     __outbyte(0x72u, pos + 0x80);
     result = val;
     __outbyte(0x73u, val);
   }
   else
   {
     __outbyte(0x70u, pos);
     result = val;
     __outbyte(0x71u, val);
   }
   return result;
 }

一个IOPort用于指示读取或写入的偏移量,另一个IOPort用于写入值。读取的工作方式相同,只不过将out值的write()替换为read(in)。四个IOPorts 0x70 到0x73是已知的,他们用于与实时时钟(RTC)设备进行交互。该设备的主要目标是允许访问时间,但它也提供了一些通常称为CMOS的存储空间。这些IOPorts已记录在PCH数据表中, 但是osdev Wiki上也有不错的资源。

关于RTC设备的一个有趣的事实是,必须始终打开它的电源,以免丢失存储中的数据。通常,计算机中装有一个小的电池(与主要电池不同),以确保该设备始终处于开机状态。这意味着具有物理访问权限的攻击者只需删除各种电源访问权限,就可以绕过此密码。联想确实意识到了这一点,甚至对此进行了 记录

快速查看开机密码后,我也有兴趣查看其他密码。

0x06 BIOS密码

我真正感兴趣的一个密码是保护BIOS配置的密码,实际上SMP和SVP密码的工作方式几乎相同。这两个驱动程序公开了前面介绍的密码界面,并使用相同的存储。

对于POP来说,了解密码存储方式的最简单方法就是查看set_pwd函数。它首先使用SHA256像POP一样对输入的哈希执行计算。有趣的是,此哈希使用与POP相同的盐,但真正有趣的部分是密码的存储方式。

模拟Eeprom

该存储使用带有GUID的协议进行, 82b244dc-8503-454b-a96a-d0d2e00bf86a该协议由驱动程序注册EmulatedEepromDxe。凭借其显式名称,我们可以推断出这可能是存储API,有趣的是,联想过去似乎在计算机中嵌入了eeprom。该协议注册了三个函数,但只有前两个用于密码管理,这意味着我们可能具有读取和写入函数。第一个函数同时用于测试和设置密码,而第二个函数仅用于设置密码的函数:这似乎很有力地表明第一个函数允许读取,而第二个函数则允许写作。现在,真正有趣的问题是EmulatedEepromDxe 驱动程序实际上在哪里存储该数据?

该协议的第一个函数具有以下原型:

 EFI_STATUS EmulEeprom_Read(void *this, UINT64 unk_enum, UINT64 index, UINT8 *pRes)

第一个参数(this)只是协议接口上的指针,最后一个参数(pRes)显然用于检索读取的值,另外两个参数清楚地指示要使用的存储空间。在index这个存储空间的偏移,但unk_enum还不清楚。与NOR或NAND闪存相反,Eeprom在擦除大小上可以具有良好的粒度。但是,由于用于处理小尺寸擦除的电路占用了空间,可用于更多存储,因此擦除通常是在“存储体”中重新分组的几个字节上进行的。实际上,这意味着编程接口实际上与NOR或NAND闪存非常相似。这是大多数eeprom被更便宜的NOR或NAND闪存取代的原因之一。在我们的情况下unk_enum实际上是在模拟的eeprom中的一个库号,在代码中将该库号翻译并添加到该index编号中,以计算读取或写入时的偏移量。

EmulEeprom_Read函数上提供的值进行一些检查,并调用另一个函数,perform_read与bank_num,该index和pRes。实际上是执行实际读取的函数。此函数调用在IOPort上读写的其他几个函数。这是逆向固件的要点,如果未记录IOPort,固件通常会很痛苦。使用了三个不同的IOPort,第一个是IOPort 0x1808,它仅在in循环读取()时使用,后跟pausex86指令。如果那还不是很明显,那就是计时器,尤其是PM计时器。在Linux上,简单的dmesg提示会给你很大的提示:ACPI: PM-Timer IO Port: 0x1808。但是,另外两个IOPort 0x1630和0x1634,并不那么容易理解。

IOPort逆向

这两个IOPort显然是用于读取和写入数据的IOPort,每个都用于读取(out)和写入(in)。IOPort 0x1634通常用常量写入,而不依赖于偏移量,读取时通常将结果检查为位字段。另一方面,IOPort 0x1630既用于写入先前计算的偏移量,又用于读取实际结果。在至少一项函数中,对此IOPort进行读取并将结果丢弃。这是与其他硬件设备连接的典型模式:一个IOPort是“配置” IOPort,用于检查另一设备的状态,指示执行的操作的类型,依此类推,在本例中为IOPort 0x1634。第二个IOPort(0x1630)是用于传输数据(无论是读取还是写入)的一种。在IOPort上进行读取可能会对设备产生副作用,因此在丢弃结果的同时执行了一次读取。这是与使用IOPort与外部设备进行讨论的经典模式,基本上与以SPI闪存进行讨论或与PCI设备进行接口的方式相同。

因此,在这一点上,我们知道这些密码的哈希值未存储在SPI闪存中,而是存储在计算机中的另一台设备上(再次),所以现在的问题是哪个?使用的两个IOPort是可变的(与固定端口相反,例如用于PCI的IOPort),这意味着这些端口号取决于系统的配置。搜索使用那些IOPort的设备通常很复杂,在那种情况下,我首先搜索了PCI设备声明的IOPorts(使用lspci),但没有找到可用信息。下一步是查看在CPU和Platorm Controller Hub(PCH)数据表中定义的变量IOPort。列举了一些时间之后,我终于在低引脚数(LPC)控制器中找到了这些IOPort的注册为LPC通用IO范围1(LGIR1)。

低引脚数总线用于与计算机内部的多个设备进行通信。特别是,它用于与称为嵌入式控制器(EC)的设备进行通信,该设备在PCH数据表中被简称为“微控制器1”。EC是一种微控制器,以负责为笔记本电脑供电而著称。那时,我还记得阅读 过Alex Matrosov和Alexandre Gazet 的演讲 Breaking Through Another Side,他们在演讲中谈到了EC及其安全影响。回顾他们的谈话,我注意到其中还引用了这两个IOPort,因此密码的哈希存储在EC中。

EC有自己的固件,因此查看它不是该项目的一部分。但是,我可以做的一件事就是尝试以与驱动程序相同的方式读取密码。我使用chipsec实现了与EC交互的小脚本 ,但是当尝试读取密码的哈希值时,只有空字节。由于我能够读取其他模拟“存储区”的内容,因此这似乎是一种保护机制:固件可能在引导阶段完成后锁定对哈希的访问。

最后一件事引起了我的兴趣:我之前提到,LenovoSvpManagerSmm存在用于SVP密码的SMM驱动程序。由于SMM与OS并行运行,因此我对查看SMM如何检索密码的哈希值很感兴趣。进行一些逆向之后,似乎该驱动程序使用了SMM替代EmulatedEepromDxe驱动程序:EmulatedEepromSmm。该驱动程序与的工作方式相同,EmulatedEepromDxe对相同的IOPort执行相同的操作。但是,LenovoSvpManagerSmm实际上是在初始化过程中检索哈希并将其存储在SMRAM中的缓冲区中。这似乎表明,如我之前的博文中所述,SMM漏洞 应允许检索这些哈希。

实际上,BIOS固件密码的哈希值存储在嵌入式控制器中,并且在引导结束后似乎已锁定。攻击者应该能够利用UEFI或SMM漏洞来获取攻击者,但这已经是一项更加复杂的任务。它们的安全性仍基于EC的安全性,但这将是另一次研究。

0x07 分析总结

总而言之,这里研究的Lenovo密码的处理非常好,拥有硬件访问计算机权限的攻击者应该能够绕过那些密码,但这并不像我最初预期的那样容易。

开机密码可以很容易地重置,这是最有问题的事情,但是仍然需要通过硬件访问(当然,或者是固件中的漏洞)。BIOS密码未存储在SPI闪存中,而是存储在EC闪存中,并且引导后似乎已锁定了读/写访问权限。这意味着计算机用户在不物理打开计算机的情况下应该无法轻松删除或更改BIOS密码。

还可以看到一个有趣的趋势:被认为是整个系统信任根的UEFI固件越来越多地被其他固件取代。Lenovo似乎将EC用于其安全性的某些部分,而且管理引擎(ME)和验证码模块(ACM)现在已成为UEFI固件的信任根。在实践中,这使攻击者的生活更加困难,但同时也提供了潜在的更广阔的攻击面,而改变信任根可能只是在改变问题。

本文翻译自:https://www.synacktiv.com/posts/reverse-engineering/a-journey-in-reversing-uefi-lenovo-passwords-management.html如若转载,请注明原文地址


文章来源: https://www.4hou.com/posts/rX72
如有侵权请联系:admin#unsafe.sh