首页
社区
课程
招聘
[翻译]嵌入式逆向工程入门:STM32F103C8T6 上 的 FreeRTOS & Libopencm3
2018-10-5 18:07 32073

[翻译]嵌入式逆向工程入门:STM32F103C8T6 上 的 FreeRTOS & Libopencm3

2018-10-5 18:07
32073

前言

最近,我开始踏上嵌入式设备的逆向之旅。我的主要目标是在嵌入式设备(当前主要是指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。

 

查看二进制文件

 

我们可以看到上述标记为蓝色和红色的内存区域:

  • 0x2000.5000 是初始的栈指针值,指向SRAM

  • 0x0800.0CC5 是我们复位程序的地址,在嵌入式设备上电和复位时执行。 位0为1表示处理器应以Thumb模式启动(如果PC位0为1,则激活Thumb模式)。因为本例处理器仅支持Thumb模式,所以这些都可以通过查看Cortex-M3规格获知。

阅读源码

工程提供有一个链接器脚本,定义了目标二进制文件的内存布局。让我们来看一下我们刚才看见的二进制文件时如何构建的。

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)。

 

然后,链接器会根据romram的大小计算初始栈指针的值,以_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

 

 

简要说明一下反汇编显示的内容:

  • 两个值进行比较(红色部分)。在本例中它俩是相同的——似乎链接器并未找到任何.init_array的值。我们假定在数组中是一些入口条目。

  • 如果两值相等,处理器则会跳转到loc_8000D0C位置,将下一个函数指针从R4加载到R3中。更新(索引后增加)后将R4(指向数组的指针)增加4到下一个值的位置。

  • 根据R3中的值进行跳转

  • 重复,直到R4==R5

阅读源码

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编辑 ,原因:
收藏
点赞1
打赏
分享
打赏 + 1.00雪花
打赏次数 1 雪花 + 1.00
 
赞赏  junkboy   +1.00 2018/10/05
最新回复 (6)
雪    币: 11716
活跃值: (133)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
junkboy 2018-10-5 18:51
2
0
支持
雪    币: 8168
活跃值: (1626)
能力值: ( LV3,RANK:20 )
在线值:
发帖
回帖
粉丝
广岛秋泽 2018-10-5 20:52
3
0
翻译的不错~
雪    币: 3614
活跃值: (502)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
七里一支花 2018-11-28 09:10
4
0
修改一个小错误:
目前比较重要的几个内存区域有:
•系统内存: 0x1FFF.0000 到 0x1FFF.F800

根据图片显示应该是: 0x1FFF.F000 到 0x1FFF.F800
雪    币:
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
wx_ubftaazf 2021-1-24 18:00
5
0
IDA会询问RAM和ROM部分,你也许会问该如何找到这部分的正确数值呢?这就是我接下来要介绍的。   完全没有介绍啊
雪    币: 226
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
jiang887786 2022-5-21 17:20
6
0
感谢翻译分享!
雪    币: 1040
活跃值: (30)
能力值: ( LV2,RANK:15 )
在线值:
发帖
回帖
粉丝
vchackcat 2024-1-3 15:23
7
0
【勘误】链接器脚本首先定义了两个具有权限的内存区块(rom可读可写,ram可读可写可执行),这里rom是可读可执行。
游客
登录 | 注册 方可回帖
返回