Подготовка инструментов под фаззинг UEFI на базе edk2 в Windows

—

от автора

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

  1. Реализовать драйвер уровня DXE;

  2. Реализовать функции драйвера на уровне выполнения Windows;

  3. Зарегистрировать драйвер в эмуляторе;

  4. Реализовать протокол драйвера в прошивке.

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 стадии, который находится в файлах:

  1. $(workspace)\EmulatorPkg\EmulatorPkg.fdf

  ##   #  DXE Phase modules   ##       EmulatorPkg/EmuAflProxyDxe/EmuAflProxyDxe.inf
  1. $(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:

работа эмулятора в контексте cmd

работа эмулятора в контексте cmd

Решение Четыре

Очевидно, что для реализации способа выше надо потратить много усилий, поэтому упростим себе жизнь и решим задачу новым способом — напишем Dll которая будет внедряться в процесс работы эмулятора.

Заметим, что концептуальная схема запуска любого драйвера, в среде эмулятора, такая:

WinHost.exe -> driver.efi -> driver.dll

Запустив procmon убеждаемся, что вызов драйвера в эмуляторе производится в едином контексте

process tree

process tree
call HelloWorld.efi

call HelloWorld.efi

Убедившись в этом, начинаем писать свою 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 начинаем писать свой парсер‑инструментатор кода Си.

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

  1. Работа препроцессора;

  2. Построение AST‑дерева;

  3. Инструментация.

Имея инструментатор, его надо встроить в процесс сборки, для этого меняем файл 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. ссылки на готовый код ниже:

  1. Инструментатор https://github.com/yrime/Edk2InstrV2;

  2. DXE драйвер;

  3. HBFA;

  4. Dll Afl.


ссылка на оригинал статьи https://habr.com/ru/articles/773548/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *