Pull to refresh

Простые приемы реверс-инжиниринга UEFI PEI-модулей на полезном примере

Reading time 7 min
Views 38K
Здравствуйте, уважаемые читатели Хабра.

После долгого перерыва с вами опять я и мы продолжаем копаться во внутренностях UEFI. На этот раз я решил показать несколько техник, которые позволяют упростить реверс и отладку исполняемых компонентов UEFI на примере устаревшего-но-все-еще-популярного PEI-модуля SecureUpdating, который призван защищать прошивку некоторых ноутбуков HP от модификации.

Предыстория такова: однажды вечером мне написал знакомый ремонтник ноутбуков из Беларуси и попросил посмотреть, почему ноутбук с замененным VideoBIOS'ом не хочет стартовать, хотя такой же точно рядом успешно стартует. Ответ оказался на поверхности — не стартующий после модификации ноутбук имел более новую версию UEFI, в которую добрые люди из HP интегрировали защиту от модификации DXE-тома (а там и находится нужный нам VideoBIOS вместе с 80% кода UEFI), чтобы злобные вирусы и не менее злобные пользователи ничего там не сломали ненароком. Тогда проблема решилась переносом PEI-модуля SecureUpdating из старой версии UEFI в новую, но через две недели тот же человек обратился вновь, на этот раз на похожем ноутбуке старая версия модуля работать отказалась, и моя помощь понадобилась вновь.
Если вас заинтересовали мои дальнейшие приключения в мире UEFI PEI-модулей с дизассемблером и пропатченными переходами — добро пожаловать под кат.

Пара ссылок на ликбез

Если вам почти ничего не понятно — ничего страшного, у меня есть несколько поясняющих терминологию статей: 1, 2, 3, почитайте и возвращайтесь. Для фанатов оригинальной документации всегда в наличии спецификация UEFI PI, там все расписано намного подробнее.

Необходимые файлы и инструменты

Для разборки вышеупомянутой прошивки нам понадобятся:
  1. Собственно файл с прошивкой, мне был выслан вот этот.
  2. Любая утилита для работы с образами UEFI, я буду использовать UEFITool на правах ее автора, но вы можете использовать любую на ваш вкус, к примеру uefi-firmware-parser или PhoenixTool — это не принципиально.
  3. Hex-редактор на ваш выбор, я воспользуюсь HxD.
  4. Дизассемблер с поддержкой PE32-файлов, здесь нам идеально подойдет IDA 6.6 Demo, т.к. PEI-модули в подавляющем большинстве случаев 32-битные и ограничения демо-версии слишком сильно не помешают. Если уважаемый xvilka сможет показать, как в radare2 подгрузить структуры из C-файла, я попробую следующий мод сделать именно в нем, а пока IDA — наше все.
  5. Из комплекта efi-utils понадобится здоровенный файл behemoth.h, содержащий в себе определения практически всех возможных структур данных, используемых в UEFI. В нашем случае понадобятся всего пара-тройка.

Отправная точка

Итак, со слов товарища-ремонтника нам известно следующее: любое изменение DXE-тома приводит к мертвому ноутбуку, моргающему Caps-lock'ом, а изменения в других частях образа к такому исходу не приводят. Это значит, что где-то хранится либо контрольная сумма, либо ЭЦП, которая проверяется кодом одного из PEI-модулей, и, если она сошлась, управление передается в фазу DXE, а если нет — оно передается куда-то туда, где нам не рады.

Нам нужно выяснить следующее:
  1. Где именно хранится КС/ЭЦП?
  2. Кто ее проверяет?
  3. И, главное, как сделать так, чтобы проверка всегда заканчивалась успешно?

Поехали!


Делай раз!

Открываем файл с прошивкой в UEFITool и смотрим внимательно:

На вид ничего необычного, кроме сообщения о том, что внутри свободного пространства одного из UEFI-томов нашлись данные, которых по спецификации там быть не должно. Именно так производители (из тех, кто не очень верит в спецификацию) обычно прячут свои контрольные суммы или цифровые подписи. Двойным щелчком по сообщению выбираем том, в котором эти самые данные нашлись, и достаем его целиком в файл dxe.vol для анализа. UEFITool закрывать не надо — еще пригодится.

Открываем полученный файл Hex-редактором и рассматриваем, начиная с конца, ведь свободное место в томе может быть только там:

Тут же находится очень подозрительный кусок данных размером 100h (помечен красным), а за ним — сигнатура $SIG, версия прошивки F.50 и кодовое имя платформы 68CPK. Таким образом, на первый вопрос ответ, предположительно, получен.

Делай два!

Чтобы ответить на второй, нужно поискать PEI-модули, которые обращаются к этому блоку данных. Это бывает достаточно непросто и часто приходится пробовать несколько вариантов. Самый простой — поискать другие вхождения сигнатуры $SIG, но в данном случае нас сразу же постигает неудача — других вхождений такой строки в образе нет. Но если блок ищется не по сигнатуре, значит он ищется либо по смещению, либо по абсолютному адресу. Смещение его внутри тома — 12FEE0h. Переключаемся на UEFITool и ищем поиском без учета заголовков Hex-паттерн E0FF12 (процессоры Intel все еще LittleEndian, поэтому порядок байт пришлось поменять):


Ииии… БИНГО, всего 2 вхождения, и оба — в одном и том же PEI-модуле с многообещающим именем SecureUpdating. Вынимаем его без заголовков в файл su.bin для дальнейшего анализа:

Таким образом, предположительно, получен ответ и на второй вопрос.

Делай три!

Осталось разобраться с третьим. Для этого нужен дизассемблер, немного знаний об устройстве PEI-модулей и много-много терпения. Запускаем IDA, соглашаемся с условиями демонстрационного режима и открываем полученный ранее файл.
Идем в Options -> Compiler... и выставляем их вот таким образом:


Затем идем в File -> Load File -> Parse C header file... и загружаем вышеупомянутый в списке необходимых файл behemoth.h с определениями UEFI-структур:

На ошибки разбора обращать внимание не стоит — они в данном случае не навредят.

Теперь открываем вкладку Structures, идем в Edit -> Add structure type... (или нажимаем Insert, так быстрее), там нажимаем Add standard structure и в появившемся списке находим самую важную для PEI-файлов структуру — EFI_PEI_SERVICES, которую и добавляем:

Заодно добавим EFI_GUID и EFI_FFS_FILE_HEADER — пригодятся.

Структура EFI_PEI_SERVICES (если совсем точно — двойной указатель на её экземпляр, созданный ядром PEI) передается в качестве параметра в точку входа каждого PEI-модуля и практически во все его функции. Сделано так потому, что часть PEI вынуждена исполняться непосредственно из flash-памяти, которая в тот момент доступна только для чтения, поэтому глобальные переменные в таких PEI-модулях недоступны и все свое приходится носить с собой. Это неприятное ограничение для программиста, зато оно очень помогает в исследовании и отладке PEI-модулей, т.к. разыменование двойного указателя — не слишком популярная процедура в обычном коде, и потому большую часть вызовов PEI-сервисов можно отследить глазами прямо в листинге. Вот к нему и вернемся, но начала вспомним (или узнаем), как выглядит точка входа в PEI-модуль. Не спешите гуглить, выглядит она вот так:
EFI_STATUS 
EFIAPI 
PeimEntry(
IN EFI_FFS_FILE_HEADER *FfsFileHeader, 
IN EFI_PEI_SERVICES **PeiServices
);

EFI_STATUS — это typedef для unsigned int, EFIAPI — typedef для stdcall, первый параметр указывает на тот FFS-файл, в котором находится вызываемый PEI-модуль (на случай, если модуль хранит рядом с собой какие-то данные и ему нужен доступ к ним), а второй — уже описанный выше двойной указатель на таблицу PEI-сервисов. Вооружившись этим знанием, смело меняем тип функции start (выделив ее и нажав клавишу Y), получается примерно так:


Теперь по листингу видно следующее: сначала идет череда вызовов функций, для которых PeiServices не нужен. Чаще всего они занимаются вводом-выводом в/из IO-портов и прочей магией такого рода, проверим это предположение, перейдя в первую по порядку:


Действительно, функция выполняет вывод данных в порт 24Eh. Следующие несколько я опущу (они очень похожие, пишем-читаем IO-порты) и перейду к тем, которые PeiServices все же используют.
Первая оказывается тривиальной и просто сохраняет PeiServices в глобальную переменную (что указывает на то, что наш PEI-модуль выполняется уже из оперативной памяти, но зоркий глаз специалиста заметил бы это еще по информации о PE-файле в UEFITool):


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


Выделенный красным фрагмент сразу после пролога и обнуления локальных переменных — тот самый заметный паттерн с разыменованием двойного указателя, о котором я говорил выше. Чтобы понять, какой именно PEI-сервис был вызван, нам и нужны были все эти танцы вокруг структур, теперь можно установить курсор на [eax + 28h], нажать T и выбрать в появившемся окне EFI_PEI_SERVICES.GetBootMode:


Посмотрев ее сигнатуру, можно сделать вывод, что var_134 — это на самом деле переменная на стеке, в которую и будет записано значение текущего режима загрузки. Затем это значение сравнивается с 11h и если не равно — вычисления идут дальше, а если все же равно — кладем в eax ноль и уходим на return. 11h в данном случае — это BOOT_ON_S3_RESUME, т.е. если система просыпается из ACPI Sleep Mode, то функция всегда возвращает 0 (а это на местном наречии EFI_SUCCESS). Если же система загружается из другого состояния, то исполнение идет дальше, и в итоге проходит через вот такое интересное место:


Ба, старые знакомые! Те самые вхождения 12FEE0h, по которым мы этот модуль и нашли. И сначала функцией CopyMem та подозрительная КС/ЭЦП копируется в буфер, а затем оригинальное место затирается байтом FFh, которым пустое место в DXE-томе и заполнено изначально, а дальше следует вызов функции, которая эту самую КС/ЭЦП проверяет.
Можно, конечно, начать теперь исследовать ее, но ведь эта часть кода вообще не исполняется, если система просыпается из S3 (что логично, поскольку ничего из DXE-тома для S3 не нужно, зато просыпаться нужно как можно скорее), и все тем не менее работает, поэтому для начала сделаем так, чтобы этот конкретный PEI-модуль думал, что у нас вечное лето и всегда S3_RESUME, и пропускал любые проверки.
Для этого достаточно поменять cmp [ebp+BootMode], 11h на xor eax, eax, тогда следующий за ним jnz не выполнится никогда, но если он никогда не должен выполнится, то проще заменить сам переход на пару NOP'ов:


Меняем в Hex-редакторе выделенный фрагмент на 90 90 и все готово.

UPD


Внезапно выяснились некоторые новые обстоятельства. Оказывается, и в этой, «старой» версии защиты имеется копия PEI-тома, которая может быть использована системой для восстановления оригинального состояния PEI-тома, и в этой копии тоже нужно заменить модуль SecureUpdating на пропатченый. Копия хранится в файле с GUID 05B3AFFD-F7CC-4C0A-A19A-A9774E2675D7 типа RAW, поэтому содержимое этого файла в текущих версиях UEFITool'а не отображается:

На деле это файл типа Freeform, и чтобы получить доступ к его содержимому, нужно извлечь его через Extract as is..., заменить в извлеченном файле байт по смещению 12h (File type) с 01 на 02 и вставить полученный файл вместо оригинального через Replace as is...:

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

Заключение


Дальнейшее — дело техники. Заменяем через Replace Body… содержимое оригинальной PE32-секции на модифицированный файл, вносим нужные нам изменения в DXE-том, сохраняем изменения и прошиваем получившийся образ на программаторе. У меня этого ноутбука не было, сделал модификацию и отправил результаты просителю. Через пару часов был получен ответ: «огромное спасибо, все работает, клиент доволен», и я с чистой совестью пошел писать статью, которую вы только что прочитали.
Спасибо за внимание и удачных вам модификаций.
Tags:
Hubs:
+31
Comments 20
Comments Comments 20

Articles