2021-05-06-UEFI基本理解

UEFI原理

UEFI启动可以分为几个阶段

image-20210316161429273

总体而言可以认为是两大部分,boot servie(BS)和runtime service(RT)

bs负责启动整个系统并将权限交给操作系统;

rt则是全程都在运行,并且具有与操作系统交互的接口;

PEI阶段

PEI阶段主要做三件事

MdeModulePkg/Core/Pei/PeiMain.inf

打开这个inf首先就可以看到一段注释

# PeiMain module is core module in PEI phase.
#
# It takes responsibilities of:
# 1) Initialize memory, PPI, image services etc, to establish PEIM runtime environment.
# 2) Dispatch PEIM from discovered FV.
# 3) Handoff control to DxeIpl to load DXE core and enter DXE phase.

在SEC阶段向PEI阶段转换的过程中,调用的是PeiCore这个函数,其中有三个参数

  @param SecCoreDataPtr  Points to a data structure containing information about the PEI core's operating
                         environment, such as the size and location of temporary RAM, the stack location and
                         the BFV location.
                         
  @param PpiList         Points to a list of one or more PPI descriptors to be installed initially by the PEI     												 core.
                         An empty PPI list consists of a single descriptor with the end-tag
                         EFI_PEI_PPI_DESCRIPTOR_TERMINATE_LIST. As part of its initialization
                         phase, the PEI Foundation will add these SEC-hosted PPIs to its PPI database such
                         that both the PEI Foundation and any modules can leverage the associated service
                         calls and/or code in these early PPIs
                         
  @param Data            Pointer to old core data that is used to initialize the
                         core's data areas.
                         If NULL, it is first PeiCore entering.

PpiList是一个PPI描述符的链表,之后PEI core会将其安装初始化

整个PeiCore函数主要可以分成三部分,对应上面的几个功能;

第一部分由三次if(OldCoreData==NULL){}else{}这样的判断组成

这个PeiCore函数会被调用两次,SEC阶段转交权限给PEI之后会执行一次,这一次中做以下事情(三个判断中if块的内容):

  1. 初始化内存(第一个if块):申请区域并置零,设置Signature,复制global PEI service表到申请的ServiceTable位置;

    if (OldCoreData == NULL) {
        //
        // If OldCoreData is NULL, means current is the first entry into the PEI Core before memory is available.
        //
        ZeroMem (&PrivateData, sizeof (PEI_CORE_INSTANCE));
        PrivateData.Signature = PEI_CORE_HANDLE_SIGNATURE;
        CopyMem (&PrivateData.ServiceTableShadow, &gPs, sizeof (gPs));
      }
    
  2. 设置Pei Service以及Memory Service(第一个if和第二个if之间);

    //
      // Cache a pointer to the PEI Services Table that is either in temporary memory or permanent memory
      //
      PrivateData.Ps = &PrivateData.ServiceTableShadow;
       
      //
      // Save PeiServicePointer so that it can be retrieved anywhere.
      //
      SetPeiServicesTablePointer ((CONST EFI_PEI_SERVICES **)&PrivateData.Ps);
       
      //
      // Initialize libraries that the PEI Core is linked against
      //
      ProcessLibraryConstructorList (NULL, (CONST EFI_PEI_SERVICES **)&PrivateData.Ps);
       
      //
      // Initialize PEI Core Services
      //
      InitializeMemoryServices   (&PrivateData, SecCoreData, OldCoreData);
    
  3. 输出Log,标识SEC已经结束,PEI开始(第二个if块);

      if (OldCoreData == NULL) {
        PERF_EVENT ("SEC"); // Means the end of SEC phase.
       
        //
        // If first pass, start performance measurement.
        //
        PERF_CROSSMODULE_BEGIN ("PEI");
        PERF_INMODULE_BEGIN ("PreMem");
       
      }
    
  4. 初始化其他的PEI Core Service(第二个和第三个if之间)

      //
      // Complete PEI Core Service initialization
      //
      InitializeSecurityServices (&PrivateData.Ps, OldCoreData);
      InitializeDispatcherData   (&PrivateData,    OldCoreData, SecCoreData);
      InitializeImageServices    (&PrivateData,    OldCoreData);
    
  5. 处理SEC阶段传过来的PPI列表(第三个if块)

      //
      // Perform PEI Core Phase specific actions
      //
      if (OldCoreData == NULL) {
        //
        // Report Status Code EFI_SW_PC_INIT
        //
        REPORT_STATUS_CODE (
          EFI_PROGRESS_CODE,
          (EFI_SOFTWARE_PEI_CORE | EFI_SW_PC_INIT)
          );
       
        //
        // If SEC provided the PpiList, process it.
        //
        if (PpiList != NULL) {
          ProcessPpiListFromSec ((CONST EFI_PEI_SERVICES **) &PrivateData.Ps, PpiList);
        }
      }
    
  6. 调用PEIM dispatcher

      //
      // Call PEIM dispatcher
      //
      PeiDispatcher (SecCoreData, &PrivateData);
       
      if (PrivateData.HobList.HandoffInformationTable->BootMode != BOOT_ON_S3_RESUME) {
        //
        // Check if InstallPeiMemory service was called on non-S3 resume boot path.
        //
        ASSERT(PrivateData.PeiMemoryInstalled == TRUE);
      }
       
      //
      // Measure PEI Core execution time.
      //
      PERF_INMODULE_END ("PostMem");
       
    
  7. 将权限交给DXE阶段的DXE IPL PPI

      //
      // Lookup DXE IPL PPI
      //
      Status = PeiServicesLocatePpi (
                 &gEfiDxeIplPpiGuid,
                 0,
                 NULL,
                 (VOID **)&TempPtr.DxeIpl
                 );
      ASSERT_EFI_ERROR (Status);
       
      if (EFI_ERROR (Status)) {
        //
        // Report status code to indicate DXE IPL PPI could not be found.
        //
        REPORT_STATUS_CODE (
          EFI_ERROR_CODE | EFI_ERROR_MAJOR,
          (EFI_SOFTWARE_PEI_CORE | EFI_SW_PEI_CORE_EC_DXEIPL_NOT_FOUND)
          );
        CpuDeadLoop ();
      }
       
      //
      // Enter DxeIpl to load Dxe core.
      //
      DEBUG ((EFI_D_INFO, "DXE IPL Entry\n"));
      Status = TempPtr.DxeIpl->Entry (
                                 TempPtr.DxeIpl,
                                 &PrivateData.Ps,
                                 PrivateData.HobList
                                 );
    

DXE阶段

在UEFI这几个阶段中,我们重点关注的是PEI、DXE阶段;

下载到的固件大部分都是DXE阶段的EFI文件,这个阶段主要做的事情是初始化各个驱动;

DXE阶段的第一个driver是DxeCore

DXE阶段核心是一个Dispatcher,它会循环Load文件并从入口点运行

MdeModulePkg/Core/Dxe/DxeMain/DxeMain.c

image-20210409113238250

MdeModulePkg/Core/Dxe/Dispatcher/Dispatcher.c

image-20210409113337386

每一个EFI文件相当于一个Handle,这个EFI运行时需要用到的Protocol会被链在其中的Protocols处;

Protocol_Interface表中又会有一项Protocol_entry指向整个的Protocol数据库;

img

这几个表的初始化都是在MdeModulePkg/Core/Dxe/Handle.c中实现的

image-20210409111628469

EDK2

EDK2是一个开发UEFI的框架,很多厂商的固件实际上都是用EDK2写的

下载下来之后可以看到目录结构

❯ tree -L 1 -d
.
├── ArmPkg
├── ArmPlatformPkg
├── ArmVirtPkg
├── BaseTools
├── Conf
├── CryptoPkg
├── DynamicTablesPkg
├── EmbeddedPkg
├── EmulatorPkg
├── FatPkg
├── FmpDevicePkg
├── IntelFsp2Pkg
├── IntelFsp2WrapperPkg
├── MdeModulePkg
├── MdePkg
├── NetworkPkg
├── OvmfPkg
├── PcAtChipsetPkg
├── RedfishPkg
├── SecurityPkg
├── ShellPkg
├── SignedCapsulePkg
├── SourceLevelDebugPkg
├── StandaloneMmPkg
├── UefiCpuPkg
├── UefiPayloadPkg
└── UnitTestFrameworkPkg

里面很多文件夹都是以Pkg结尾的

每一个Pkg都是一个UEFI的包,完成UEFI的一部分功能

其中需要特别注意EmulatorPkgMdePkgArmVirtPkgOvmfPkg

这三个包比较特别,EmulatorPkg是模拟器的包,可以在编译出一个可执行文件,运行后是一个UEFI Shell,在里面可以执行一些EFI的程序;旧版本的可能是分别的Nt32PkgUnix32Pkg,两个平台的模拟器分别是一个文件夹,在比较新的EDK2中将其合并了;

ArmVirtPkgOvmfPkg都是为虚拟机专门提供的包,编译之后可以生成虚拟机运行用到的固件;

MdePkg则是整个EFI文件的基础,里面包含了一些比较核心的功能。

我们以ArmVirtPkg为例,看一下文件夹下的结构

.
├── FdtClientDxe
├── HighMemDxe
├── Include
├── KvmtoolPlatformDxe
├── Library
├── PlatformCI
├── PlatformHasAcpiDtDxe
├── PrePi
├── VirtioFdtDxe
├── XenAcpiPlatformDxe
├── XenPlatformHasAcpiDtDxe
├── XenioFdtDxe
├── ArmVirt.dsc.inc
├── ArmVirtKvmTool.dsc
├── ArmVirtKvmTool.fdf
├── ArmVirtPkg.ci.yaml
├── ArmVirtPkg.dec
├── ArmVirtQemu.dsc
├── ArmVirtQemu.fdf
├── ArmVirtQemuFvMain.fdf.inc
├── ArmVirtQemuKernel.dsc
├── ArmVirtQemuKernel.fdf
├── ArmVirtRules.fdf.inc
├── ArmVirtXen.dsc
├── ArmVirtXen.fdf
└── VarStore.fdf.inc

12 directories, 14 files

因为这个Pkg是为虚拟机提供可以运行的固件,这里面根据虚拟机不同实际上是好几个包

我们只看其中一个,例如只看Qemu

├── ArmVirtPkg.ci.yaml
├── ArmVirtPkg.dec
├── ArmVirtQemu.dsc
├── ArmVirtQemu.fdf
├── ArmVirtQemuFvMain.fdf.inc
├── ArmVirtQemuKernel.dsc
├── ArmVirtQemuKernel.fdf

相关的几个文件主要是ArmVirtQemu*这样的文件,另外ArmVirtPkg.ci.yamlArmVirtPkg.dec是针对整个包的文件;

fdf文件

fdf文件全称为flash device file,是用来标识哪些文件可以写到Flash中的文件;

fdf文件分Define、FD、FV几个部分

FD

我们打开这个ArmVirtQemu.fdf文件,里面就会写到

################################################################################
#
# FD Section
# The [FD] Section is made up of the definition statements and a
# description of what goes into  the Flash Device Image.  Each FD section
# defines one flash "device" image.  A flash device image may be one of
# the following: Removable media bootable image (like a boot floppy
# image,) an Option ROM image (that would be "flashed" into an add-in
# card,) a System "Flash"  image (that would be burned into a system's
# flash) or an Update ("Capsule") image that will be used to update and
# existing system flash.
#
################################################################################

FD Section是由声明和描述组成的,每一个FD节都表示有一个Flash Device

并且Flash存在这样几种可选的类型:

  • Removable media bootable image
  • Option ROM image
  • System Flash image
  • Update

FD节这部分有一段区域是写着最终文件区域的划分方式

################################################################################
#
# Following are lists of FD Region layout which correspond to the locations of different
# images within the flash device.
#
# Regions must be defined in ascending order and may not overlap.
#
# A Layout Region start with a eight digit hex offset (leading "0x" required) followed by
# the pipe "|" character, followed by the size of the region, also in hex with the leading
# "0x" characters. Like:
# Offset|Size
# PcdOffsetCName|PcdSizeCName
# RegionType <FV, DATA, or FILE>
#
################################################################################

这个区域里面一项一项都是以

Offset|Size

这样的形式标识的,并且内存空间必须是连续的;

在这个Qemu的fdf文件中主要是两个部分

0x00000000|0x00001000
DATA = {
!if $(ARCH) == AARCH64
  0x00, 0x04, 0x00, 0x14   # 'b 0x1000' in AArch64 ASM
!else
  0xfe, 0x03, 0x00, 0xea   # 'b 0x1000' in AArch32 ASM
!endif
}

0x00001000|$(FVMAIN_COMPACT_SIZE)
gArmTokenSpaceGuid.PcdFvBaseAddress|gArmTokenSpaceGuid.PcdFvSize
FV = FVMAIN_COMPACT

最终镜像0~0x1000区域放着一个jmp 0x1000的指令

之后从0x1000开始才是具体的镜像的空间

我们与最终编译生成的二进制比较一下,看看是不是这样

image-20210329110837803

最终生成的文件在Build/ArmVirtQemu-AARCH64/DEBUG_GCC5/FV/QEMU_EFI.fd

可以看到这个文件开头几个字节是\x00\x04\x00\x14

用让rasm2汇编一下b 0x1000可以看到就是这几个字节

image-20210329111434113

这个fd文件之后一直到0x1000都是\xff

和之前我们看到的fdf文件是一致的

image-20210329111613528

从0x1000开始有内容,这之后的内容按照fdf文件里的描述,就是FVMAIN_COMPACT了,这部分在FV中定义

0x00001000|$(FVMAIN_COMPACT_SIZE)
gArmTokenSpaceGuid.PcdFvBaseAddress|gArmTokenSpaceGuid.PcdFvSize
FV = FVMAIN_COMPACT

FV

FV是Flash Volume

注释里面写着FV这个节的作用是定义那些组件能够被放在Flash Device中,另外也指定了顺序;

################################################################################
#
# FV Section
#
# [FV] section is used to define what components or modules are placed within a flash
# device file.  This section also defines order the components and modules are positioned
# within the image.  The [FV] section consists of define statements, set statements and
# module statements.
#
################################################################################

用binwalk看一下这个fd文件

image-20210329112141996

从0x1000开始的是一个UEFI PI firmware

下面有一些DEBUG用的源码;

最后是一个LZMA compressed data

编译EDK2

这边为了与项目结合编译了ARM64版本的EDK2

以下均在Ubuntu 18.04.5虚拟机中完成

首先安装依赖

sudo apt-get install python3 python3-dist uuid-dev iasl build-essential git gcc-5 nasm

下载EDK2源码

mkdir ~/src
cd ~/src
git clone https://github.com/tianocore/edk2.git
cd edk2
git submodule update --init 

之后下载交叉编译的工具链

mkdir ~/toolchain
cd ~/toolchain
wget https://armkeil.blob.core.windows.net/developer/Files/downloads/gnu-a/9.2-2019.12/binrel/gcc-arm-9.2-2019.12-x86_64-aarch64-none-elf.tar.xz

墙内网不一定能一次下载下来,最好打开断点续传或者用axel来下载

之后解压

tar xf gcc-arm-9.2-2019.12-x86_64-aarch64-none-elf.tar.xz

运行repo里给的环境设置脚本

cd ~/src/edk2
make -C BaseTools
. edksetup.sh

之后配置安装时需要的几个环境变量

export EDK_TOOLS_PATH=$HOME/src/edk2/BaseTools
export GCC5_AARCH64_PREFIX=$WORKSPACE/toolchain/gcc-arm-9.2-2019.12-x86_64-aarch64-none-elf/bin/aarch64-none-elf-

下面需要修改edk2/Conf中的配置文件

vim ~/src/edk2/Conf/target.txt

在里面修改这几项内容

ACTIVE_PLATFORM		= SecurityPkg/SecurityPkg.dsc
TARGET				= DEBUG
TARGET_ARCH			= AARCH64
TOOCHAIN_TAG		= GCC5

这样的操作编译出来的是SecurityPkg这个包,如果要编译其他的包需要修改ACTIVE_PLATFORM

之后在根目录执行build就可以

生成的包在edk2/Build里面

EFI Emulator

开发EFI Emulator主要想要模拟运行起来DXE阶段的driver以及PEI阶段的PEIM

之后可以在模拟执行的基础上增加Fuzz、符号执行等功能,用于漏洞挖掘;

从DXE阶段开始,第一个任务是模拟DxeCore这个文件

DxeCore的模拟

DxeCore是MdeModulePkg/Dxe/DxeMain/这个模块下的内容,主要功能在前面第一章介绍DXE原理的地方有进行说明;

这个函数的入口点需要传入一个HobStart作为参数

image-20210410124519124

要成功模拟起来DxeMain就需要构造出来一个Hob List

我们向上寻找调用的位置,在DxeIplPeim这里(PEI运行中的最后一个PEIM,其作用主要是交接权限给DXE)

可以看到最终调用是通过HandOffToDxeCore(DxeCoreEntryPoint,HobList)实现的

image-20210410124905413

那么我们只需要顺着这里去看HobList究竟是什么样的构造就可以;

在这一句调用中,HobList是通过参数传递过来的,所以我们需要继续向上找调用DxeLoadCore的地方

image-20210410125032390

位于MdeModulePkg/Core/Pei/PeiMain/PeiMain.c中,是PEI阶段的PeiCore负责调用了DxeIpl

这里是以加载PPI的方式加载了DxeIpl,之后进入入口点即转交权限;

这里传入的HobListPrivateData.HobList

image-20210410125315421

这里HobList是一个EFI_PEI_HOB_POINTERS类型,定义在PeiMain.h

image-20210410155405242

EFI_PEI_HOB_POINTERS这个结构的定义在MdePkg/Include/Pi/PiHob.h文件汇总

image-20210410125619350

这里先这样,我们回到PeiCore那里,看看是在哪里对PrivateDataHobList有初始化和赋值

在PeiCore中的InitializeMemoryServices函数这里

image-20210410153906374

其中设置了PrivateData->HobList.Raw

image-20210410154006158

但是实际上这里直接先设置为了SecCoreData->PeiTemporaryRamBase,这又涉及到前一个阶段Sec阶段的内容了,我们暂且先不看;

接下来调用了PeiCoreBuildHobHandoffInfoTable这个函数

这个函数定义在Core/Pei/Hob/Hob.c

image-20210410154203964

这里构建了Hob

相关工具

[1] EmulatorPkg,比较新版本的EDKII中模拟的efi的组件,合并了原本windows和unix的两个部分;

[2] uefireverse

[3] Crafting An EFI emulator,这个文章作者实现了efi_dxe_emulator