前言
最近,我开始踏上嵌入式设备的逆向之旅。我的主要目标是在嵌入式设备(当前主要是指ARM)逆向工程中取得立足点。嵌入式设备上存在着许多可行的软件配置方案,从“全面盛开”的Linux系统到不需要运行OS的裸二进制文件。
因为巧合,我选择使用当前这套方案开始嵌入式固件逆向。我一个朋友带来了一个散装的STM32F103C8T6的微控板。随后我又找到了一本最近发布的好书,面向初学者,关于用GCC和FreeRTOS在这块板子上做开发。而且,就目前而言,FreeRTOS是一种流行的嵌入式设备操作系统,因此我选择了这一套方案。
环境搭建
Warren Gay,也就是上面提到那本书的作者,发布了一个已经配置好的开发环境git仓库,用于构建基于FreeRTOS、libopencm3以及这块板子的系统。
新建工程时请遵循Readme所述的前提条件,随后只需:
cd ./rtos
make -f Project.mk PROJECT=name
cd ./name
make
你就会得到第一个main.bin,我会在之后的文章中分析这个二进制文件。为了理解一个二进制文件如何被构建(和链接)以及理解ARM汇编,我会同时分析源码和二进制文件。
IDA设置
你可以先去阅读之后章节,再返回这里了解如何加载固件到IDA。
新建一个工程,在processor type里选择ARM little endian,打开processor options。在截图上,你能看到适用于我这个环境的设置。我的Cortex-M3基于ARMv7-M架构,所以才这样设置。
确认设置后,IDA会询问RAM和ROM部分,你也许会问该如何找到这部分的正确数值呢?这就是我接下来要介绍的。
在主窗口,IDA为我们显示了固件,但是代码尚未进行反汇编。要让IDA成功反汇编代码,我们需要启用Thumb模式:按下Alt+G键输入0x1并确定。
接下来,设置0x0800.0000和0x0800.0004的值为双字,然后跳转到在0x0800.0004: 0x0800.0CC5出现的值,这就是我们的程序入口点——reset_handler(又是之后这一系列的入口)。右键单击在0x0800.0CC4,并将其定义为“Code”,我将在后续章节解释刚刚发生了什么。
看到接下来的这个图片就代表着你已经准备好了:
STM32F103C8T6 启动与初始化
该从何处入手逆向嵌入式固件映像呢?入口点就很重要——例如在ELF二进制文件中,你可以在ELF文件头里找到二进制文件的程序入口点。也可以从导出函数或者甚至是调试符号开始入手,这每一个都能作为逆向未知二进制文件的起点。
在该固件中并不存在这些入口点。正如在介绍部分所提到的,我们的入口点是一个blob(注:程序块),和一个能在其上运行blob的微控制器。
阅读文档into the documentation
首先我们可以查看微控制器的文档,本文使用的是STM32F103C8。在现实场景中找到合适的资源可能十分困难,对于上述微控制器,我们在后续各节所需要的一切资料都记录在程序或参考手册中,你可以从这里获取。
内存布局
微控制器能从闪存,嵌入式SRAM或系统内存启动。这取决于从0x0000.0000到0x0800.0000地址范围的启动pin配置是对齐于闪存还是系统内存区域。
目前比较重要的几个内存区域有:
- 系统内存: 0x1FFF.F000 到 0x1FFF.F800
- 闪存: 0x0800.0000 到 0x0801.FFFF
- SRAM 位于 0x2000.0000
向量表
在使用的内核ARM Cortex-M3中,启动过程围绕着复位(reset)异常进行构建。在设备启动或重启时,内核会假定向量表位于0x0000.0000。向量表包含着异常例程和栈指针的初始值。有一个向量表偏移寄存器(SCB_VTOR),可用于加载相对于0x0000.0000的偏移量,以在运行时重定位向量表。
在上电时,微控制器首先从0x0000.0000地址加载初始的栈指针,随后将复位向量(0x0000.0004)的地址加载到程序计数寄存器(R15)中,并在此地址继续执行。如下图所示,复位向量位于0x.0000.0004,初始栈指针的值位于0x0000.0000。
查看二进制文件
我们可以看到上述标记为蓝色和红色的内存区域:
阅读源码
工程提供有一个链接器脚本,定义了目标二进制文件的内存布局。让我们来看一下我们刚才看见的二进制文件时如何构建的。
MEMORY
{
rom (rx) : ORIGIN = 0x08000000, LENGTH = 64K
ram (rwx) : ORIGIN = 0x20000000, LENGTH = 20K
}
/* Enforce emmition of the vector table. */
EXTERN (vector_table)
/* Define the entry point of the output file. */
ENTRY(reset_handler)
/* Define sections. */
SECTIONS
{
.text : {
*(.vectors) /* Vector table */
*(.text*) /* Program code */
. = ALIGN(4);
*(.rodata*) /* Read-only data */
. = ALIGN(4);
} >rom
[...]
PROVIDE(_stack = ORIGIN(ram) + LENGTH(ram));
// file: stm32f103c8t6.ld
链接器脚本首先定义了两个具有权限的内存区块(rom可读可写,ram可读可写可执行)。然后在section定义里,我们可以看到.text段由它所包含的不同部分进行定义,尤其是放置在.text段开头的向量表。
.text : {
*(.vectors) /* Vector table */
...
} > rom
这可以指示链接器将所有文件(那个"*"号)中名为.vectors的所有区块包含到text段中并将其放入ROM里,该节在MEMORY语句中有定义。
我们逆向固件的向量表定义在libopencm3的vector.c
文件中。
__attribute__ ((section(".vectors")))
vector_table_t vector_table = {
.initial_sp_value = &_stack,
.reset = reset_handler,
.nmi = nmi_handler,
.hard_fault = hard_fault_handler,
/* Those are defined only on CM3 or CM4 */
#if defined(__ARM_ARCH_7M__) || defined(__ARM_ARCH_7EM__)
.memory_manage_fault = mem_manage_handler,
.bus_fault = bus_fault_handler,
.usage_fault = usage_fault_handler,
.debug_monitor = debug_monitor_handler,
#endif
.sv_call = sv_call_handler,
.pend_sv = pend_sv_handler,
.systick = sys_tick_handler,
.irq = {
IRQ_HANDLERS
}
};
// file: stm32f103c8t6/libopencm3/lib/cm3/vector.c
这跟逆向分析得到的二进制文件结构是一致的:4-7字节是reset_handler,它是后来定义在vector.c文件里的函数的地址;最前面的4个字段是初始sp地址。由于rom被定义成从ORIGIN = 0x0800.0000的位置开始,复位例程也就被放在那里,如同在后面的小节“查看二进制文件”里遇到的一样,处于地址范围(0x0800.000 + 64K,64 1024: 0x1.0000可寻址字节,表示rom*的高位地址0x0801.FFFF)。
然后,链接器会根据rom和ram的大小计算初始栈指针的值,以_stack符号提供,其地址随后会包含在vector_table结构体中——并作为固件的前4字节。
因此,我们不仅发现,复位例程是我们逆向工程的起点,我们也已知道了此设备的固件从闪存中启动。请记住:起始于0x0800.0000的闪存和起始于0x1FFF.0000的SRAM都映射到0x0000.0000。
因为内存区域0x0800.0000是一个别名(使用相应的BOOT引脚配置),所以你也可以将固件与ORIGIN的rom值链接为0x0000.0000。在这种情况下,固件中的启动源会难以察觉。
libopencm3: 调用main函数
libopencm3中的reset_handler非常简单。尽管简单,IDA还是会显示在调用用户提供的main函数前所必须完成的工作。
我们已经找到了地址位于0x0000.0004的初始入口点——0x0800.0CC5。 我们在那里开始进行逆向工程。
查看二进制文件
现在你应该回头去了解一下介绍部分,特别是如何设置IDA Pro环境。
.data 和 .bss 节初始化
除开.text节之外,链接器脚本还指定了.bss节和.data节。.bss节存有未初始化或初始化为零的变量,而.data节则是保存已初始化后的变量。
通过一开始查看文档然后再观察二进制文件,我们已经发现一个事实,那就是RAM起始于0x2000.0000。在反汇编代码中我们也可以推断出相同的结论:
如反汇编视图所示,第一个基址0x2000.0000加载到R3中,然后从0x0800.0D5C开始的16字节(0x2000.0010 - 0x2000.0000,CMP R3, R1)被复制到R3指向的地址。这16个字节构成了.data节,也即初始化变量。记住索引后的ARM指令(图中红色),它在STR/LDR操作完成后会自动递增(本例中为4)第二个操作数。我们可以在二进制固件中看到.data节
之后,.data节(R3进一步递增)后紧随着.bss节。由于.bss节存有的是用零初始化的未定义数据,因此我们可以很轻易地发现它:从0x2000.0010开始直到0x2000.452C的所有内容都是用零初始化(R1保持值为0并将其写入[R3])
.init_array
reset_handler的下一部分似乎是任意类型的初始化数组,它包含有一个函数指针列表。 从源代码中我们可以知道这是.init_array
简要说明一下反汇编显示的内容:
阅读源码
FreeRTOS固件的reset_vector是从libopencm3库中编译得到,该库定义了libopencm3/lib/cm3/vector.c
中的向量:
void __attribute__ ((weak, naked)) reset_handler(void)
{
volatile unsigned *src, *dest;
funcp_t *fp;
for (src = &_data_loadaddr, dest = &_data;
dest < &_edata;
src++, dest++) {
*dest = *src;
}
while (dest < &_ebss) {
*dest++ = 0;
}
[...]
for (fp = &__preinit_array_start; fp < &__preinit_array_end; fp++) {
(*fp)();
}
[...]
main();
[...]
}
如前所述,首先将.data和.bss进行初始化。当定义区块时,_data_loadaddr 和 _ebss 符号则在链接器文件中定义:
.data : {
_data = .;
*(.data*) /* Read-write initialized data */
. = ALIGN(4);
_edata = .;
} >ram AT >rom
_data_loadaddr = LOADADDR(.data);
.bss : {
*(.bss*) /* Read-write zero initialized data */
*(COMMON)
. = ALIGN(4);
_ebss = .;
} >ram
main函数
在.bss, .data和所有的init函数调用完毕后,main函数得以调用。
在这里很显然能看出哪一个是main函数,我认为在某些固件中找到main函数可能会比这更困难。
FreeRTOS: Finding Tasks
我省略了FreeRTOS的介绍部分,因为网络上有足够的资源总结了FreeRTOS让你能够理解以下内容。最重要的就是要知道FreeRTOS由任务组成,这些任务是由用户编写的。此外还有一个由系统创建的IDLE任务,该任务会在没有用户编写的任务工作时进行调用。
因为我们并不想对FreeRTOS本身进行逆向,而是针对用于编写的任务进行逆向,我内心询问自己:应该如何才能只找到这些?
我想到了两种可行的方法
寻找xTaskCreate原型
BaseType_t xTaskCreate( TaskFunction_t pxTaskCode,
const char * const pcName,
const configSTACK_DEPTH_TYPE usStackDepth,
void * const pvParameters,
UBaseType_t uxPriority,
TaskHandle_t * const pxCreatedTask )
我们可以判断在后续的汇编代码里:
- R0 保存大过0x0800.0000的地址 (任务的函数指针)
- R1 保存字符串指针,可以是在.data或在.text段中
- R2 保存一个整型数
- R3 可以保存任何值
- 两个栈上的参数:第5个:整型数,第6个:NULL或一个地址(变量TaskHandle_t)
在本例中,如果我们反汇编了找到的这个main函数,我们可以看到:
如前所述,当调用xTaskCreate时,R0会存有一个指向用户写入地址的函数指针——这也正是我们在main函数中所发现的(已标记为红色)
寻找IDLE任务
正如我在介绍部分所提到的,FreeRTOS 会自动创建一个在后台运行的IDLE任务。对于该任务,xTaskCreate的第二个参数(pcName)默认设置为“IDLE”。逆向工程师如果好运,嵌入式开发的工程师没有修改默认名称,在这种情况下,我们就可以在二进制文件中的某处找到“IDLE”:
通过交叉引用(CTRL-x),我们可以找到加载这个字符串的位置:
IDLE任务在函数vTaskStartScheduler中创建。我们之前已经看到了“xTaskCreate”调用在汇编代码中的模样,我们可以在xTaskStartScheduler找到原型,其中xTaskCreate()用于IDLE任务。
IDLE任务位于0x0800.0541。使用上述数值准备栈和寄存器,然后将带有链接的分支传给xTaskCreate。通过交叉引用xTaskCreate,我们可以找到所有用户实现的任务并开始对其逆向......但这(希望)将是另一篇博文的一部分了。
阅读源码
现在就到了不是那么有趣的部分,因为它实现的任务非常无聊。但是为了程序的完整,这里还是给出调用的main函数:
main(void) {
gpio_setup();
xTaskCreate(task1,"LED",100,NULL,configMAX_PRIORITIES-1,NULL);
vTaskStartScheduler();
for (;;)
;
return 0;
}
// rtos/projectname/main.cs
原文链接: https://blog.3or.de/starting-embedded-reverse-engineering-freertos-libopencm3-on-stm32f103c8t6.html
翻译: 看雪翻译小组 vancir
校对: 看雪翻译小组 hanbingxzy
[CTF入门培训]顶尖高校博士及硕士团队亲授《30小时教你玩转CTF》,视频+靶场+题目!助力进入CTF世界
最后于 2019-2-2 11:17
被kanxue编辑
,原因: