A hitchhiker’s guide to UEFI programming in Windows

Wang Mingjie unpacks the entire process of developing UEFI programs, covering everything from building to deployment on actual systems with Secure Boot enabled. While not a technical deep-dive, his guide provides engineers-at-large with a grasp of different elements of the UEFI environment, and understand its impact on the execution and deployment of UEFI programs. Mingjie also includes various practical considerations to assist readers in evaluating different deployment options.

d*classified
d*classified
25 min readMay 25, 2024

--

Photo by Atlas Green on Unsplash

Introduction

UEFI (Unified Extensible Firmware Interface) development is largely done using the EDK II (EFI Development Kit II) repo. EFI intially began as a project within Intel to replace the BIOS. Efforts began in 1998 under the Tiano project and it comprises a specification and a firmware that implements it. In 2004, Intel open sourced the foundation code of their EFI firmware. In 2005, major players in the computer industry formed the UEFI Forum to take over ownership of the specificaton and in 2006, they released the UEFI 2.0 specification. The open-sourced code subsequently evolved into EDK and finally into today’s EDK II. A community of developers, called TianoCore, maintains the source code on GitHub.

UEFI development on Windows used to be a complex process requiring numerous manual steps. It implements a custom build tool chain and does not provide any support for development via IDE. While various guides can be found online and the build process has improved since then through the use of containers, it is still nowhere near the convenience of IDEs. In addition, few resources exist on how to install/deploy UEFI executables, and even less on how to have them run with Secure Boot enabled.

Thus this guide will attempt to plug these gaps and hopefully help those seeking to deploy UEFI executables, be it for their own personal use or in production environments.

Readers are expected to have a basic understanding of:

  • What is UEFI
  • C programming language
  • Visual Studio IDE

Installing & building

Given the aforementioned development challenges, Alex Ionescu (one of the authors of Windows Internals) created VisualUefi to wrap EDK II’s source code in a Visual Studio solution. Building is done within the IDE, completely bypassing EDK II’s custom build tool chain. Development of one’s own UEFI executables is done through a second solution.

Follow the instructions in the VisualUefi repo.

Running

UEFI executables cannot run directly in the OS; instead, they can only run in the UEFI environment which is before the OS boots. As such, VisualUEFI uses QEMU and EDK II’s OVMF (Open Virtual Machine Firmware) module to emulate the firmware execution environment. VisualUEFI integrates QEMU and OVMF in such a way that UEFI executable can be run almost just like regular command line executables.

Running from EFI shell

This section covers the exact same steps as those in VisualUefi, just with screenshots added.

In the Samples solution, press Ctrl+F5 to launch QEMU. It boots directly into the EFI shell which can then be used to execute the sample UEFI executables:

Using F5 also works but this starts the Visual Studio debugger which is useless here as the debugger can attach to neither QEMU nor the UEFI executables.

The solution’s build output directory VisualUefi\samples\x64\Release\ is mounted as a virtual volume under the label fs1:. Listing its contents shows the same files and folders as one would see in Windows:

First, load UefiDriver.efi:

Use the devtree -b command to check that the driver is loaded. The -b flag displays the output one screen at a time. Comparison of the output before and after loading:

The drivers command can also be used likewise:

Then run the application:

Running from new boot option — install application

On launching QEMU, press and hold Esc until it enters the firmware menu. Alternatively, enter the exit command in EFI shell to exit to the firmware menu.

Select Boot Maintenance Manager.

Select Boot Options.

Select Add Boot Option.

Select the first option.

Select UefiApplication.efi.

Enter any description; this will be the display name of the new boot option. The Optional Data field specifies the arguments that are passed to the UEFI executable. Leave it blank as the sample application does not use it.

Save changes and return to the firmware menu page. Select the Boot Manager option:

The newly added boot option is shown as the last entry. Simply select it to run. One would immediately return to the firmware menu once the application ends, so the output would just flash by. To have the output persist, one would have to change the application’s code.

One approach is to have the application wait for the user’s keystroke before returning. This can be achieved with the WaitForKeyStroke function in edk2\MdeModulePkg\Library\CustomizedDisplayLib\CustomizedDisplayLibInternal.c. However, we're unable to use it as this library isn't included in the EDK-II solution. Creating Visual Studio projects in VisualUEFI to build new libraries is beyond the scope of this guide, so one can simply duplicate the code into the application for now.

EFI_STATUS
WaitForKeyStroke(
OUT EFI_INPUT_KEY* Key
)
{
EFI_STATUS Status;
UINTN Index;
while (TRUE) {
Status = gST->ConIn->ReadKeyStroke(gST->ConIn, Key);
if (!EFI_ERROR(Status)) {
break;
}
if (Status != EFI_NOT_READY) {
continue;
}
gBS->WaitForEvent(1, &gST->ConIn->WaitForKey, &Index);
}
return Status;
}

Calling code:

Print(L"Press any key to continue...\n");
EFI_INPUT_KEY keyInput;
efiStatus = WaitForKeyStroke(&keyInput);
if (EFI_ERROR(efiStatus))
{
Print(L"Failed to get keystroke: %lx\n", efiStatus);
goto Exit;
}

Beware that there are 3 issues with the sample application’s code:

  • Line 94:
efiStatus = ShellInitialize();

This only works when in the EFI shell. Since we’re now running our application first instead of the shell, ShellInitialize() will fail.

  • Line 104:
efiStatus = ShellOpenFileByName(L"fs1:\\UefiApplication.efi",

It assumes that the volume label fs1: is always available and that the executable is always saved there. However, it is the EFI shell that assigns the volume labels, so without it there is no fs1: and ShellOpenFileByName() will fail.

  • Line 134:
efiStatus = gBS->LocateProtocol(&gEfiSampleDriverProtocolGuid, NULL, &sampleProtocol);

It tries to access the sample driver, but we have not installed our driver yet, so this will also fail.

The code will always goto Exit; on failure, so one should insert the code to read the user's keystroke near the top to ensure that it gets executed.

To avoid having to always enter the boot manager UI, one can change the boot order to boot the new option first instead of the EFI shell. To do so, go to Boot Maintenance Manager > Boot Options > Change Boot Order:

In this case, application would exit into the EFI shell instead of the firmware menu. This is because when booting, control would return to the firmware menu if a boot option returns EFI_SUCCESS; otherwise, the next boot option would be executed, and so on until one returns EFI_SUCCESS or all boot options are exhausted (per UEFI Specification section 3.1.1). Given the 3 issues mentioned above, the application would not return EFI_SUCCESS and thus the firmware executes the UEFI shell which is the next option.

Note that an exception to the above boot option handling is when the application calls ExitBootServices(). However this is beyond the scope of this guide.

Running from new boot option — install driver

At the Boot Maintenance Manager page, select Driver Options:

Select Add Driver Option.

Select Add Driver Option Using File.

Select the first option.

Select UefiDriver.efi.

Enter any description; this will be the display name of the new driver option. The Optional Data field specifies the arguments that are passed to the UEFI executable. Leave it blank as the sample driver does not use it.

The Load Option Reconnect field enables the driver to override any driver that was loaded before execution of the Firmware Boot Manager (per UEFI Specification section 3.1.3). Recall the output of the devtree and drivers commands - all the entries besides that of the sample driver are drivers that came built-in with the firmware and were loaded before execution of the Firmware Boot Manager. A device should be managed by only one driver, otherwise compatibility issues may arise. The sample driver does not override any existing driver, so enabling the option would be unnecessary and would have no net effect. Save and exit this page.

Note that the Firmware Boot Manager refers to the firmware component responsible for automatically executing the boot and driver options on boot; it is not to be confused with the boot manager UI which is for users to manually select which boot option to run. Also note that to effect the driver override, after the last driver option is processed, all the UEFI drivers in the system would be disconnected and reconnected.

The Firmware Boot Manager automatically processes all driver options before processing boot options, thus unlike boot options, the firmware menu does not provide any way to explicitly load a particular driver option, nor is there a need to — all installed driver options would be loaded.

Manual installation via EFI shell

Besides using the firmware menu, one can also use the bcfg command in the EFI shell to install new boot and driver options. See output of help bcfg for more details.

This would be useful in systems where the firmware does not provide any ability to add new boot options and/or driver options. This works because) UEFI Specification section 3 dictates the exact mechanism (NVRAM variables, also termed UEFI variables) by which these options are implemented, i.e. all UEFI-compliant firmware must implement and handle the options in the described manner. bcfg operates on the same mechanism, so it is able to modify the options even when the firmware ostensibly does not support this.

In fact, one can directly modify the NVRAM variables to install/modify the boot and driver options. However, this is beyond the scope of this guide.

Running from existing boot option

One may have noticed that in the boot manager page, the Device Path description of the sample application option shows the full path to the executable; however, the name of the executable is missing from the EFI shell option:

Listing the contents of the partition pointed to by that option’s Device Path (EFI shell assigns it the fs0: volume label) shows that the only UEFI executable there is EnrollDefaultKeys.efi, which certainly doesn't sound like the EFI shell:

What is happening is that the firmware Boot Manager would enumerate all removable media and fixed media devices, creating a boot option for each device (provided their file system is FAT, as detailed in UEFI Specification section 13.3). When booting such entries, the firmware would append the following to the device path: \EFI\BOOT\BOOT{machine type short-name}.EFI where machine type short-name defines a PE32+ image format architecture. Possible values are:

The PE Executable Machine Type is specified in the machine field of the COFF file header as defined in the Microsoft Portable Executable and Common Object File Format Specification, Revision 6.0.

To support multiple CPU architectures, a device can store multiple executables within the \EFI\BOOT\ subdirectory, each built for a different architecture. See UEFI Specification sections 3.1.2 and 3.5 for more details.

Thus listing all .efi within fs0: would reveal a bootx64.efi within FS0:\efi\boot\. This is the EFI shell:

Thus one can adopt the same approach for the sample application. Create the \EFI\BOOT\ subdirectory under VisualUefi\samples\x64\Release\, copy the sample application into it, and rename it to BOOTx64.efi. To run it, simply select the existing UEFI Misc Device option in the boot manager UI. Alternatively, reorder the boot options so that QEMU boots it first.

This approach is also how UEFI supports booting from removable media. For example when creating a Windows installation flash drive, the installer executable is saved in \EFI\BOOT\BOOTx64.efi. To install, one enters the firmware menu to select the option of booting from the flash drive. Alternatively, if the flash drive was already configured as the first boot option, the system would automatically boot into the installer.

EFI Shell

The shell is essentially the UEFI firmware’s version of the command prompt / bash shell. It supports many of the same commands as bash — use the help command to list all supported commands. It enables users to access and/or modify files on the system without having to boot into the OS. The UEFI firmware natively supports only FAT file systems, so those are the only file systems the shell can access. Support for other file systems depends on whether the relevant UEFI driver is installed and loaded.

startup.nsh
On launching the shell, one would be greeted with the following prompt:

Press ESC in 5 seconds to skip startup.nsh, any other key to continue.

The shell executes startup.nsh on every run. By configuring the shell to be the first boot option (already so by default for QEMU), this enables scripted operations to be run automatically on the system without having to boot into the OS.

The script file is absent by default, but one can create their own in the root directory of the EFI system partition (ESP). To try it out, in shell, enter the following command:

echo "@echo test startup script" > fs0:\startup.nsh

This creates the script file in fs0:, which is the volume label of the ESP here.

Exit shell with the exit command and run it again:

EFI system partition (ESP)

This is a special partition defined by the UEFI Specification that is accessible by the UEFI firmware. If you’re wondering what’s the deal with this, since accessing partitions is a basic operation, know that this used to be a basic operation only for Operating Systems. BIOS, the previous generation of firmware which UEFI replaces, cannot access a full partition. The only elements it could access on a disk are the MBR (Master Boot Record) and VBRs (Volume Boot Records); these have also been replaced by UEFI.

The ESP is typically where the OS bootloader and/or boot manager is located. On Windows, the partition is not directly accessible as it’s not assigned any drive letter by default, as seen in diskmgmt.msc:

To access it, open elevated cmd and use the command mountvol X: /S to assign it a drive letter, where X is any drive letter that's unused.

To check the location of the Windows boot manager:

X:\>dir /b/s *.efi
X:\EFI\Microsoft\Boot\bootmgr.efi
X:\EFI\Microsoft\Boot\memtest.efi
X:\EFI\Microsoft\Boot\bootmgfw.efi
X:\EFI\Microsoft\Boot\SecureBootRecovery.efi
X:\EFI\Boot\bootx64.efi

bootmgfw.efi is the Windows boot manager. Also notice the EFI\Boot\bootx64.efi entry - this is just a copy of bootmgfw.efi and serves as the fallback should bootmgfw.efi be missing or become corrupted.

Secure Boot

So far all the UEFI executables have been run with Secure Boot disabled. Secure Boot is a security feature of the UEFI firmware that allows only trusted UEFI executables to run. This protects the system from malicious UEFI executables (e.g. bootkits) and should always be enabled in production environments. Trust is defined as having the signature of the executable being present in a database of authorized signatures and being absent in the database of forbidden signatures. Signature is defined as one of the following:

  • SHA-1 Authenticode hash of the executable
  • SHA-256 Authenticode hash of the executable
  • SHA-224 Authenticode hash of the executable
  • SHA-384 Authenticode hash of the executable
  • SHA-512 Authenticode hash of the executable
  • RSA-2048 signature of the SHA-256 Authenticode hash of the executable
  • RSA-2048 signature of the SHA-1 Authenticode hash of the executable
  • Modulus of the RSA-2048 key used to sign the executable (public key exponent is assumed to be 0x10001)
  • DER-encoded X.509 certificate of the key used to sign the executable
  • Other more complex ones, e.g. involving timestamp

See Windows Authenticode Portable Executable Signature Format, section “Calculating the PE Image Hash” on how the Authenticode hash is calculated. For X.509 certificate, a match is found as long as the executable’s signing certificate is present at any level of the X.509 certificate chain (per UEFI Specification sections 8.2.6 and 32.6.3.3).

Thus to have the sample executables run with Secure Boot enabled, one needs to get one of these signatures into the Secure Boot authorized database. Using a certificate is the more flexible option as it allows any executable signed with the corresponding private key to run — this also enables repeated modification and rebuilding of executables which is useful during development and testing. However, it is less secure than using a hash since there is the risk of private key compromise.

To install a signature, enter the firmware menu:

Select Device Manager.

Select Secure Boot Configuration.

The Current Secure Boot State is set to Disabled. This entry is not configurable and is only meant to display the current state. The Attempt Secure Boot option itself is disabled (explained later), and the only option that can be changed is Secure Boot Mode. Change this from Standard Mode and Custom Mode which is the only other option available:

Select Custom Secure Boot Options.

Each of the five options corresponds to a type of Secure Boot key. DB is the database of authorized signatures and DBX is that of forbidden signatures. Note that while the UEFI Specification uses the term "key", the entries stored in each type of "key" are in fact one of the aforementioned types of signatures; we'll follow the Specification and use "key" to refer to signatures in general. If we look into each of the entries:

We see the options to either enroll new keys or delete existing ones. There is no option to list existing keys, but the delete option can be used to do so. Doing so reveals that all the keys are empty. This is why Secure Boot cannot be enabled — the PK (Platform Key) must be present in order for Secure Boot to be enabled. We can create one ourselves, but recall that we saw an EnrollDefaultKeys.efi earlier.

Install default Secure Boot keys

Run EnrollDefaultKeys.efi in the EFI shell to install the default Secure Boot keys. This overwrites all existing keys:

Secure Boot is in Setup Mode when the PK is absent. Installing PK automatically transitions Secure Boot into User Mode (per UEFI Specification section 32.3, more on this later).

Exit the shell and look at Secure Boot Configuration again:

Now Secure Boot is enabled and the Attempt Secure Boot option is enabled and set.

Switch Secure Boot Mode to Custom again and list the keys:

  • PK: the Enroll PK option which was previously enabled is now disabled. This is because there can only be one PK on a system.
  • KEK: there are two entries named with GUID. All Secure Boot keys are associated with a SignatureOwner GUID which indicates the owner of the key:
  • d5c1df0b-1bac-4edf-ba48-08834009ca5a: this owner is unknown
  • 77fa9abd-0359-4d32-bd60-28f4e78f784b: this is Microsoft (explained later).
  • DB: there are two entries, both belonging to Microsoft (explained later).
  • DBX: here the keys are grouped into lists based on type. There is only 1 list present, with a type of SHA256. Note that all types of Secure Boot keys are stored in typed lists; it's just that the firmware here decided to display this information only for DBX. If we select the list:

We see the same unknown GUID that is in KEK, and the hash E3B0C44298FC1C149AFBF4C8996FB92427AE41E4649B934CA495991B7852B855 is just that of an empty file. This is most likely provided as an example. In an update-to-date system, the DBX is filled with entries of known vulnerable UEFI executables. The list of these executables and the binary blob to update DBX is maintained by the UEFI Forum here. Microsoft maintains a mirror of this list in a GitHub repo.

  • DBT: this remains empty.

Now if we try to boot:

Which is expected since neither the EFI shell nor our sample application are signed, nor are their signatures present in DB.

Create EFI signing certificate

To sign the sample application, we’ll first create the certificate. We’ll use command line tools available in the Windows SDK. The tools are located in %programfiles(x86)%\Windows Kits\10\bin\10.0.22621.0\x64\. The name of the subdirectories will vary based on the installed SDK's version.

First, use the following command in the command prompt to create a private key and its associated certificate:

MakeCert.exe -a sha256 -n "CN=SampleEfiSigner" -r -sv SampleEfiSigner.pvk SampleEfiSigner.cer

The output files would be in DER-encoded X.509 format, which is one of the supported types of DB signature.

Explanation of options:

  • -a The signature's digest algorithm (default is SHA1)
  • -n Certificate subject X509 name
  • -r Create a self signed certificate
  • -sv Subject's PVK file, to be created if not present

The command produces a popup prompt asking for the password to use for the private key file:

Set any password and press OK.

This produces a second prompt asking for the password that was set in the previous step. This is for the self-signing operation:

The output of the MakeCert command should just be:

Succeeded

Then, convert the .pvk file to .pfx so that it can be used by signtool to sign the UEFI executable:

pvk2pfx.exe -pvk SampleEfiSigner.pvk -pi <password> -spc SampleEfiSigner.cer -pfx SampleEfiSigner.pfx

Replace <password> with the password of the private key file. The command produces no output.

Explanation of options:

  • -pvk input PVK file name
  • -pi PVK file's password
  • -spc input SPC file name
  • -pfx output PFX file name

Sign the UEFI executable

Use signtool which is located in the same directory as the earlier tools:

signtool.exe sign /fd sha256 /ac SampleEfiSigner.cer /f SampleEfiSigner.pfx /p <password> <efiToSign>

Replace <password> with the password of the private key file. Replace <efiToSign> with the target's filename. The command should output:

Done Adding Additional Store
Successfully signed: <efiToSign>

Explanation of options:

  • /fd The file digest algorithm to use for creating file signatures (default is SHA1)
  • /ac Add an additional certificate to the signature block
  • /f File of the signing certificate. If the file does not contain private keys, use the "/csp" and "/kc".
  • /p The password for the PFX file

Open up the file’s properties window to verify that it has been signed. A new Digital Signatures tab should be present:

Install certificate into DB

Move SampleEfiSigner.cer into VisualUefi\samples\x64\Release\; we'll need to access it from the firmware menu later. From firmware menu, navigate to DB Options and select Enroll Signature:

Select Enroll Signature Using File.

Select the first option.

Select SampleEfiSigner.cer. Note that the firmware here supports only DER-encoded X.509 certificates and the UEFI executable itself. For the latter, it'll compute the executable's SHA-256 Authenticode hash and save it as the signature.

The UI returns to the Enroll Signature page. The Signature GUID field corresponds to the signature's SignatureOwner. Enter a random value or leave it blank to default to an all 0's GUID:

The signature is now present under the Delete Signature menu:

If the UEFI executable itself was selected instead of the certificate, then the entry’s description would be SHA256_GUID instead of PKCS7_GUID:

Now try booting:

Secure Boot security

Besides using the firmware UI to enroll signatures, one can also do so programmatically via the UEFI Runtime Service function SetVariable(). This can be invoked by both UEFI executables and OS executables (in Windows this is exposed as SetFirmwareEnvironmentVariable()), which begs the question - how is this secure? When Secure Boot is enabled, only trusted UEFI executables are allowed to run, so it would be reasonable that they're also allowed to modify the signatures. However, the OS can run untrusted code and malwares are prime examples - they can modify the DB to whitelist their bootkits, or remove vulnerable executables from DBX. Windows do restrict only privileged users to invoke SetFirmwareEnvironmentVariable() (e.g. administrators), but this can still be bypassed via privilege escalation attacks. This is where PK and KEK comes in.

When Secure Boot is in Setup Mode (i.e. PK is absent), users can modify any Secure Boot key via firmware UI or SetVariable() without any restriction by the firmware. Once Secure Boot enters User Mode (i.e. PK is installed), users can continue to modify Secure Boot keys via firmware UI; but for SetVariable(), the firmware would now require the to-be-written data to be signed. The target Secure Boot key determines which signing key is required:

  • DB, DBX: signed by private key corresponding to either PK or KEK
  • PK, KEK: signed by private key corresponding to PK

A single type of key would have sufficed to secure DB and DBX but a distinction was made between PK and KEK to demarcate different levels of ownership of the Secure Boot keys. The PK is owned by the computer hardware’s OEM and there is only one per computer. For HP and Dell:

The PK’s role is to control installation of KEK. There can be multiple KEKs per computer, each corresponding to an entity that is trusted to control installation of signatures into DB and DBX. Typically this is just the OEM and the Operating System. For HP and Dell:

For Windows systems, this will be Microsoft’s KEK:

  • Microsoft Corporation KEK CA 2011
  • Microsoft Corporation KEK 2K CA 2023

CA 2023 is meant to replace CA 2011 which is expiring in 2026.

For more details on Secure Boot, see UEFI Specification chapter 32 and Microsoft’s documentation on Secure Boot key creation and management.

Microsoft keys as defaults

For website certificates, whether to trust them is just a matter of checking whether they chain to trusted root or intermediate certificates from CAs (Certificate Authorities). These often come preinstalled in the OS and updates are also handled by the OS.

However, if one wants to distribute a UEFI executable compatible with Secure Boot, there is no CA that they can approach to obtain a signing certificate, nor is there one to sign their executable.

For Windows, its boot manager (which is a UEFI application) is able to run right out of the box with Secure Boot enabled because Microsoft works with OEMs to install Microsoft’s KEK and DB keys in all retail PCs and motherboards during manufacturing. Not everyone has the ability to do likewise, but thankfully Microsoft offers a UEFI signing service (akin to their Windows driver signing service). Once signed, a UEFI executable would be Secure-Boot-compatible with the same range of hardware as Windows.

This arrangement means that Microsoft is the de facto Secure Boot CA in all but name, and this is also why Microsoft’s keys are treated as defaults. Besides the aforementioned KEK, the following certificates are also installed in the DB in Windows systems:

  • Microsoft Windows Production PCA 2011
  • Windows UEFI CA 2023

The Windows boot manager is signed with one of the above two keys.

  • Microsoft Corporation UEFI CA 2011
  • Microsoft UEFI CA 2023

All third party UEFI executables are signed with one of the above two keys.

Recall that two entries were created in DB after running EnrollDefaultKeys.efi - these are the 2011 certificates. The 2023 certificates are meant to replace the 2011 ones which are expiring in 2026.

Besides directly signing UEFI executables, other options would be for Microsoft to:

  • issue certificates that chain to their UEFI CA certificate and install them into DB via OS updates.
  • use their KEK to sign DB updates submitted by developers to install whatever signature the developers want.

However, these options provide more avenues for bad actors to distribute bootkits to all Windows systems (either by posing as legitimate developers when submitting requests to Microsoft, or by stealing the signing keys of trusted developers). Microsoft certainly does not want to take on the responsibility of vetting submitters and given the critical nature of boot modules in a system’s security and stability, it is in Microsoft’s interest to ensure that they:

  • are not malicious, and do not provide any means to bypass Secure Boot (e.g. execute unsigned code)
  • do not cause compatibility issues with Microsoft’s own boot modules

This explains why the only option offered by Microsoft is to submit the executable to them for signing.

This arrangement also means that should any vulnerability be found in a Microsoft-signed UEFI executable, the only way to blacklist it is to add its Authenticode hash to DBX — Microsoft’s UEFI CA certificate cannot be added to DBX as this would also blacklist all other non-vulnerable executables. This is how the DBX ended up with hundreds of entries today due to vulnerabilities with the Windows boot manager (BlackLotus) and shim/GRUB2 (Boot Hole).

Customizing keys

For more control and security, one may wish to remove unnecessary default keys and/or install custom keys. One may also not want to, or simply cannot submit their UEFI executable to Microsoft for signing:

Microsoft UEFI CA signs only those products that are for public availability and are needed for inter-operability across all UEFI Secure Boot supported devices. If a product is specific to a particular OEM or organization and is not available externally, you should sign it with your private key and add the certificate to Secure Boot database.

All these means customizing the Secure Boot keys on the target computer. However, the default Secure Boot mode in retail systems is User Mode (i.e. PK is installed), so the only way to do so would be to manually access the system’s firmware in person. While doable for limited deployments, it is impractical for mass deployments (e.g. in enterprises) — visiting every system or collecting them back would require too much time and effort. Making the changes remotely would only be possible if the key updates were signed with the relevant keys (i.e. PK or KEK), but as explained earlier, neither OEMs nor Microsoft who own these keys would entertain such requests due to security concerns.

A possible alternative would be to have the OEM customize the keys or remove the PK before delivering the systems; however, this would most likely cost extra and would not address systems that have already been delivered.

Lastly, note that should the default PK and/or KEK be replaced with custom ones, then all subsequent updates to the KEK and/or DB/DBX by the OEM/Microsoft would fail. Thus if one wants those updates, then they must be signed with the private key corresponding to the custom PK/KEK before being deployed to affected computers. Signing and installation of the updates can be achieved with the aforementioned signtool.exe and PowerShell commands Format-SecureBootUEFI and Set-SecureBootUEFI. Details on how to do so are beyond the scope of this guide.

Running on actual systems

All the methods used to run UEFI executables in QEMU also apply to actual physical systems. The only differences to take note of are:

  1. The solution’s output directory would not be mounted as a volume in the system. Thus the executable(s) to run must be saved into a volume accessible by the firmware. This can be the ESP, a local FAT volume, or a FAT removable media.
  2. If BitLocker is enabled and is configured to use the TPM as a key protector, then disabling Secure Boot would prompt the system to enter BitLocker recovery mode. Depending on which PCR banks are configured to seal the BitLocker key, running UEFI executables, even signed ones, may also induce BitLocker recovery mode. Why this occurs is beyond the scope of this guide. Ensure that the BitLocker recovery key is available offline and suspend BitLocker before changing firmware settings and testing UEFI executables.
  3. Availability of the EFI shell is subject to the system’s brand and model. If present, it should be accessible from the firmware menu. If absent, it can be obtained from the following range of official edk2 releases: [edk2-stable201905, edk2-stable202002]. Older versions of the shell were committed as part of the edk2 repo in the ShellBinPkg folder. The folder was removed on 24/04/2019 and its contents relocated to the releases page. For the latest version, one can build it themselves or download it from third party sources, e.g. pbatard/UEFI-Shell. Note that all the edk2 releases are unsigned and the firmware’s built-in EFI shell may also be unsigned, so one must either disable Secure Boot or install their signatures into DB in order to run them.
  4. The firmware UI and available configurations will differ depending on the system’s brand and model, so the steps to change settings such as disabling Secure Boot and adding new boot and driver options would be different.

Other points to note

  1. The VisualUefi repo is no longer being actively maintained and is severely outdated — the edk2 commit it is synced to is from 2019 while for openssl it’s 2018. One would have to manually update the submodules to the latest commit from remote, then update the Visual Studio projects due to changes to the submodules’ source trees.
  2. VisualUefi builds only selected edk2 libraries and does not include all edk2 source code. One would have to create new Visual Studio projects to include additional libraries.
  3. Debugging UEFI executables can be challenging as there is no way to attach a debugger for live debugging. Logging to console can help but this would not be practical for large programs with heavy logging. Writing to log files is another solution, but one would then have to wait for the program to run to completion before they could examine the log files. Another solution would be to run the UEFI executable in a virtual machine and log to a pipe that is connected to the host. The host runs a program to read and print the pipe’s contents to a console so that one can review the logs at runtime. This requires modifying VisualUefi to build new edk2 libraries and also modifying edk2 source code.

--

--