Так повелось в мире, что время от времени необходимо проводить исследования безопасности драйверов и прошивок. Одним из способов исследования является — фаззинг (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 драйвер
На платформе эмулятора 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;
HBFA;
