UEFI固件安全 · 2023年3月6日 0

安全启动绕过漏洞分析

CVE-2022-21894 安全启动绕过漏洞分析。

项目地址

https://github.com/Wack0/CVE-2022-21894

https://github.com/ASkyeye/CVE-2022-21894-Payload

漏洞原理

由于Windows启动程序允许通过truncatememory设置来移除内存map中包含 “持久 “序列化数据范围的内存块,从而导致了安全启动绕过。

  • BCD选项truncatememory将从内存映射中移除所有高于指定物理地址的内存。
  • 在初始化过程中,从内存中读取序列化的安全启动策略之前,会对每个启动应用程序执行这一操作。
  • 可以用来从内存映射中移除序列化的安全启动策略。
  • 这导致了能在启动程序中使用危险的设置(bootdebugtestigningnointegritychecks),从而破坏安全启动。

在没有安全启动的系统中,可以用bcdedit命令开启调试选项或进入测试模式,但受安全引导策略保护的系统无法使用该类命令:

image-20230309150945387

利用方法

要确保序列化的安全启动策略被分配到已知的物理地址上。

  • 默认情况下,它被分配在可能的最低地址。
  • 最初,在使用从BCD加载的任何配置之前,序列化的安全启动策略在它被加载后再被分配到物理地址。
    • 从RS1开始,序列化的安全启动策略在加载一个引导程序时被分配。
    • 从RS2开始,当序列化一个安全引导策略时,任何现有的序列化安全引导策略都将被释放。
  • 在加载启动程序时,如果BCD选项osdevice是一个BitLocker加密的分区,其中的VMK是使用TPM得出的,那么序列化的安全启动策略会被重新分配。
    • 可以通过在成功解封TPM后设置密钥标志位的第0位来伪造,这个位可以在BitLocker元数据中手动设置,并添加额外的元数据来指定被用于完整性验证的安全启动

avoidlowmemory选项用来确保所有物理内存都分配在指定的物理地址上。

  • 在Windows 10往后,如启用了VBS,那么这个选项是无法使用的,但由于它是在启动程序初始化过程中使用的,在序列化的安全启动策略从内存中读取之前,可以加载bootmgr并指定一个自定义的BCD路径(使用bcdfilepath选项,即custom:22000023)来绕过这个限制。
  • 如果操作系统卷上存在BitLocker,或者目标系统正在运行TH1或TH2,那么这个方法就会失败;因此也可以用Windows 8.x的bootmgr运行一次攻击,禁用VBS,然后再换回原始引导程序。
    • Windows 10改变了启动程序的初始化,将所有TPM的PCR都封存一次,所以Windows 8.x bootmgr在Windows 10+系统上解封VMK会失败。

hvloader.efi可以用nointegritychecks选项加载一个自签名的mcupdate.dll,其入口点会在ExitBootServices之前被调用。

另外,在非AMD64系统上,TH2之前的winload.efi可以通过使用testigning选项来允许在证书中使用szOID_NT5_CRYPTO EKU的自签名二进制文件。

payload分析

示例payload很简单,只是在死循环里等待一个中断:

#include <stdint.h>
#include <stdbool.h>

static inline __forceinline void WaitForInterrupt() {
   #if defined(_M_X64) || defined(_M_IX86)
   __halt();
   #elif defined(_M_ARM) || defined(_M_ARM64)
   __wfi();
   #else
   #error "Unsupported architecture"
   #endif
}

uint32_t PocMain(void** FunctionTableOut, void** FunctionTableIn) {
   // We don't want to return back to the boot application.
   while (1) WaitForInterrupt();
   return 0xC00000BBL; // STATUS_NOT_SUPPORTED
}

因为mcpudate.dll在启用了分页的虚拟地址上运行,所以不能直接调用EFI函数。

  • 要调用EFI函数,payload需要调用BlImgLoadPEImageExBlImgLoadPEImageFromSourceBuffer,设置标志位0,以1:1物理地址:虚拟地址映射的方式加载额外的payload
  • 或者调用BlImgAllocateImageBuffer,并设置相同的位,以1:1物理地址:虚拟地址的方式分配内存,然后加载额外的payload(或重映射自己)

这是映射第二阶段payload的示例代码:mcupdate

首先从HvLoader.efi中获取EFI系统表、image handle、BlpArchSwitchContextBlImgAllocateImageBuffer的起始地址的偏移。

HvLoader.efi是一个EFI应用程序,用于加载外部hypervisor loader。

HvLoader.efi可以加载一个指定的hypervisor loader二进制文件(DLL,EFI等),并通过HvLoader.efi 的ImageHandle来调用它的入口点。之后hypervisor loader就能访问HvLoader.efi的命令行选项,并使用这些选项作为配置参数。

HvLoader.efi的第一个命令行选项指向hypervisor loader二进制文件的路径。

目前HvLoader.efi驻留在EFI分区,即/boot/efi,并且只能访问该分区。所有路径参数都要相对于/boot/efi分区。

   hvLoaderAddr = functionTableIn[3];                //包含hvloader.efi中的地址
   printProc = (PVOID)((DWORD_PTR)hvLoaderAddr + 0xAE48);        //用于打印字符

   for (PBYTE searchAddr = printProc; searchAddr; searchAddr--)    //查找最近的MZ头(保证是hvloader.efi的头)
  {
       if (searchAddr[0] == 'M' && searchAddr[1] == 'Z' && searchAddr[2] == 0x90 && searchAddr[4] == 0x03)
      {
           hvLoaderBase = searchAddr;
           break;
      }
  }

   if (!hvLoaderBase)
       goto Done;

   hvDosHeader = (PIMAGE_DOS_HEADER)hvLoaderBase;
   hvNtHeaders = (PIMAGE_NT_HEADERS)((DWORD_PTR)hvLoaderBase + hvDosHeader->e_lfanew);

   if (hvNtHeaders->OptionalHeader.CheckSum != 0xEC35E) //再次检查是否为hvloader.efi
       goto Done;
   //获取到的EFI系统表、image handle、BlpArchSwitchContext和BlImgAllocateImageBuffer地址
   fpBlImgAllocateImageBuffer = (BL_IMG_ALLOCATE_IMAGE_BUFFER)((DWORD_PTR)hvLoaderBase + 0x3CC0C);
   fpBlpArchSwitchContext = (PVOID)((DWORD_PTR)hvLoaderBase + 0xC550);

   efiImageHandle = *(PVOID *)((DWORD_PTR)hvLoaderBase + 0x113670);
   efiSystemTable = *(PVOID *)((DWORD_PTR)hvLoaderBase + 0x1136C8);

然后1:1分配物理内存:虚拟内存buffer:  

 if (!NT_SUCCESS(fpBlImgAllocateImageBuffer(&imageBuffer, imageSize, BL_MEMORY_TYPE_APPLICATION, BL_MEMORY_ATTRIBUTE_RWX, 0, 0b00000001)))
       goto Done;

   if (!imageBuffer)
       goto Done;

复制下阶段payload头:

MEMCPY(imageBuffer, efiApp, efiNtHeaders->OptionalHeader.SizeOfHeaders);

映射下阶段payload节区:    

secHeader = (PIMAGE_SECTION_HEADER)((UINT64)&efiNtHeaders->OptionalHeader + efiNtHeaders->FileHeader.SizeOfOptionalHeader);

   for (WORD i = 0; i < efiNtHeaders->FileHeader.NumberOfSections; i++)
  {
       section = &secHeader[i];

       if (section->SizeOfRawData)
      {
           dest = (PVOID)(imageBuffer + section->VirtualAddress);
           src = (PVOID)(efiApp + section->PointerToRawData);

           MEMCPY(dest, src, section->SizeOfRawData);
      }
  }

调用入口点,这里的自定义入口点,将BlpArchSwitchContext的地址传递给下阶段payload,在它调用EFI服务之前,需要以FirmwareContext为参数调用BlpArchSwitchContext,并在返回前将其还原:    

if (efiNtHeaders->OptionalHeader.AddressOfEntryPoint)
  {
       entry = (BOOT_ENTRY)(imageBuffer + efiNtHeaders->OptionalHeader.AddressOfEntryPoint);
       entry(efiImageHandle, efiSystemTable, fpBlpArchSwitchContext);                          
  }

复现

原始bcd配置选项:

image-20230310093535079

poc_amd64_19041根目录的bcd配置选项:

image-20230310103349671

该bcd会加载有漏洞的/minram/bootmgr.efi,并把avoidlowmemory选项设置为0x10000000,将custom:22000023选项指向/minram/bcd

avoidlowmemory选项用来确保所有物理内存都分配在指定的物理地址上。

在Windows 10往后,如启用了VBS,那么这个选项是无法使用的,但由于它是在启动程序初始化过程中使用的,在序列化的安全启动策略从内存中读取之前,可以加载bootmgr并指定一个自定义的BCD路径(使用bcdfilepath选项,即custom:22000023)来绕过这个限制。

minram\bcd的配置选项:

image-20230310103446330

该bcd加载/maxram/hvloader.efi,并将truncatememory设置为0x10000000,nointegritycheckstestsigning设置为yes。

truncatememory将从内存映射中移除所有高于指定物理地址的内存。那么通过设置该选项,就可以删除avoidlowmemory

然后通过设置nointegritycheckstestsigning来破坏安全启动,之后就可以使用hvloader.efi来加载自签名的代码了。

hvloader.efi会识别运行平台,加载并执行ESP:\system32中的mcupdate*.dll

image-20230313141321996
image-20230310111954329

这种修改方式需要修改、添加的文件太多,在实战过程中使用成功率较低,且无法免杀。

总之随便看看就是了,没啥用。