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

Комментарии (1)


  1. iminfinitylol
    12.12.2023 04:43

    интересная статья