静态BIOS / UEFI分析或如何获取依赖关系图

“我昨天完成了锻造,
我欺骗了两个计划……”
VS维索斯基的歌...

大约3年前(2016年初),在GitHub上的UEFITool项目问题上出现了用户的愿望:为BIOS / UEFI中包含的可执行模块构建“依赖关系图”。

甚至进行了很小的讨论,结果终于清楚了这一任务绝非易事,其解决方案的可用功能还不够,那时的前景还很模糊...

这个问题仍然处于不确定状态,并有无限期实现的希望(但这种愿望可能仍然存在,而且希望,如您所知,将永远消亡!)。

有一个建议:最后,找到解决这个问题的方法!

定义条款


进一步假设我们正在处理Intel 64和IA-32架构。

为了明确确定我们决定构建的内容,我们将必须更详细地检查BIOS / UEFI各个阶段的功能。

如果仔细查看FFS固件卷中提供的文件类型,可以发现大多数现有文件都包含带有可执行模块的部分。

即使我们考虑使用新型固件ASUS或ASRock,您也可以在其中轻松找到多达150个EFI_FV_FILETYPE_FREEFORM类型的文件,其中包含不同格式的图片,但是即使在这些固件中,可执行文件也比其他类型的文件更多。

+--------------------------------------------------------------------------+ | File Types Information | +--------------------------------------------------------------------------+ | EFI_FV_FILETYPE_RAW = 6 | | EFI_FV_FILETYPE_FREEFORM = 83 | | EFI_FV_FILETYPE_SECURITY_CORE = 1 | | EFI_FV_FILETYPE_PEI_CORE = 1 | | EFI_FV_FILETYPE_DXE_CORE = 1 | | EFI_FV_FILETYPE_PEIM = 57 | | EFI_FV_FILETYPE_DRIVER = 196 | | EFI_FV_FILETYPE_APPLICATION = 1 | | EFI_FV_FILETYPE_SMM = 60 | | EFI_FV_FILETYPE_SMM_CORE = 1 | | EFI_FV_FILETYPE_PAD = 4 | +--------------------------------------------------------------------------+ | Total Files : = 411 | +--------------------------------------------------------------------------+ 
一些普通(普通)固件组成的示例。

尽管此表中未标记包含可执行模块的文件,但是,根据定义,它们将全部包含在此列表中,但后缀为RAW,FREEFORM和PAD的文件除外。

后缀为“ CORE”的文件(SECURITY_CORE,PEI_CORE和DXE_CORE)是相应的“内核”(相应阶段的头模块),它们从其他阶段(或在启动之后)接收控制,SMM_CORE是DXE阶段的子阶段,在此阶段被调用实现。 只能根据用户的请求执行应用,对阶段没有特定的约束。

没有列出最常见的文件类型:PEIM(PEI阶段模块),DRIVER(DXE阶段模块)和SMM(DXE子阶段模块)。 PEI和DXE阶段的CORE模块包括一个调度程序,该调度程序控制相应阶段的加载/启动模块的顺序。

在上面的示例中,没有组合的选项,我们不会记住它们:尽管它们是在真实固件中找到的,但很少见。 希望获得更多详细信息的人员可以参考CodeRush1、2、3条 。 并且还引用了他的建议:“对于原始文档的拥护者, UEFI PI规范始终可用,所有内容都进行了更详细的描述。”

每个可执行固件模块都是PE +(便携式可执行文件)格式的模块或其派生版本(Terse可执行文件:TE格式)。 PE +格式可执行模块是一组“稍微”打包的结构化数据,其中包含加载程序将该模块映射到内存所需的信息。

PE +格式(结构)本身在各个PE +模块之间没有任何交互作用的机制。 加载并开始执行后的每个可执行模块都是一个独立的自主进程, (嗯,应该是这样!) ,即 该模块不应“假设”任何在其外部进行的操作。

一个UEFI阶段的各个独立可执行模块之间的交互组织是通过相应阶段的CORE模块进行组织的。 各个可执行模块可以定义(安装)协议,请求(定位)并使用其他模块声明的协议,设置/声明事件以及声明(通知)事件处理程序。

因此,对于每个可执行固件模块,我们对以下工件的存在很感兴趣:

  1. 该模块定义的协议列表。 (每个协议均由唯一编号-guid标识)。
  2. 该模块使用的协议列表(尝试使用)。
  3. 此模块宣布的事件列表。 (该事件具有唯一编号-guid)。
  4. 此模块中存在的事件处理程序列表(已实现并可以安装/初始化)。
如果对于每个可执行阶段模块,我们都知道上面1-4节中列出的所有工件,则可以认为已定义了给定BIOS / UEFI阶段的静态依赖关系图 。 (换句话说,如果我们定义了描述模块之间相互依赖关系的所有信息)。
我们将仅考虑静态分析的选项,这意味着实现项1-4的代码的某些元素可能无法实现(是“死”代码的片段),或者仅对于输入数据/参数的某些选项才可以实现。

到目前为止,我们考虑的所有内容仅基于BIOS / UEFI规范。 并且为了理解所讨论固件的现有可执行模块的“关系”,我们将不得不更深入地研究它们的结构,这意味着我们至少应该部分逆转它们(恢复原始算法)。

如上所述,PE +格式可执行模块只是用于加载程序的一组结构,在内存中构建了一个将控制权转移到的对象,并且该对象本质上由处理器指令以及这些指令的数据组成。
我们将说,如果有可能解决将该模块中显示的命令和数据分开的问题,则可以对可执行模块进行彻底的反汇编
同时,我们不会对结构和数据类型施加任何要求,如果对于属于加载程序接收的可执行模块映像的每个字节,我们可以清楚地说出它属于两个类别中的哪一个即可:命令字节或数据字节就足够了。

完全拆卸可执行模块本身的任务通常并不容易,而且,在一般情况下,它在算法上是无法解决的。 我们将不讨论这个问题的细节,也不会破坏矛,我们将此声明视为公理。

假设:

  1. 我们已经解决了特定BIOS / UEFI执行模块的完全拆卸问题,即 我们设法将命令和数据分开。
  2. 该模块具有“ C”语言的源代码(在当前的BIOS / UEFI固件中,大多数模块仅以“ C”语言开发)。

即使在这种情况下,仅将获得的结果(汇编器文本只是处理器指令的文本表示形式)与“ C”语言的源代码进行比较,几乎绝对需要良好的经验/资格,绝对退化的情况除外。

完整的示例研究表明,难以识别反汇编结果或将其与源代码进行比较,这并不是我们当前计划的一部分。
让我们仅考虑一个示例,当在汇编列表中遇到“间接调用”命令-隐式过程调用时。

这是表中引用的过程调用的示例。 包含指向各种过程的链接的表是实现任意协议接口表示的典型情况。

这样的表不必仅由对过程的引用组成;没有人禁止在该结构中存储任意数据(这是典型的“ C”结构的示例)。

这是这种调用的一种形式(代替ecx寄存器,几乎所有32位处理器寄存器的变体都是可能的):
FF 51 18呼叫dword ptr [ecx + 18h]
经过分析,获得了类似的命令,只有知道我们使用此命令调用其接口的对象(协议)的类型,才有可能弄清楚正在调用哪种过程,其参数列表,返回结果的类型和值是可能的。

如果我们知道在前面的示例中“ ecx”寄存器包含一个指针(EFI_PEI_SERVICES表的开始地址),则可以通过以下更易理解和“愉悦”的方式接收(呈现)此命令:
FF 51 18呼叫[exx + EFI_PEI_SERVICES.InstallPpi]
获取有关参与“间接调用”命令的寄存器内容的信息通常超出了“典型”反汇编程序的功能,反汇编程序的任务只是将处理器的二进制(二进制)代码分析并将其转换为人类可读的形式-相应处理器命令的文本表示形式。

为了解决此问题,通常需要使用二进制可执行模块中不可用的其他(元)信息(由于编译和链接而丢失-在从一种算法表示到另一种表示的转换中使用,但处理器不再需要执行接收到的命令)。

如果仍然可以从其他来源获得此元数据,然后使用它们并进行其他分析,则我们将更直观(更准确)地表示“间接调用”命令。

实际上,这种高级分析已经使人联想到“反编译”过程,尽管结果看起来不像是使用“ C”语言编写的模块的源代码,但是,将来,我们将这一过程称为“间接调用”“部分“反编译”

因此,我们准备确定足够的条件来构造给定BIOS / UEFI阶段的可执行固件模块的相互依赖关系图:
要获得静态依赖图 (任何阶段-PEI或DXE),完全反汇编相应阶段的所有可执行模块(至少分离所有命令),并反编译反汇编模块中存在的“间接调用”命令就足够了。
关于我们的“间接呼叫”团队的知识如何与模块间的交互联系在一起,立即有很多问题。
如上所述,整个交互管理服务由相应阶段的“ CORE”模块提供,并且这些阶段中的服务被设计为“基本”服务表。

由于PEI和DXE阶段中模块之间的交互模型尽管在意识形态上(结构上)相似,但在技术上仍然不同,因此建议从一些形式上的考虑转向考虑为PEI阶段直接构建静态依赖图的特定方式。

我们甚至能够确定和制定必要和充分的条件,以便为PEI阶段构造静态依赖图

为PEI阶段构建静态依赖关系图


完全拆卸 PEI阶段可执行模块以及反编译这些模块中存在的间接调用命令的问题的解决方案的描述超出了我们的故事的范围,并且不会在其中给出-这种材料的数量表示可能超出本作品的大小。

随着时间的流逝,这可能会作为单独的材料发生,但就目前而言-知道如何做。

我们只需要注意的是,使用元数据,加上存在某种用于构造二进制代码的结构,实际上可以完全分解可执行的BIOS / UEFI模块。 现在或将来都不应正式证明这一事实。 至少在分析/处理来自不同制造商的一百(100)个BIOS / UEFI中,没有任何示例无法完全拆卸

此外,仅提供特定结果(并附有解释:什么,如何以及多少...)。

EFI_PEI_SERVICES结构是PEI阶段的基本结构,它作为参数传递到每个PEI模块的入口点,并包含指向PEI模块运行所需的基本服务的链接。

我们仅对结构开头的字段感兴趣:



IDA Pro反汇编程序中EFI_PEI_SERVICES类型的实际结构的片段。

这就是它在源代码中以“ C”语言显示的方式(请记住,这只是结构的一部分):

 struct EFI_PEI_SERVICES { EFI_TABLE_HEADER Hdr; EFI_PEI_INSTALL_PPI InstallPpi; EFI_PEI_REINSTALL_PPI ReInstallPpi; EFI_PEI_LOCATE_PPI LocatePpi; EFI_PEI_NOTIFY_PPI NotifyPpi; //...      ... }; 

与所有“基本”服务表(服务表)一样,EFI_PEI_SERVICES结构的开头是EFI_TABLE_HEADER结构。 此标头结构中显示的值使我们可以明确地说,如果EFI_PEI_SERVICES结构本身实际上存在于来自反汇编程序的片段中(请参见“ Hdr.Signature”字段),则至少是此结构的模板!

 struct EFI_TABLE_HEADER { UINT64 Signature; UINT32 Revision; UINT32 HeaderSize; UINT32 CRC32; UINT32 Reserved; }; 

在此过程中,我们可以确定固件是在UEFI PI规范的版本为1.2的时候开发的,其相关时期是从2009年到2013年,但是目前(2019年初),该规范的当前版本已经增长(从字面上讲是日渐增长)。到1.7版。

在“ Hdr.HeaderSize”字段中,您可以确定结构的总长度为78h(顾名思义,它不是标头的长度,而是EFI_PEI_SERVICES整个结构的长度)。

接口EFI_PEI_SERVICES分为7个类别/类。 我们只列出它们:

  1. PPI服务。
  2. 引导模式服务。
  3. HOB服务。
  4. 固件批量服务。
  5. PEI内存服务。
  6. 状态码服务。
  7. 重置服务。

所有进一步的叙述将与属于PPI Services类别/类的过程直接相关,这些过程旨在组织PEI阶段可执行模块之间的模块间交互。

PEI阶段只有四个。

通常,无需猜测每个接口的用途:功能完全由接口名称决定,所有详细信息均在规范中

以下是这些过程的原型:

 typedef EFI_STATUS (__cdecl *EFI_PEI_INSTALL_PPI)( const EFI_PEI_SERVICES **PeiServices, const EFI_PEI_PPI_DESCRIPTOR *PpiList); typedef EFI_STATUS (__cdecl *EFI_PEI_REINSTALL_PPI)( const EFI_PEI_SERVICES **PeiServices, const EFI_PEI_PPI_DESCRIPTOR *OldPpi, const EFI_PEI_PPI_DESCRIPTOR *NewPpi); typedef EFI_STATUS (__cdecl *EFI_PEI_LOCATE_PPI)( const EFI_PEI_SERVICES **PeiServices, const EFI_GUID *Guid, UINTN Instance, EFI_PEI_PPI_DESCRIPTOR **PpiDescriptor, void **Ppi); typedef EFI_STATUS (__cdecl *EFI_PEI_NOTIFY_PPI)( const EFI_PEI_SERVICES **PeiServices, const EFI_PEI_NOTIFY_DESCRIPTOR *NotifyList); 

我们仅注意,除了调用“ PPI Services”类的过程/接口的“间接调用”命令之外,还可以对这些过程进行显式(直接-非表格形式)调用,这有时会在执行模块中进行,在执行模块中定义/创建了EFI_PEI_SERVICES结构。

我将告诉您一个小秘密:奇怪的是,尽管这是PEI阶段的“基本”服务表,但是,如实践所示,它不仅可以在PEI_CORE模块中定义。

实际上,有些固件在其中定义/形成并在多个模块中使用了EFI_PEI_SERVICES结构,而这些固件绝不是PEI_CORE模块的副本。

因此,以下代码选项是可能的:

 seg000:00785F0D B8 8C A6 78+ mov eax, offset ppiList_78A68C seg000:00785F12 50 push eax ; PpiList seg000:00785F13 57 push edi ; PeiServices seg000:00785F14 89 86 40 0E+ mov [esi+0E40h], eax seg000:00785F1A E8 70 FC FF+ call InstallPpi 

显式调用“ InstallPpi”过程的示例。

 seg000:00787CBB 8B 4D FC mov ecx, [ebp+PeiServices] seg000:00787CBE 50 push eax ; PpiList seg000:00787CBF C7 00 10 00+ mov dword ptr [eax], 80000010h seg000:00787CC5 C7 43 3C A8+ mov dword ptr [ebx+3Ch], offset guid_78A9A8 seg000:00787CCC 8B 11 mov edx, [ecx] seg000:00787CCE 51 push ecx ; PeiServices seg000:00787CCF FF 52 18 call [edx+EFI_PEI_SERVICES.InstallPpi] 

对InstallPpi接口的隐式调用的示例。

 FF 51 18 call dword ptr [ecx+18h] FF 51 18 call [ex+EFI_PEI_SERVICES.InstallPpi] FF 51 1 call dword ptr [ecx+1Ch] FF 51 1C call [ex+EFI_PEI_SERVICES.ReInstallPpi] FF 51 20 call dword ptr [ecx+20h] FF 51 20 call [ex+EFI_PEI_SERVICES.LocatePpi] FF 51 24 call dword ptr [ecx+24h] FF 51 24 call [ex+EFI_PEI_SERVICES.NotifyPpi] 
身份验证之前和之后的隐式接口调用示例。

我们注意到一个特征:在IA-32体系结构的PEI阶段中,PPI Services类的接口的偏移量为18h,1Ch,20h和24h。

现在,我们声明以下语句:
要构建PEI阶段的静态依赖关系图有必要并且充分完全拆卸该阶段的所有可执行模块(至少分离所有命令),并在已拆卸的模块中反编译偏移量为18h,1Ch,20h,24h的“间接调用”命令。
实际上,我们已经完全制定了解决该问题的算法,并且一旦我们设法隔离了对PPI Services类的接口/过程的所有调用,仅需确定将哪些参数传递给这些调用即可。 这项任务可能不是最琐碎的,但正如实践所示,它是完全可以解决的,我们拥有与此相关的所有数据。

现在是真实PEI阶段模块的真实数据的真实示例。 我们不会有意表明获得了哪家公司的BIOS / UEFI结果,仅举例说明它们的外观。

PEIM模块描述的两个示例,其中包含有关在这些模块中使用PPI Services接口的完整信息


  -- File 04-047/0x02F/: "TcgPlatformSetupPeiPolicy" : [007CCAF0 - 007CD144] DEPENDENCY_START EFI_PEI_READ_ONLY_VARIABLE_ACCESS_PPI DEPENDENCY_END Install Protocols: [1] TCG_PLATFORM_SETUP_PEI_POLICY Locate Protocols: [2] EFI_PEI_READ_ONLY_VARIABLE_ACCESS_PPI 
 -- File 04-048/0x030/: "TcgPei" : [007CD160 - 007CF5DE] DEPENDENCY_START EFI_PEI_MASTER_BOOT_MODE_PEIM_PPI EFI_PEI_READ_ONLY_VARIABLE_ACCESS_PPI AND DEPENDENCY_END Install Protocols: [1] AMI_TCG_PLATFORM_PPI [2] EFI_PEI_TCG_PPI [2] PEI_TPM_PPI Locate Protocols: [1] EFI_PEI_TCG_PPI [1] EFI_PEI_READ_ONLY_VARIABLE_ACCESS_PPI [1] TCG_PLATFORM_SETUP_PEI_POLICY [5] PEI_TPM_PPI Notify Events: [1] AMI_TCM_CALLBACK ReInstall Protocols: [1] PEI_TPM_PPI 

按使用协议的接口类型列出的协议


扰流器下面是PPI Services类的每个接口的PPIM协议列表的简化示例。

列表的格式如下:
 | 序列号|  name_PPI |  guid_PPI | 可执行文件名:用户名|

*****在“固件”中安装99 Ppi


*****在“固件”中找到194 Ppi


*****在“固件”中重新安装5 Ppi


*****在“固件”中通知29 Ppi


在特定BIOS / UEFI中引用的协议的所有指南的最终列表,带有图例的图例指示在哪些“ PPI服务”中找到这些协议


扰流板下方是找到的97 PPi指南,这些指南明确用于特定固件中,有关数据已在前面给出。

列表中的每个项目前面都有一个图例,该图例反映了对特定协议的所有使用类型。

 "D" - in DEPENDENCY section used "I" - in "InstallPpi" functions used "L" - in "LocatePpi" functions used "R" - in "ReInstallPpi" functions used "N" - in "NotifyPpi" functions used 

*****在“固件”中列出Ppi




以下协议列表间隔在此BIOS / UEFI中值得注意:

  1. 38-50号。
    定义任何模块未使用的协议/事件(InstallPpi)。
  2. 第87-95号。
    尝试请求此固件的任何模块未安装的协议。
  3. 第96-97号。
    两个“ Notify”事件,尽管没有在可执行模块中声明这些过程,但没有模块会费心地声明相应的接口。

结论


  • 来自不同制造商的BIOS / UEFI获得了与上述相似的结果,因此所有示例都是匿名的。
  • 实际上,已经解决了逆转可执行BIOS / UEFI模块算法的更一般的任务,结果图形是附带结果,是一种额外的好处。
  • 对于可执行BIOS / UEFI模块, “获取静态依赖关系图”任务的正确解决方案需要对二进制代码进行静态分析,其中包括可执行模块的完全反汇编和对这些模块的间接调用命令的部分反编译

Source: https://habr.com/ru/post/zh-CN440052/


All Articles