最近,我开始踏上嵌入式设备的逆向之旅。我的主要目标是在嵌入式设备(当前主要是指ARM)逆向工程中取得立足点。嵌入式设备上存在着许多可行的软件配置方案,从“全面盛开”的Linux系统到不需要运行OS的裸二进制文件。
因为巧合,我选择使用当前这套方案开始嵌入式固件逆向。我一个朋友带来了一个散装的STM32F103C8T6的微控板。随后我又找到了一本最近发布的好书,面向初学者,关于用GCC和FreeRTOS在这块板子上做开发。而且,就目前而言,FreeRTOS是一种流行的嵌入式设备操作系统,因此我选择了这一套方案。
Warren Gay,也就是上面提到那本书的作者,发布了一个已经配置好的开发环境git仓库,用于构建基于FreeRTOS、libopencm3以及这块板子的系统。
新建工程时请遵循Readme所述的前提条件,随后只需:
你就会得到第一个main.bin,我会在之后的文章中分析这个二进制文件。为了理解一个二进制文件如何被构建(和链接)以及理解ARM汇编,我会同时分析源码和二进制文件。
你可以先去阅读之后章节,再返回这里了解如何加载固件到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”,我将在后续章节解释刚刚发生了什么。
看到接下来的这个图片就代表着你已经准备好了:
该从何处入手逆向嵌入式固件映像呢?入口点就很重要——例如在ELF二进制文件中,你可以在ELF文件头里找到二进制文件的程序入口点。也可以从导出函数或者甚至是调试符号开始入手,这每一个都能作为逆向未知二进制文件的起点。
在该固件中并不存在这些入口点。正如在介绍部分所提到的,我们的入口点是一个blob(注:程序块),和一个能在其上运行blob的微控制器。
首先我们可以查看微控制器的文档,本文使用的是STM32F103C8。在现实场景中找到合适的资源可能十分困难,对于上述微控制器,我们在后续各节所需要的一切资料都记录在程序或参考手册中,你可以从这里获取。
微控制器能从闪存,嵌入式SRAM或系统内存启动。这取决于从0x0000.0000到0x0800.0000地址范围的启动pin配置是对齐于闪存还是系统内存区域。
目前比较重要的几个内存区域有:
在使用的内核ARM Cortex-M3中,启动过程围绕着复位(reset)异常进行构建。在设备启动或重启时,内核会假定向量表位于0x0000.0000。向量表包含着异常例程和栈指针的初始值。有一个向量表偏移寄存器(SCB_VTOR),可用于加载相对于0x0000.0000的偏移量,以在运行时重定位向量表。
在上电时,微控制器首先从0x0000.0000地址加载初始的栈指针,随后将复位向量(0x0000.0004)的地址加载到程序计数寄存器(R15)中,并在此地址继续执行。如下图所示,复位向量位于0x.0000.0004,初始栈指针的值位于0x0000.0000。
我们可以看到上述标记为蓝色和红色的内存区域:
0x2000.5000 是初始的栈指针值,指向SRAM
0x0800.0CC5 是我们复位程序的地址,在嵌入式设备上电和复位时执行。 位0为1表示处理器应以Thumb模式启动(如果PC位0为1,则激活Thumb模式)。因为本例处理器仅支持Thumb模式,所以这些都可以通过查看Cortex-M3规格获知。
工程提供有一个链接器脚本,定义了目标二进制文件的内存布局。让我们来看一下我们刚才看见的二进制文件时如何构建的。
链接器脚本首先定义了两个具有权限的内存区块(rom可读可写,ram可读可写可执行)。然后在section定义里,我们可以看到.text段由它所包含的不同部分进行定义,尤其是放置在.text段开头的向量表。
这可以指示链接器将所有文件(那个"*"号)中名为.vectors的所有区块包含到text段中并将其放入ROM里,该节在MEMORY语句中有定义。
我们逆向固件的向量表定义在libopencm3的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中的reset_handler非常简单。尽管简单,IDA还是会显示在调用用户提供的main函数前所必须完成的工作。
我们已经找到了地址位于0x0000.0004的初始入口点——0x0800.0CC5。 我们在那里开始进行逆向工程。
现在你应该回头去了解一下介绍部分,特别是如何设置IDA Pro环境。
除开.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])
reset_handler的下一部分似乎是任意类型的初始化数组,它包含有一个函数指针列表。 从源代码中我们可以知道这是.init_array
简要说明一下反汇编显示的内容:
两个值进行比较(红色部分)。在本例中它俩是相同的——似乎链接器并未找到任何.init_array的值。我们假定在数组中是一些入口条目。
如果两值相等,处理器则会跳转到loc_8000D0C位置,将下一个函数指针从R4加载到R3中。更新(索引后增加)后将R4(指向数组的指针)增加4到下一个值的位置。
根据R3中的值进行跳转
FreeRTOS固件的reset_vector是从libopencm3库中编译得到,该库定义了libopencm3/lib/cm3/vector.c
中的向量:
如前所述,首先将.data和.bss进行初始化。当定义区块时,_data_loadaddr 和 _ebss 符号则在链接器文件中定义:
在.bss, .data和所有的init函数调用完毕后,main函数得以调用。
在这里很显然能看出哪一个是main函数,我认为在某些固件中找到main函数可能会比这更困难。
我省略了FreeRTOS的介绍部分,因为网络上有足够的资源总结了FreeRTOS让你能够理解以下内容。最重要的就是要知道FreeRTOS由任务组成,这些任务是由用户编写的。此外还有一个由系统创建的IDLE任务,该任务会在没有用户编写的任务工作时进行调用。
因为我们并不想对FreeRTOS本身进行逆向,而是针对用于编写的任务进行逆向,我内心询问自己:应该如何才能只找到这些?
我想到了两种可行的方法
我们可以判断在后续的汇编代码里:
在本例中,如果我们反汇编了找到的这个main函数,我们可以看到:
如前所述,当调用xTaskCreate时,R0会存有一个指向用户写入地址的函数指针——这也正是我们在main函数中所发现的(已标记为红色)
正如我在介绍部分所提到的,FreeRTOS 会自动创建一个在后台运行的IDLE任务。对于该任务,xTaskCreate的第二个参数(pcName)默认设置为“IDLE”。逆向工程师如果好运,嵌入式开发的工程师没有修改默认名称,在这种情况下,我们就可以在二进制文件中的某处找到“IDLE”:
通过交叉引用(CTRL-x),我们可以找到加载这个字符串的位置:
IDLE任务在函数vTaskStartScheduler中创建。我们之前已经看到了“xTaskCreate”调用在汇编代码中的模样,我们可以在xTaskStartScheduler找到原型,其中xTaskCreate()用于IDLE任务。
IDLE任务位于0x0800.0541。使用上述数值准备栈和寄存器,然后将带有链接的分支传给xTaskCreate。通过交叉引用xTaskCreate,我们可以找到所有用户实现的任务并开始对其逆向......但这(希望)将是另一篇博文的一部分了。
现在就到了不是那么有趣的部分,因为它实现的任务非常无聊。但是为了程序的完整,这里还是给出调用的main函数:
[招生]科锐逆向工程师培训(2024年11月15日实地,远程教学同时开班, 第51期)
最后于 2019-2-2 11:17
被kanxue编辑
,原因: