Прежде чем перейти к статье, хочу вам представить, экономическую онлайн игру Brave Knights, в которой вы можете играть и зарабатывать. Регистируйтесь, играйте и зарабатывайте!
Так повелось в мире, что время от времени необходимо проводить исследования безопасности драйверов и прошивок. Одним из способов исследования является - фаззинг (Fuzzing). Не будем останавливаться на описание на самого процесса фаззинга, для этого есть эта статья, отметим только, что в основном его используют для исследования прикладных приложений. И тут возникает вопрос: как профаззить прошивку, в частности прошивку UEFI? Здесь будет рассказано об одном из способов с использованием программного эмулятора EDKII, чтобы проводить фаззинг без развертывания аппаратных стендов. И что важно, все это сделаем в Windows.
Сразу, что такое EDKII? - это среда разработки и эмулятор ПО согласно спецификации UEFI. Про разработку в EDKII есть ряд статей (вот и вот), а наша задача связать эмулятор EDKII и фаззер.
А реализовывать инструментацию будем под фаззер WinAFL.
Стартуем!
Как это сделать? Вот несколько решений раз, два, три и четыре.
Решение Раз
Первое решение - найти что-нибудь готовое - это HBFA.
HBFA - анализатор ПО из экспериментального набора edk2-staging, в Windows работает в связке с DynamoRio - динамическим инструментатором кода. В ходе исследования было выяснено, что если в прошивке есть код, завязанный на определенную сборочную систему с специальными библиотеками, то данное решение, без серьезной переработки готового кода, не встраивается. Тогда переходим к решению два.
Решение Два
Решение два - это внедрение в код систему Intel® ITS ( Intel® Intelligent Test System) для инструментации. Система используется для определения меры покрытия кода.
В результате выполнения инструментированного кода Intel® ITS создается файл exec. Для того, что бы связать выполнение такого кода с фаззером и оценить количество путей, надо производить подсчет контрольной суммы от файла exec и записи его в карту AFL.
что еще за карта AFL?
Карта AFL это массив данных размера 65536 куда по некому алгоритму вносятся метки посещения ветвлений кода. Взаимодействие с данной картой производится посредством разделяемой памяти (shared memory), идентификатор которой передается посредством переменой среды AFL_STATIC_CONFIG
HANDLE mem = OpenFileMapping(FILE_MAP_ALL_ACCESS, false, shm);
areaPtr = MapViewOfFile(mem, FILE_MAP_ALL_ACCESS, 0, 0, 0);
if(areaPtr == NULL){
out << "shm value failed" << std::endl;
out.close();
}
__afl_area_ptr = (char*)areaPtr;
Решение простое, но дает не честный и не совсем правильный отчет о покрытие кода, поэтому переходим к следующему решению.
Решение Три
Это написание собственного драйвера AFL в UEFI.
Цель драйвера - прокинуть вызовы функций Windows в эмулятор EDKII. Для этого необходимо выполнить следующие действия:
Реализовать драйвер уровня DXE;
Реализовать функции драйвера на уровне выполнения Windows;
Зарегистрировать драйвер в эмуляторе;
Реализовать протокол драйвера в прошивке.
DXE драйвер
На платформе эмулятора EmulatorPkg определен протокол EMU_IO_THUNK_PROTOCOL. Протокол EMU_IO_THUNK_PROTOCOL используется для абстрагирования зависимых от ОС операция ввода-вывода для других протоколов UEFI, например: GOP, SimpleFileSystem и так далее. Одним из таких будет и драйвер для взаимодействия с AFL. Драйвер DXE для AFL будет является экземпляром протокола EFI_BLOCK_IO_PROTOCOL.
EFI_BLOCK_IO_PROTOCOL - протокол для реализации контроллера, протокол EMU также является его реализацией.
Таким образом драйвер DXE для AFL делится на зависящую от ОС реализацию протокола EMU_IO_THUNK_PROTOCOL и независимую часть, для реализации драйвера на уровне UEFI EFI_BLOCK_IO_PROTOCOL.
Перейдем к созданию заголовочного файла драйвера уровня EMU $(workspace)\EmulatorPkg\EmuAflProxyDxe\EmuAflProxy.h.
Здесь указывается структура EMU_AFL_PROXY_PRIVATE:
extern EFI_DRIVER_BINDING_PROTOCOL gEmuAflProxyDriverBinding;
extern EFI_COMPONENT_NAME_PROTOCOL gEmuAflProxyComponentName;
extern EFI_COMPONENT_NAME2_PROTOCOL gEmuAflProxyComponentName2;
#define EMU_AFL_PROXY_PRIVATE_SIGNATURE SIGNATURE_32 ('E', 'A', 'F', 'l')
typedef struct {
UINTN Signature;
EMU_IO_THUNK_PROTOCOL *IoThunk;
EFI_AFL_PROXY_PROTOCOL AflProxy;
EFI_AFL_PROXY_PROTOCOL *Io;
EFI_UNICODE_STRING_TABLE *ControllerNameTable;
} EMU_AFL_PROXY_PRIVATE;
#define EMU_AFL_PROXY_PRIVATE_DATA_FROM_THIS(a) \
CR (a, \
EMU_AFL_PROXY_PRIVATE, \
AflProxy, \
EMU_AFL_PROXY_PRIVATE_SIGNATURE \
)
В этой структуре содержатся следующие поля:
EFI_UNICODE_STRING_TABLE - структура содержащая локализацию и имя драйвера;
EMU_IO_THUNK_PROTOCOL - протокол использующийся для абстрагирования зависимых от ОС операций ввода-вывода для других протоколов UEFI;
EFI_AFL_PROXY_PROTOCOL - протокол реализации нашего драйвера UEFI;
UINTN Signature - идентификатор драйвера.
Ниже представлена инициализация полей этой структуры:
Private->Signature = EMU_AFL_PROXY_PRIVATE_SIGNATURE;
Private->IoThunk = EmuIoThunk;
Private->Io = EmuIoThunk->Interface;
Private->AflProxy.afl_maybe_log = EmuAflMaybeLog;
Private->ControllerNameTable = NULL;
IoThunk
- указатель на протокол EMU_IO_THUNK_PROTOCOL;Private->Io
- указатель к требуемой функции, где будет вызванаPrivate->Io->afl_maybe_log
- определяется путем получения указателя на самого себя;Private->AflProxy
- указывает, что при обращении к интерфейсу драйвера, будет вызвана EmuAflMaybeLog.
Теперь определим саму функцию EmuAflMaybeLog в файле $(workspace)\EmulatorPkg\EmuAflProxyDxe\EmuAflProxy.c
EFI_STATUS
EFIAPI
EmuAflMaybeLog(
IN EFI_AFL_PROXY_PROTOCOL *This,
IN UINT32 date
);
Private = EMU_AFL_PROXY_PRIVATE_DATA_FROM_THIS(This);
Status = Private->Io->afl_maybe_log(Private->Io, date);
Когда создаем драйвер, надо определить функции его создания и удаления. Эти функции определенны в структуре EFI_DRIVER_BINDING_PROTOCOL. Ниже указаны прототипы функций, которыми будет эта структура будет проинициализирована:
EmuAflProxyDriverBindingSupported
;EmuAflProxyDriverBindingStart
;EmuAflProxyDriverBindingStop
.
EFI_STATUS
EFIAPI
EmuAflProxyDriverBindingSupported(
IN EFI_DRIVER_BINDING_PROTOCOL* This,
IN EFI_HANDLE ControllerHandle,
IN EFI_DEVICE_PATH_PROTOCOL* RemainingDevicePath
);
EFI_STATUS
EFIAPI
EmuAflProxyDriverBindingStart(
IN EFI_DRIVER_BINDING_PROTOCOL* This,
IN EFI_HANDLE ControllerHandle,
IN EFI_DEVICE_PATH_PROTOCOL* RemainingDevicePath
);
EFI_STATUS
EFIAPI
EmuAflProxyDriverBindingStop(
IN EFI_DRIVER_BINDING_PROTOCOL *This,
IN EFI_HANDLE ControllerHandle,
IN UINTN NumberOfChildren,
IN EFI_HANDLE *ChildHandleBuffer
);
Определим структуру EFI_DRIVER_BINDING_PROTOCOL с указанием функций инициализации драйвера:
EFI_DRIVER_BINDING_PROTOCOL gEmuAflProxyDriverBinding = {
EmuAflProxyDriverBindingSupported,
EmuAflProxyDriverBindingStart,
EmuAflProxyDriverBindingStop,
0xa,
NULL,
NULL
};
В спецификации EFIAPI определена функция EfiLibInstallDriverBindingComponentName2
которая отвечает за регистрацию имени драйвера, ее надо вызвать при начальной инициализации драйвера. Сделаем это в функции InitializeEmuAflProxy
- она будет точкой входа в драйвер.
EFI_STATUS
EFIAPI
InitializeEmuAflProxy(
IN EFI_HANDLE ImageHandle,
IN EFI_SYSTEM_TABLE* SystemTable
)
{
EFI_STATUS Status;
Status = EfiLibInstallDriverBindingComponentName2(
ImageHandle,
SystemTable,
&gEmuAflProxyDriverBinding,
ImageHandle,
&gEmuAflProxyComponentName,
&gEmuAflProxyComponentName2
);
ASSERT_EFI_ERROR(Status);
return Status;
}
В спецификации драйвера указывается точка входа. В файле $(workspace)\EmulatorPkg\EmuAflProxyDxe\EmuAflProxyDxe.inf
указываем эту функцию:
[Defines]
INF_VERSION = 0x00010005
BASE_NAME = EmuAflProxy
FILE_GUID = 537B2273-9362-FCA4-7A3F-41949F0E4EDB
MODULE_TYPE = UEFI_DRIVER
VERSION_STRING = 1.0
ENTRY_POINT = InitializeEmuAflProxy
Вызов функций Windows
Реализацию функций зависимых от ОС делаем в файле $(workspace)\EmulatorPkg\Win\Host\WinAflProxy.c
#define WIN_AFL_PROXY_PRIVATE_SIGNATURE SIGNATURE_32 ('A', 'F', 'L', 'P')
typedef struct {
UINTN Signature;
EMU_IO_THUNK_PROTOCOL* Thunk;
EFI_AFL_PROXY_PROTOCOL AflProxy;
} WIN_AFL_PROXY_PRIVATE;
#define WIN_AFL_PROXY_PRIVATE_DATA_FROM_THIS(a) \
CR (a, \
WIN_AFL_PROXY_PRIVATE, \
AflProxy, \
WIN_AFL_PROXY_PRIVATE_SIGNATURE \
)
extern EFI_AFL_PROXY_PROTOCOL gWinAflProxyProtocol;
Здесь определяем структуру EMU_IO_THUNK_PROTOCOL для драйвера с именем mWinAflProxyThunkIo. Она будет использоваться для регистрации драйвера в эмуляторе:
EFI_STATUS
WinAflMaybeLog(
IN EFI_AFL_PROXY_PROTOCOL *This,
IN UINT32 date
);
EFI_STATUS
WinAflProxyThunkOpen(
IN EMU_IO_THUNK_PROTOCOL* This
);
EFI_STATUS
WinAflProxyThunkClose(
IN EMU_IO_THUNK_PROTOCOL* This
);
EFI_AFL_PROXY_PROTOCOL gWinAflProxyProtocol = {
WinAflMaybeLog
};
EMU_IO_THUNK_PROTOCOL mWinAflProxyThunkIo = {
&gEfiAflProxyProtocolGuid,
NULL,
NULL,
0,
WinAflProxyThunkOpen,
WinAflProxyThunkClose,
NULL
};
В этой структуре устанавливаются функции управления драйвером, такие как функция открытия драйвера WinAflProxyThunkOpen и его закрытия WinAflProxyThunkClose. Так же в функции WinAflProxyThunkOpen прописывается вызов функции WinAflMaybeLog, в которой уже производится непосредственный вызов функций Windows:
CopyMem(&Private->AflProxy, &gWinAflProxyProtocol, sizeof(Private->AflProxy));
Ниже код функции WinAflProxyThunkOpen:
WIN_AFL_PROXY_PRIVATE* Private;
Private = AllocateZeroPool(sizeof(*Private));
if (Private == NULL) {
return EFI_OUT_OF_RESOURCES;
}
Private->Signature = WIN_AFL_PROXY_PRIVATE_SIGNATURE;
Private->Thunk = This;
CopyMem(&Private->AflProxy, &gWinAflProxyProtocol, sizeof(Private->AflProxy));
This->Interface = &Private->AflProxy;
По итогу, стек вызовов при инициализации драйвера будет выглядеть так:
EmuAflProxyDriverBindingStart -> Status = EmuIoThunk->Open(EmuIoThunk); -> WinAflProxyThunkOpen;
Регистрация драйвера
Драйвер регистрируем на уровне выполнения кода зависимого от Windows, для этого в файле $(workspace)\WinAflProxy2\EmulatorPkg\Win\Host\WinHost.h
объявляем структуру mWinAflProxyThunkIo описанную выше:
extern EMU_IO_THUNK_PROTOCOL mWinAflProxyThunkIo;
а в файле $(workspace)\WinAflProxy2\EmulatorPkg\Win\Host\WinHost.c
добавляем ее в эмулятор:
AddThunkProtocol (&mWinAflProxyThunkIo, (CHAR16*)PcdGetPtr(PcdEmuAflProxy), TRUE);
В файле $(workspace)\WinAflProxy2\EmulatorPkg\Win\Host\WinHost.inf
указываем GUID драйвера, по нему, при старте EmulatorPkg, драйвер будет добавляться в контекст эмулятора:
[Protocols]
gEfiAflProxyProtocolGuid
Завершаем регистрацию добавлением драйвера в список драйверов прошивки. Это список драйверов загружаемых на DXE стадии, который находится в файлах:
$(workspace)\EmulatorPkg\EmulatorPkg.fdf
##
# DXE Phase modules
##
EmulatorPkg/EmuAflProxyDxe/EmuAflProxyDxe.inf
$(workspace)\EmulatorPkg\EmulatorPkg.dsc
#
# UEFI & PI
#
UefiAflProxy|MdePkg/Library/UefiAflProxy/UefiAflProxy.inf
Официальная документация на эти файлы.
Регистрация интерфейса
Здесь создается интерфейс AflProxy драйвера, не зависимого от ОС. Любой другой модуль UEFI будет обращаться к драйверу через этот интерфейс.
В $(workspace)\MdePkg\Include\Protocol\AflProxy.h
объявляем глобальный идентификатор GUID драйвера EFI:
extern EFI_GUID gEfiAflProxyProtocolGuid;
И прототип функции afl_maybe_log, которая является основной функцией в драйвере:
typedef
EFI_STATUS
(EFIAPI* EFI_AFL_MAYBE_LOG) (
IN EFI_AFL_PROXY_PROTOCOL* This,
IN UINT32 date
);
struct _EFI_AFL_PROXY_PROTOCOL {
EFI_AFL_MAYBE_LOG afl_maybe_log;
};
Также создаем библиотеку для обращения к интерфейсу AflProxy для упрощения вызова его функций. В ее составе будет два файла:
Заголовочный файл, где прописан прототип: $(workspace)\MdePkg\Include\Library\UefiAflProxy.h
EFI_STATUS afl_log2( IN UINT32 data);
и ее непосредственная реализация $(workspace)\MdePkg\Library\UefiAflProxy\UefiAflProxy.c
:
EFI_STATUS afl_log2( IN UINT32 data) {
EFI_STATUS Status = 0;
EFI_AFL_PROXY_PROTOCOL* AflProxy;
Status = gBS->LocateProtocol(
&gEfiAflProxyProtocolGuid,
NULL,
(VOID**)&AflProxy
);
if (EFI_ERROR(Status)) {
return Status;
}
AflProxy->afl_maybe_log(AflProxy, data);
return Status;
}
Отметим, что код
Status = gBS->LocateProtocol(
&gEfiAflProxyProtocolGuid,
NULL,
(VOID**)&AflProxy
);
производит открытие протокола и в боевых проектах, лучше это действие разделить на процесс инициализации протокола и вызова функций его интерфейса.
Тестируем
Добавляем вызов afl_log2, с аргументом 200, в приложение HelloWorld.efi и пересобираем эмулятор.
EFI_STATUS
EFIAPI
UefiMain (
IN EFI_HANDLE ImageHandle,
IN EFI_SYSTEM_TABLE *SystemTable
)
{
UINT32 Index;
afl_log2(200);
Index = 0;
...
Сборка прошла успешно,
запускаем WinHost.exe, и вызываем приложение HelloWorld.efi:
Запуск прошел успешно. Также видно, что в cmd отображено число 200, что значит что драйвер успешно вызвал функции Windows в контексте EFI:
Решение Четыре
Очевидно, что для реализации способа выше надо потратить много усилий, поэтому упростим себе жизнь и решим задачу новым способом - напишем Dll которая будет внедряться в процесс работы эмулятора.
Заметим, что концептуальная схема запуска любого драйвера, в среде эмулятора, такая:
WinHost.exe -> driver.efi -> driver.dll
Запустив procmon убеждаемся, что вызов драйвера в эмуляторе производится в едином контексте
Убедившись в этом, начинаем писать свою Dll - создаем ее заголовочный файл:
Inject.h
#pragma once
#define IMPORT __declspec(dllimport)
#define EXPORT extern "C" __declspec(dllexport)
EXPORT void WinAflProxyDll(int point);
В этом файле обозначена только одна экспортируемая функция WinAflProxyDll
, именно она будет вызываться в эмуляторе. Обратим внимание на эту строку extern "C" __declspec(dllexport)
- она необходима, что бы не было конфликта имена при импорте функции в эмулятор.
Файл Inject.cpp
В этом файле содержится реализация экспортируемой функции:
EXPORT void WinAflProxyDll(int point)
По сути, ее код ничем не отличается от того кода, который описан здесь. Просто поподробней пройдемся по ее содержимому.
Обмен данными, между исследуемым приложением и winafl осуществляется при помощи разделяемой памяти shared memory, а адрес этой памяти передается между процессами через переменную среды AFL_STATIC_CONFIG
. В приложении, значение переменной среды получаем функцией GetEnvironmentVariable
:
GetEnvironmentVariable(TEXT("AFL_STATIC_CONFIG"), envbuff, env_size);
Само это значение имеет следующий вид: id_mem : counter_loop
, где counter_loop
отвечает за количество запусков при использовании __afl_persistent_loop()
, а id_mem
это сам идентификатор разделяемой памяти. Пример: 5e0ff10ec40c0919:1000.
Код ниже уже непосредственно отвечает за открытие разделяемой памяти и получения на нее указателя:
HANDLE mem = OpenFileMapping(FILE_MAP_ALL_ACCESS, FALSE, shm);
areaPtr = MapViewOfFile(mem, FILE_MAP_ALL_ACCESS, 0, 0, 0);
Файл Source.def
Создаем файл DEF для описания атрибутов Dll:
LIBRARY "Edk2WinAflDll"
EXPORTS WinAflProxyDll
Добавление Dll в эмулятор
К сожалению, без внедрения кода в эмулятор руками обойтись не удалось, но получилось в разы его уменьшить
В файл winhost.c добавляем импорт функции из Dll:
#pragma once
#define IMPORT __declspec(dllimport)
#define EXPORT extern "C" __declspec(dllexport)
IMPORT void WinAflProxyDll(int point);
В контексте работы эмулятора выделим адрес 0x10000000, по нему будет записан указатель на эту функцию:
#define addr 0x10000000
typedef struct Variable {
void (*var)(int);
}Variable;
Выделение памяти и запись по данному адресу указателя на функцию WinAflProxyDll
делается для того, что бы в любом контексте выполнения эмулятора, был доступ к функции из Dll:
VirtualAlloc((void*)addr, sizeof(Variable), MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
((Variable*)addr)->var = (void(*)(int))(WinAflProxyDll);
callAflProxyDll(7);
Получать же динамически адрес с использованием import Table не получится, так как функции winapi, из контекста драйвера DXE, недоступны.
Вызов функции из произвольного места будет производится функцией callAflProxyDll
:
#ifndef _INJECT_EDK_H_
#define _INJECT_EDK_H_
#define addrInject 0x10000000
inline void callAflProxyDll(int point) {
void(*f_ptr)(int);
f_ptr = *((void(**)(int))(addrInject));
f_ptr(point);
}
#endif
минутка неожиданности
Изначально вызов функции по адресу планировался при помощи ассемблерных вставок, но оказалось, что MSBuild поддерживает их только для х86 кода, а для кода х64 поддержки нет.
__asm {
push point
mov eax, addr
call[eax]
add esp, 4
}
Надо еще добавить Dll в конфигурационный файл эмулятора, для этого в файл $(workspace)\EmulatorPkg\Win\Host\WinHost.inf
добавляем путь до Dll:
[BuildOptions]
MSFT:*_VS2017_IA32_DLINK_FLAGS = /LIBPATH:"%VCToolsInstallDir%lib\x86"
...
Advapi32.lib \Edk2WinAflDll.lib
Инструментация
Что такое инструментация кода? Это процесс внедрения исполняемых инструкций в уже готовый код. Инструментация бывает статическая и динамическая.
Статическая инструментация производится перед запуском исследуемого приложения, динамическая же в процессе выполнения кода. У обоих видов инструментации есть свои плюсы и минусы, например статическая инструментация сопровождается сложностями ее проведения, но при этом статически инструментированный код обладает высоким быстродействием, а динамическая инструментация проста в использовании, но сильно сокращает производительность.
Учитывая, что запуск эмулятора EDK высокозатратная операция, будем использовать статическую инструментацию.
Стандартная статическая инструментация afl производится при сборке, путем вставки ассемблерных инструкций в промежуточный код gcc.
Для MSBuild идем тем же путем, находим опцию генерации промежуточного ассемблерного кода /FA
иии... фиаско. Узнаем, что промежуточный ассемблерный код не является исполняемым, а существует только для демонстрации.
Тогда вооружившись стандартом С++11 начинаем писать свой парсер-инструментатор кода Си.
Волевым решением решаем, описания парсера-инструментатора не будет, ищущий его найдет. Скажем только, что процесс его работы разбит на три стадии:
Работа препроцессора;
Построение AST-дерева;
Инструментация.
Имея инструментатор, его надо встроить в процесс сборки, для этого меняем файл conf\build_rule
:
[C-Code-File]
<Command.MSFT, Command.INTEL>
"$(CC)" /Fo${dst} $(DEPS_FLAGS) $(CC_FLAGS) $(INC) ${src}
#python your parser "$(CC)" /Fo${dst} $(DEPS_FLAGS) $(CC_FLAGS) $(INC) ${src}
Сюда добавляется вызов инструментатора.
Выводы?
Если, при использовании EDKII под Windows, проблема внедрения инструкций в эмулятор под фаззинг решена, на данный считаю лучшим способ четыре, то инструментация кода является ахилесовой пятой этого процесса. И хоть уже адекватные инструментаторы под x86, то под х64 надо еще поработать и попотеть.
Готовый инструментатор на основе внедрения инструкций в код не стабилен и в будущем от него надо уйти.
Спасибо!
PS. ссылки на готовый код ниже:
Инструментатор https://github.com/yrime/Edk2InstrV2;
DXE драйвер;
HBFA;
Dll Afl.