问答
发起
提问
文章
攻防
活动
Toggle navigation
首页
(current)
问答
商城
实战攻防技术
漏洞分析与复现
NEW
活动
摸鱼办
搜索
登录
注册
QEMU固件模拟技术-stm32仿真分析及IRQ仿真实践
# 概述 上一篇文件介绍了luaqemu的实现,也提到luaqemu并没有对中断相关api进行封装,本节主要基于stm32f205-soc的实现来介绍中断的仿真,并提供一个用于测试qemu设备模拟的裸板程序来测试中...
概述 == 上一篇文件介绍了luaqemu的实现,也提到luaqemu并没有对中断相关api进行封装,本节主要基于stm32f205-soc的实现来介绍中断的仿真,并提供一个用于测试qemu设备模拟的裸板程序来测试中断的仿真。 本文相关代码地址 ```php https://github.com/hac425xxx/qemu-fuzzing/commit/609538e1407de884f6c9e4d222431c9032abc25b https://github.com/hac425xxx/qemu-fuzzing/commit/7bc0e0aa35363c18fcf5b89dacab73a0a9bef147 ``` stm32f205-soc实现 =============== 为了仿真某个设备,我们需要通过阅读硬件文档或者通过逆向程序逻辑来获取外设的行为,然后再在qemu中进行模拟,stm32f205的手册可以直接在网上下载 ```php https://www.st.com/resource/en/reference_manual/cd00225773-stm32f205xx-stm32f207xx-stm32f215xx-and-stm32f217xx-advanced-arm-based-32-bit-mcus-stmicroelectronics.pdf ``` qemu中名字为netduino2的Machine使用到了stm32f205-soc这个设备,可以使用 -M 指定使用该设备 ```php qemu-system-arm -M netduino2 ``` netduino2的初始化函数为netduino2\_init ```php static void netduino2_init(MachineState *machine) { DeviceState *dev; dev = qdev_create(NULL, TYPE_STM32F205_SOC); qdev_prop_set_string(dev, "cpu-type", ARM_CPU_TYPE_NAME("cortex-m3")); object_property_set_bool(OBJECT(dev), true, "realized", &error_fatal); armv7m_load_kernel(ARM_CPU(first_cpu), machine->kernel_filename, FLASH_SIZE); } ``` 函数逻辑如下: 1. 首先创建stm32f205-soc设备,然后设置cpu-type为 `cortex-m3` 2. 然后通过设置 realized 触发stm32f205\_soc\_realize函数的调用 3. 最后armv7m\_load\_kernel把命令行-kernel指定的文件加载到虚拟机内存。 ```php static void stm32f205_soc_class_init(ObjectClass *klass, void *data) { DeviceClass *dc = DEVICE_CLASS(klass); dc->realize = stm32f205_soc_realize; dc->props = stm32f205_soc_properties; } static const TypeInfo stm32f205_soc_info = { .name = TYPE_STM32F205_SOC, .parent = TYPE_SYS_BUS_DEVICE, .instance_size = sizeof(STM32F205State), .instance_init = stm32f205_soc_initfn, .class_init = stm32f205_soc_class_init, }; ``` 下面分析stm32f205\_soc\_realize的实现 初始化flash和sram ------------- stm32f205的内存映射如下 [![](https://shs3.b.qianxin.com/attack_forum/2021/04/attach-c5064d3e9f74e1ccf756d1ebde703d9e74152dcb.png)](https://shs3.b.qianxin.com/attack_forum/2021/04/attach-c5064d3e9f74e1ccf756d1ebde703d9e74152dcb.png) stm32f205\_soc\_realize主要实现了红框标注的三个内存区域 1. 位于0x8000000处的flash区域 2. 位于0x0处的区域,是flash的alias区域 3. 位于0x20000000处的sram区域 函数入口首先设置了flash和sram. ```php MemoryRegion *system_memory = get_system_memory(); MemoryRegion *sram = g_new(MemoryRegion, 1); MemoryRegion *flash = g_new(MemoryRegion, 1); MemoryRegion *flash_alias = g_new(MemoryRegion, 1); MemoryRegion *demo_mem = g_new(MemoryRegion, 1); memory_region_init_ram(flash, NULL, "STM32F205.flash", FLASH_SIZE, &error_fatal); memory_region_init_alias(flash_alias, NULL, "STM32F205.flash.alias", flash, 0, FLASH_SIZE); memory_region_set_readonly(flash, true); memory_region_set_readonly(flash_alias, true); memory_region_add_subregion(system_memory, FLASH_BASE_ADDRESS, flash); memory_region_add_subregion(system_memory, 0, flash_alias); memory_region_init_ram(sram, NULL, "STM32F205.sram", SRAM_SIZE, &error_fatal); memory_region_add_subregion(system_memory, SRAM_BASE_ADDRESS, sram); ``` 1. 主要就是新建flash区域和flash\_alias,然后通过memory\_region\_add\_subregion把这两个区域放到对应的地址,这样0x0和0x8000000实际指向的是同一块RAM。 2. 然后新建sram区域,并把sram放到0x20000000处。 初始化外设 ----- 在初始化flash和sram后,会逐步初始化用到的外设,这里以UART外设为例进行介绍 ### UART外设 #### 初始化 uart使用sysbus\_mmio\_map把外设的寄存器区域映射为mmio内存,然后使用sysbus\_connect\_irq初始化外设需要的irq。 ```php /* Attach UART (uses USART registers) and USART controllers */ for (i = 0; i < STM_NUM_USARTS; i++) { dev = DEVICE(&(s->usart[i])); qdev_prop_set_chr(dev, "chardev", serial_hd(i)); object_property_set_bool(OBJECT(&s->usart[i]), true, "realized", &err); if (err != NULL) { error_propagate(errp, err); return; } busdev = SYS_BUS_DEVICE(dev); sysbus_mmio_map(busdev, 0, usart_addr[i]); sysbus_connect_irq(busdev, 0, qdev_get_gpio_in(armv7m, usart_irq[i])); } ``` s->usart在stm32f205\_soc\_initfn中创建 ```php static void stm32f205_soc_initfn(Object *obj) { for (i = 0; i < STM_NUM_USARTS; i++) { sysbus_init_child_obj(obj, "usart[*]", &s->usart[i], sizeof(s->usart[i]), TYPE_STM32F2XX_USART); } ``` 实际就是创建了TYPE\_STM32F2XX\_USART设备 ```php static const TypeInfo stm32f2xx_usart_info = { .name = TYPE_STM32F2XX_USART, .parent = TYPE_SYS_BUS_DEVICE, .instance_size = sizeof(STM32F2XXUsartState), .instance_init = stm32f2xx_usart_init, .class_init = stm32f2xx_usart_class_init, }; ``` 调用sysbus\_init\_child\_obj函数初始化设备时会调用stm32f2xx\_usart\_init ```php static const MemoryRegionOps stm32f2xx_usart_ops = { .read = stm32f2xx_usart_read, .write = stm32f2xx_usart_write, .endianness = DEVICE_NATIVE_ENDIAN, }; static void stm32f2xx_usart_init(Object *obj) { STM32F2XXUsartState *s = STM32F2XX_USART(obj); sysbus_init_irq(SYS_BUS_DEVICE(obj), &s->irq); memory_region_init_io(&s->mmio, obj, &stm32f2xx_usart_ops, s, TYPE_STM32F2XX_USART, 0x400); sysbus_init_mmio(SYS_BUS_DEVICE(obj), &s->mmio); } ``` 函数做的工作如下 1. 初始化设备的irq,保存到s->irq 2. 初始化s->mmio,设置memory\_region的大小为0x400,mmio内存访问的回调函数由stm32f2xx\_usart\_ops指定 3. sysbus\_init\_mmio主要是把s->mmio的指针保存到设备mmio数组中,以便后续使用sysbus\_mmio\_map把memory\_region挂载到对应的地址。 #### mmio映射 stm32f205-soc实现了6个uart设备,设备mmio的起始地址分别为 ```php static const uint32_t usart_addr[STM_NUM_USARTS] = { 0x40011000, 0x40004400, 0x40004800, 0x40004C00, 0x40005000, 0x40011400 }; ``` 其中4个uart设备在手册memory map中的截图如下 [![](https://shs3.b.qianxin.com/attack_forum/2021/04/attach-824a9dae36b6e341109e767d213d2a207a572ec0.png)](https://shs3.b.qianxin.com/attack_forum/2021/04/attach-824a9dae36b6e341109e767d213d2a207a572ec0.png) 然后在stm32f205\_soc\_realize函数里面会调用sysbus\_mmio\_map把设备的memory\_region挂载到指定的位置 ```php sysbus_mmio_map(busdev, 0, usart_addr[i]); ``` #### 中断初始化 ##### qemu中断模型 **概念** qemu使用GPIO来实现中断系统,其简单的原理如下 ```php Device.[GPIO_OUT] ->[GPIO_IN].GIC.[GPIO_OUT]->[GPIO_IN].core ``` 1. 首先CPU有GPIO\_IN接口 2. 然后中断控制器(GIC)有GPIO\_IN和GPIO\_OUT, GPIO\_OUT和CPU的GPIO\_IN接口关联 3. 设备的GPIO\_OUT和GIC的GPIO\_IN关联 4. 当有中断发生时,设备通过GPIO\_OUT通知GIC,GIC通过GPIO\_OUT通知GPIO\_IN。 中断依赖qemu\_irq结构体 ```php struct IRQState { Object parent_obj; qemu_irq_handler handler; // irq处理函数 void *opaque; int n; // irq的编号 }; typedef struct IRQState *qemu_irq; ``` 要触发一个`irq`,可以使用`qemu_set_irq`函数 ```php void qemu_set_irq(qemu_irq irq, int level) { if (!irq) return; irq->handler(irq->opaque, irq->n, level); // 调用irq的回调函数,传入中断号n } ``` GPIO\_IN通过qdev\_init\_gpio\_in初始化 ```php void qdev_init_gpio_in(DeviceState *dev, qemu_irq_handler handler, int n) ``` 初始化n个GPIO\_IN接口,每个GPIO\_IN接口的回调函数为handler,实际就是新建n个qemu\_irq对象,qemu\_irq的回调函数为handler。 GPIO\_OUT初始化函数为sysbus\_init\_irq ```php /* Request an IRQ source. The actual IRQ object may be populated later. */ void sysbus_init_irq(SysBusDevice *dev, qemu_irq *p) ``` `qemu`使用`sysbus_connect_irq`将`GPIO_OUT`和`GPIO_IN`关联 ```php void sysbus_connect_irq(SysBusDevice *dev, int n, qemu_irq irq) 把dev中的第n个gpio_out和irq关联 实际就是把irq保存为第n个gpio_out的值 ``` ##### **实例分析** 比如在armv7m\_nvic\_realize调用qdev\_init\_gpio\_in初始化num\_irq个GPIO\_IN ```php static void armv7m_nvic_realize(DeviceState *dev, Error **errp) { qdev_init_gpio_in(dev, set_irq_level, s->num_irq); ``` uart设备在stm32f2xx\_usart\_init函数中通过sysbus\_init\_irq初始化一个GPIO\_OUT ```php sysbus_init_irq(SYS_BUS_DEVICE(obj), &s->irq); ``` 这样**第0个GPIO就指向了s->irq**。 `stm32f205_soc_realize`会使用`sysbus_connect_irq`把设备的第0个GPIO和 nvic 的特定GPIO\_IN进行关联。 实质上就是把 **s->irq = qdev\_get\_gpio\_in(armv7m, timer\_irq\[i\])**。 ```php sysbus_connect_irq(busdev, 0, qdev_get_gpio_in(armv7m, usart_irq[i])); ``` `timer_irq` 保存了每个`uart`设备需要使用的IRQ号 ```php static const int usart_irq[STM_NUM_USARTS] = {37, 38, 39, 52, 53, 71}; ``` [![](https://shs3.b.qianxin.com/attack_forum/2021/04/attach-025da4d43898814b6da83582ff1701363a2aad20.png)](https://shs3.b.qianxin.com/attack_forum/2021/04/attach-025da4d43898814b6da83582ff1701363a2aad20.png) 此外还有一个需要注意的点,这里的irq号和其在异常向量表中的位置存在以下关系 ```php IRQ 号 = IRQ处理函数在异常向量表中的序号 - CPU内置异常数目 ``` 以stm32f205-soc为例,其使用的CPU为cortex-m3,CPU的内部中断数目为16个 [![](https://shs3.b.qianxin.com/attack_forum/2021/04/attach-cdf51f24c1611a9981b2693c67c95f5c4819d452.png)](https://shs3.b.qianxin.com/attack_forum/2021/04/attach-cdf51f24c1611a9981b2693c67c95f5c4819d452.png) 比如异常向量表的第17号中断的irq编号为 `17 - 16 = 1`,下图是设备手册异常向量表中IRQ开头部分: [![](https://shs3.b.qianxin.com/attack_forum/2021/04/attach-5a8b037a4520298f04dbae3cde58689e4d5ae93e.png)](https://shs3.b.qianxin.com/attack_forum/2021/04/attach-5a8b037a4520298f04dbae3cde58689e4d5ae93e.png) uart设备在stm32f2xx\_usart\_write中需要触发特定中断时会调用 ```php if (s->usart_cr1 & USART_CR1_RXNEIE && s->usart_sr & USART_SR_RXNE) { qemu_set_irq(s->irq, 1); } ``` s->irq 在之前使用sysbus\_connect\_irq时就被设置成nvic中对应irq的qemu\_irq结构 这里实际会调用set\_irq\_level通知nvic指定的中断到来 ```php /* callback when external interrupt line is changed */ static void set_irq_level(void *opaque, int n, int level) { n += NVIC_FIRST_IRQ; // irq 号 + CPU内置异常树(16) vec = &s->vectors[n]; if (level != vec->level) { vec->level = level; if (level) { armv7m_nvic_set_pending(s, n, false); } } } ``` 主要就是根据IRQ号n,找到对应的异常信息 `vec`, 然后判断vec的状态(高定平(`level=1`),还是低电平(`level=0`)) 如果是高电平,则会进入`armv7m_nvic_set_pending`通知CPU中断到来,实际也是调用CPU之前注册的GPIO\_IN的回调函数通知。 因此qemu的中断实现其实是依赖于qemu\_irq来实现,比如NVIC要通知CPU中断到来,实际就是调用CPU的qemu\_irq中的回调函数实现。 固件加载 ---- netduino2\_init在初始化stm32f205-soc后,调用armv7m\_load\_kernel加载二进制到内存 ```php armv7m_load_kernel(ARM_CPU(first_cpu), machine->kernel_filename, FLASH_SIZE); void armv7m_load_kernel(ARMCPU *cpu, const char *kernel_filename, int mem_size) { .................. if (kernel_filename) { image_size = load_elf_as(kernel_filename, NULL, NULL, NULL, &entry, &lowaddr, NULL, big_endian, EM_ARM, 1, 0, as); if (image_size < 0) { image_size = load_image_targphys_as(kernel_filename, 0, mem_size, as); lowaddr = 0; } } qemu_register_reset(armv7m_reset, cpu); } ``` 1. `machine->kernel_filename`通过命令的 `-kernel` 选项指定 2. `armv7m_load_kernel`首先尝试调用`load_elf_as`以elf格式加载 3. 如果加载失败,就调用 `load_image_targphys_as` 直接把文件加载到0地址处 裸板程序和IRQ请求调试 ============ 本节基于stm32f205-soc的进行修改实现QEMU对中断的模拟,然后开发裸板程序对模拟的中断进行验证。 stm32f205-soc修改 --------------- ```php qemu_irq stm32f2xx_irq_demo_handler = NULL; #define IRQ_DEMO_BASE 0x88990000 static void stm32f2xx_irq_demo_write(void *opaque, hwaddr addr, uint64_t val64, unsigned int size) { qemu_set_irq(stm32f2xx_irq_demo_handler, 1); return; } static const MemoryRegionOps stm32f2xx_irq_demo_ops = { .write = stm32f2xx_irq_demo_write, .endianness = DEVICE_NATIVE_ENDIAN, }; static void stm32f205_soc_realize(DeviceState *dev_soc, Error **errp) { memory_region_init_io(demo_mem, NULL, &stm32f2xx_irq_demo_ops, s, "irq-demo-mmio", 0x1000); memory_region_add_subregion(system_memory, IRQ_DEMO_BASE, demo_mem); stm32f2xx_irq_demo_handler = qdev_get_gpio_in(armv7m, 20); // 拿到nvic的irq 20 的 irq ``` 1. 首先在stm32f205\_soc\_realize中获取IRQ为20的NVIC.GPIO\_IN,即其对应的qemu\_irq结构,然后保存到stm32f2xx\_irq\_demo\_handler中 2. 注册0x88990000处内存写回调函数为stm32f2xx\_irq\_demo\_write 3. 当往0x88990000写数据时会进入stm32f2xx\_irq\_demo\_write 4. 在stm32f2xx\_irq\_demo\_write函数中会调用qemu\_set\_irq触发 IRQ 20 中断 裸板程序 ---- 根据手册定义异常向量表,当系统启动时会调用`Reset_Handler`,当`IRQ-20`中断触发时会进入`demo_irq_handler` ```php // ISR vecotor data .section .isr_vector, "a" g_pfnVectors: .word stack_top .word Reset_Handler .word Default_Handler // NMI .word Default_Handler // HardFault .word Default_Handler // MemManage .word Default_Handler // BusFault .word Default_Handler // UsageFault .word 0 .word 0 .word 0 .word 0 .word Default_Handler // SVC .word 0 .word 0 .word 0 .word 0 .word 0 .word 0 .word 0 .word 0 .word 0 .word 0 .word 0 .word 0 .word 0 .word 0 .word 0 .word 0 .word 0 .word 0 .word 0 .word 0 .word 0 .word 0 .word 0 .word 0 .word demo_irq_handler // CAN1_RX0 for demo_irq ``` 中断处理程序定义 ```php .thumb_func Reset_Handler: @ MOV R0, #0 @ MSR PRIMASK, R0 bl main_func b . .thumb_func demo_irq_handler: bl demo_irq_func b . ``` 相关函数实现 ```php #define USART1_BASE_ADDR 0x40011000 #define USART_DR 0x04 #define IRQ_DEMO_BASE 0x88990000 #define NVIC_MEM_BASE 0xe000e000 void print_func(unsigned char* s) { while (*s != 0) { *(volatile unsigned char*)(USART1_BASE_ADDR + USART_DR) = *s; s++; } } void enable_demo_irq() { unsigned int irq = 20 + 16; unsigned int offset = (irq - 16) / 8; offset += 0x180; offset -= 0x80; *(volatile unsigned char*)(NVIC_MEM_BASE + offset) = 1 << 4; } void main_func() { print_func("main_func!\n"); enable_demo_irq(); // 配置 nvic 的mmio,让 20号 irq 的 enabled=1 *(volatile unsigned int*)(IRQ_DEMO_BASE + 4) = 33; // 触发 demo_irq, 下面进入 demo_irq_func print_func("end main_func!\n"); return; } void demo_irq_func() { print_func("demo_irq_func!\n"); return; } ``` **print\_func函数** 通过写UART的内存实现输出 **main函数** 1. 首先打印一个日志,然后调用enable\_demo\_irq设置 nvic 控制器,让IRQ-20启用 2. 然后触发对`IRQ_DEMO_BASE`内存的写,让qemu端触发IRQ-20中断 **demo\_irq\_func函数** 打印日志 使用qemu加载固件执行的输出如下 ```php $ qemu-system-arm -M netduino2 -kernel startup.bin -nographic main_func! demo_irq_func! ``` 可以看到首先进入了main函数,触发IRQ-20中断后进入了demo\_irq\_func。 **注意** 由于系统启动时NVIC中每个异常向量的enable状态为0,从而导致即使使用qemu\_set\_irq通知中断到来,实际也不会被CPU处理. ```php static MemTxResult nvic_sysreg_write(void *opaque, hwaddr addr, uint64_t value, unsigned size, MemTxAttrs attrs) { switch (offset) { case 0x100 ... 0x13f: /* NVIC Set enable */ offset += 0x80; setval = 1; /* fall through */ case 0x180 ... 0x1bf: /* NVIC Clear enable */ startvec = 8 * (offset - 0x180) + NVIC_FIRST_IRQ; for (i = 0, end = size * 8; i < end && startvec + i < s->num_irq; i++) { if (value & (1 << i) && (attrs.secure || s->itns[startvec + i])) { s->vectors[startvec + i].enabled = setval; } } nvic_irq_update(s); ``` 所以在触发中断前要通过写NVIC的MMIO内存来设置NVIC中异常向量的enable为1 ```php void enable_demo_irq() { unsigned int irq = 20 + 16; unsigned int offset = (irq - 16) / 8; offset += 0x180; offset -= 0x80; *(volatile unsigned char*)(NVIC_MEM_BASE + offset) = 1 << 4; } ``` 总结 == 本文以stm32f205-soc为例子介绍了针对真实硬件仿真的实现,并分析了qemu的中断模型,最后给出仿真中断的例子。 参考链接 ==== ```php https://blog.csdn.net/alex_mianmian/article/details/98174812 https://www.cnblogs.com/utank/p/11304226.html ```
发表于 2021-04-26 22:15:04
阅读 ( 6657 )
分类:
安全工具
0 推荐
收藏
1 条评论
hac425
2021-04-26 22:15
文章发布后代码仓开源
请先
登录
后评论
请先
登录
后评论
hac425
14 篇文章
×
发送私信
请先
登录
后发送私信
×
举报此文章
垃圾广告信息:
广告、推广、测试等内容
违规内容:
色情、暴力、血腥、敏感信息等内容
不友善内容:
人身攻击、挑衅辱骂、恶意行为
其他原因:
请补充说明
举报原因:
×
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!