QEMU固件模拟技术分析-luaqemu实现分析

# 概述 在嵌入式安全领域常常需要分析各种不同形态的固件,如果需要动态执行某些代码或者对固件进行Fuzzing测试,则需要对固件代码进行仿真,常用的仿真工具一般为qemu和unicorn。unicorn适...

概述

在嵌入式安全领域常常需要分析各种不同形态的固件,如果需要动态执行某些代码或者对固件进行Fuzzing测试,则需要对固件代码进行仿真,常用的仿真工具一般为qemu和unicorn。unicorn适合模拟执行固件中的某些代码片段,而对于中断、异步执行则不支持,而大量的嵌入式固件都是以中断驱动的,对于中断的模拟则需要依赖于qemu的全系统模拟。

本文将以luaqemu为例介绍在使用qemu来模拟固件、外设时可以借鉴的技术,代码地址

  1. https://github.com/Comsecuris/luaqemu

简单的说luaqemu通过修改部分qemu代码并在一些关键的执行点增加回调函数,使得用户可以通过lua脚本来加载固件、监控固件代码的执行,设置观察点、断点等。

示例luaqemu脚本如下

  1. require('hw.arm.luaqemu')
  2. machine_cpu = 'cortex-r5'
  3. memory_regions = {
  4. region_rom = {
  5. name = 'mem_rom',
  6. start = 0x0,
  7. size = 0x180000
  8. },
  9. region_ram = {
  10. name = 'mem_ram',
  11. start = 0x180000,
  12. size = 0xC0000
  13. },
  14. }
  15. file_mappings = {
  16. main_rom = {
  17. name = 'rom.bin',
  18. start = 0x0,
  19. size = 0x180000
  20. },
  21. main_ram = {
  22. name = 'kernel.bin',
  23. start = 0x180000,
  24. size = 0xC0000
  25. }
  26. }
  27. cpu = {
  28. env = {
  29. thumb = true,
  30. },
  31. reset_pc = 0
  32. }

该脚本的作用如下

  1. machine_cpu指定cpu的类型
  2. 利用memory_regions初始化两块内存,起始地址和内存大小分别为:(0x0, 0x180000)(0x180000, 0xC0000)
  3. file_mappings将特定的文件加载到内存中指定的位置,代码中将rom.bin文件加载到0x0地址,大小为0x180000,将kernel.bin文件加载到0x180000地址,大小为0xC0000
  4. cpu关键字指定cpu的属性,设置了cpu的指令类型为thumb,reset_pc设置虚拟机启动后的pc寄存器的值为0,表示虚拟机启动后执行的第一条指令。

初始化

luaqemu新增了一个luaarm的机器,代码位于

  1. hw/arm/luaarm.c

代码通过宏和数据结构指定machine的类型和初始化函数

  1. static void lua_class_init(ObjectClass *oc, void *data)
  2. {
  3. MachineClass *mc = MACHINE_CLASS(oc);
  4. mc->desc = "Lua ARM Meta Machine";
  5. mc->init = lua_init;
  6. }
  7. static const TypeInfo lua_machine_type = {
  8. .name = MACHINE_TYPE_NAME("luaarm"),
  9. .parent = TYPE_MACHINE,
  10. .class_init = lua_class_init,
  11. };
  12. static void lua_machine_init(void)
  13. {
  14. type_register_static(&lua_machine_type);
  15. }
  16. type_init(lua_machine_init)

可以看到lua_init为machine的入口函数

  1. static void lua_init(MachineState *machine)
  2. {
  3. // 加载 lua_script 指定的脚本,命令行参数指定
  4. luaL_loadfile(lua_state, lua_script)
  5. // 设置 cpu 的类型
  6. machine->cpu_model = lua_tostring(lua_state, -1);
  7. // 根据lua脚本来设置虚拟机的状态、固件的加载、回调函数注册
  8. init_memory_regions();
  9. init_luastate(machine);
  10. init_file_mappings();
  11. init_cpu_state();
  12. init_vm_states();
  13. // 执行 lua 脚本的 post_init 函数
  14. }

该函数会根据lua脚本函数设置虚拟机的状态、固件的加载、回调函数注册等,函数的主要流程如下

  1. 首先根据命令行参数加载指定的lua脚本
  2. 设置CPU的类型,内存映射关系
  3. 加载文件到虚拟机的内存
  4. 初始化CPU的状态(寄存器)
  5. 注册一系列回调函数,比如设置断点、指令执行回调等

通过搜索lua_script关键字可以找到设置命令行参数的位置位于vl.c的main函数里面

  1. case QEMU_OPTION_lua:
  2. lua_script = optarg;
  3. break;

我们也可以通过类似的方式注册需要的命令行参数

init_luastate 会把虚拟机的cpu对象保存到luastate全局变量里面

  1. static void init_luastate(MachineState *machine)
  2. {
  3. ARMCPU *cpu;
  4. ObjectClass *cpu_oc;
  5. CPUState *cs;
  6. cpu_oc = cpu_class_by_name(TYPE_ARM_CPU, machine->cpu_model);
  7. if (!cpu_oc) {
  8. error_report("machine \"%s\" not found, exiting\n", machine->cpu_model);
  9. exit(1);
  10. }
  11. cpu = ARM_CPU(object_new(object_class_get_name(cpu_oc)));
  12. cs = CPU(cpu);
  13. luastate.cpu = cpu;
  14. luastate.cs = cs;
  15. luastate.machine = machine;
  16. luastate.bp_pc = 0;
  17. luastate.bp_pc_ptr = NULL;
  18. luastate.old_wp_ptr = NULL;
  19. g_hash_table_foreach(breakpoints, add_cpu_breakpoints, NULL);
  20. }

内存申请

qemu内存模型

qemu 使用 MemoryRegion 组织虚拟机的物理内存空间,MemoryRegion 表示一段逻辑内存区域,它的类型如下:

  1. RAM:普通内存,qemu通过向主机申请虚拟内存来实现。
  2. MMIO:MMIO内存在读写时会调用初始化mr时指定的回调函数,回调函数由MemoryRegionOps指定,在memory_region_init_io时指定
  3. ROM:只读内存,只读内存的读操作和RAM相同,禁止写操作。
  4. ROM device:只读设备,读操作和RAM行为相同,只读设备的允许写操作,写操作和MMIO行为相同,会触发callback。
  5. IOMMU region:将对一段内存的访问转发到另一段内存上,这种类型的内存只用于模拟IOMMU的场景。
  6. container:容器,管理多个MR的MR,用于将多个MR组织成一个内存区域,比如整个虚机的内存地址区域,它被抽象成一个容器,包括了所有虚拟的内存区间。
  7. alias:主要是让不同物理地址映射到同一个 MemoryRegion ,类似于memory banking。

下面介绍一些常用内存的使用方式

申请ram

qemu使用memory_region_init_ram初始化MemoryRegion为ram类型

  1. void memory_region_init_ram(MemoryRegion *mr,
  2. struct Object *owner,
  3. const char *name,
  4. uint64_t size,
  5. Error **errp)

使用实例

  1. MemoryRegion *system_memory = get_system_memory();
  2. MemoryRegion *flash = g_new(MemoryRegion, 1);
  3. memory_region_init_ram(flash, NULL, "STM32F205.flash", FLASH_SIZE, &error_fatal);
  4. memory_region_add_subregion(system_memory, 0, flash);

qemu通过MemoryRegion的组合来表示虚拟机的物理内存空间,qemu在启动时会创建一个system_memory的MemoryRegion,system_memory是一个全局变量可以通过get_system_memory函数获取。

  1. static void memory_map_init(void)
  2. {
  3. system_memory = g_malloc(sizeof(*system_memory));
  4. memory_region_init(system_memory, NULL, "system", UINT64_MAX);
  5. address_space_init(&address_space_memory, system_memory, "memory");
  6. system_io = g_malloc(sizeof(*system_io));
  7. memory_region_init_io(system_io, NULL, &unassigned_io_ops, NULL, "io",
  8. 65536);
  9. address_space_init(&address_space_io, system_io, "I/O");
  10. }

system_memory的大小为UINT64_MAX, 表示了整个物理内存空间,这个只是一个初始化,如果物理地址空间中的某些区域是ramrom或者是mmio内存就可以通过memory_region_add_subregion来定义子区域的MemoryRegion类型。

  1. void memory_region_add_subregion(MemoryRegion *mr,
  2. hwaddr offset,
  3. MemoryRegion *subregion)
  4. 其中mr为父MemoryRegion
  5. subregion 为子MemoryRegion
  6. offset表示 相对于 mr 起始地址的偏移
  7. 函数的作用:mr offset 处内存由 subregion 重新定义

回到本节的实例,流程如下

  1. 首先使用get_system_memory获取表示整个物理内存空间的MemoryRegion。
  2. 然后新建一个flash的MemoryRegion并使用memory_region_init_ram指定该MemoryRegion是一个RAM类型的,大小为FLASH_SIZE。
  3. 使用memory_region_add_subregion把flash挂载到system_memory起始地址偏移0处。

由于system_memory表示的是虚拟机的整个物理内存空间,执行完之后虚拟机物理地址0处的内存是RAM类型,大小为FLASH_SIZE,可以像内存使用一样直接读写。

申请rom

使用方式和申请ram的一样,不同的申请得到的内存为只读的

  1. void memory_region_init_rom(MemoryRegion *mr,
  2. struct Object *owner,
  3. const char *name,
  4. uint64_t size,
  5. Error **errp)

使用实例

  1. memory_region_init_rom(&s->rom, NULL, "imx6ul.rom", FSL_IMX6UL_ROM_SIZE, &error_abort);
  2. memory_region_add_subregion(get_system_memory(), FSL_IMX6UL_ROM_ADDR, &s->rom);

执行之后虚拟机 [FSL_IMX6UL_ROM_ADDR, FSL_IMX6UL_ROM_ADDR + FSL_IMX6UL_ROM_SIZE] 这段物理地址空间为ROM内存,只读。

申请mmio

使用的函数为memory_region_init_io

  1. void memory_region_init_io(MemoryRegion *mr,
  2. Object *owner,
  3. const MemoryRegionOps *ops,
  4. void *opaque,
  5. const char *name,
  6. uint64_t size)

申请之后,对mr内存区域的读写会调用ops指定回调函数进行处理,这种类型的内存是模拟外设时常用的内存类型,因为在ARM芯片中外设的寄存器空间会挂载在系统内存总线上,所以可以通过访问内存来访问外设的寄存器空间,从而控制外设的行为。

使用实例

  1. static uint64_t demo_read(void *opaque, hwaddr offset,
  2. unsigned size)
  3. {
  4. return data[offset];
  5. }
  6. static void demo_write(void *opaque, hwaddr offset,
  7. uint64_t value, unsigned size)
  8. {
  9. // 进行具体的写操作
  10. return;
  11. }
  12. static const MemoryRegionOps demo_ops = {
  13. .read = demo_read,
  14. .write = demo_write,
  15. .endianness = DEVICE_NATIVE_ENDIAN,
  16. };
  17. memory_region_init_io(&demo_mr, NULL, &demo_ops, NULL, "demo-mmio", 0x1000);
  18. memory_region_add_subregion(system_mem, 0x110000, &demo_mr);

执行完后虚拟机物理内存 [0x110000, 0x110000+0x1000] 这块区域为 mmio内存,当对这块内存进行读写操作时会调用demo_ops中指定的回调函数,比如读内存时调用demo_read, 写内存时调用demo_write。

以写内存为例

  1. static void demo_write(void *opaque, hwaddr offset,
  2. uint64_t value, unsigned size)
  3. {
  4. // 进行具体的写操作
  5. return;
  6. }

offset为虚拟机写内存的地址相对MemoryRegion起始地址的偏移,比如现在写的地址是 0x110012,写的数据大小为2个字节,值为 0xaabb

那么进入demo_write时的参数信息如下

  1. offset: 0x110012-0x110000 --> 0x12
  2. value: 0xaabb
  3. size: 2

申请alias内存

申请函数

  1. void memory_region_init_alias(MemoryRegion *mr,
  2. Object *owner,
  3. const char *name,
  4. MemoryRegion *orig,
  5. hwaddr offset,
  6. uint64_t size)

使用实例

  1. memory_region_init_ram(flash, NULL, "flash", FLASH_SIZE, &error_fatal);
  2. memory_region_init_alias(flash_alias, NULL, "flash.alias", flash, 0, FLASH_SIZE);
  3. memory_region_add_subregion(system_memory, 0x08000000, flash);
  4. memory_region_add_subregion(system_memory, 0, flash_alias);

执行完后 0x08000000 和 0 地址的内存是同一块,对 0x08000000 写数据,0地址也可以读到修改后的数据。

luaqemu申请内存的实现

lua脚本通过memory_regions定义内存申请

  1. memory_regions = {
  2. region_rom = {
  3. name = 'mem_rom',
  4. start = 0x0,
  5. size = 0x180000
  6. },
  7. region_ram = {
  8. name = 'mem_ram',
  9. start = 0x180000,
  10. size = 0xC0000
  11. },
  12. }
  1. static void init_memory_regions(void)
  2. {
  3. MemoryRegion *sysmem = get_system_memory();
  4. lua_get_global("memory_regions", THROW_ERROR);
  5. // 遍历 memory_regions
  6. while (lua_next(lua_state, -2)) {
  7. add_memory_region(sysmem);
  8. lua_pop(lua_state, 1);
  9. }
  10. }

遍历memory_regions然后对其中的每一项使用 add_memory_region 处理每一个内存映射

  1. static void add_memory_region(MemoryRegion *sm)
  2. {
  3. region_start = lua_get_unsigned("start", THROW_ERROR);
  4. region_size = lua_get_unsigned("size", THROW_ERROR);
  5. region_name = lua_get_string("name", THROW_ERROR);
  6. memory_region = g_new(MemoryRegion, 1);
  7. memory_region_allocate_system_memory(memory_region, NULL, region_name, region_size);
  8. memory_region_add_subregion(sm, region_start, memory_region);
  9. }

主要逻辑就是根据lua脚本的memory_region定义,使用memory_region_allocate_system_memory分配内存,然后使用memory_region_add_subregion挂载到system_memory中。

文件加载

lua脚本使用file_mappings定义文件加载的路径、地址和大小

  1. file_mappings = {
  2. main_rom = {
  3. name = 'examples/bcm4358/bcm4358.rom.bin',
  4. start = 0x0,
  5. size = 0x180000
  6. },
  7. main_ram = {
  8. name = 'kernel',
  9. start = 0x180000,
  10. size = 0xC0000
  11. }
  12. }

处理文件加载的逻辑位于init_file_mappings函数

  1. static void init_file_mappings(void)
  2. {
  3. lua_get_global("file_mappings", THROW_ERROR);
  4. while (lua_next(lua_state, -2)) {
  5. add_file(); // 具体处理
  6. lua_pop(lua_state, 1);
  7. }
  8. }

遍历file_mappings每一项,然后调用add_file处理每一个文件映射

  1. static void add_file(void)
  2. {
  3. mapping_fn = lua_get_string("name", THROW_ERROR);
  4. mapping_type = lua_get_string("type", NOTHROW_ERROR);
  5. if (!strcasecmp(mapping_fn, "kernel")) {
  6. mapping_fn = luastate.machine->kernel_filename;
  7. }
  8. if (mapping_type && !strcasecmp(mapping_type, "elf")) {
  9. load_arm_elf(mapping_fn);
  10. } else {
  11. load_flat_file(mapping_fn, mapping_start, mapping_size);
  12. }
  13. }

主要是分两种情况进行处理,如果name为kernel,就调用load_arm_elf加载elf文件到内存,否则就使用load_flat_file把文件直接加载到内存的指定位置。

  1. static void load_flat_file(const char *file_path, hwaddr start, uint64_t size)
  2. {
  3. char *fn = NULL;
  4. if (NULL == (fn = qemu_find_file(QEMU_FILE_TYPE_BIOS, file_path))) {
  5. error_report("Couldn't find rom image '%s'.", file_path);
  6. exit(4);
  7. }
  8. if (0 > load_image_targphys(fn, start, size)) {
  9. error_report("Couldn't map file to memory\n");
  10. exit(5);
  11. }
  12. g_free(fn);
  13. }

load_flat_file主要就是先qemu_find_file找到文件,然后使用load_image_targphys加载到指定的位置。

设置CPU状态和执行回调

init_cpu_state为处理函数

  1. static const keyword_table_t kwt[] =
  2. {
  3. {"reset_pc", init_reset_addr},
  4. {"env", init_cpu_env},
  5. {"callbacks", init_cpu_callbacks},
  6. {{0, 0}}
  7. };
  8. static int handle_keyword(int type, const char *key)
  9. {
  10. unsigned int n = sizeof(kwt) / sizeof(*kwt);
  11. int i = 0;
  12. for (;i < n; i++) {
  13. if (!strcmp(kwt[i].keyword, key)) {
  14. kwt[i].fptr(type);
  15. return 0;
  16. }
  17. }
  18. error_report("keyword '%s' not known", key);
  19. return -1;
  20. }
  21. static void init_cpu_state(void)
  22. {
  23. int m_type = 0;
  24. const char *m_name = NULL;
  25. lua_get_global("cpu", NOTHROW_ERROR);
  26. while (lua_next(lua_state, -2)) {
  27. m_name = lua_tostring(lua_state, -2);
  28. m_type = lua_type(lua_state, -1);
  29. handle_keyword(m_type, m_name);
  30. lua_pop(lua_state, 1);
  31. }
  32. }

主要就是根据关键字来调用对应的处理函数

init_reset_addr

用于设置系统启动后的PC值

  1. static void init_reset_addr(int type)
  2. {
  3. double d = 0;
  4. uint64_t addr;
  5. ARMCPU *cpu = ARM_CPU(luastate.cs);
  6. if (type != LUA_TNUMBER) {
  7. return;
  8. }
  9. d = lua_tonumber(lua_state, -1);
  10. lua_number2unsigned(addr, d);
  11. cpu->rvbar = addr;
  12. return;
  13. }

主要就是从lua脚本中提取reset_pc的值,暂时保存在cpu->rvbar,后面会在init_cpu_env设置pc。

  1. static void init_cpu_env(int type)
  2. {
  3. ........
  4. if (cpu->rvbar) {
  5. cpu_set_pc(luastate.cs, cpu->rvbar); // 设置 pc寄存器
  6. }

init_cpu_env

函数首先根据cpu->rvbar设置cpu的pc,然后会设置是否使用thumb指令集,后面会设置miss_max用于检测死循环,最后调用init_cpu_env_registers设置其他的回调函数。

  1. static void init_cpu_env(int type)
  2. {
  3. if (cpu->rvbar) {
  4. cpu_set_pc(luastate.cs, cpu->rvbar);
  5. }
  6. luastate.cpu->env.thumb = lua_get_boolean("thumb", 0);
  7. luastate.cs->crs.miss_max = lua_get_unsigned("stuck_max", 0);
  8. init_cpu_env_registers();
  9. }

miss_max固件代码死循环检测

在嵌入式固件中,在执行过程中如果发生了异常(比如发现某个硬件设备工作不正常),会进入死循环

  1. Infinite_Loop:
  2. b Infinite_Loop

最开始仿真固件时,就会由于某些硬件设备没有仿真正确,从而让固件代码进入了死循环,luaqemu实现了一种方式可以快速的检测发生死循环的位置.

首先在 init_cpu_env 中设置 miss_max,表示同一个状态进入次数的最大值,状态通过arm_cpu_state_hash计算得到

  1. uint64_t arm_cpu_state_hash(CPUState *cs, int flags)
  2. {
  3. ARMCPU *cpu = ARM_CPU(cs);
  4. CPUARMState *env = &amp;cpu->env;
  5. int max_regs = !is_a64(env) ? sizeof(env->regs) / sizeof(env->regs[0]) : sizeof(env->xregs) / sizeof(env->xregs[0]);
  6. uint64_t hash = !is_a64(env) ? env->regs[15] : env->pc;
  7. int i = 0;
  8. if (!is_a64(env)) {
  9. for (; i < max_regs; i++) {
  10. hash += env->regs[i];
  11. }
  12. } else {
  13. for (; i < max_regs; i++) {
  14. hash += env->xregs[i];
  15. }
  16. }
  17. return hash;
  18. }

该函数其实就是把所有cpu寄存器的值加在一起作为hash,用于标识每个状态。

然后在cpu_tb_exec中每个tb执行前会去计算当前状态的执行次数

  1. static inline tcg_target_ulong cpu_tb_exec(CPUState *cpu, TranslationBlock *itb)
  2. {
  3. if (cpu->crs.miss_max != 0) {
  4. record_cpu_state(cpu, 0);
  5. }

record_cpu_state的逻辑相对简单,根据当前cpu的寄存器状态计算hash(arm_cpu_state_hash),然后更新对应hash的执行次数,如果次数达到阈值(miss_count),就调用回调函数

  1. void record_cpu_state(CPUState *cpu, int flags)
  2. {
  3. CPUClass *cc = CPU_GET_CLASS(cpu);
  4. uint64_t hash = 0;
  5. if (cc->cpu_state_hash) {
  6. hash = cc->cpu_state_hash(cpu, flags);
  7. if (g_hash_table_contains(cpu->crs.cpu_states, GUINT_TO_POINTER(hash))) {
  8. cpu->crs.miss_count++;
  9. if (cpu->crs.miss_count >= cpu->crs.miss_max &amp;&amp; cpu->crs.state_cb != NULL) {
  10. cpu->crs.state_cb(cpu);
  11. }
  12. } else {
  13. cpu->crs.ns++;
  14. if (cpu->crs.ns >= cpu->crs.miss_max) {
  15. g_hash_table_remove_all(cpu->crs.cpu_states);
  16. cpu->crs.miss_count = 0;
  17. }
  18. g_hash_table_insert(cpu->crs.cpu_states, GUINT_TO_POINTER(hash), GUINT_TO_POINTER(hash));
  19. }
  20. }
  21. }

回调函数定义

  1. static void set_cpu_stuck_state_cb(void)
  2. {
  3. printf("Found stuck state callback. Make sure to set \"stuck_max\" in env block.\n");
  4. luastate.cs->crs.state_cb = cpu_stuck_callback;
  5. }

cpu_stuck_callback就是调用lua脚本里面指定的回调函数

lua脚本示例

  1. cpu = {
  2. env = {
  3. stuck_max = 200000,
  4. stuck_cb = lua_stuck_cb,
  5. ... }
  6. }

此外还有一个比较关键的点,由于qemu在执行时会把有跳转关系的tb链接到一起,所以如果程序一直死循环,则正常情况下cpu_tb_exec不会被执行多次,因为如果tb链接到一起后就不会进入cpu_tb_exec了。

luaqemu的做法是在tb_find里面当需要检测死循环时禁用tb链接

  1. /* See if we can patch the calling TB. */
  2. if (last_tb &amp;&amp; !qemu_loglevel_mask(CPU_LOG_TB_NOCHAIN) &amp;&amp; !cpu->crs.miss_max) {
  3. if (!tb->invalid) {
  4. tb_add_jump(last_tb, tb_exit, tb);
  5. }
  6. }

设置CPU寄存器初始值

  1. /* target/arm/cpu.h */
  2. static void init_cpu_env_registers(void)
  3. {
  4. int reg_i, reg_v = 0;
  5. char reg_s[4] = {0};
  6. lua_get_field("regs", 0);
  7. for (reg_i = 0; reg_i < sizeof(luastate.cpu->env.regs) / sizeof(*(luastate.cpu->env.regs)); reg_i++) {
  8. snprintf(reg_s, sizeof(reg_s), "r%d", reg_i);
  9. lua_pushstring(lua_state, reg_s);
  10. lua_gettable(lua_state, -2); /* get table[name] */
  11. if (lua_isnil(lua_state, -1)) {
  12. lua_pop(lua_state, 1);
  13. continue;
  14. } else {
  15. reg_v = lua_tointeger(lua_state, -1);
  16. debug_print("'%s' -> %x\n", reg_s, reg_v);
  17. luastate.cpu->env.regs[reg_i] = reg_v;
  18. }

关键逻辑就是根据regs的值,设置对应寄存器。

init_cpu_callbacks

该函数主要处理针对cpu事件的回调函数,比如指令执行、基本块执行等

  1. static const cb_keyword_table_t cb_kwt[] =
  2. {
  3. {"stuck_state_cb", &amp;luastate.stuck_state_cb, set_cpu_stuck_state_cb},
  4. {"exec_insn_cb", &amp;luastate.exec_insn_cb, NULL},
  5. {"exec_block_cb", &amp;luastate.exec_block_cb, NULL},
  6. {"post_exec_block_cb", &amp;luastate.post_exec_block_cb, NULL},
  7. {{0, 0, 0}}
  8. };

其中stuck_state_cb在上一节中已经说过

指令执行回调

luaqemu在gen_intermediate_code翻译指令的位置插入了lua_cpu_exec_insn_callback回调函数

  1. #ifdef CONFIG_LUAJIT
  2. uint64_t insn_bytes;
  3. if (dc->thumb) {
  4. insn_bytes = arm_lduw_code(env, dc->pc, dc->sctlr_b);
  5. if (insn_bytes >> 12 == 15 ||
  6. (insn_bytes >> 12 == 14 &amp;&amp; (insn_bytes &amp; (1 << 11)))) { // thumb2, see disas_thumb2_insn use
  7. insn_bytes = arm_ldl_code(env, dc->pc, dc->sctlr_b);
  8. }
  9. } else {
  10. insn_bytes = arm_ldl_code(env, dc->pc, dc->sctlr_b);
  11. }
  12. lua_cpu_exec_insn_callback(dc->pc, insn_bytes);
  13. #endif

lua_cpu_exec_insn_callback 其实就是调用lua侧的函数

基本块执行回调

cpu_tb_exec中基本块执行前调用lua_cpu_post_exec_block_callback,基本块执行后调用lua_cpu_post_exec_block_callback

lua_cpu_post_exec_block_callback和lua_cpu_post_exec_block_callback最后都是调用lua侧的函数

  1. /* Execute a TB, and fix up the CPU state afterwards if necessary */
  2. static inline tcg_target_ulong cpu_tb_exec(CPUState *cpu, TranslationBlock *itb)
  3. {
  4. #ifdef CONFIG_LUAJIT
  5. lua_cpu_exec_block_callback(itb->pc);
  6. #endif
  7. ret = tcg_qemu_tb_exec(env, tb_ptr);
  8. #ifdef CONFIG_LUAJIT
  9. lua_cpu_post_exec_block_callback(itb->pc);
  10. #endif

设置执行断点

处理函数为init_vm_states

  1. static void init_vm_states(void)
  2. {
  3. qemu_add_vm_change_state_handler(lua_vm_state_change, NULL);
  4. lua_get_global("breakpoints", NOTHROW_ERROR);
  5. while (lua_next(lua_state, -2)) {
  6. util_breakpoint_insert(lua_tointeger(lua_state, -2), bp_func);
  7. lua_pop(lua_state, 1);
  8. }
  9. }

主要逻辑就是调用qemu_add_vm_change_state_handler注册一个回调函数,当虚拟机状态变化时(比如命中断点、观察点等)调用对应函数。

然后处理breakpoints,用util_breakpoint_insert给地址插入断点,当断点命中执行 bp_func

  1. static void util_breakpoint_insert(uint64_t addr, int bp_func)
  2. {
  3. if(luastate.cs) {
  4. cpu_breakpoint_insert(luastate.cs, addr, BP_LUA, NULL);
  5. }
  6. g_hash_table_insert(breakpoints, GUINT_TO_POINTER(addr), GINT_TO_POINTER(bp_func));
  7. }

主要就是调用cpu_breakpoint_insert插入断点,然后把断点的地址和回调函数保存到全局哈希表里面。

下面看一下断点处理流程

  1. static void lua_vm_state_change(void *opaque, int running, RunState state)
  2. {
  3. // 获取当前pc值
  4. uint64_t old_pc = lua_current_pc();
  5. switch (state) {
  6. case RUN_STATE_DEBUG:
  7. handle_vm_state_breakpoint(old_pc);
  8. break;

init_vm_states处注册了lua_vm_state_change回调函数,当命中断点、命中watchpoint、单步执行时会触发RUN_STATE_DEBUG事件,断点就是在这里处理,最后会进入handle_vm_state_breakpoint处理断点事件

  1. static inline void handle_vm_state_breakpoint(uint64_t pc)
  2. {
  3. int bp_func;
  4. bp_func = GPOINTER_TO_INT(g_hash_table_lookup(breakpoints, GUINT_TO_POINTER(pc)));
  5. if (bp_func) {
  6. trigger_breakpoint(bp_func);
  7. if (pc == lua_current_pc()) {
  8. cpu_breakpoint_remove(luastate.cs, pc, BP_LUA);
  9. luastate.bp_pc = pc;
  10. luastate.bp_pc_ptr = &amp;luastate.bp_pc;
  11. cpu_single_step(luastate.cs, 1);
  12. }
  13. tb_flush(luastate.cs);
  14. } else {
  15. if (!luastate.bp_pc_ptr) {
  16. return;
  17. }
  18. cpu_single_step(luastate.cs, 0);
  19. cpu_breakpoint_insert(luastate.cs, luastate.bp_pc, BP_LUA, NULL);
  20. vm_start();
  21. luastate.bp_pc_ptr = NULL;
  22. }
  23. }

下面简单介绍下断点的处理流程

  1. 断点触发时进入handle_vm_state_breakpoint,然后根据pc搜索回调函数,然后trigger_breakpoint调用回调函数。
  2. 如果回调函数里面没有修改cpu的pc指针,则会调用cpu_breakpoint_remove临时删除该断点,并设置luastate.bp_pcluastate.bp_pc_ptr为此时的pc,即触发断点的pc。
  3. 然后cpu_single_step启用cpu的单步模式,下次执行一条指令后,会再次触发断点事件,进入该函数,此时bp_func为NULL。
  4. 然后会进入else分支,首先调用cpu_single_step关闭单步执行模式,然后调用cpu_breakpoint_insert重新把断点插到之前删除的位置。

至此luaqemu的初始化工作完成,下面分析luaqemu提供给lua脚本的一些api的实现

Luaqemu API实现分析

断点相关

设置断点

  1. void lua_breakpoint_insert(uint64_t addr, void (*func)(void))
  2. {
  3. int bp_func = luaL_ref(lua_state, LUA_REGISTRYINDEX);
  4. util_breakpoint_insert(addr, bp_func);
  5. }

删除断点

  1. void lua_breakpoint_remove(uint64_t addr)
  2. {
  3. util_breakpoint_remove(addr);
  4. }
  5. static void util_breakpoint_remove(uint64_t addr)
  6. {
  7. int bp_fun = GPOINTER_TO_INT(g_hash_table_lookup(breakpoints, GUINT_TO_POINTER(addr)));
  8. cpu_breakpoint_remove(luastate.cs, addr, BP_LUA);
  9. g_hash_table_remove(breakpoints, GUINT_TO_POINTER(addr));
  10. }

首先调用cpu_breakpoint_remove把断点撤销,然后从哈希表中删除断点。

watchpoint

新增

lua_watchpoint_insert调用util_watchpoint_insert进行具体的观察点设置

  1. void lua_watchpoint_insert(uint64_t addr, uint64_t size, int flags, watchpoint_cb func)
  2. {
  3. util_watchpoint_insert(addr, size, flags, func);
  4. }
  5. static void util_watchpoint_insert(uint64_t addr, uint64_t size, int flags, watchpoint_cb cb)
  6. {
  7. watchpoint_t *wp;
  8. flags |= BP_STOP_BEFORE_ACCESS;
  9. wp = g_malloc0(sizeof(*wp));
  10. wp->addr = addr;
  11. wp->len = size;
  12. wp->flags = flags;
  13. wp->fptr = cb;
  14. cpu_watchpoint_insert(luastate.cs, addr, size, flags, NULL);
  15. if (NULL == (watchpoints = g_list_append(watchpoints, wp))) {
  16. error_report("%s error adding watchpoint\n", __func__);
  17. g_free(wp);
  18. }
  19. }

util_watchpoint_insert会调用cpu_watchpoint_insert设置观察点,最后把观察点的信息设置到watchpoints列表中

watchpoint在lua_vm_state_change中进行处理,命中观察点时check_watchpoint函数会触发RUN_STATE_DEBUG事件

  1. static inline void handle_vm_state_watchpoint(CPUWatchpoint *wpt, watchpoint_t *owp)
  2. {
  3. GList *iterator;
  4. watchpoint_t *wp;
  5. if (luastate.old_wp_ptr &amp;&amp; owp &amp;&amp; luastate.old_wp_ptr == owp) {
  6. cpu_single_step(luastate.cs, 0);
  7. cpu_watchpoint_insert(luastate.cs, owp->addr, owp->len, owp->flags, NULL);
  8. vm_start(); /* this is expensive */
  9. return;
  10. }
  11. for(iterator = watchpoints; iterator; iterator = iterator->next) {
  12. wp = iterator->data;
  13. if (wp->addr == wpt->vaddr &amp;&amp; wp->len == wpt->len &amp;&amp; (wp->flags &amp; wpt->flags)) {
  14. watchpoint_args_t arg;
  15. arg.len = wpt->len;
  16. arg.flags = wpt->flags;
  17. arg.addr = wpt->vaddr;
  18. wp->fptr(&amp;arg);
  19. cpu_watchpoint_remove(luastate.cs, wp->addr, wp->len, wp->flags);
  20. luastate.old_wp_ptr = wp;
  21. cpu_single_step(luastate.cs, 1);
  22. // TODO: introduce flag potentially to control this behavior
  23. tb_flush(luastate.cs);
  24. return;
  25. }
  26. }
  27. }
  28. static void lua_vm_state_change(void *opaque, int running, RunState state)
  29. {
  30. switch (state) {
  31. case RUN_STATE_DEBUG:
  32. if (luastate.old_wp_ptr) {
  33. handle_vm_state_watchpoint(NULL, luastate.old_wp_ptr);
  34. luastate.old_wp_ptr = NULL;
  35. return;
  36. }
  37. if (luastate.cs->watchpoint_hit) {
  38. handle_vm_state_watchpoint(luastate.cs->watchpoint_hit, NULL);
  39. luastate.cs->watchpoint_hit = NULL;
  40. }
  41. break;

流程如下:

  1. 第一次进入old_wp_ptr为空,watchpoint_hit为命中的观察点结构
  2. handle_vm_state_watchpoint 函数会遍历观察点列表,找到回调函数进行调用,然后调用cpu_watchpoint_remove临时删除观察点
  3. cpu_single_step启用单步模式,让单步执行一条指令
  4. 再次进入lua_vm_state_change,此时luastate.old_wp_ptr为上次触发观察点的结构
  5. 此时关闭单步模式,然后重新把观察点插入

删除

从全局watchpoints列表中删除并调用cpu_watchpoint_remove撤销观察点。

  1. void lua_watchpoint_remove(uint64_t addr, uint64_t size, int flags)
  2. {
  3. util_watchpoint_remove(addr, size, flags);
  4. }
  5. static void util_watchpoint_remove(uint64_t addr, uint64_t size, int flags)
  6. {
  7. GList *iterator;
  8. watchpoint_t *wp;
  9. for(iterator = watchpoints; iterator; iterator = iterator->next) {
  10. wp = iterator->data;
  11. if (wp->addr == addr &amp;&amp; wp->len == size &amp;&amp; wp->flags == flags) {
  12. watchpoints = g_list_delete_link(watchpoints, iterator);
  13. cpu_watchpoint_remove(luastate.cs, wp->addr, wp->len, wp->flags);
  14. g_free(wp);
  15. return;
  16. }
  17. }
  18. error_report("%s could not find matching watchpoint\n", __func__);
  19. }

执行相关

lua_continue让虚拟机继续运行

  1. void lua_continue(void)
  2. {
  3. vm_start();
  4. }

寄存器操作

lua_set_pc 设置pc寄存器的值

  1. void lua_set_pc(uint64_t addr)
  2. {
  3. if (!is_a64(&amp;luastate.cpu->env)) {
  4. luastate.cpu->env.regs[15] = addr;
  5. } else {
  6. luastate.cpu->env.pc = addr;
  7. }
  8. }

lua_get_register 获取寄存器的值,就是根据索引去cpu->env结构里面取

  1. uint64_t lua_get_register(uint8_t reg)
  2. {
  3. if (!is_a64(&amp;luastate.cpu->env)) {
  4. if (reg >= sizeof(luastate.cpu->env.regs) / sizeof(*(luastate.cpu->env.regs))) {
  5. error_report("%s '%d' exceeds cpu registers", __func__, reg);
  6. return 0;
  7. }
  8. return luastate.cpu->env.regs[reg];
  9. } else {
  10. if (reg >= sizeof(luastate.cpu->env.xregs) / sizeof(*(luastate.cpu->env.xregs))) {
  11. error_report("%s '%d' exceeds cpu registers", __func__, reg);
  12. return 0;
  13. }
  14. return luastate.cpu->env.xregs[reg];
  15. }
  16. }

lua_set_register 设置寄存器的值,实现类似。

读写虚拟机内存

API列表

  1. uint8_t lua_read_byte(uint64_t);
  2. uint16_t lua_read_word(uint64_t);
  3. uint32_t lua_read_dword(uint64_t);
  4. uint64_t lua_read_qword(uint64_t);
  5. void lua_read_memory(uint8_t *, uint64_t, size_t);
  6. void lua_write_byte(uint64_t, uint8_t);
  7. void lua_write_word(uint64_t, uint16_t);
  8. void lua_write_dword(uint64_t, uint32_t);
  9. void lua_write_qword(uint64_t, uint64_t);
  10. void lua_write_memory(uint64_t, uint8_t *, size_t);

以lua_write_memory为例,这个是往某个地址写一段内存

  1. static inline int lua_memory_rw(target_ulong addr, uint8_t *buf, int len, bool is_write)
  2. {
  3. CPUClass *cc = CPU_GET_CLASS(luastate.cs);
  4. if (cc->memory_rw_debug) {
  5. return cc->memory_rw_debug(luastate.cs, addr, buf, len, is_write);
  6. }
  7. return cpu_memory_rw_debug(luastate.cs, addr, buf, len, is_write);
  8. }
  9. void lua_write_memory(uint64_t addr, uint8_t *src, size_t len)
  10. {
  11. lua_memory_rw(addr, src, len, 1);
  12. }
  13. void lua_read_memory(uint8_t *dest, uint64_t addr, size_t size)
  14. {
  15. lua_memory_rw(addr, dest, size, 0);
  16. }

主要就是调用 cpu_memory_rw_debug 进行内存的写

MMIO内存处理

注册

  1. void lua_trapped_physregion_add(uint64_t addr, uint64_t size, TprReadCb readCb, TprWriteCb writeCb)
  2. {
  3. util_trapped_physregion_add(addr, size, readCb, writeCb);
  4. }
  5. static void util_trapped_physregion_add(uint64_t addr, uint64_t size, TprReadCb readCb, TprWriteCb writeCb)
  6. {
  7. MemoryRegion *sysmem = get_system_memory();
  8. tpr = g_malloc0(sizeof(TrappedPhysRegion));
  9. tpr->readCb = readCb;
  10. tpr->writeCb = writeCb;
  11. tpr->ops.read = trapped_physregion_read;
  12. tpr->ops.write = trapped_physregion_write;
  13. tpr->ops.endianness = DEVICE_NATIVE_ENDIAN;
  14. snprintf(tpr->name, TPR_NAME_SIZE, "TPR_%" PRIx64 "-%" PRIx64 , addr, (addr+size));
  15. memory_region_init_io(&amp;tpr->region, NULL, &amp;tpr->ops, tpr, tpr->name, size);
  16. memory_region_add_subregion(sysmem, addr, &amp;tpr->region);
  17. if (NULL == (trapped_physregions = g_list_append(trapped_physregions, tpr))) {
  18. memory_region_del_subregion(sysmem, &amp;tpr->region);
  19. g_free(tpr);
  20. }
  21. }

核心点就是调用memory_region_init_io注册mmio内存,使得对该内存的读写会调用对应的回调函数,并把tpr作为第一个参数传入。

当对内存读写时会调用 trapped_physregion_readtrapped_physregion_write

  1. /* function stolen from memory.c */
  2. static hwaddr memory_region_to_absolute_addr(MemoryRegion *mr, hwaddr offset)
  3. {
  4. MemoryRegion *root;
  5. hwaddr abs_addr = offset;
  6. abs_addr += mr->addr;
  7. for (root = mr; root->container; ) {
  8. root = root->container;
  9. abs_addr += root->addr;
  10. }
  11. return abs_addr;
  12. }
  13. uint64_t trapped_physregion_read(void *opaque, hwaddr addr, unsigned size)
  14. {
  15. TrappedPhysRegion *tpr = opaque;
  16. TprReadCbArgs cbArgs;
  17. hwaddr addr2;
  18. addr2 = memory_region_to_absolute_addr(&amp;tpr->region, addr);
  19. cbArgs.opaque = opaque;
  20. cbArgs.addr = addr2;
  21. cbArgs.size = size;
  22. tpr->readCb(&amp;cbArgs);
  23. return 0;
  24. }
  25. void trapped_physregion_write(void *opaque, hwaddr addr, uint64_t data, unsigned size)
  26. {
  27. TrappedPhysRegion *tpr = opaque;
  28. TprWriteCbArgs cbArgs;
  29. hwaddr addr2;
  30. addr2 = memory_region_to_absolute_addr(&amp;tpr->region, addr);
  31. cbArgs.opaque = opaque;
  32. cbArgs.addr = addr2;
  33. cbArgs.data = data;
  34. cbArgs.size = size;
  35. tpr->writeCb(&amp;cbArgs);
  36. }

核心逻辑就是首先获取访问内存的地址,然后调用TrappedPhysRegion里面的回调函数。

删除

找到对应的region,然后调用memory_region_del_subregion删掉。

  1. void lua_trapped_physregion_remove(uint64_t addr, uint64_t size)
  2. {
  3. util_trapped_physregion_remove(addr, size);
  4. }
  5. static void util_trapped_physregion_remove(uint64_t addr, uint64_t size)
  6. {
  7. GList *iterator;
  8. TrappedPhysRegion *tpr;
  9. MemoryRegion *sysmem = get_system_memory();
  10. for(iterator = trapped_physregions; iterator; iterator = iterator->next) {
  11. tpr = iterator->data;
  12. if (tpr->region.addr == addr &amp;&amp; tpr->region.size == size) {
  13. trapped_physregions = g_list_delete_link(trapped_physregions, iterator);
  14. memory_region_del_subregion(sysmem, &amp;tpr->region);
  15. g_free(tpr);
  16. return;
  17. }
  18. }
  19. }

总结

本文分析了luaqemu的实现,luaqemu支持监控基本块、指令级别的监控,支持观察点、断点的设置,支持mmio内存的申请,而且提供了友好的用户接口,可以简单的对虚拟机内存进行读写,唯一不足的是没有中断相关的API。

下篇文章介绍如何使用QEMU模拟设备中断。

参考链接

  1. https://blog.csdn.net/huang987246510/article/details/104012839
  2. https://comsecuris.com/blog/posts/luaqemu_bcm_wifi/
  • 发表于 2021-04-26 22:07:35
  • 阅读 ( 7301 )
  • 分类:安全工具

0 条评论

hac425
hac425

19 篇文章