Реализации машины в qemu

30a1c4b992c297b1a6631c3575943bd5

В процессе обратной разработки прошивок иногда возникает задача по ее эмуляции, например, для фаззинг тестирования или детального изучения поведения в динамике. На практике обычно для этого хватает фреймворков avatar2, unicorn, qiling и подобных. Однако они поддерживают далеко не все платформы и имеют ряд ограничений для решения таких задач. При разработке эмулятора PLC я столкнулся с тем, что ни один фреймворк для эмуляции не поддерживал требуемую платформу.

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

Полезные статьи по теме:  

https://habr.com/ru/post/522378/

https://habr.com/ru/post/466549/

https://airbus-seclab.github.io/qemu_blog/

https://github.com/Gyumeijie/qemu-object-model

https://qemu.readthedocs.io/en/latest/devel/index.html  

Для начала соберем свежий образ qemu  

git clone https://github.com/qemu/qemu.git

В качестве примера будем писать машину под платформу aarch64, поэтому собираем только ее

cd qemu
mkdir build
cd build
../configure --target-list=aarch64-softmmu --without-default-devices
make -j 4

Для проверки доступных машин можно выполнить следующую команду    

./qemu-system-aarch64 -M ?

Итак, приступим. Для начала в файл конфигурации Kconfig в основной папке платформы добавляем новый тип, описывающий конфигурацию нашей машины, сюда в дальнейшем будем добавлять зависимости самой машины    

config EDU_AARCH64
    bool

В файл meson.build допишем строчку с параметрами сборки  

arm_ss.add(when: '{CONFIG_EDU_AARCH64}', if_true: files('edu_aarch64.c'))

Далее реализуем саму машину в простейшем варианте:  

#include "qemu/osdep.h"
#include "hw/boards.h"

static void arm_edu_init(MachineState *mcs)
{
}

static void arm_edu_machine_init(MachineClass *mc)
{
    mc->desc = "Education machine";
    mc->init = arm_edu_init;
}

DEFINE_MACHINE("arm_edu", arm_edu_machine_init)

В этом фрагменте кода происходит примерно следующее: при помощи макроса DEFINE_MACHINE, объявленного в заголовке "hw/boards.h", объявляется новая машина. Макрос регистрирует новый тип данных наследуемый отTYPE_MACHINE, объявляет подобие базового конструктора класса, в котором преобразует переданную область памяти к структуре MachineClass и передает управление в функицю arm_edu_machine_init, где в дальнейшем мы имеем возможность заполнить эту структуру.

Приведу наиболее интересный ее фрагмент с небольшими комментариями:  

struct MachineClass {
    ...
    char *name; // Имя машины, заполняется макросом DEFINE_MACHINE
    const char *alias;
    const char *desc; // Описание отображаемое при выводе списка машин
    ...
    // Таблица с виртуальными функциями под каждое событие 
    void (*init)(MachineState *state);
    void (*reset)(MachineState *state);
    void (*wakeup)(MachineState *state);
    ...
    ram_addr_t default_ram_size; // Размер RAM по умолчанию, передается в класс MachineStatetate
    // если пользователь не указал другой размер в параметрах
    const char *default_cpu_type; // CPU по умолчанию, передается в класс MachineState
    ...
};

Подробнее с этой структурой можно ознакомится в том же заголовочном файле где объявлен макрос.

Для того, чтобы скрипт сборки добавил и нашу машину, ее необходимо включить в файле конфигурации, например в configs/devices/aarch64-softmmu/default.mak. После этого можно повторно собирать qemu.

После сборки проверяем наличие нашей машины в списке доступных. Если все прошло хорошо, результат будет примерно такой:  

$./qemu-system-ppc -M ? |grep arm_edu
arm_edu              Education machine

Если попытаться запустить машину, то ничего не произойдет, поскольку отсутствует реализация инициализации, т.е. машины как таковой пока еще не существует. Попробуем это исправить дополнив исходный файл с машиной:

...
#include "qapi/error.h"
#include "hw/arm/arm_edu.h"
#include "hw/boards.h"
#include "exec/memory.h"
#include "hw/loader.h"

static void arm_edu_init(MachineState *machine)
{
    MemoryRegion *rom = g_new(MemoryRegion, 1);
    hwaddr firmware_addr = ROM_START_ADDR;
    ARMCPU *cpu = NULL;
    int res;

    info_report("Loading cpu %s", machine->cpu_type);
    cpu = ARM_CPU(cpu_create(machine->cpu_type)); // Создаем процессор

    // подробнее о работе с регионами памяти в include/exec/memory.h
    memory_region_add_subregion(get_system_memory(), RAM_START_ADDR,
                                machine->ram);  // Добавляем инициализированную qemu RAM

    memory_region_init_rom(rom, NULL, "ROM", MAX_FIRMWARE_SIZE,
                                &error_fatal); //инициализируем новый регион памяти

    memory_region_add_subregion(get_system_memory(), firmware_addr,
                                rom);

    if(!machine->firmware){
        error_report("Firmware filename is required, use -bios option");
        exit(EXIT_FAILURE);
    }

    info_report("Loading firmware %s", machine->firmware);
    res = load_image_targphys(machine->firmware, firmware_addr, // include/hw/loader.h
                                MAX_FIRMWARE_SIZE); // Просим qemu загрузить файл в память

    if (res < 0) {
        error_report("Failed to load firmware from %s", machine->firmware);
        exit(EXIT_FAILURE);
    }

    cpu->rvbar = ROM_START_ADDR;  // Выставляем регистр RVBAR содержащий адрес 
}

static void arm_edu_machine_init(MachineClass *mc)
{
    ...
    mc->default_cpu_type = ARM_CPU_TYPE_NAME("cortex-a72");
    mc->default_cpus = 1;
    mc->max_cpus = 1;
    mc->default_ram_size = 256 * MiB;
    mc->default_ram_id = "dram";
}

В заголовочный файл поместим объявление констант и архитектурные зависимости  

#include "target/arm/cpu.h"
#define RAM_START_ADDR    0x10000000
#define ROM_START_ADDR    0x0
#define MAX_FIRMWARE_SIZE 0x8000000

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

Теперь, когда наша машина умеет полноценно запускать код, напишем небольшую прошивку для ее тестирования. Собирать при помощи gcc-aarch64 будем вот такой простенький код

.text
.globl entry

entry:
    mov x1, #356
    mov x2, #478
    mul x0, x1, x2
    b entry

Чтобы не лезть лишний раз в гугл  

sudo apt install gcc make gcc-aarch64-linux-gnu binutils-aarch64-linux-gnu
aarch64-linux-gnu-as test.S -o test.o
aarch64-linux-gnu-objcopy test.o test -O binary

Наконец, запускаем qemu для теста  

./qemu-system-aarch64 -M arm_edu \
                      -bios ~/firmware/test \
                      -s -S \
                      -nographic

Параметры -s -S используются для запуска gdb сервера на порту 1234, -nographic отключает графический дисплей. Для машин собранных с поддержкой такового qemu запускает его автоматически.  

В соседнем окне подключаем gdb

gdb-multiarch -q
(gdb) target remote tcp::1234

Готово. На выходе имеем полноценно работающую машину, в которой уже можно частично отлаживать кусочки ассемблерного кода на уровне EL3.

В следующих статьях рассмотрим реализацию MMIO регистров, UART интерфейса, сетевой карты и виртуального таймера.

© Habrahabr.ru