Здравствуй, читатель. За время моего почти десятилетнего отсутствия в мире x86 и UEFI довольно много воды утекло, и то, что раньше считалось интересными, но мало кому нужными настройками прошивки (к примеру, Above 4G Decoding и Resizable BAR) теперь считается фичами первой необходимости (без которых современные видеокарты Nvidia и AMD теряют в производительности, а карты Intel могут и вовсе не работать). При этом прогресс не превратил мощные старые системы в совсем уж полный хлам, и потому есть смысл научить этих старых псов новым трюкам, если это возможно.


Во многих случаях вопрос с Resizable BAR довольно успешно решается добавлением драйвера ReBarDXE в DXE-том прошивки, и редактированием NVRAM для включения Above 4G Decoding. К сожалению, наш замечательный DXE-драйвер должен отработать в самом начале фазы PCI Enumeration, и потому не может быть запущен через какие-либо другие механизмы, кроме добавления в DXE-том прошивки.

К сожалению, мы здесь оказываемся с другой стороны баррикад относительно производителя системы, который должен со своей стороны препятствовать добавлению в прошивку своих устройств всяких непонятных DXE-драйверов с горы. Делают они это с переменным успехом, у некоторых получается лучше, у некоторых - хуже, а некоторые заранее отказываются от любой "безопасности".

В этот вот конкретном случае ко мне на верстак попала прошивка рабочей станции HP Z840 (материнская плата M60, версия 2.61), которая упрямо отказывалась стартовать после любых (даже минимальных и неинвазивных) модификаций DXE-тома, и при этом не была накрыта Intel BootGuard, т.е. являлась корнем доверия самой себе. В HP, конечно, прекрасно понимали, что цена такой "защиты" - грош, и потому на более новых системах реализовали и BootGuard, и собственную систему мониторинга и восстановления "неавторизованных" изменений в прошивке (маркетинговое название SureStart).

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

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

В поисках подозреваемого

Для того, чтобы разобрать прошивку подопытной системы и снять мешающую защиту, нам потребуются:

Качаем, распаковываем UEFI-капсулу из CAB-архива, открываем M60_0261.cap в UEFITool NE, видим следующее:

Том DXE
Том DXE

Кроме UEFI-томов в этом файле также присутствуют непустые регионы (которые UEFITool называет Padding), в этой прошивки их целых пять. Чаще всего в них хранятся данные DMI, прошивка Embedded Controller'а, и прочее подобное, но и хеши\подписи следует тоже поискать в них в первую очередь.

Для того, чтобы проверить хеш\подпись какого-то региона прошивки, его требуется сначала найти, причем быстро (потому что проверка эта - довольно долгая процедура, каждый раз замедляющая загрузку). Скорее всего, где-то в прошивке имеется карта содержимого SPI-чипа, в которой перечислены все "интересные" места, включая и сам DXE-том, и место, где хранится его хеш\подпись. Чтобы проверить, если ли у прошивки такая карта на самом деле, достаточно поискать все вхождения адреса (либо смещения, либо размера) какого-либо тома, и посмотреть, где именно они найдутся. Так и поступим:

Ищем адрес 0хFF6A1000, не забывая об обратном порядке байт
Ищем адрес 0хFF6A1000, не забывая об обратном порядке байт

Внезапно, мы нашли не только PEI/DXE/SMM с говорящими названиями вроде FlashInfoPei, но и два последовательных (с разницей в 16 байт) вхождения адреса DXE-тома внутри одного из Padding'ов, присмотримся к этому вхождению повнимательнее:

Адрес и размер DXE-тома дважды, и наш подозреваемый
Адрес и размер DXE-тома дважды, и наш подозреваемый

Опа, здравствуйте, непонятный блок с высокой энтропией сразу после адреса и размера DXE-тома, вы подозрительно похожи на криптографическую подпись. Теперь нужно понять, кто именно проверяет эту подпись, и пояснить ему, что проверять нужно менее тщательно.

Для этого вернемся к уже упомянутому выше PEI-модулю с картой (FlashInfoPei, он же 9219007F-D094-4761-9EB5-C14CF9D716C0). Откроем его в IDA, обработаем плагином efiXplorer, и увидим, что все, что этот модуль делает - публикует PEI-to-PEI Interface (структура с указателями на функции, которые могут вызывать другие PEI-модули) с GUID 7BBE8ED7-9F34-4CFA-A586-E74F10C6C788 и единственной функцией GetFlashInfo(), которая после (тривиальной) декомпиляции и небольшой разметки выглядит вот так:

EFI_STATUS GetFlashInfo(UINT32 *a)
{
  if ( !a )
    return EFI_INVALID_PARAMETER;
  a[0] = 0xFF600000;  // Физический адрес региона BIOS
  a[1] = 0xA00000;    // Размер региона BIOS
  a[2] = 0x1000000;   // Полный размер SPI-чипа
  // Основной PEI-том
  a[3] = 0xFFEB8000;  // Физический адрес
  a[4] = 0x8B8000;    // Смещение внутри региона BIOS 
  a[5] = 0x146000;    // Размер
  // Часть паддинга перед основным PEI-томом
  a[6] = 0xFFEB0000;  
  a[7] = 0x8B0000;    
  a[8] = 0x7000;      
  // Паддинг перед томом с микрокодами
  a[9] = 0xFFE7F000;  
  a[10] = 0x87F000;   
  a[11] = 0x1000;     
  // Часть паддинга перед DXE-томом
  a[12] = 0xFF620000; 
  a[13] = 0x20000;    
  a[14] = 0x40000;    
  // Свободное место
  a[15] = 0xFFEB7000;
  a[16] = 0x8B7000;
  a[17] = 0x1000;
  // Запасной PEI-том
  a[18] = 0xFFD39000;
  a[19] = 0x739000;
  a[20] = 0x146000;
  // Часть паддинга перед запасным PEI-томом
  a[21] = 0xFFD31000;
  a[22] = 0x731000;
  a[23] = 0x7000;
  // Подпись основного PEI-тома
  a[24] = 0xFFD00000;
  a[25] = 0x700000;
  a[26] = 0x1000;
  // Прошивка EC
  a[27] = 0xFF660000;
  a[28] = 0x60000;
  a[29] = 0x40000;
  // Свободное место
  a[30] = 0xFFD38000;
  a[31] = 0x738000;
  a[32] = 0x1000;
  // Boot-блок (часть прошивки, которая не обновляется)
  a[33] = 0xFFE7F000;
  a[34] = 0x87F000;
  a[35] = 0x181000;
  // NVRAM-том
  a[36] = 0xFFCA0000;
  a[37] = 0x6A0000;
  a[38] = 0x40000;
  // Хранилище переменных VSS2
  a[39] = 0xFFCA0000;
  a[40] = 0x6A0000;
  a[41] = 0x1E000;
  // Часть паддинга перед DXE-томом
  a[42] = 0xFF600000;  
  a[43] = 0;           
  a[44] = 0x10000;     
  // Подпись DXE-тома
  a[45] = 0xFF6A0000;  
  a[46] = 0xA0000;     
  a[47] = 0x1000;      
  // DXE-том
  a[48] = 0xFF6A1000;
  a[49] = 0xA1000;     
  a[50] = 0x5FF000;
  // Пустая запись
  a[51] = 0;
  a[52] = 0xFFFFFFFF;
  a[53] = 0;
  // Пустая запись
  a[54] = 0;
  a[55] = 0xFFFFFFFF;
  a[56] = 0;
  // Основной том с микрокодами
  a[57] = 0xFFE80000;
  a[58] = 0x880000;
  a[59] = 0x30000;
  // Запасной том с микрокодами
  a[60] = 0xFFD01000;
  a[61] = 0x701000;
  a[62] = 0x30000;
  // Всё
  return 0;
}

Итого в карте, который FlashInfoPei предоставляет всем остальным PEI-модулям, нашлись и сам DXE-том, и блок по адресу 0xFF6A0000 со скриншота выше.

Теперь посмотрим, кто именно является потребителем этой важной информации, и выберем среди них модуль, который либо сам считает какие-либо хеши, либо предоставляет эту возможность другим модулям. Для этого поищем GUID нашего FlashInfoPpi:

Искомый GUID нашелся внутри DEPEX-секции
Искомый GUID нашелся внутри DEPEX-секции

Ага, значит у нас тут есть модуль, которому так нужна карта региона BIOS, что он не запустится, пока её не опубликуют. При этом его размер 140 Кб, а сам он (небольшой спойлер) реагирует на кличку PeiCrptDriver, и содержит в себе ASCII-строки вроде "SHA-256 part of OpenSSL 0.9.8w 23 Apr 2012". Вот он, наш подозреваемый!

Препарируем подозреваемого

Вынимаем PE32-образ из прошивки при помощи UEFITool NE, открываем его в IDA 9.x, обрабатываем плагином efiXplorer (который любезно подтянет нужные типы и определения автоматически), и после декомпиляции и небольшой разметки получаем следующее:

Точка входа в PeiCrptDriver
Точка входа в PeiCrptDriver

Оказывается, что драйвер этот сначала проверяет, что мы не находимся на пути просыпания из S3 Sleep (если так, то ничего ни проверять не требуется), затем производит некие проверки, а затем публикует PPI с GUID FF3D0A61-2245-424D-8DBE-7B6744B72E75, которым могут воспользоваться другие PEI-модули в дальнейшем.

Посмотрим, что там за проверки, и что за функции публикует подозреваемый в своем PPI.

Пока что непонятные функции, которые публикует подозреваемый
Пока что непонятные функции, которые публикует подозреваемый

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

После непродолжительного реверс-инжениринга, получаем примерно следующее:

Результаты реверса
Результаты реверса

Первые 8 функций - это просто примитивы для хеширования буферов произвольного размера и расположения алгоритмами SHA1 и SHA256, затем идет интересная HashAndCompare, а все последующие, в том числе упомянутая выше DoVerifications, так или иначе вызывают HashAndCompare, и их результат напрямую зависит от нее.
Таким образом, для того, чтобы этот драйвер перестал проверять какие-либо хеши\подписи и не мешал модификации DXE тома, нам нужно сделать так, чтобы HashAndCompare прекратила что-то там хешировать и сравнивать, и начала возвращать успех при любых входных данных.

Выглядит наша функция вот так:

HashAndCompare  proc near 

arg_0           = dword ptr  8
arg_4           = dword ptr  0Ch
arg_8           = dword ptr  10h
arg_C           = dword ptr  14h

push    ebp
mov     ebp, esp
xor     eax, eax ; Теперь в EAX лежит 0
cmp     [ebp+arg_0], eax ; Сравним первый параметр с 0
jz      short loc_error ; Если равно - это ошибка, выполняем переход
cmp     [ebp+arg_4], eax ; Сравним второй паметр с 0
jz      short loc_error ; И т.п.
cmp     [ebp+arg_8], eax
jz      short loc_error
cmp     [ebp+arg_C], 7FFFFFFFh
ja      short loc_error
cmp     [ebp+arg_C], eax
jz      short loc_error
; 
; Здесь реальная работа функции, которая нас не интересует
;
retn
loc_error:  
xor     al, al ; Теперь в AL лежит 0, это возвращаемое значение
pop     ebp 
retn
HashAndCompare  endp

Итого, при ошибке функция возвращает 0 в регистре AL, т.е. для того, чтобы она всегда возвращала 1, нам потребуется превратить ее в следующее:

push    ebp
mov     ebp, esp
xor     eax, eax ; Теперь в EAX лежит 0
inc     eax ; Теперь в AL лежит 1, это возвращаемое значение
pop     ebp 
retn

Команда cmp [ebp+8], eax в бинарном виде занимает 3 байта (39 45 08), и комбинация inc eax, pop ebp, retn тоже занимает 3 байта (40 5D C3), поэтому достаточно заменить одно на другое, и наш драйвер, и все пользователи его PPI - все будут твердо уверены в том, что прошивка прошла все проверки и не модифицирована никем, век воли не видать!

Итого: ищем в нашем драйвере последовательность 33 C0 39 45 08 74, меняем на 33 C0 40 5D C3 74, проверяем - загружается. Добавляем в DXE-том драйвер ReBarDXE - загружается. Настраиваем все остальное, вставляем видеокарту Intel Arc A770, которая раньше не работала - и теперь она работает!

Результат успешной модификации
Результат успешной модификации

Заключение

В этот раз нам повезло, и не пришлось выдумывать обход BootGuard или SureStart, и получилось достаточно быстро найти и нейтрализовать мешавший код, но на более современных машинах так просто уже не будет, потому что BootGuard не даст модифицировать содержимое PEI-тома, и патчить модули из него не получится без предварительной замены чипсета или поиска гораздо более интересных уязвимостей чем "мы храним код проверяльщика на том же устройстве, которое проверяем".