Pull to refresh

Статический анализ «BIOS/UEFI» или как получить Dependency Graph

Reading time13 min
Views10K
«Я вчера закончил ковку,
Я два плана залудил…»
… V.S. Vysotsky song...

Уже почти 3 года назад (в начале 2016) в issue проекта UEFITool на GitHub появилось пожелание пользователей: построить «Dependency Graph» для исполняемых модулей, входящих в BIOS/UEFI.

Завязалось даже небольшое обсуждение, в результате которого окончательно выяснилось, что данная задача отнюдь не тривиальна, имеющегося функционала для её решения недостаточно, перспективы в тот момент туманны…

И остался этот вопрос в подвешенном состоянии, с перспективой реализации в неопределённом будущем (но желание, наверное, осталось, а надежда, как известно, умирает последней!).

Есть предложение: отыскать, наконец, решение этой проблемы!

Определяемся с терминами


В дальнейшем предполагается, что мы имеем дело с Intel 64 и IA-32 Architecture.

Для того, чтоб однозначно определиться, что ж мы всё-таки решили построить, нам придётся более детально разобраться с функционированием отдельных фаз работы BIOS/UEFI.

Если внимательно посмотреть на типы файлов, представленные в FFS томов прошивки, то окажется, что большая часть из имеющихся файлов имеет в своём составе секцию с исполняемыми модулями.

Даже если рассматривать новомодные прошивки ASUS или ASRock, в которых можно не напрягаясь найти до полутора сотен файлов типа EFI_FV_FILETYPE_FREEFORM, содержащих картинки разных форматов, тем не менее, даже в этих прошивках исполняемых файлов пока больше, чем файлов других типов.

+--------------------------------------------------------------------------+
|  File Types Information                                                  |
+--------------------------------------------------------------------------+
|    EFI_FV_FILETYPE_RAW                     =    6                        |
|    EFI_FV_FILETYPE_FREEFORM                =   83                        |
|    EFI_FV_FILETYPE_SECURITY_CORE           =    1                        |
|    EFI_FV_FILETYPE_PEI_CORE                =    1                        |
|    EFI_FV_FILETYPE_DXE_CORE                =    1                        |
|    EFI_FV_FILETYPE_PEIM                    =   57                        |
|    EFI_FV_FILETYPE_DRIVER                  =  196                        |
|    EFI_FV_FILETYPE_APPLICATION             =    1                        |
|    EFI_FV_FILETYPE_SMM                     =   60                        |
|    EFI_FV_FILETYPE_SMM_CORE                =    1                        |
|    EFI_FV_FILETYPE_PAD                     =    4                        |
+--------------------------------------------------------------------------+
|  Total Files :                             =  411                        |
+--------------------------------------------------------------------------+
Пример состава какой-то обычной (рядовой) прошивки.

Хотя в этой таблице и не помечены файлы, имеющие в своём составе исполняемые модули, тем не менее, это будут (по определению) все в данном списке, кроме файлов с суффиксами RAW, FREEFORM и PAD.

Файлы с суффиксом «CORE» (SECURITY_CORE, PEI_CORE и DXE_CORE) – это соответствующие «ядра» (головные модули соответствующей фазы), получающие управление из других фаз (или после старта), SMM_CORE является суб-фазой DXE-фазы и вызывается во время её выполнения. APPLICATION может выполняться только по запросу пользователя, конкретной привязки к фазам не имеет.

Остались не перечисленными самые распространённые типы файлов: PEIM (модули PEI-фазы), DRIVER (модули DXE-фазы) и SMM (модули суб-фазы DXE). В состав CORE-модулей фаз PEI и DXE входит диспетчер, который и управляет последовательностью загрузки/запуска модулей соответствующей фазы.

В приведённом выше примере отсутствуют комбинированные варианты, мы о них и не будем вспоминать: в реальных прошивках они хоть и встречаются, но достаточно редко. Желающим получить более детальную и подробную информацию предлагаем обратиться к статьям CodeRush 1, 2, 3. А также процитируем его совет: «Для фанатов оригинальной документации всегда в наличии спецификация UEFI PI, там все расписано намного подробнее.»

Каждый исполняемый модуль прошивки является модулем формата PE+ (Portable Executable) или производным от него (Terse Executable:TE-формат). Исполняемый модуль PE+ формата представляет собой набор «слегка» упакованных структурированных данных, содержащих информацию, необходимую загрузчику для отображения данного модуля в память.

Сам формат (структура) PE+ не располагает каким-либо механизмом взаимодействия между отдельными PE+ модулями. Каждый исполняемый модуль после загрузки и начала выполнения представляет собой автономный независимый процесс, (ну должно быть так!), т.е. модуль не должен ничего «предполагать» о том, что делается вне его.

Организация взаимодействия между отдельными исполняемыми модулями одной UEFI фазы организуется средствами «CORE»-модуля соответствующей фазы. Отдельные исполняемые модули могут определять (Install) протоколы, запрашивать (Locate) и использовать протоколы, объявленные другими модулями, устанавливать/объявлять события, объявлять (Notify) обработчики событий.

Таким образом, для каждого исполняемого модуля прошивки нас интересует наличие следующих артефактов:

  1. Список протоколов, которые данный модуль определяет. (Каждый протокол идентифицируется уникальным номером – guid).
  2. Список протоколов, которые данный модуль использует (пытается использовать).
  3. Список событий, которые данный модуль объявляет. (Событие имеет уникальный номер – guid).
  4. Список обработчиков событий присутствующих (реализованы и могут быть установлены/инициализированы) в данном модуле.
Static Dependency Graph для заданной фазы BIOS/UEFI считается определённым, если для каждого исполняемого модуля фазы, нам известны все артефакты, перечисленные выше в п.п.1-4. (Другими словами, если у нас определена вся информация, описывающая взаимозависимости между модулями).
Мы будем рассматривать только вариант статического анализа, это означает, что некоторые элементы кода, реализующие п.п.1-4 могут быть недостижимы (являются фрагментами «dead» кода) или будут достижимы только при определённых вариантах входных данных/параметрах.

Всё, что мы рассматривали до настоящего времени, основывается только на спецификации BIOS/UEFI. А чтоб разобраться во «взаимоотношениях» имеющихся исполняемых модулей рассматриваемой прошивки, нам придётся несколько углубиться в их структуру, а значит, заняться хотя бы частичным их реверсом (восстановлением исходных алгоритмов).

Как уже было написано выше, исполняемый модуль PE+ формата – это всего-навсего набор структур для загрузчика, строящего в памяти объект, на который будет передаваться управление, и этот объект по своей природе состоит из команд процессора, а также данных для этих команд.
Будем говорить, что произведено полное дизассемблирование исполняемого модуля, если удалось решить задачу разделения команд и данных, представленных в данном модуле.
При этом мы не будем накладывать никаких требований на структуру и типы данных, достаточно, если для каждого байта, принадлежащего образу исполняемого модуля, полученному загрузчиком, мы можем однозначно сказать, к какой из двух категорий он принадлежит: байт команды или байт данных.

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

Предположим:

  1. Мы уже решили задачу полного дизассемблирования для конкретного исполнительного модуля BIOS/UEFI, т.е. мы сумели разделить команды и данные.
  2. Имеется исходный текст модуля на языке «C» (в нынешних BIOS/UEFI прошивках модули большей частью разработаны как раз на языке «C»).

Даже в этом случае просто сопоставление полученных результатов (ассемблерный текст – это просто текстовое представление команд процессора) с исходными текстами на языке «C» потребует почти всегда неслабого опыта/квалификации, исключением будут только абсолютно вырожденные случаи.

Полное изучение примеров, показывающих трудности идентификации или сопоставления результатов дизассемблирования с исходным кодом не входит в наши текущие планы.
Рассмотрим лишь пример, когда в полученном ассемблерном листинге мы встречаем «Indirect Call» команду – неявный вызов процедуры.

Это пример вызова процедуры, ссылка на которую задаётся в таблице. Таблица, содержащая ссылки на различные процедуры, – типичный случай реализации представления интерфейсов произвольного протокола.

Подобная таблица не обязательно должна состоять только из ссылок на процедуры, никто не запрещает хранить в этой структуре (а это пример типичной «C»-шной структуры) просто произвольные данные.

Вот одна из форм такого вызова (вместо регистра “ecx” возможны почти все варианты 32-х разрядных регистров процессора):
FF 51 18 call dword ptr [ecx+18h]
Попав, в результате анализа, на подобную команду, разобраться, что за процедура вызывается, список её параметров, тип и значение возвращаемого результата, возможно только, если нам известен тип объекта (протокола), вызов интерфейса которого производится данной командой.

Если нам известно, что в предыдущем примере регистр “ecx” содержит указатель (адрес начала таблицы EFI_PEI_SERVICES), мы можем получить (представить) эту команду в следующем более понятном и «приятном» виде:
FF 51 18 call [eсx+EFI_PEI_SERVICES.InstallPpi]
Получение информации о содержимом регистра, участвующего в «Indirect Call» команде, чаще всего выходит за пределы возможностей «типичного» дизассемблера, задачей которого является просто анализ и преобразование двоичного (binary) кода процессора в человеко-читабельный вид – текстовое представление соответствующей команды процессора.

Для решения этой задачи часто требуется использовать дополнительную (Мета) информацию, которая отсутствует в двоичном исполняемом модуле (утеряна в результате компиляции и линковки – она используется при преобразованиях из одного представления алгоритма в другое, но уже не нужна процессору для выполнения полученных команд).

Если эти Метаданные всё-таки доступны нам из дополнительных источников, то, используя их и проводя дополнительный анализ, мы и получаем более понятное (и более точное) представление «Indirect Call» команды.

Вообще-то этот расширенный анализ уже больше напоминает процесс «декомпиляции», хоть результат и не выглядит, как исходный текст модуля на языке «C», тем не менее, в дальнейшем будем именовать сей процесс декомпиляцией команд, являющихся «Indirect Call» или «частичной» декомпиляцией.

Итак, мы уже готовы для определения достаточных условий построения так желаемого нами графа взаимозависимости исполняемых модулей прошивки для заданной фазы BIOS/UEFI:
Для получения Static Dependency Graph (любой из фаз – PEI или DXE) достаточно осуществить полное дизассемблирование всех исполняемых модулей соответствующей фазы (по крайней мере отделить все команды), и произвести декомпиляцию «Indirect Call» команд, присутствующих в дизассемблированных модулях.
Возникает сразу уйма вопросов по поводу того, каким образом связаны наши знания о «Indirect Call» командах с межмодульными взаимодействиями.
Как уже упоминалось выше, весь сервис по организации взаимодействия предоставляется «CORE»-модулем соответствующей фазы, а сервисы в фазах оформлены в виде «базовых» таблиц сервисов.

Так как модели взаимодействия модулей в PEI и DXE фазах хоть и идеологически (конструктивно) подобны, технически всё-таки отличаются, предлагается перейти от несколько формальных рассуждений к рассмотрению конкретного непосредственного построения Static Dependency Graph для PEI-фазы.

Нам даже удастся определить и сформулировать необходимые и достаточные условия возможности построения Static Dependency Graph для PEI-фазы.

Построение Static Dependency Graph для PEI-фазы


Описания решения задачи полного дизассемблирования исполняемых модулей PEI-фазы и декомпиляции «Indirect Call» команд, присутствующих в этих модулях, выходит за рамки нашего повествования и приводиться в нём не будет – само по себе изложение этого материала по объёму может превышать размеры данного опуса.

Вполне возможно, что со временем это произойдёт в виде отдельного материала, а пока – «know how».

Отметим только, что привлечение Метаданных, плюс наличие определённой структуры построения бинарного кода, позволяет на практике осуществлять полное дизассемблирование исполняемых модулей BIOS/UEFI. Формального доказательства данного факта не предполагается ни сейчас ни в будущем. По крайней мере при анализе/обработке более сотни (100) BIOS/UEFI различных производителей не встретилось примеров, где бы не удалось осуществить полное дизассемблирование .

Далее только конкретные результаты (с объяснениями: что, как и почем…).

Структура EFI_PEI_SERVICES – это базовая структура PEI-фазы, которая передается в качестве параметра в точку входа каждого PEI-модуля и содержит ссылки на базовые сервисы, необходимые PEI-модулям для функционирования.

Нас будут интересовать только поля, расположенные в самом начале структуры:



Фрагмент реальной структуры типа EFI_PEI_SERVICES в дизассемблере IDA Pro.

А вот так она представляется в исходном коде на языке «C» (напоминаю, это только фрагмент структуры):

struct EFI_PEI_SERVICES {
	EFI_TABLE_HEADER	Hdr;
	EFI_PEI_INSTALL_PPI	InstallPpi;
	EFI_PEI_REINSTALL_PPI	ReInstallPpi;
	EFI_PEI_LOCATE_PPI	LocatePpi;
	EFI_PEI_NOTIFY_PPI	NotifyPpi;
    //...Ещё много ссылок на различные базовые сервисы...
};

В начале структуры EFI_PEI_SERVICES, как и во всех «базовых» таблицах сервисов (Services Tables), находится EFI_TABLE_HEADER структура. Значения, представленные в этой заголовочной структуре, позволяют нам однозначно утверждать, что на фрагменте из дизассемблера реально присутствует если и не сама структура EFI_PEI_SERVICES (см. поле «Hdr.Signature»), то, по крайней мере, шаблон этой структуры!

struct EFI_TABLE_HEADER {
	UINT64	Signature;
	UINT32	Revision;
	UINT32	HeaderSize;
	UINT32	CRC32;
	UINT32	Reserved;
};

Попутно мы можем установить, что прошивка разрабатывалась в то время, когда версия UEFI PI спецификации была 1.2, период актуальности которой был с 2009 по 2013 год, ну а на текущий момент (начало 2019) актуальная версия спецификации уже доросла (буквально на днях подросла) до версии 1.7.

Из поля «Hdr.HeaderSize» можно определить, что полная длина структуры равна 78h (и это отнюдь не длина заголовка, как должно следовать из названия, а длина всей структуры EFI_PEI_SERVICES).

Интерфейсы EFI_PEI_SERVICES разделены на 7 категорий/классов. Мы их просто перечислим:

  1. PPI Services.
  2. Boot Mode Services.
  3. HOB Services.
  4. Firmware Volume Services.
  5. PEI Memory Services.
  6. Status Code Services.
  7. Reset Services.

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

И их всего четыре для PEI-фазы.

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

Ниже приводятся прототипы этих процедур:

typedef EFI_STATUS (__cdecl *EFI_PEI_INSTALL_PPI)(
	const EFI_PEI_SERVICES		**PeiServices,
	const EFI_PEI_PPI_DESCRIPTOR	*PpiList);
typedef EFI_STATUS (__cdecl *EFI_PEI_REINSTALL_PPI)(
	const EFI_PEI_SERVICES		**PeiServices,
	const EFI_PEI_PPI_DESCRIPTOR	*OldPpi,
	const EFI_PEI_PPI_DESCRIPTOR	*NewPpi);
typedef EFI_STATUS (__cdecl *EFI_PEI_LOCATE_PPI)(
	const EFI_PEI_SERVICES		**PeiServices,
	const EFI_GUID			*Guid,
	UINTN				Instance,
	EFI_PEI_PPI_DESCRIPTOR 		**PpiDescriptor,
	void				**Ppi);
typedef EFI_STATUS (__cdecl *EFI_PEI_NOTIFY_PPI)(
	const EFI_PEI_SERVICES		**PeiServices,
	const EFI_PEI_NOTIFY_DESCRIPTOR	*NotifyList);

Отметим только, что кроме «Indirect Call» команд, вызывающих процедуры/интерфейсы класса «PPI Services» возможен явный (непосредственный – не табличный) вызов этих процедур, что иногда случается в исполнительных модулях, где производится определение/создание структуры EFI_PEI_SERVICES.

Открою один небольшой секрет: как ни странно, хоть это и «базовая» таблица сервисов PEI-фазы, тем не менее, как показывает практика, она может определяться не только в PEI_CORE модуле.

В реальной природе существуют прошивки, в которых структура EFI_PEI_SERVICES определялась/формировалась и использовалась в нескольких модулях, и это были отнюдь не копии PEI_CORE модуля.

Таким образом, возможны следующие варианты кода:

seg000:00785F0D B8 8C A6 78+	mov     eax, offset ppiList_78A68C
seg000:00785F12 50		push    eax			   ; PpiList
seg000:00785F13 57		push    edi			   ; PeiServices
seg000:00785F14 89 86 40 0E+	mov     [esi+0E40h], eax
seg000:00785F1A E8 70 FC FF+	call    InstallPpi

Пример явного вызова процедуры «InstallPpi».

seg000:00787CBB 8B 4D FC	mov     ecx, [ebp+PeiServices]
seg000:00787CBE 50		push    eax			   ; PpiList
seg000:00787CBF C7 00 10 00+	mov     dword ptr [eax], 80000010h
seg000:00787CC5 C7 43 3C A8+	mov     dword ptr [ebx+3Ch], offset guid_78A9A8
seg000:00787CCC 8B 11		mov     edx, [ecx]
seg000:00787CCE 51		push    ecx			   ; PeiServices
seg000:00787CCF FF 52 18	call    [edx+EFI_PEI_SERVICES.InstallPpi]

Пример неявного вызова интерфейса «InstallPpi».

FF 51 18  call    dword ptr [ecx+18h]
FF 51 18  call    [eсx+EFI_PEI_SERVICES.InstallPpi]

FF 51 1С  call    dword ptr [ecx+1Ch]
FF 51 1C  call    [eсx+EFI_PEI_SERVICES.ReInstallPpi]

FF 51 20  call    dword ptr [ecx+20h]
FF 51 20  call    [eсx+EFI_PEI_SERVICES.LocatePpi]

FF 51 24  call    dword ptr [ecx+24h]
FF 51 24  call    [eсx+EFI_PEI_SERVICES.NotifyPpi]
Примеры неявных вызовов интерфейсов до и после идентификации.

Отметим одну характерную особенность: в случае PEI-фазы для IA-32 архитектуры интерфейсы, класса «PPI Services» имеют смещения (offset) 18h, 1Ch, 20h, 24h.

А теперь сформулируем следующее утверждение:
Для построения Static Dependency Graph PEI-фазы необходимо и достаточно осуществить полное дизассемблирование всех исполняемых модулей фазы (по крайней мере отделить все команды), и произвести декомпиляцию «Indirect Call» команд со смещениями 18h, 1Ch, 20h, 24h в дизассемблированных модулях.
По сути дела, мы полностью сформулировали алгоритм решения задачи, и как только удалось выделить все вызовы интерфейсов/процедур класса «PPI Services», остаётся только определить, какие параметры передаются в эти вызовы. Задача может быть и не самая тривиальная, но, как показала практика, полностью разрешимая, у нас есть для этого все данные.

А теперь реальные примеры реальных данных для реальных модулей PEI-фазы. Намеренно не указываем, для BIOS/UEFI какой фирмы были получены результаты, просто приводятся примеры, как они выглядят.

Два примера описания PEIM-модулей с полной информацией об использовании «PPI Services» интерфейсов в этих модулях


    -- File 04-047/0x02F/: "TcgPlatformSetupPeiPolicy" : [007CCAF0 - 007CD144]
                DEPENDENCY_START
                  EFI_PEI_READ_ONLY_VARIABLE_ACCESS_PPI
                DEPENDENCY_END
        Install Protocols:
            [1] TCG_PLATFORM_SETUP_PEI_POLICY
        Locate Protocols:
            [2] EFI_PEI_READ_ONLY_VARIABLE_ACCESS_PPI

    -- File 04-048/0x030/: "TcgPei"                   : [007CD160 - 007CF5DE]
                DEPENDENCY_START
                  EFI_PEI_MASTER_BOOT_MODE_PEIM_PPI
                  EFI_PEI_READ_ONLY_VARIABLE_ACCESS_PPI
                      AND
                DEPENDENCY_END
        Install Protocols:
            [1] AMI_TCG_PLATFORM_PPI
            [2] EFI_PEI_TCG_PPI
            [2] PEI_TPM_PPI
        Locate Protocols:
            [1] EFI_PEI_TCG_PPI
            [1] EFI_PEI_READ_ONLY_VARIABLE_ACCESS_PPI
            [1] TCG_PLATFORM_SETUP_PEI_POLICY
            [5] PEI_TPM_PPI
        Notify Events:
            [1] AMI_TCM_CALLBACK
        ReInstall Protocols:
            [1] PEI_TPM_PPI

Списки протоколов по типам интерфейсов, в которых они использовались


Ниже под спойлерами приводятся сокращенные примеры списков PPIM-протоколов для каждого из интерфейсов класса «PPI Services».

Формат списков следующий:
| порядковый номер | имя_PPI | guid_PPI | имя_исполняемого_модуля : адрес_использования |

***** Install 99 Ppi in "Firmware"


***** Locate 194 Ppi in "Firmware"


***** ReInstall 5 Ppi in "Firmware"


***** Notify 29 Ppi in "Firmware"


Заключительный список всех guid-ов протоколов, на которые есть ссылки в конкретном BIOS/UEFI с легендой, указывающей в каких «PPI Services» встречаются данные протоколы


Ниже под спойлером приводится список из 97 PPi-guid-ов, встречающихся и явно используемых в конкретной прошивке, данные по которой приводились ранее.

Каждый элемент из списка предваряется легендой, в которой отражены все виды использования конкретного протокола.


	"D" - in DEPENDENCY section used
	"I" - in "InstallPpi"   functions used
	"L" - in "LocatePpi"    functions used
	"R" - in "ReInstallPpi" functions used
	"N" - in "NotifyPpi"    functions used

***** List Ppi in "Firmware"




В данном BIOS/UEFI обращают на себя внимание следующие интервалы списка протоколов:

  1. №№ 38-50.
    Определение протоколов/событий (InstallPpi), которые не используются ни одним модулем.
  2. №№ 87-95.
    Попытка запроса протоколов, которые не были установлены ни одним модулем данной прошивки.
  3. №№ 96-97.
    Два «Notify» события, для которых ни один модуль не удосужился объявить соответствующий интерфейс, соответственно, эти процедуры хоть и объявлены в исполняемых модулях, но никогда не будут работать.

Заключение


  • Результаты, подобные приведённым выше, были получены для BIOS/UEFI различных производителей, именно поэтому все примеры анонимны.
  • На самом деле решались более общие задачи реверса алгоритмов исполняемых модулей BIOS/UEFI, а полученный граф – побочный результат, этакий дополнительный бонус.
  • Корректное решение задачи «Получения Static Dependency Graph» для исполняемых модулей BIOS/UEFI требует проведения статического анализа бинарного кода, который включает в себя проведение полного дизассемблирования исполняемых модулей и частичной декомпиляции «Indirect Call» команд этих модулей.
Tags:
Hubs:
Total votes 45: ↑45 and ↓0+45
Comments21

Articles