QEMU固件模擬技術分析-luaqemu實現分析


文章首發於

https://forum.butian.net/share/123

概述

在嵌入式安全領域常常需要分析各種不同形態的固件,如果需要動態執行某些代碼或者對固件進行Fuzzing測試,則需要對固件代碼進行仿真,常用的仿真工具一般為qemu和unicorn。unicorn適合模擬執行固件中的某些代碼片段,而對於中斷、異步執行則不支持,而大量的嵌入式固件都是以中斷驅動的,對於中斷的模擬則需要依賴於qemu的全系統模擬。

本文將以luaqemu為例介紹在使用qemu來模擬固件、外設時可以借鑒的技術,代碼地址

https://github.com/Comsecuris/luaqemu

簡單的說luaqemu通過修改部分qemu代碼並在一些關鍵的執行點增加回調函數,使得用戶可以通過lua腳本來加載固件、監控固件代碼的執行,設置觀察點、斷點等。

示例luaqemu腳本如下

require('hw.arm.luaqemu')
 
 machine_cpu = 'cortex-r5'
 
 memory_regions = {
     region_rom = {
         name = 'mem_rom',
         start = 0x0,
         size = 0x180000
     },
     region_ram = {
         name = 'mem_ram',
         start = 0x180000,
         size = 0xC0000
     },
 }
 
 file_mappings = {
     main_rom = {
         name = 'rom.bin',
         start = 0x0,
         size = 0x180000
     },
     main_ram = {
         name = 'kernel.bin',
         start = 0x180000,
         size = 0xC0000
     }
 }
 
 cpu = {
     env = {
         thumb = true,
     },
     reset_pc = 0
 }

該腳本的作用如下

  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的機器,代碼位於

hw/arm/luaarm.c

代碼通過宏和數據結構指定machine的類型和初始化函數

static void lua_class_init(O bjectClass *oc, void *data)
{
    MachineClass *mc = MACHINE_CLASS(oc);

    mc->desc = "Lua ARM M eta Machine";
    mc->init = lua_init;
}

static const TypeInfo lua_machine_type = {
    .name = MACHINE_TYPE_NAME("luaarm"),
    .parent = TYPE_MACHINE,
    .class_init = lua_class_init,
};

static void lua_machine_init(void)
{
    type_register_static(&lua_machine_type);
}

type_init(lua_machine_init)

可以看到lua_init為machine的入口函數

static void lua_init(MachineState *machine)
{	
	// 加載 lua_s cript 指定的腳本,命令行參數指定
    luaL_loadfile(lua_state, lua_s cript)

	// 設置 cpu 的類型
    machine->cpu_model = lua_tostring(lua_state, -1);
    
    // 根據lua腳本來設置虛擬機的狀態、固件的加載、回調函數注冊
 
    init_memory_regions();
    init_luastate(machine);
    init_file_mappings();
    init_cpu_state();
    init_vm_states();

    // 執行 lua 腳本的 post_init 函數
}

該函數會根據lua腳本函數設置虛擬機的狀態、固件的加載、回調函數注冊等,函數的主要流程如下

  1. 首先根據命令行參數加載指定的lua腳本
  2. 設置CPU的類型,內存映射關系
  3. 加載文件到虛擬機的內存
  4. 初始化CPU的狀態(寄存器)
  5. 注冊一系列回調函數,比如設置斷點、指令執行回調等

通過搜索lua_s cript關鍵字可以找到設置命令行參數的位置位於vl.c的main函數里面

            case QEMU_OPTION_lua:
                lua_s cript = optarg;
                break;

我們也可以通過類似的方式注冊需要的命令行參數

init_luastate 會把虛擬機的cpu對象保存到luastate全局變量里面

static void init_luastate(MachineState *machine)
{
    ARMCPU *cpu;
    O bjectClass *cpu_oc;
    CPUState *cs;

    cpu_oc = cpu_class_by_name(TYPE_ARM_CPU, machine->cpu_model);
    if (!cpu_oc) {
        error_report("machine \"%s\" not found, exiting\n", machine->cpu_model);
        exit(1);
    }

    cpu = ARM_CPU(O bject_new(O bject_class_get_name(cpu_oc)));
    cs = CPU(cpu);

    luastate.cpu = cpu;
    luastate.cs = cs;
    luastate.machine = machine;
    luastate.bp_pc = 0;
    luastate.bp_pc_ptr = NULL;
    luastate.old_wp_ptr = NULL;

    g_hash_table_foreach(breakpoints, add_cpu_breakpoints, NULL);
}

內存申請

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類型

void memory_region_init_ram(MemoryRegion *mr,
                            struct O bject *owner,
                            const char *name,
                            uint64_t size,
                            Error **errp)

使用實例

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

qemu通過MemoryRegion的組合來表示虛擬機的物理內存空間,qemu在啟動時會創建一個system_memory的MemoryRegion,system_memory是一個全局變量可以通過get_system_memory函數獲取。

static void memory_map_init(void)
{
    system_memory = g_malloc(sizeof(*system_memory));

    memory_region_init(system_memory, NULL, "system", UINT64_MAX);
    address_space_init(&address_space_memory, system_memory, "memory");

    system_io = g_malloc(sizeof(*system_io));
    memory_region_init_io(system_io, NULL, &unassigned_io_ops, NULL, "io",
                          65536);
    address_space_init(&address_space_io, system_io, "I/O");
}

system_memory的大小為UINT64_MAX, 表示了整個物理內存空間,這個只是一個初始化,如果物理地址空間中的某些區域是ramrom或者是mmio內存就可以通過memory_region_add_subregion來定義子區域的MemoryRegion類型。

void memory_region_add_subregion(MemoryRegion *mr,
                                 hwaddr offset,
                                 MemoryRegion *subregion)
其中mr為父MemoryRegion
subregion 為子MemoryRegion
offset表示 相對於 mr 起始地址的偏移

函數的作用: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的一樣,不同的申請得到的內存為只讀的

void memory_region_init_rom(MemoryRegion *mr,
                            struct O bject *owner,
                            const char *name,
                            uint64_t size,
                            Error **errp)

使用實例

memory_region_init_rom(&s->rom, NULL, "imx6ul.rom", FSL_IMX6UL_ROM_SIZE, &error_abort);
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

void memory_region_init_io(MemoryRegion *mr,
                           O bject *owner,
                           const MemoryRegionOps *ops,
                           void *opaque,
                           const char *name,
                           uint64_t size)

申請之后,對mr內存區域的讀寫會調用ops指定回調函數進行處理,這種類型的內存是模擬外設時常用的內存類型,因為在ARM芯片中外設的寄存器空間會掛載在系統內存總線上,所以可以通過訪問內存來訪問外設的寄存器空間,從而控制外設的行為。

使用實例

static uint64_t demo_read(void *opaque, hwaddr offset,
                                               unsigned size)
{
	
    return data[offset];
}

static void demo_write(void *opaque, hwaddr offset,
                                            uint64_t value, unsigned size)
{
	// 進行具體的寫操作
    return;
}

static const MemoryRegionOps demo_ops = {
    .read = demo_read,
    .write = demo_write,
    .endianness = DEVICE_NATIVE_ENDIAN,
};

memory_region_init_io(&demo_mr, NULL, &demo_ops, NULL, "demo-mmio",  0x1000);
memory_region_add_subregion(system_mem, 0x110000, &demo_mr);

執行完后虛擬機物理內存 [0x110000, 0x110000+0x1000] 這塊區域為 mmio內存,當對這塊內存進行讀寫操作時會調用demo_ops中指定的回調函數,比如讀內存時調用demo_read, 寫內存時調用demo_write。

以寫內存為例

static void demo_write(void *opaque, hwaddr offset,
                                            uint64_t value, unsigned size)
{
	// 進行具體的寫操作
    return;
}

offset為虛擬機寫內存的地址相對MemoryRegion起始地址的偏移,比如現在寫的地址是 0x110012,寫的數據大小為2個字節,值為 0xaabb

那么進入demo_write時的參數信息如下

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

申請alias內存

申請函數

void memory_region_init_alias(MemoryRegion *mr,
                              O bject *owner,
                              const char *name,
                              MemoryRegion *orig,
                              hwaddr offset,
                              uint64_t size)

使用實例

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

執行完后 0x08000000 和 0 地址的內存是同一塊,對 0x08000000 寫數據,0地址也可以讀到修改后的數據。

luaqemu申請內存的實現

lua腳本通過memory_regions定義內存申請

memory_regions = {
    region_rom = {
        name = 'mem_rom',
        start = 0x0,
        size = 0x180000
    },
    region_ram = {
        name = 'mem_ram',
        start = 0x180000,
        size = 0xC0000
    },
}
static void init_memory_regions(void)
{
    MemoryRegion *sysmem = get_system_memory();
    lua_get_global("memory_regions", THROW_ERROR);
    
    // 遍歷 memory_regions
    while (lua_next(lua_state, -2)) { 
        add_memory_region(sysmem);
        lua_pop(lua_state, 1);
    }
}

遍歷memory_regions然后對其中的每一項使用 add_memory_region 處理每一個內存映射

static void add_memory_region(MemoryRegion *sm)
{
    region_start = lua_get_unsigned("start", THROW_ERROR);
    region_size  = lua_get_unsigned("size", THROW_ERROR);
    region_name  = lua_get_string("name", THROW_ERROR);

    memory_region = g_new(MemoryRegion, 1);
    memory_region_allocate_system_memory(memory_region, NULL, region_name, region_size);
    memory_region_add_subregion(sm, region_start, memory_region);
}

主要邏輯就是根據lua腳本的memory_region定義,使用memory_region_allocate_system_memory分配內存,然后使用memory_region_add_subregion掛載到system_memory中。

文件加載

lua腳本使用file_mappings定義文件加載的路徑、地址和大小

 file_mappings = {
     main_rom = {
         name = 'examples/bcm4358/bcm4358.rom.bin',
         start = 0x0,
         size = 0x180000
     },
     main_ram = {
         name = 'kernel',
         start = 0x180000,
         size = 0xC0000
     }
 }

處理文件加載的邏輯位於init_file_mappings函數

static void init_file_mappings(void)
{
    lua_get_global("file_mappings", THROW_ERROR);

    while (lua_next(lua_state, -2)) {
        add_file();  // 具體處理
        lua_pop(lua_state, 1);
    }
}

遍歷file_mappings每一項,然后調用add_file處理每一個文件映射

static void add_file(void)
{
    mapping_fn    = lua_get_string("name", THROW_ERROR);
    mapping_type  = lua_get_string("type", NOTHROW_ERROR); 
    if (!strcasecmp(mapping_fn, "kernel")) {
        mapping_fn = luastate.machine->kernel_filename;
    }

    if (mapping_type && !strcasecmp(mapping_type, "elf")) {
        load_arm_elf(mapping_fn);
    } else {
        load_flat_file(mapping_fn, mapping_start, mapping_size);
    }
}

主要是分兩種情況進行處理,如果name為kernel,就調用load_arm_elf加載elf文件到內存,否則就使用load_flat_file把文件直接加載到內存的指定位置。

static void load_flat_file(const char *file_path, hwaddr start, uint64_t size)
{
    char *fn = NULL;
    if (NULL == (fn = qemu_find_file(QEMU_FILE_TYPE_BIOS, file_path))) {
        error_report("Couldn't find rom image '%s'.", file_path);
        exit(4);
    }

    if (0 > load_image_targphys(fn, start, size)) {
        error_report("Couldn't map file to memory\n");
        exit(5);
    }
    g_free(fn);
}

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

設置CPU狀態和執行回調

init_cpu_state為處理函數

static const keyword_table_t kwt[] =
{
    {"reset_pc", init_reset_addr},
    {"env", init_cpu_env},
    {"callbacks", init_cpu_callbacks},
    {{0, 0}}
};

static int handle_keyword(int type, const char *key)
{
    unsigned int n = sizeof(kwt) / sizeof(*kwt);
    int i = 0;
    for (;i < n; i++) {
        if (!strcmp(kwt[i].keyword, key)) {
            kwt[i].fptr(type);
            return 0;
        }
    }
    error_report("keyword '%s' not known", key);
    return -1;
}

static void init_cpu_state(void)
{
    int m_type = 0;
    const char *m_name = NULL;

    lua_get_global("cpu", NOTHROW_ERROR);

    while (lua_next(lua_state, -2)) {
        m_name = lua_tostring(lua_state, -2);
        m_type = lua_type(lua_state, -1);

        handle_keyword(m_type, m_name);

        lua_pop(lua_state, 1);
    }

}

主要就是根據關鍵字來調用對應的處理函數

init_reset_addr

用於設置系統啟動后的PC值

static void init_reset_addr(int type)
{
    double d = 0;
    uint64_t addr;
    ARMCPU *cpu = ARM_CPU(luastate.cs);

    if (type != LUA_TNUMBER) {
        return;
    }
    d = lua_tonumber(lua_state, -1);
    lua_number2unsigned(addr, d);

    cpu->rvbar = addr;

    return;
}

主要就是從lua腳本中提取reset_pc的值,暫時保存在cpu->rvbar,后面會在init_cpu_env設置pc。

static void init_cpu_env(int type)
{
    ........
    if (cpu->rvbar) {
        cpu_set_pc(luastate.cs, cpu->rvbar); // 設置 pc寄存器
    }

init_cpu_env

函數首先根據cpu->rvbar設置cpu的pc,然后會設置是否使用thumb指令集,后面會設置miss_max用於檢測死循環,最后調用init_cpu_env_registers設置其他的回調函數。

static void init_cpu_env(int type)
{

    if (cpu->rvbar) {
        cpu_set_pc(luastate.cs, cpu->rvbar);
    }

    luastate.cpu->env.thumb   = lua_get_boolean("thumb", 0);
    luastate.cs->crs.miss_max = lua_get_unsigned("stuck_max", 0);

    init_cpu_env_registers();
}

miss_max固件代碼死循環檢測

在嵌入式固件中,在執行過程中如果發生了異常(比如發現某個硬件設備工作不正常),會進入死循環

Infinite_Loop:
    b   Infinite_Loop

最開始仿真固件時,就會由於某些硬件設備沒有仿真正確,從而讓固件代碼進入了死循環,luaqemu實現了一種方式可以快速的檢測發生死循環的位置.

首先在 init_cpu_env 中設置 miss_max,表示同一個狀態進入次數的最大值,狀態通過arm_cpu_state_hash計算得到

uint64_t arm_cpu_state_hash(CPUState *cs, int flags)
{
    ARMCPU *cpu = ARM_CPU(cs);
    CPUARMState *env = &cpu->env;

    int max_regs = !is_a64(env) ? sizeof(env->regs) / sizeof(env->regs[0]) : sizeof(env->xregs) / sizeof(env->xregs[0]);
    uint64_t hash = !is_a64(env) ? env->regs[15] : env->pc;
    int i = 0;

    if (!is_a64(env)) {
        for (; i < max_regs; i++) {
            hash += env->regs[i];
        }
    } else {
        for (; i < max_regs; i++) {
            hash += env->xregs[i];
        }
    }
    return hash;
}

該函數其實就是把所有cpu寄存器的值加在一起作為hash,用於標識每個狀態。

然后在cpu_tb_exec中每個tb執行前會去計算當前狀態的執行次數

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

record_cpu_state的邏輯相對簡單,根據當前cpu的寄存器狀態計算hash(arm_cpu_state_hash),然后更新對應hash的執行次數,如果次數達到閾值(miss_count),就調用回調函數

void record_cpu_state(CPUState *cpu, int flags)
{
    CPUClass *cc = CPU_GET_CLASS(cpu);
    uint64_t hash = 0;

    if (cc->cpu_state_hash) {
        hash = cc->cpu_state_hash(cpu, flags);
        if (g_hash_table_contains(cpu->crs.cpu_states, GUINT_TO_POINTER(hash))) {
            cpu->crs.miss_count++;
            if (cpu->crs.miss_count >= cpu->crs.miss_max && cpu->crs.state_cb != NULL) {
                cpu->crs.state_cb(cpu);
            }
        } else {
            cpu->crs.ns++;
            if (cpu->crs.ns >= cpu->crs.miss_max) {
                g_hash_table_remove_all(cpu->crs.cpu_states);
                cpu->crs.miss_count = 0;
            }
            g_hash_table_insert(cpu->crs.cpu_states, GUINT_TO_POINTER(hash), GUINT_TO_POINTER(hash));
        }
    }
}

回調函數定義

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

cpu_stuck_callback就是調用lua腳本里面指定的回調函數

lua腳本示例

cpu = {
    env = {
        stuck_max = 200000,
        stuck_cb = lua_stuck_cb,
... }
}

此外還有一個比較關鍵的點,由於qemu在執行時會把有跳轉關系的tb鏈接到一起,所以如果程序一直死循環,則正常情況下cpu_tb_exec不會被執行多次,因為如果tb鏈接到一起后就不會進入cpu_tb_exec了。

luaqemu的做法是在tb_find里面當需要檢測死循環時禁用tb鏈接

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

設置CPU寄存器初始值

/* target/arm/cpu.h */
static void init_cpu_env_registers(void)
{
    int reg_i, reg_v = 0;
    char reg_s[4] = {0};

    lua_get_field("regs", 0);

    for (reg_i = 0; reg_i < sizeof(luastate.cpu->env.regs) / sizeof(*(luastate.cpu->env.regs)); reg_i++) {
        snprintf(reg_s, sizeof(reg_s), "r%d", reg_i);
        lua_pushstring(lua_state, reg_s);
        lua_gettable(lua_state, -2); /* get table[name] */

        if (lua_isnil(lua_state, -1)) {
            lua_pop(lua_state, 1);
            continue;
        } else {
            reg_v = lua_tointeger(lua_state, -1);
            debug_print("'%s' -> %x\n", reg_s, reg_v);
            luastate.cpu->env.regs[reg_i] = reg_v;
        }

關鍵邏輯就是根據regs的值,設置對應寄存器。

init_cpu_callbacks

該函數主要處理針對cpu事件的回調函數,比如指令執行、基本塊執行等

static const cb_keyword_table_t cb_kwt[] =
{
    {"stuck_state_cb",     &luastate.stuck_state_cb, set_cpu_stuck_state_cb},
    {"exec_insn_cb",       &luastate.exec_insn_cb, NULL},
    {"exec_block_cb",      &luastate.exec_block_cb, NULL},
    {"post_exec_block_cb", &luastate.post_exec_block_cb, NULL},
    {{0, 0, 0}}
};

其中stuck_state_cb在上一節中已經說過

指令執行回調

luaqemu在gen_intermediate_code翻譯指令的位置插入了lua_cpu_exec_insn_callback回調函數

#ifdef CONFIG_LUAJIT
        uint64_t insn_bytes;
        if (dc->thumb) {
            insn_bytes = arm_lduw_code(env, dc->pc, dc->sctlr_b);
            if (insn_bytes >> 12 == 15 ||
                (insn_bytes >> 12 == 14 && (insn_bytes & (1 << 11)))) { // thumb2, see disas_thumb2_insn use
                insn_bytes = arm_ldl_code(env, dc->pc, dc->sctlr_b);
            }
        } else {
            insn_bytes = arm_ldl_code(env, dc->pc, dc->sctlr_b);
        }

        lua_cpu_exec_insn_callback(dc->pc, insn_bytes);
#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側的函數

/* Execute a TB, and fix up the CPU state afterwards if necessary */
static inline tcg_target_ulong cpu_tb_exec(CPUState *cpu, TranslationBlock *itb)
{


#ifdef CONFIG_LUAJIT
    lua_cpu_exec_block_callback(itb->pc);
#endif

    ret = tcg_qemu_tb_exec(env, tb_ptr);

#ifdef CONFIG_LUAJIT
    lua_cpu_post_exec_block_callback(itb->pc);
#endif

設置執行斷點

處理函數為init_vm_states

static void init_vm_states(void)
{
    qemu_add_vm_change_state_handler(lua_vm_state_change, NULL);
    
    lua_get_global("breakpoints", NOTHROW_ERROR);

    while (lua_next(lua_state, -2)) {
        util_breakpoint_insert(lua_tointeger(lua_state, -2), bp_func);
        lua_pop(lua_state, 1);
    }
}

主要邏輯就是調用qemu_add_vm_change_state_handler注冊一個回調函數,當虛擬機狀態變化時(比如命中斷點、觀察點等)調用對應函數。

然后處理breakpoints,用util_breakpoint_insert給地址插入斷點,當斷點命中執行 bp_func

static void util_breakpoint_insert(uint64_t addr, int bp_func)
{
    if(luastate.cs) {
        cpu_breakpoint_insert(luastate.cs, addr, BP_LUA, NULL);
    }
    g_hash_table_insert(breakpoints, GUINT_TO_POINTER(addr), GINT_TO_POINTER(bp_func));
}

主要就是調用cpu_breakpoint_insert插入斷點,然后把斷點的地址和回調函數保存到全局哈希表里面。

下面看一下斷點處理流程

static void lua_vm_state_change(void *opaque, int running, RunState state)
{
   // 獲取當前pc值
    uint64_t old_pc = lua_current_pc();

    switch (state) {
        case RUN_STATE_DEBUG:
                handle_vm_state_breakpoint(old_pc);
            break;

init_vm_states處注冊了lua_vm_state_change回調函數,當命中斷點、命中watchpoint、單步執行時會觸發RUN_STATE_DEBUG事件,斷點就是在這里處理,最后會進入handle_vm_state_breakpoint處理斷點事件

static inline void handle_vm_state_breakpoint(uint64_t pc)
{
    int bp_func;
    bp_func = GPOINTER_TO_INT(g_hash_table_lookup(breakpoints, GUINT_TO_POINTER(pc)));
    if (bp_func) {
        trigger_breakpoint(bp_func);
        if (pc == lua_current_pc()) {
            cpu_breakpoint_remove(luastate.cs, pc, BP_LUA);
            luastate.bp_pc = pc;
            luastate.bp_pc_ptr = &luastate.bp_pc;
            cpu_single_step(luastate.cs, 1);
        }
        tb_flush(luastate.cs);
    } else {
        if (!luastate.bp_pc_ptr) {
            return;
        }
        cpu_single_step(luastate.cs, 0);
        cpu_breakpoint_insert(luastate.cs, luastate.bp_pc, BP_LUA, NULL);
        vm_start(); 
        luastate.bp_pc_ptr = NULL;
    }
}

下面簡單介紹下斷點的處理流程

  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實現分析

斷點相關

設置斷點

void lua_breakpoint_insert(uint64_t addr, void (*func)(void))
{
    int bp_func = luaL_ref(lua_state, LUA_REGISTRYINDEX);
    util_breakpoint_insert(addr, bp_func);
}

刪除斷點

void lua_breakpoint_remove(uint64_t addr)
{
    util_breakpoint_remove(addr);
}

static void util_breakpoint_remove(uint64_t addr)
{
    int bp_fun = GPOINTER_TO_INT(g_hash_table_lookup(breakpoints, GUINT_TO_POINTER(addr)));

    cpu_breakpoint_remove(luastate.cs, addr, BP_LUA);
    g_hash_table_remove(breakpoints, GUINT_TO_POINTER(addr));
}

首先調用cpu_breakpoint_remove把斷點撤銷,然后從哈希表中刪除斷點。

watchpoint

新增

lua_watchpoint_insert調用util_watchpoint_insert進行具體的觀察點設置

void lua_watchpoint_insert(uint64_t addr, uint64_t size, int flags, watchpoint_cb func)
{
  util_watchpoint_insert(addr, size, flags, func);
}

static void util_watchpoint_insert(uint64_t addr, uint64_t size, int flags, watchpoint_cb cb)
{
    watchpoint_t *wp;

    flags |= BP_STOP_BEFORE_ACCESS;
    wp = g_malloc0(sizeof(*wp));
    wp->addr = addr;
    wp->len = size;
    wp->flags = flags;
    wp->fptr = cb;

    cpu_watchpoint_insert(luastate.cs, addr, size, flags, NULL);

    if (NULL == (watchpoints = g_list_append(watchpoints, wp))) {
        error_report("%s error adding watchpoint\n", __func__);
        g_free(wp);
    }
}

util_watchpoint_insert會調用cpu_watchpoint_insert設置觀察點,最后把觀察點的信息設置到watchpoints列表中

watchpoint在lua_vm_state_change中進行處理,命中觀察點時check_watchpoint函數會觸發RUN_STATE_DEBUG事件

static inline void handle_vm_state_watchpoint(CPUWatchpoint *wpt, watchpoint_t *owp)
{
    GList *iterator;
    watchpoint_t *wp;

    if (luastate.old_wp_ptr && owp && luastate.old_wp_ptr == owp) {
        cpu_single_step(luastate.cs, 0);
        cpu_watchpoint_insert(luastate.cs, owp->addr, owp->len, owp->flags, NULL);
        vm_start(); /* this is expensive */
        return;
    }
    for(iterator = watchpoints; iterator; iterator = iterator->next) {
        wp = iterator->data;
        if (wp->addr == wpt->vaddr && wp->len == wpt->len && (wp->flags & wpt->flags)) {

            watchpoint_args_t arg;
            arg.len = wpt->len;
            arg.flags = wpt->flags;
            arg.addr = wpt->vaddr;
            wp->fptr(&arg);

            cpu_watchpoint_remove(luastate.cs, wp->addr, wp->len, wp->flags);
            luastate.old_wp_ptr = wp;
            cpu_single_step(luastate.cs, 1);
            // TODO: introduce flag potentially to control this behavior
            tb_flush(luastate.cs);
            return;
        }
    }
}

static void lua_vm_state_change(void *opaque, int running, RunState state)
{
    switch (state) {
        case RUN_STATE_DEBUG:
            if (luastate.old_wp_ptr) {
                handle_vm_state_watchpoint(NULL, luastate.old_wp_ptr);
                luastate.old_wp_ptr = NULL;
                return;
            }
            if (luastate.cs->watchpoint_hit) {
                handle_vm_state_watchpoint(luastate.cs->watchpoint_hit, NULL);
                luastate.cs->watchpoint_hit = NULL;
            } 
            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撤銷觀察點。

void lua_watchpoint_remove(uint64_t addr, uint64_t size, int flags)
{
  util_watchpoint_remove(addr, size, flags);
}


static void util_watchpoint_remove(uint64_t addr, uint64_t size, int flags)
{
    GList *iterator;
    watchpoint_t *wp;

    for(iterator = watchpoints; iterator; iterator = iterator->next) {
        wp = iterator->data;
        if (wp->addr == addr && wp->len == size && wp->flags == flags) {
            watchpoints = g_list_delete_l ink(watchpoints, iterator);
            cpu_watchpoint_remove(luastate.cs, wp->addr, wp->len, wp->flags);
            g_free(wp);
            return;
        }
    }
    error_report("%s could not find matching watchpoint\n", __func__);
}

執行相關

lua_continue讓虛擬機繼續運行

void lua_continue(void)
{
    vm_start();
}

寄存器操作

lua_set_pc 設置pc寄存器的值

void lua_set_pc(uint64_t addr)
{
    if (!is_a64(&luastate.cpu->env)) {
        luastate.cpu->env.regs[15] = addr;
    } else {
        luastate.cpu->env.pc = addr;
    }
}

lua_get_register 獲取寄存器的值,就是根據索引去cpu->env結構里面取

uint64_t lua_get_register(uint8_t reg)
{
    if (!is_a64(&luastate.cpu->env)) {
        if (reg >= sizeof(luastate.cpu->env.regs) / sizeof(*(luastate.cpu->env.regs))) {
            error_report("%s '%d' exceeds cpu registers", __func__, reg);
            return 0;
        }
        return luastate.cpu->env.regs[reg];
    } else {
        if (reg >= sizeof(luastate.cpu->env.xregs) / sizeof(*(luastate.cpu->env.xregs))) {
            error_report("%s '%d' exceeds cpu registers", __func__, reg);
            return 0;
        }
        return luastate.cpu->env.xregs[reg];
    }
}

lua_set_register 設置寄存器的值,實現類似。

讀寫虛擬機內存

API列表

uint8_t lua_read_byte(uint64_t);
uint16_t lua_read_word(uint64_t);
uint32_t lua_read_dword(uint64_t);
uint64_t lua_read_qword(uint64_t);
void lua_read_memory(uint8_t *, uint64_t, size_t);
void lua_write_byte(uint64_t, uint8_t);
void lua_write_word(uint64_t, uint16_t);
void lua_write_dword(uint64_t, uint32_t);
void lua_write_qword(uint64_t, uint64_t);
void lua_write_memory(uint64_t, uint8_t *, size_t);

以lua_write_memory為例,這個是往某個地址寫一段內存

static inline int lua_memory_rw(target_ulong addr, uint8_t *buf, int len, bool is_write)
{
	CPUClass *cc = CPU_GET_CLASS(luastate.cs);
	if (cc->memory_rw_debug) {
		return cc->memory_rw_debug(luastate.cs, addr, buf, len, is_write);
	}
	return cpu_memory_rw_debug(luastate.cs, addr, buf, len, is_write);
}

void lua_write_memory(uint64_t addr, uint8_t *src, size_t len)
{
	lua_memory_rw(addr, src, len, 1);
}

void lua_read_memory(uint8_t *dest, uint64_t addr, size_t size)
{
	lua_memory_rw(addr, dest, size, 0);
}

主要就是調用 cpu_memory_rw_debug 進行內存的寫

MMIO內存處理

注冊

void lua_trapped_physregion_add(uint64_t addr, uint64_t size, TprReadCb readCb, TprWriteCb writeCb)
{
    util_trapped_physregion_add(addr, size, readCb, writeCb);    
}

static void util_trapped_physregion_add(uint64_t addr, uint64_t size, TprReadCb readCb, TprWriteCb writeCb)
{
    MemoryRegion *sysmem = get_system_memory();    

    tpr = g_malloc0(sizeof(TrappedPhysRegion));

    tpr->readCb = readCb;
    tpr->writeCb = writeCb;

    tpr->ops.read = trapped_physregion_read;
    tpr->ops.write = trapped_physregion_write;
    tpr->ops.endianness = DEVICE_NATIVE_ENDIAN;
    snprintf(tpr->name, TPR_NAME_SIZE, "TPR_%" PRIx64 "-%" PRIx64 , addr, (addr+size));

    memory_region_init_io(&tpr->region, NULL, &tpr->ops, tpr, tpr->name, size);
    memory_region_add_subregion(sysmem, addr, &tpr->region);

    if (NULL == (trapped_physregions = g_list_append(trapped_physregions, tpr))) {
        memory_region_del_subregion(sysmem, &tpr->region);
        g_free(tpr);
    }
}

核心點就是調用memory_region_init_io注冊mmio內存,使得對該內存的讀寫會調用對應的回調函數,並把tpr作為第一個參數傳入。

當對內存讀寫時會調用 trapped_physregion_readtrapped_physregion_write

/* function stolen from memory.c */
static hwaddr memory_region_to_absolute_addr(MemoryRegion *mr, hwaddr offset)
{
    MemoryRegion *root;
    hwaddr abs_addr = offset;

    abs_addr += mr->addr;
    for (root = mr; root->container; ) {
        root = root->container;
        abs_addr += root->addr;
    }

    return abs_addr;
}

uint64_t trapped_physregion_read(void *opaque, hwaddr addr, unsigned size)
{
    TrappedPhysRegion *tpr = opaque;
    TprReadCbArgs cbArgs;
    hwaddr addr2;

    addr2 = memory_region_to_absolute_addr(&tpr->region, addr);    

    cbArgs.opaque = opaque;
    cbArgs.addr = addr2;
    cbArgs.size = size;
    tpr->readCb(&cbArgs);

    return 0;
}

void trapped_physregion_write(void *opaque, hwaddr addr, uint64_t data, unsigned size)
{
    TrappedPhysRegion *tpr = opaque;
    TprWriteCbArgs cbArgs;
    hwaddr addr2;

    addr2 = memory_region_to_absolute_addr(&tpr->region, addr);

    cbArgs.opaque = opaque;
    cbArgs.addr = addr2;
    cbArgs.data = data;
    cbArgs.size = size;
    tpr->writeCb(&cbArgs);
}

核心邏輯就是首先獲取訪問內存的地址,然后調用TrappedPhysRegion里面的回調函數。

刪除

找到對應的region,然后調用memory_region_del_subregion刪掉。

void lua_trapped_physregion_remove(uint64_t addr, uint64_t size)
{
    util_trapped_physregion_remove(addr, size);
}

static void util_trapped_physregion_remove(uint64_t addr, uint64_t size)
{
    GList *iterator;
    TrappedPhysRegion *tpr;
    MemoryRegion *sysmem = get_system_memory();

    for(iterator = trapped_physregions; iterator; iterator = iterator->next) {
        tpr = iterator->data;
        if (tpr->region.addr == addr && tpr->region.size == size) {
            trapped_physregions = g_list_delete_l ink(trapped_physregions, iterator);
            memory_region_del_subregion(sysmem, &tpr->region);
            g_free(tpr);
            return;
        }
    }
}

總結

本文分析了luaqemu的實現,luaqemu支持監控基本塊、指令級別的監控,支持觀察點、斷點的設置,支持mmio內存的申請,而且提供了友好的用戶接口,可以簡單的對虛擬機內存進行讀寫,唯一不足的是沒有中斷相關的API。

下篇文章介紹如何使用QEMU模擬設備中斷。

參考鏈接

https://blog.csdn.net/huang987246510/article/d etails/104012839
https://comsecuris.com/blog/posts/luaqemu_bcm_wifi/


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM