UEFI原理
UEFI启动可以分为几个阶段
总体而言可以认为是两大部分,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块的内容):
-
初始化内存(第一个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)); }
-
设置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);
-
输出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"); }
-
初始化其他的PEI Core Service(第二个和第三个if之间)
// // Complete PEI Core Service initialization // InitializeSecurityServices (&PrivateData.Ps, OldCoreData); InitializeDispatcherData (&PrivateData, OldCoreData, SecCoreData); InitializeImageServices (&PrivateData, OldCoreData);
-
处理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); } }
-
调用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");
-
将权限交给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
MdeModulePkg/Core/Dxe/Dispatcher/Dispatcher.c
每一个EFI文件相当于一个Handle,这个EFI运行时需要用到的Protocol会被链在其中的Protocols处;
Protocol_Interface表中又会有一项Protocol_entry指向整个的Protocol数据库;
这几个表的初始化都是在MdeModulePkg/Core/Dxe/Handle.c
中实现的
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的一部分功能
其中需要特别注意EmulatorPkg
、MdePkg
、ArmVirtPkg
和OvmfPkg
这三个包比较特别,EmulatorPkg
是模拟器的包,可以在编译出一个可执行文件,运行后是一个UEFI Shell,在里面可以执行一些EFI的程序;旧版本的可能是分别的Nt32Pkg
和Unix32Pkg
,两个平台的模拟器分别是一个文件夹,在比较新的EDK2中将其合并了;
ArmVirtPkg
和OvmfPkg
都是为虚拟机专门提供的包,编译之后可以生成虚拟机运行用到的固件;
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.yaml
和ArmVirtPkg.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
开始才是具体的镜像的空间
我们与最终编译生成的二进制比较一下,看看是不是这样
最终生成的文件在Build/ArmVirtQemu-AARCH64/DEBUG_GCC5/FV/QEMU_EFI.fd
可以看到这个文件开头几个字节是\x00\x04\x00\x14
用让rasm2汇编一下b 0x1000
可以看到就是这几个字节
这个fd文件之后一直到0x1000
都是\xff
和之前我们看到的fdf
文件是一致的
从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文件
从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
作为参数
要成功模拟起来DxeMain就需要构造出来一个Hob List
我们向上寻找调用的位置,在DxeIplPeim
这里(PEI运行中的最后一个PEIM,其作用主要是交接权限给DXE)
可以看到最终调用是通过HandOffToDxeCore(DxeCoreEntryPoint,HobList)
实现的
那么我们只需要顺着这里去看HobList
究竟是什么样的构造就可以;
在这一句调用中,HobList
是通过参数传递过来的,所以我们需要继续向上找调用DxeLoadCore的地方
位于MdeModulePkg/Core/Pei/PeiMain/PeiMain.c
中,是PEI阶段的PeiCore负责调用了DxeIpl
这里是以加载PPI的方式加载了DxeIpl,之后进入入口点即转交权限;
这里传入的HobList
是PrivateData.HobList
这里HobList是一个EFI_PEI_HOB_POINTERS
类型,定义在PeiMain.h
而EFI_PEI_HOB_POINTERS
这个结构的定义在MdePkg/Include/Pi/PiHob.h
文件汇总
这里先这样,我们回到PeiCore
那里,看看是在哪里对PrivateData
中HobList
有初始化和赋值
在PeiCore中的InitializeMemoryServices
函数这里
其中设置了PrivateData->HobList.Raw
但是实际上这里直接先设置为了SecCoreData->PeiTemporaryRamBase
,这又涉及到前一个阶段Sec阶段的内容了,我们暂且先不看;
接下来调用了PeiCoreBuildHobHandoffInfoTable
这个函数
这个函数定义在Core/Pei/Hob/Hob.c
这里构建了Hob
相关工具
[1] EmulatorPkg,比较新版本的EDKII中模拟的efi的组件,合并了原本windows和unix的两个部分;
[2] uefireverse
[3] Crafting An EFI emulator,这个文章作者实现了efi_dxe_emulator