Простые приемы реверс-инжениринга UEFI PEI-модулей на полезном примере
Здравствуйте, уважаемые читатели Хабра.После долгого перерыва с вами опять я и мы продолжаем копаться во внутренностях UEFI. На этот раз я решил показать несколько техник, которые позволяют упростить реверс и отладку исполняемых компонентов UEFI на примере устаревшего-но-все-еще-популярного PEI-модуля SecureUpdating, который призван защищать прошивку некоторых ноутбуков HP от модификации.Предыстория такова: однажды вечером мне написал знакомый ремонтник ноутбуков из Беларуси и попросил посмотреть, почему ноутбук с замененным VideoBIOS’ом не хочет стартовать, хотя такой же точно рядом успешно стартует. Ответ оказался на поверхности — не стартующий после модификации ноутбук имел более новую версию UEFI, в которую добрые люди из HP интегрировали защиту от модификации DXE-тома (а там и находится нужный нам VideoBIOS вместе с 80% кода UEFI), чтобы злобные вирусы и не менее злобные пользователи ничего там не сломали ненароком. Тогда проблема решилась переносом PEI-модуля SecureUpdating из старой версии UEFI в новую, но через две недели тот же человек обратился вновь, на этот раз на похожем ноутбуке старая версия модуля работать отказалась, и моя помощь понадобилась вновь.Если вас заинтересовали мои дальнейшие приключения в мире UEFI PEI-модулей с дизассемблером и пропатченными переходами — добро пожаловать под кат.
Пара ссылок на ликбез Если вам почти ничего не понятно — ничего страшного, у меня есть несколько поясняющих терминологию статей: 1, 2, 3, почитайте и возвращайтесь. Для фанатов оригинальной документации всегда в наличии спецификация UEFI PI, там все расписано намного подробнее.Необходимые файлы и инструменты Для разборки вышеупомянутой прошивки нам понадобятся: Собственно файл с прошивкой, мне был выслан вот этот. Любая утилита для работы с образами UEFI, я буду использовать UEFITool на правах ее автора, но вы можете использовать любую на ваш вкус, к примеру uefi-firmware-parser или PhoenixTool — это не принципиально. Hex-редактор на ваш выбор, я воспользуюсь HxD. Дизассемблер с поддержкой PE32-файлов, здесь нам идеально подойдет IDA 6.6 Demo, т.к. PEI-модули в подавляющем большинстве случаев 32-битные и ограничения демо-версии слишком сильно не помешают. Если уважаемый xvilka сможет показать, как в radare2 подгрузить структуры из C-файла, я попробую следующий мод сделать именно в нем, а пока IDA — наше все. Из комплекта efi-utils понадобится здоровенный файл behemoth.h, содержащий в себе определения практически всех возможных структур данных, используемых в UEFI. В нашем случае понадобятся всего пара-тройка. Отправная точка Итак, со слов товарища-ремонтника нам известно следующее: любое изменение DXE-тома приводит к мертвому ноутбуку, моргающему Caps-lock’ом, а изменения в других частях образа к такому исходу не приводят. Это значит, что где-то хранится либо контрольная сумма, либо ЭЦП, которая проверяется кодом одного из PEI-модулей, и, если она сошлась, управление передается в фазу DXE, а если нет — оно передается куда-то туда, где нам не рады.Нам нужно выяснить следующее:
Где именно хранится КС/ЭЦП? Кто ее проверяет? И, главное, как сделать так, чтобы проверка всегда заканчивалась успешно? Поехали! Делай раз! Открываем файл с прошивкой в 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 + 28], нажать 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 и все готово.
Заключение Дальнейшее — дело техники. Заменяем через Replace Body… содержимое оригинальной PE32-секции на модифицированный файл, вносим нужные нам изменения в DXE-том, сохраняем изменения и прошиваем получившийся образ на программаторе. У меня этого ноутбука не было, сделал модификацию и отправил результаты просителю. Через пару часов был получен ответ: «огромное спасибо, все работает, клиент доволен», и я с чистой совестью пошел писать статью, которую вы только что прочитали.Спасибо за внимание и удачных вам модификаций.