嵌入式设备固件一般可以分为两类:

  • 有文件系统(如路由器固件这种)
  • 无文件系统(如STM32这种嵌入式设备固件,程序直接存放在flash中)

针对第一类固件,分析起来比较简单,直接binwalk分离。然后去逆向对应的服务对应的二进制程序即可,也不需要考虑程序的加载地址。

而对于第二类固件,往往就会比较复杂,需要分析程序的加载地址,也需要考虑外设的地址映射。

本文将从SCTF2020的题目Password Lock入手,分析STM32固件作为入门

题目信息

这是一个STM32F103C8T6 MCU密码锁, 它具有4个按键,分别为1, 2, 3, 4. 分别对应GPIO_PA1, GPIO_PA2, GPIO_PA3, GPIO_PA4.

  1. flag1格式为SCTF{正确的按键密码}
  2. 输入正确的密码, 它将通过串口(PA9–TX)发送flag2

确定固件加载地址

这里有两个方法可以确定,一个是直接分析Intel Hex文件,另一个是去看芯片对应的内存映射图(可以通过ALLDATASHEET.COM去搜索芯片信息)

分析Intel Hex文件

本模块可以参考Intel HEX - Wikipedia — 英特尔十六进制 - 维基百科

Intel Hex可以直接用记事本打开,我们以本题的第一行为例分析其数据格式

1
:020000040800F2
  • 第一个符合:代表没一行数据的起始,为start code
  • 之后的第一个byte02为Byte Count,代表data的长度
  • 紧接着的两个字节0000为Address
  • 接下来的一个字节04为record type,总共有6个类型(00数据,01文件结尾,02拓展分段地址,03开始段地址,04拓展线性地址,05起始线性地址)
  • 接下来直到最后一个字节之前为数据data,
  • 最后一个字节F2为校验和,校验和为所有字节之和的补码

这里主要record type这个字段需要注意

hex type Description Example
00 数据 由byte count字段决定data字段的数据长度 :0B0010006164647265737320676170A7
01 文件结尾 一般为hex文件最后一行,代表文件结束 :00000001FF
02 扩展分段地址 byte count固定为02,起始地址为data段值乘以16 :020000021200EA
03 开始段地址 对于8086处理器指定起始执行地址 :0400000300003800C1
04 扩展线性地址 data段代表起始地址的高16位 :020000040800F2
05 起始线性地址 data表示大端的32位地址 :04000005000000CD2A

针对本题的起始数据为:020000040800F2,可以很明显的看到类型为04,则可以计算出对应地址为0x08000000

查看内存映射

在上面的datasheet网站中可以搜索到:STM32F103C8T6 Datasheet本题对应的datasheet,查看memory mapping章节可以得到下图。

memory maping

从图中也可以比较直观的找到flash段的地址在0x08000000,此外还可以看到一些外设的地址段PERIPHERALS,这也是后面分析时要考虑的。

IDA分析

将hex文件拖入IDA后,首先设置process type为小端,然后设置架构为arm-v7(这个可以在手册中查到),如图:

set type

跟踪流程可以跟到sub_8000428,基本可以确定其就是主函数main,红色区域地址是因为IDA没有加载这个地址的段。

preview

对照内存映射图,我们可以知道这些0x40000000的地址属于外设的地址映射,也就是对应了不同的针脚。可以在Edit -> Segment -> create segment,手动添加sram和peripherals段即可,再次F5即可看到区域变为正常黄色。

接下来就需要手动查阅手册,为每个地址改变量名为设备名称,方便后续分析,在我看来这也是最耗时最麻烦的一步。但其如果使用Ghidra和SVD-loader插件会快很多,可以直接自动识别更改,但我在尝试的时候没有成功,不知什么原因,悲 :(

改完差不多就是这个样子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
int __cdecl main(int argc, const char **argv, const char **envp)
{
init((int)&loc_8000428 + 1, (int)argv);
RCC_APB2ENR = 0x4005;
RCC_AHBENR = 1;
PA_CRL = 0x44488884;
PA_CRH = 0x444444B4;
PA_ODR = 0x1E;
EXTI_IMR = 0x1E;
EXTI_FTSR = 0x1E;
AFIO_EXTICR2 = 0;
AFIO_EXTICR3 = 0;
unkown(7);
unkown(8);
unkown(9);
unkown(10);
RCC_APB2RSTR |= 0x4000u;
RCC_APB2RSTR &= 0xFFFFBFFF;
USART1_BRR = 0x271;
USART1_CR1 = 0x2008;
USART1_SEND('S');
USART1_SEND('C');
USART1_SEND('T');
USART1_SEND('F');
USART1_SEND('{');
USART1_CR3 = 0x80;
DMA_CMAR4 = 0x20000000;
DMA_CPAR4 = &USART_DR;
DMA_CNDTR4 = 0x1E;
DMA_CCR4 = 0x492;
unkown(14);
EXTI_SWIER |= 2u;
delay(1);
EXTI_SWIER |= 0x10u;
delay(1);
EXTI_SWIER |= 0x10u;
delay(1);
EXTI_SWIER |= 4u;
delay(1);
EXTI_SWIER |= 0x10u;
delay(1);
EXTI_SWIER |= 2u;
delay(1);
EXTI_SWIER |= 8u;
delay(1);
while ( 1 )
;
}

另外还要对中断向量表做处理,可以用如下IDA python脚本更改段为dword

1
2
for i in range(0x8000000,0x80000eb,1):del_items(i)
for i in range(0x8000000,0x80000eb,4):create_dword(i)

vector

图里可以很明显的看出存在一些函数,查阅手册可以找到其为中断处理的EXTI函数

函数逻辑分析

这条也没什么捷径可走,需要一边查手册一边看mian函数内的每一步赋值和函数的意义,基本经过这个流程可以搞清楚GPIO、EXTI、USART这些作用之类的