首页
社区
课程
招聘
[翻译] Lenovo ThinkPad P51s固件SMM驱动逆向及漏洞分析
2020-3-9 11:21 8313

[翻译] Lenovo ThinkPad P51s固件SMM驱动逆向及漏洞分析

2020-3-9 11:21
8313

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

翻译: 看雪翻译小组 - Nxe

 

校对: 看雪翻译小组 - jasonk龙莲

 

原文链接:https://www.synacktiv.com/posts/exploit/through-the-smm-class-and-a-vulnerability-found-there.html


 

去年夏天, 我终于开始逆向电脑的固件, 这台电脑是我的Lenovo ThinkPad P51s, 已经有了一些年头了. 我对此感兴趣的原因之一是该固件的独立BIOS供应商(IBV, 专门开发固件的公司)看起来是Phoenix Technologies¹ 而不是大多数情况下我所见到的AMI固件. 尽管大多数固件都使用EDKII, 不同的IBV意味着固件的大量代码会不一样.

 

我先从SMM驱动着手分析, 很快发现了一个漏洞: 在一个SWSMI处理程序中对 SMRAM的外部调用(callout of SMRAM). Lenovo 在8月修补了这个漏洞, 但是我找不到任何关于这个漏洞的信息.

 

更新 (2020-01-20): 发布了这篇博客以后@yngweijw联系了我们, 他告诉我们说这个漏洞实际上是他报告给Lenovo的CVE-2019-6170, 并且有一篇公开信息. 祝贺他找到了这个漏洞! 所以你可以忽略我下面文章中关于静默修复的部分 :).

 

我们先简单介绍一下SMM和UEFI(如果你对它们熟悉可以放心跳过), 然后再解释该漏洞, 之后是该漏洞的利用, 这个利用方法在之前的另一篇博客里有公开: Code Check(mate) in SMM.

 

以下内容也已经公开发表, 幻灯片可以在这里下载到.

SMM和UEFI

UEFI是一个描述开发固件(特别是BIOS)接口的标准集规范. 该固件是启动时CPU首要运行目标之一. 它负责初始化并设置硬件, 以便让操作系统能够运行. 该固件存储在计算机中的SPI闪存中. 攻击者如果能攻击成功该固件, 主要的优点是能实现硬盘以外的攻击持久化.

 

系统管理模式(SMM)是一个Intel CPU模式. 通常也称为 ring-2 , 因为它的特权高于内核或者虚拟层. SMM拥有自己的内存区段, 叫作SMRAM, 可以阻止其他模式的访问. SMM可以被看作是一个"安全世界(secure world)" , 与ARM上的Trust Zone类似. 然而, 它最初的目标不是提供安全特性而是用来处理计算机特别的需求, 比如高级电源管理(APM, 已被ACPI代替). 现在它也被用来保护对含有UEFI代码的SPI闪存的写入访问.

 

mode_intel.png

 

Intel手册中"处理器工作模式之间的转换"

 

如上图所示, SMM可以从任一"普通"模式访问. SMM也支持16bits, 32bits和64bits, 这使得它成为所有其他模式的备份. 在普通模式和SMM之间的转换是由系统管理中断(SMI)触发时产生的. 当该中断触发时, 处理器切换到SMM: 它首先将现在CPU的状态保存到名为"保存状态(Saved State)"的内存区域(确保之后能够恢复), 然后切换包括指令指针在内的上下文, 使其执行SMRAM中的代码.

 

smram_map.png

 

基本的SMRAM内存映射, SMBASE可能与SMRAM的开头不对齐

 

SMRAM是一段UEFI固件保留给SMM模式使用的物理RAM区域. SMRR²可以保护该区域使得"普通"模式无法访问, 也可以使得DMA方式无法访问. SMBASE是一个必须在该区域范围内的地址, 它用来确定当切换到SMM时保存状态存储的位置, 以及指令指针应该设置在哪个位置. 每个核心都有一个SMBASE(为了防止两个核心同时切换模式时会覆盖写入彼此的保存状态), 并且在SMRAM内部没有对SMBASE地址位置的限制.

 

存在许多种SMI, 但对攻击者来说,其中有一种SoftWare SMI(SWSMI)很容易令人产生兴趣. 当在ioport0xb2写入值时会触发SWSMI. 在发生该切换后, 代码通常会寻找与写入该ioport值相对应的 SWSMI处理程序. 这些处理程序通常是64位编写的.

 

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

 

这些驱动提供了位于普通模式下的一组 服务(特别是EFI_BOOT_SERVICESEFI_RUNTIME_SERVICES), 这些服务提供了一系列基本功能, 例如分配和访问非易失性变量.

 

EFI_BOOT_SERVICES服务还允许注册和访问 协议. 协议允许驱动共享功能, 并以GUID进行区分. 实际上在UEFI启动阶段中, 因为所有内存访问都是在物理内存中发生的, 所以协议只会将GUID与指针相关联. 这些协议中, 有些是公开并有文档的(一部分在UEFI标准中, 一部分在edk2中), 而其余的协议是各个厂商所特有的. 在DXE阶段结束时, 固件会锁住SMRAM以防止对其进行访问, 之后会尝试启动引导加载程序以过渡到操作系统.

漏洞

初始逆向工程

 

当我开始逆向固件时, 我首先识别哪个协议³被驱动用来注册SWSMI处理程序. 在这个例子中, 它们使用了传统的EFI_SMM_SW_DISPATCH2_PROTOCOL协议, 该协议在edk2(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. 通过GUIDff052503-1af9-4aeb-83c4-c2d4ceb10ca3使用包含了一些用于SMM的服务的EFI_SMM_SYTEM_TABLE2 (gSmst)来获取 (UnkProtocolGuid)的未记录协议(unk_protocol).

  2. 调用第一个函数, 其具有新的未知GUIDeee19e05-079a-4d17-8f46-cf811260db26 (&swsmi_oemint15_guid), 并使用它来获取一个数(swsmi_number).

  3. 上一步中获取的swsmi_number会在上下文中设置以注册SWSMI处理程序, 该值必须写在IOPort0xb2上.

  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_guidadd_swsmi_to_list_no_check(显然这些是我起的名).

 

基本上来说, 该驱动允许将SWSMI数与GUID相关联. 我们可以请求该驱动找到下个可用的SWSMI值(使用第一个函数)或者提供SWSMI值(使用第三个函数). 第二个函数仅允许从GUID获取SWSMI值.

 

这些关联存储在普通世界(normal world)中的一个链表里, 该链表会被开始时注册的配置表所引用. 这会使得SMM外的程序获得其需要使用功能的正确SWSMI值. 这可能是为了防止不同驱动以及注册SWSMI处理程序的不同组件之间SWSMI值发生碰撞.

 

有了以上所有信息, 很容易就能动态获取SWSMI值. 我在UEFI shell中使用chipsec<sup>4</sup>匹配到了SWSMI值与GUID:

  1. 从GUID7E791691-5752-4392-B888-EFF9C74F5D77获取配置表ct_swsmi_allocator.

  2. ct_swsmi_allocator + 0x38处的指针位于双向链表的开头(这是一个守卫节点, 实际上该元素后面没有数据). 我们可以遍历这个链表, 直到头部再次被访问.

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

    • elt-0x8处是幻数0x4E415353.
    • SWSMI数以qword类型保存在elt+0x10.
    • GUID在elt+0x18.

一旦我们获取到了GUID和SWSMI数之间的关联性, 我们就可以触发SWSMI处理程序中的代码. 现在我们来试一下.

 

漏洞

 

SmmOEMInt15中的SWMSI处理程序的第一个动作是从保存状态中获取RSI寄存器的值. 这是通过使用EFI_MM_CPU_PROTOCOL(之前叫EFI_SMM_CPU_PROTOCOL)协议完成的, 该协议同样也有记录, 并且是edk2中的一部分(MdePkg/Include/Protocol/MmCpu.h). 该协议会搜索CPU在保存状态对应寄存器中所保存的值并返回该值. 这对SWSMI处理程序来说是个十分有趣的开始, 因为这个值实际上是用户输入数据.

 

更加有趣的是, 这个值之后会用作一个结构体的指针. 该结构体的头两个字节被用来作为一个枚举(enum)来切换调用不同的处理程序. 我开始快速逆向这些处理程序, 但实际上没有完成, 因为我很快地在处理程序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函数以两个十分有趣的基本块开始:

 

callout-oemint15.png

 

_handler_internal_3E00函数的开头_

 

它做的第一件事是检查*(controlled+2)处的值是否为0, 如果是0的话会在一些奇怪的命令(没错, 它实际上是在0x4的地址处写入0xFFFEFFFE, 因为我们实际上在没有任何保护的物理内存中, 这样做不会引起崩溃)之后调用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. 其可以被锁定, 且一旦被锁定后, 就不能关闭. 因为这是一个类SMRP机制, 通常的内核绕过手段可以使用, 但是会有一些缺点:

  • 固件不像内核那样标准化, 绕过的技巧可能无法移植复现,

  • 从普通世界来看, SMM是一个大黑箱, 数据和通信理论上受到限制,

  • 没有ASLR机制, 但是不同电脑和固件版本中地址可能会有不同.

由于以上原因, 某个漏洞利用程序可能无法在其他存在漏洞的固件上正常运行

 

此时如果我们想要利用SMRAM的外部调用触发0x3E00处理程序的代码, 会发生下面的事情:

 

call_out_smram.png

 

触发外部调用

  1. 我们以正确的数, RSI中正确的值以及内存中正确的值触发了SWSMI, 实现外部调用.

  2. CPU会将现在状态在SMRAM中某处保存.

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

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

  5. 然后调用该函数.

  6. 再之后...它就会崩溃. 因为SMM_CODE_CHK_EN被激活, 对普通世界中的代码调用永远不会执行, 所以原本的代码如果不经任何修改根本不能用.

现在我们知道了, 目标是能稳定地在SMM中执行我们的代码, 希望能在有相同漏洞的两个不同固件之间实现移植可用. 为了做到这点, 我使用了在之前另一篇博客Code Check(mate) in SMM.中提到的技术.

 

基本方法是利用切换到SMM时CPU设置的保存状态. 保存状态总是位于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, 从而允许我们获取完整控制.

 

现在会是以下流程:

 

call_out_smram_exploit.png

 

绕过CodeChk的方法

  1. 首先我们将EFI_BOOT_SERVICES结构中的LocateHandleBuffer地址重写为我们放置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漏洞时会遇到的经典问题. 通常有三种主流的方法来获取它: 你可以猜, 可以爆破, 或者可以读取包含该值的MSRIA32_SMBASE. 前两种方法有很大几率会让电脑崩溃, 而且不幸的是IA32_SMBASE寄存器只能从SMM内读取, 这就成了一个先有鸡还是先有蛋的问题. 因为这个原因, 我开始寻找一种更好的方法, 能让我们不用控制硬件可靠地获取SMBASE.

 

SMBASEPiSmmCpuDxeSMM驱动中初始化, 这个驱动是开源的, 并且在edk2中可用. 当初始化SMBASE时, 它做的第一件事是计算需要保留内存的大小. 因为每个CPU都需要一个SMBASE, 因此保留0x10000是不够的, 但是为了优化内存空间, 驱动会避免为每个CPU保留那么多的内存. 驱动会计算TileSize来决定SMBASE的偏移量应该是多少, 尽管计算是在驱动中动态完成的, 但实际上这个偏移量总是0x2000字节. 我们现在知道了SMBASE的相对位置, 以及0x10000 + TileSize * (number_of_cpu - 1)字节内存会被保留.

 

为了保留内存, 驱动会在SmmAllocatePages函数上使用包装器, 并且不会指定一块特殊地址来映射这段内存. 默认情况下, SmmAllocatePages会先尝试搜索可利用空间表(freelist), 如果没有结果的话会使用最高可用地址. 在启动的时候没有理由会释放这么大段内存, 意味着我们可以放心地忽略可利用空间表. 关于SmmAllocatePages的最后一个有趣的地方是, 它也用来映射SMM驱动, 并且当完成对SMBASE的分配时, 我们知道了最后分配的驱动是PiSmmCpuDxeSMM驱动. 此时我们知道了内存布局如下图所示:

 

mem_layout.png

 

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可以得到用于计算的正确数字.

 

此时我们有了用于利用漏洞的一切东西:

 

call_out_smram_exploit_with_leak.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中的代码执行漏洞.

Conclusion

结论

这个Lenovo P51s上的漏洞在2019年8月被悄悄地修复了. 补丁非常简单, 0x3E00命令的处理程序被删除了. 然而, 如我们之前所看到的, 该处理程序的原本代码会引起计算机崩溃, 之所以将它删除, 可能是因为它不再起作用, 或者是该功能不再被使用. 这个例子完美地展示了对SMM_CODE_CHK_EN加固的关注: 纵然还是很容易来绕过(我们仍使用了泄露), 但它迫使着BIOS开发者移除SMRAM的外部调用, 因此这种类型的漏洞还是在消失.

 

最后, 其实这个漏洞也没有什么价值, 因为在SPI闪存中不能够获得攻击持久化. Lenovo P51s固件利用了最近的另一个机制:Intel Boot Guard (IBG), 它能在启动阶段对固件代码检查代码签名和完整性检查. 现在的SMM漏洞只是第一步, 之后就需要我们绕过IBG以实现攻击持久化.

注释


  1. Phoenix Technologies是一家独立BIOS供应商, 它们的固件基于EDKII, 并且调用了 Phoenix SecureCore Technology (SCT). 进行简单的字符串搜索 Phoenix 或者 SecureCore 关键字能让我们辨别出它们的固件. 以我所知最常见的几家独立BIOS供应商是AMI, PhoenixInsyde.

  2. 系统管理范围寄存器(System Management Range Registers, SMRR)由两个型号专用寄存器(Model Specific Register, MSR)组成, 它用来定义物理内存受普通访问保护的范围. 这些MSR通常是确定SMRAM范围的最简单的方法, 它们应当在SMM初始化结束时被固件设置.

  3. 随着时间推移, 存在了许多种协议, 有些制造商(OEM或者IBV)也定义它们自己的协议. 这个Lenovo固件中, 实际上是由LenovoSecuritySmiDispatch驱动注册了一个9f5e8c5e-0373-4a08-8db5-1f913316c5e4未记录的协议, 该协议为其他驱动提供了通过一种特殊的SWSMI来注册处理程序的方式, 但与这篇博客无关.

  4. Chipsec是一个开源的工具, 用来转储UEFI固件, 并提供大量的工具用来分析和测试UEFI固件中的安全性. 我通常在UEFI shell中启动它, 从而避免与OS相关的一些问题. 另一个很好用的工具是UEFITool, 可以解析, 提取和替换固件中的内容. 这两个工具加上一个好的反编译器对入门审计UEFI固件差不多就足够了.


[CTF入门培训]顶尖高校博士及硕士团队亲授《30小时教你玩转CTF》,视频+靶场+题目!助力进入CTF世界

收藏
点赞1
打赏
分享
最新回复 (1)
雪    币: 74
活跃值: (30)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
东来 2020-4-15 18:35
2
0
谢谢分享
游客
登录 | 注册 方可回帖
返回