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