UEFI to Windows Communication via NVRAM Variables
bekoo

bekoo @bekoo

About: Reverse Engineer | Malware Developer

Location:
Turkey
Joined:
Dec 23, 2020

UEFI to Windows Communication via NVRAM Variables

Publish Date: Jul 20
0 0

In this article, we will discuss how to establish communication between a UEFI driver and a Windows driver. Specifically, we’ll explore how to use NVRAM-based UEFI variables to exchange data between the two environments.

What is NVRAM?

NVRAM (Non-Volatile RAM) is a key component in UEFI-based systems, used to store persistent configuration and system state data. Unlike regular RAM, NVRAM retains its contents across reboots and power-offs, making it essential for managing firmware-level settings.

In UEFI, NVRAM is organized into variables—structured key-value pairs that include metadata (attributes) defining how and when they can be accessed. These variables are used for critical system functions such as boot management, secure boot key storage, hardware configuration, and OEM-specific settings.

UEFI Variables

UEFI Variables is specified with a combination of a GUID and Unicode String. The GUID of a variable can prevent name collisions between different vendors.

The Boot Manager Chapter of UEFI Specification defines the EFIGLOBAL_VARIABLE_GUID, also known gEfiGlobalVariableGuid in EDK II. If we want to use a global variable, such as SecureBoot Status in our UEFI Driver, we need to benefit from the gEfiGlobalVariableGuid. Here are some global variable list from EDK II Repo:

  • EFI_PLATFORM_LANG_CODES_VARIABLE_NAME L"PlatformLangCodes"
  • EFI_BOOT_CURRENT_VARIABLE_NAME L"BootCurrent"
  • EFI_SIGNATURE_SUPPORT_NAME L"SignatureSupport"

[...]

Each UEFI Variable has attributes that the describe visibility persistence. Here's list of the attributes:

  • BOOTSERVICE_ACCESS

The variable has permissions for write and read access at the pre-boot before ExitBootServices() called, which means that the variable is not available after called ExitBootServices() and the contents of the variable will be deleted on the next system reset.

  • BOOTSERVICE_ACCESS | RUNTIME_ACCESS

The variable has permissions for write and read access before ExitBootServices() called, but its contents will remain as read-only when ExitBootServices() called, also the contents will be deleted on the next system reboot.

  • NON_VOLATILE | BOOTSERVICE_ACCESS

The variable has permissions for write and read access before ExitBootServices() called and its contents are persistent through system reset.

  • NON_VOLATILE | BOOTSERVICE_ACCESS | RUNTIME_ACCESS

The variable has permissions for write and read in both pre-boot and OS Runtime environment. Its contents are persistent through system reset.

Access UEFI Variables with Services

When a UEFI Driver store configuration information via UEFI Variables, it can access them with services provided by EFI_HII_CONFIG_ACCESS_PROTOCOL The Services SetVariable() and GetVariable() are used to set and get configuration information. We will see these two services through the article.

1 - SetVariable()

SetVariable() service sets the contents of the variable, also it can be used to create a new variable, modify the variable or to delete an existing variable. Here are parameters of the service:

 typedef
 EFI_STATUS
 SetVariable (
   IN CHAR16            *VariableName,
   IN EFI_GUID          *VendorGuid,
   IN UINT32            Attributes,
   IN UINTN             DataSize,
   IN VOID              *Data
);
Enter fullscreen mode Exit fullscreen mode

Let's create a project for SetVariable:

#include <Uefi.h>
#include <Library/UefiApplicationEntryPoint.h>
#include <Library/UefiLib.h>
#include <Library/PcdLib.h>
#include <Library/UefiBootServicesTableLib.h>
#include <Library/UefiRuntimeServicesTableLib.h>

EFI_STATUS EFIAPI UefiMain(EFI_HANDLE ImageHandle, EFI_SYSTEM_TABLE *SystemTable) {
  CHAR16 *VariableName = L"MyUEFIVar";
  EFI_GUID VendorGuid = { 0xa1b2c3d4, 0x1234, 0x5678, {0x9a,0xbc,0xde,0xf1,0x23,0x45,0x67,0x89} };
  UINT8 ContentOfVariable[4] = { 0xDE, 0xAD, 0xBE, 0xEF };
  UINTN DataSize = sizeof(ContentOfVariable);
  EFI_STATUS Status = EFI_SUCCESS;

  UINT32 Attributes = EFI_VARIABLE_NON_VOLATILE |
                      EFI_VARIABLE_BOOTSERVICE_ACCESS |
                      EFI_VARIABLE_RUNTIME_ACCESS;


  Status = gRT->SetVariable(
      VariableName,
      &VendorGuid,
      Attributes,
      DataSize,
      &ContentOfVariable
  );
  if (EFI_ERROR(Status)) {
      Print(L"Write Operation Failed!\n");
      return Status;
  }
  Print(L"The content successfully written!\n");

  return EFI_SUCCESS;
}
Enter fullscreen mode Exit fullscreen mode

In the code, we wrote the content of ContentOfVariable. Firstly, we created a variable name, GUID and values for UEFI Variable, then we executed the SetVariable. As a result of these processes, our UEFI Variable has these information:

  • UEFI Variable Name: MyUEFIVar
  • GUID: A1B2C3D4-1234-5678-9ABC-DEF123456789
  • Content: 0xDEADBEEF

Here's result:

Now we can access it with these information.

2 - GetVariable()

GetVariable() service get content of the variable. Here are parameters of the service:

typedef
EFI_STATUS
GetVariable (
  IN CHAR16           *VariableName,
  IN EFI_GUID         *VendorGuid,
  OUT UINT32          *Attributes OPTIONAL,
  IN OUT UINTN        *DataSize,
  OUT VOID            *Data OPTIONAL
 );
Enter fullscreen mode Exit fullscreen mode

Now let's develop the project with GetVariable service:

#include <Uefi.h>
#include <Library/UefiApplicationEntryPoint.h>
#include <Library/UefiLib.h>
#include <Library/PcdLib.h>
#include <Library/UefiBootServicesTableLib.h>
#include <Library/UefiRuntimeServicesTableLib.h>

EFI_STATUS EFIAPI UefiMain(EFI_HANDLE ImageHandle, EFI_SYSTEM_TABLE *SystemTable) {
  CHAR16 *VariableName = L"MyUEFIVar";
  EFI_GUID VendorGuid = { 0xa1b2c3d4, 0x1234, 0x5678, {0x9a,0xbc,0xde,0xf1,0x23,0x45,0x67,0x89} };
  UINT8 ContentOfVariable[4] = { 0xDE, 0xAD, 0xBE, 0xEF };
  UINTN DataSize = sizeof(ContentOfVariable);
  EFI_STATUS Status = EFI_SUCCESS;

  UINT32 Attributes = EFI_VARIABLE_NON_VOLATILE |
                      EFI_VARIABLE_BOOTSERVICE_ACCESS |
                      EFI_VARIABLE_RUNTIME_ACCESS;


  Status = gRT->SetVariable(
      VariableName,
      &VendorGuid,
      Attributes,
      DataSize,
      &ContentOfVariable
  );
  if (EFI_ERROR(Status)) {
      Print(L"Failed to write!\n");
      return Status;
  }
  Print(L"The content successfully written!\n");

  UINT8 Content[4] = { 0 };
  DataSize = sizeof(Content);
  Status = gRT->GetVariable(
      VariableName,
      &VendorGuid,
      &Attributes,
      &DataSize,
      &Content
  );
  if (EFI_ERROR(Status)) {
    Print(L"Failed to read the variable!\n");
    return Status;
  }

  for (int x = 0; x < DataSize; x++) {
    Print(L"Content: 0x%02x\n", Content[x]);
  }
  return EFI_SUCCESS;
}
Enter fullscreen mode Exit fullscreen mode

After SetVariable called, we called GetVariable service to get content of the our UEFI Variable, then we printed the result. Here's the result:

And now we can focus our Windows Driver.

Coding Windows Driver

Essentially there's a routine which we can use for our purpose in ntoskrnl. ExGetFirmwareEnvironmentVariable can be used to read UEFI Variables. So coding of the windows driver will not be difficult. We can call this routine with UEFI Variable information:

#include <ntddk.h>

NTSTATUS DriverUnload(PDRIVER_OBJECT DriverObject)
{
    UNREFERENCED_PARAMETER(DriverObject);
    DbgPrint("Driver unloaded.\n");

    return STATUS_SUCCESS;
}

NTSTATUS DriverEntry(PDRIVER_OBJECT DriverObject, PUNICODE_STRING RegistryPath)
{
    UNREFERENCED_PARAMETER(RegistryPath);
    DriverObject->DriverUnload = DriverUnload;

    UNICODE_STRING VariableName = RTL_CONSTANT_STRING(L"MyUEFIVar");
    UINT8 Content[4] = { 0 };
    ULONG BufferSize = sizeof(Content);
    GUID VendorGuid = {
        0xa1b2c3d4, 0x1234, 0x5678, {0x9a,0xbc,0xde,0xf1,0x23,0x45,0x67,0x89}
    };
    NTSTATUS Status = STATUS_SUCCESS;

    Status = ExGetFirmwareEnvironmentVariable(&VariableName, &VendorGuid, &Content, &BufferSize, NULL);
    if (!NT_SUCCESS(Status)) {
        DbgPrintEx(0, 0, "Failed to Read Data!\n");
        return Status;
    }

    for (ULONG x = 0; x < BufferSize; x++) {
        DbgPrintEx(0, 0, "The value from UEFI Variable: 0x%02x\n", Content[x]);
    }

    return STATUS_SUCCESS;
}

Enter fullscreen mode Exit fullscreen mode

In the project, we accessed the content of the our UEFI Variable with ExGetFirmwareEnvironmentVariable routine. Here's result:

When ExGetFirmwareEnvironmentVariable called, The following routines are called after the routine:

First of all it calls ExpGetFirmwareEnvironmentVariable:

And then the parameters of the ExGetFirmwareEnvironmentVariable are passed to IoGetEnvironmentVariableEx:

Creating UEFI Variable from Windows Driver

Also we can create and set UEFI Variable from Windows Driver. ExSetFirmwareEnvironmentVariable can be used for these purpose:

#include <ntddk.h>
#pragma warning(disable: 4057)

#define EFI_VARIABLE_NON_VOLATILE        0x00000001
#define EFI_VARIABLE_BOOTSERVICE_ACCESS  0x00000002
#define EFI_VARIABLE_RUNTIME_ACCESS      0x00000004

NTSTATUS DriverUnload(PDRIVER_OBJECT DriverObject)
{
    UNREFERENCED_PARAMETER(DriverObject);
    DbgPrintEx(0, 0, "Driver unloaded.\n");

    return STATUS_SUCCESS;
}

NTSTATUS DriverEntry(PDRIVER_OBJECT DriverObject, PUNICODE_STRING RegistryPath)
{
    UNREFERENCED_PARAMETER(RegistryPath);
    DriverObject->DriverUnload = DriverUnload;

    UNICODE_STRING VariableName = RTL_CONSTANT_STRING(L"YUPIIIIIII");
    WCHAR Buffer[] = L"HELLOOO from Windows!!";
    ULONG BufferSize = sizeof(Buffer);
    GUID VendorGuid = {
        0xa1b2c3d4, 0x1234, 0x5678, {0x9a,0xbc,0xde,0xf1,0x23,0x45,0x67,0x89}
    };
    NTSTATUS Status = STATUS_SUCCESS;

    UINT32 Attributes = EFI_VARIABLE_NON_VOLATILE | EFI_VARIABLE_BOOTSERVICE_ACCESS | EFI_VARIABLE_RUNTIME_ACCESS;

    Status = ExSetFirmwareEnvironmentVariable(&VariableName, &VendorGuid, &Buffer, BufferSize, Attributes);
    if (!NT_SUCCESS(Status)) {
        DbgPrintEx(0, 0, "Failed to write the value! Error Code: 0x%x\n", Status);
        return Status;
    }
    DbgPrintEx(0, 0, "Done!\n");


    return STATUS_SUCCESS;
}
Enter fullscreen mode Exit fullscreen mode

And UEFI Driver:

#include <Uefi.h>
#include <Library/UefiApplicationEntryPoint.h>
#include <Library/UefiLib.h>
#include <Library/PcdLib.h>
#include <Library/UefiBootServicesTableLib.h>
#include <Library/UefiRuntimeServicesTableLib.h>

EFI_STATUS EFIAPI UefiMain(EFI_HANDLE ImageHandle, EFI_SYSTEM_TABLE *SystemTable) {
  CHAR16 *VariableName = L"YUPIIIIIII";
  EFI_GUID VendorGuid = { 0xa1b2c3d4, 0x1234, 0x5678, {0x9a,0xbc,0xde,0xf1,0x23,0x45,0x67,0x89} };
  CHAR16 Buffer[32];
  UINTN DataSize = sizeof(Buffer);
  EFI_STATUS Status = EFI_SUCCESS;

  UINT32 Attributes = EFI_VARIABLE_NON_VOLATILE |
                      EFI_VARIABLE_BOOTSERVICE_ACCESS |
                      EFI_VARIABLE_RUNTIME_ACCESS;

  Status = gRT->GetVariable(VariableName, &VendorGuid, &Attributes, &DataSize, &Buffer);
  if (EFI_ERROR(Status)) {
      Print(L"Failed to read the data!\n");
      return Status;
  }
  Print(L"The Content: %s\n", Buffer);

  return EFI_SUCCESS;
}
Enter fullscreen mode Exit fullscreen mode

Here's result:

Conclusion

In this article, we demonstrated how UEFI and Windows drivers can communicate with each other using NVRAM variables. By leveraging SetVariable and GetVariable in UEFI, and ExGetFirmwareEnvironmentVariable / ExSetFirmwareEnvironmentVariable in Windows, we established a persistent and reliable data channel between the two environments.

This method enables developers to build seamless integrations between firmware and the OS, allowing for flexible configurations, feature toggles, or telemetry mechanisms across reboots.

References

  1. UEFI.org - Runtime Services

  2. TianoCore Docs - GetVariable() and SetVariable()

Comments 0 total

    Add comment