SCTF 2020 Password Lock Plus 入门STM32逆向

本文通过SCTF2020的STM32门锁固件题目,介绍了STM32的正向开发方法,逆向分析方法,以及IDA在分析固件的时候一些使用技巧。最终,通过静态分析以及动态模拟调试的方法分别获得 flag1:门锁密码 以及 flag2:UART输出的信息

题目信息

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

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

提示: 中断向量表, 芯片数据手册,固件没有禁用jtag, 可以进行动态调试. 对按键的触发方式有特殊要求, 自行分析.

附件:firmware.hex

解题思路

很巧的是《CTF特训营》这本书第六篇IoT部分就是以STM32的题目为例,完整的讲了这种题目怎么上手分析。购买链接如下:

整体思路就是给了一个intel hex格式的固件,并根据提示的芯片信息确定程序的内存布局以及外设寄存器对应的内存地址,然后就可以去分析该程序了。分析的过程就是看懂程序代码,因为程序主要是操作硬件,所以需要理解各种外设寄存器的使用。这些外设的寄存器会映射成物理内存地址,所以使用的具方法就是读写内存。另外这里说一下,intel hex的格式虽然是纯字符串,但其实IDA直接可以识别并且加载的,并不需要转成二进制,而且intel hex中可能包含程序的加载地址以及入口地址,省去了分析纯二进制固件相关地址以及IDA的设置。

以附件的前四行和后四行为例:

:020000040800F2
:100000004004002001010008090100080B0100085C
:100010000D0100080F010008110100080000000098
:1000200000000000000000000000000013010008B4

:1006000000000000000000000000000000000000EA
:0406100000000000E6
:04000005080000ED02
:00000001FF
  • 第一行记录了扩展线性地址,其实就是程序的加载基址:0x08000000
  • 倒数第二行记录了程序入口地址:0x080000ED
  • 最后一行标志文件结束
  • 其余行全部是文件数据

查找芯片信息

找到一些关于STM32的资料,其中网课老师非常幽默,手册更是STM32开发者必不可少之法宝:

也是《CTF特训营》书中推荐了一个查找芯片手册(datasheet)的网站,alldatasheet,在这里直接搜索芯片型号就可以,PDF文档如下:

另外也可以在ST的官网找到这个芯片的的信息以及datasheet:

其中第一页简述了STM32的一些参数,以及比较重要的内存映射图如下:

image

从内存映射图得到的有助于我们分析代码的信息:

  • 0x08000000-0x0801FFFF: Flash 128k,用于来存储程序代码,不用把程序拷贝到RAM中,而直接在Flash中执行,这种技术叫XIP
  • 0x20000000-0x20004FFF: SRAM 20k,用于程序运算时存放变量
  • 0x40000000-0x40023FFF: Peripherals 144k,外设寄存器的映射地址,通过读写这些内存地址实现对外围设备的控制

关于STM32的小问答:

问题:在之前的hex文件中,我们知道程序的加载基址是0x08000000,也是Flash的起始地址,所以这里直接就是从Flash中执行程序,这个Flash是STM32芯片内部的Flash,请问你能确定这种Flash的类型么?是NOR Flash还是NAND Flash?

回答:因为从内存映射和程序基址的对应关系来看,就是直接在Flash中执行的,NOR Flash有就地执行(XIP)的特性,而NAND Flash没有,所以应当是NOR Flash。

参考:

问题:STM32大多定义为单片器,写的程序一般也是裸机程序,请问Linux可以运行在单片机上么?

回答:可以,但是有些功能可能无法支持,比如内存隔离等。无论是IoT、android、IOS还是工控,其实他们的爸爸都是嵌入式,从原理来说都是计算机,只是应用场景不同设计和考虑。故只要是计算机,有CPU,有输入输出,有足够的内存以及存储,就能将Linux移植过去,毕竟操作系统就是管理硬件的一个软件。

参考:

IDA分析技巧

大概理解的STM32的运行原理就可以分析固件程序了,从宏观上来看,这是个裸机程序,内存主要分为三个段:

  • 0x08000000-0x0801FFFF: Flash
  • 0x20000000-0x20004FFF: SRAM
  • 0x40000000-0x40023FFF: Peripherals

从微观上来看,Flash段中的程序除了包括代码,还包括中断向量表。Peripherals段中的寄存器各不相同,用到的每个寄存器都需要大概的理解。如果你直接用IDA加载这个hex文件,什么都不配置的话,IDA除了hex文件中已知的加载基址和程序入口地址,其他的啥也分析不出来,甚至还会把指令识别为x86的机器码,那么我们如何把我们知道的这些信息告诉IDA呢?

设置加载参数

这里只需要设置CPU参数即可,加载地址的设置参考:为啥要分析固件的加载地址?

image

这里首先可以看到IDA是支持Intel Hex格式的文件,把其解析成对应的二进制。如果选择了Binary file,就真的是加载ASCII码对应的二进制了。然后我们选择处理器类型为ARM小端,而且我们在STM32的手册中可以看到其CPU内核系列是Cortex M3,指令集是ARMv7-M,也可以进行配置。这次再进入IDA就能分析出一些函数了:

image

找到入口地址

本题的入口地址0x080000EC已经包括在hex文件中了,如果没有这个信息则需要想其他办法识别出来,一般可以通过识别中断向量表来找到设备reset时的中断处理函数,跟着这个函数走就是入口。在STM32中文参考手册V10.pdf,找到:

image

可以看到这里的0x00000004地址就是Reset的中断向量,按照刚才的内存布局来看,直接使用这个地址肯定是不对的。而且中断向量表用户应当是可以修改的,那么猜测中断向量表应当也是存在Flash中,即应当在0x08000000-0x0801FFFF这段地址空间中,那么是不是0x08000004就是Reset的中断向量呢?答案是肯定的!但是中断向量表基址是固定的么?一定在最开头么?可不可以修改呢?参考:STM32 中断向量表的位置 、重定向,回答:Cortex-M3内核则是固定了中断向量表的位置而起始地址是可变化的。

所以我们可以在0x8000004处开始讲数据转换为Dword,可以将鼠标放在数据上,然后右键取消当前IDA给出的定义,然后按键盘D键,直到将数据转化为Dword即可。得到的地址是0x8000101,我们跳过去看到:

seg000:08000100 06 48                       LDR     R0, =(nullsub_1+1)
seg000:08000102 80 47                       BLX     R0              ; nullsub_1
seg000:08000104 06 48                       LDR     R0, =(start+1)
seg000:08000106 00 47                       BX      R0              ; start

在ARM里如果跳转到一个奇数的地址上,则是切换处理器为THUMB模式(一条指令的长度为2个字节),其中跳入nullsub函数前把下面指令的地址压到LR中,nullsub的功能是跳回LR,所以nullsub就像他的名字一样,什么都没有做。关于ARM的基本知识可以参考:ARM PWN入门。然后从start(IDA给出的标记,这个信息本题中存在intel hex中的最后一行中),往后跟一会就找到了main函数:loc_8000428,可见IDA并没有分析出这个是函数,标记是loc而不是sub。我们按F5,转换为函数以及伪代码后看到一片红色:

image

不要害怕,认真分析红色的位置其实就是IDA没有创建的内存段,因为加载程序hex的时候的地址以及程序大小只让IDA加载了0x8000000附近的内存,而我们之前获得SRAM以及Peripherals的地址信息都没有告诉IDA,所以接下来就创建内存段让这些红色消失。

新建segment

通过如图方式创建新的内存段:

image

当然也可以通过IDApython来创建:

ida_segment.add_segm(0,0x20000000,0x20004FFF,"SRAM","DATA")
ida_segment.add_segm(0,0x40000000,0x40023FFF,"Peripherals","DATA")

然后发现伪代码页面并没有什么改变,还是红色,其实只要在按一次F5就可以刷新结果了。然后可以翻一遍所有函数了,应该都没有讨厌的红色了。

修复中断向量表

上面找到入口地址的修复中断向量表的方法比较繁琐,我们大概可以分析出,程序入口地址0x080000EC之前应当都是中断向量表,所以也可以使用如下IDC或者IDApython,对于数据转换在《IDA pro权威指南》以及网上的博客给出的API在IDApython和IDC中都为MakeDword(addr),但是实际上这个函数再IDC中可以使用,但是在IDApython中已经不支持了。通过在IDApython的输入框中使用dir()方法获得当前可以使用的对象及方法发现一个函数有点类似:create_dword(addr),尝试这个API在两种语言中都支持,文档如下:

直接在IDA下面的代码输入处执行即可,如果使用老的API:MakeDword(addr),IDC代码如下:

auto i = 0x8000000; for(;i<0x80000eb;i++){ MakeUnkn(i,0); MakeDword(i-i%4); }

如果使用新的API,IDApython如下两行:

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

识别中断处理函数

修复完中断向量表后我们大概观察一下,这五个地方可能是有函数:

seg000:0800005C 6D 01 00 08                 DCD 0x800016D
seg000:08000060 AD 01 00 08                 DCD 0x80001AD
seg000:08000064 E5 01 00 08                 DCD 0x80001E5
seg000:08000068 2D 02 00 08                 DCD 0x800022D
seg000:08000078 49 01 00 08                 DCD 0x8000149

我们对比一下STM32中文参考手册V10.pdf的中断向量表:

image

跳转到这几个函数上,因为IDA没有分析出来这是函数,所以我们需要手动创建函数:右键create function,然后再F5,就可以啦。分析这五个函数,对应到他们的功能,感觉前四个就是密码按键中断的对应的处理函数,最后一个DMA相关暂时不知道是干什么的。总之这一趟忙活下来,IDA基本就彻底看懂了这个题目的intel hex到底是个啥了。但是我们目前还不是很懂,接下来就是对着STM32的文档,研究程序是怎么用的外设寄存器,并分析程序函数并解题了。

STM32调试工具

不过在解题之前,还要介绍一个可以开发STM32的IDE,当然它不仅是开发,还能在没有STM32板子的情况下,直接调试固件的HEX文件,这个过程你叫他仿真、调试、模拟、运行都可以,反正就是能模拟一个板子,然后把软件跑起来,还能看到各种外设的状态,它就是MDK。虽然这个名字你可能不熟,如果我说他的爸爸是keil,那么你一定回想起你的大学学习8051的时光。2005年keil被ARM公司收购,keil产品线更名为Microcontroller Development Kit(MDK),不过软件仍然以μ Vision命名,这个软件只有windows版。官网:http://www.keil.com/。所以他不仅支持8051,还支持STM32这种ARM架构的单片机。想知道这个软件支持多少板子,请参考:MDK5 Software Packs

当然,这里给出破解版方便大家学习,keil mdk 5.25 破解版:

调试STM32需要在MDK5 Software Packs下载板级支持包,在windows上直接双击运行即可完成支持包的安装:

关于如何用MDK跑起一个STM32的Intel hex程序,我还真没在网上找到一步步的详细指导,《CTF特训营》书中是有详细的指导,大概的流程:

  1. 新建STM32工程
  2. 导入hex文件
  3. 设置debug选项卡,配置STM32参数
  4. LOAD hex进行debug

因为都是图形化的点来点去,不是很好描述,想被详细指导的去买书吧。不过自己在上述步骤中还是遇到了一些问题:

  1. LOAD提示没有文件:LOAD的hex文件可能没在LOAD指令执行的当前目录下所,最好给个绝对地址,或者尝试父目录等
  2. debug说内存没有权限:debug选项卡中,Dialog DLL的配置要手工敲进去,书中没提这事。

在《STM32F1开发指南-库函数版本_V3.1》中有类似仿真配置,不过比较简略。网上类似的调试配置如下,不过对于调试CTF题目可能有多余步骤:

最精简的适合CTF选手的步骤还是在《CTF特训营》中,因为这个仿真操作看起来的确小众。

解题

首先就是分析程序用了什么寄存器,并学明白这些寄存器应该怎么用。不过在学寄存器之前我们重新来审一遍题目:

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

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

引脚认识

GPIO我们有所了解,可以参考:从树莓派的wiringPi库分析Linux对GPIO的控制原理,之前没有分析到底怎么控制寄存器来控制GPIO,这次我们就来分析这个。不过树莓派和STM32的GPIO控制的寄存器的使用方法是不同的,不过思想是相同的。不过在直接分析GPIO前,还有一些疑问,PA是啥?PA1是啥?PA9是啥?PA9为啥和TX放一起?

读完以上文章就明白了,这其实是引脚复用

  • PA、PB、PC、PD等每组都是16个GPIO引脚
  • PA、PB、PC、PD等每组都可能有引脚和其他功能复用
  • 通过设置每个引脚的配置寄存器来决定是否复用

所以我们去看一下这个引脚排列,在手册中就有,不过这手册都是一个系列的,都属于这一个系列的不同芯片引脚个数与封装方式都不同,首先我们要确定STM32F103C8T6的封装方式,在手册中也能查到,不过在官网是直接能方便的看到是LQFP 48封装,另外百度百科有图片,也能看出有48个引脚:

image

其中Default属性含义为默认复用,参考:STM32引脚列表中主功能,默认复用功能和重定义功能的区别

引脚设置

那么我们怎么设置并使用PA1、PA2、PA3、PA4、PA9呢?比如当前我们需要设置,让PA1、PA2、PA3、PA4作为输入,PA9设置复用为UART的输出。那么应该怎么办呢?仍然参考手册:

如果不想看密密麻麻的手册可以参考:

总之是配置一些寄存器,这些寄存器映射到了内存地址,最后可以用这张图来表示:

image

英文大写字母简写真是让人头大,查英文手册:STM32英文参考手册.pdf,结果如下,可以望文生义。

  • CRL: Configuration Register Low
  • CRH: Configuration Register High
  • IDR: Intput Data Register
  • ODR: Output Data Register
  • BRR: Bit Reset Register
  • BSRR: Bit Set/Reset Register
  • LCKR: configuration LoCK Register

IDA标记寄存器

对着手册,把所有用到的寄存器都标出来,标记完的代码大概如下:

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 )
    ;
}

这时就是纯看文档,理解上述寄存器的关系。

理清程序逻辑

理清后就知道是配合了GPIO、EXTI、USART、DMA功能完成了一个密码锁,首先配置GPIO的PA1-4为输入,结合EXTI设置GPIO中断,同时利用USRAT1发送一些flag2,并且配置了DMA写内存可以直接USART1输出。其中中断处理函数会使用SRAM中的一块内存作为密码输入的正确个数的标记,所以有两种方法能看出密码:

  1. 分析中断处理函数的密码状态
  2. 程序开机时在main函数中模拟输入了一次密码,就是如下:
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);

即:SCTF{1442413},这就是flag1。并且分析中断处理函数,当密码输入正确时会在USART1口输出flag2。所以当程序执行时,第一次在main里模拟输入的密码就会触发输出flag2,即如果能直接运行这个程序,观察USART1口应该直接就能看到flag2。使用MDK动态调试观察USART1:

image

当然也可以自己分析flag2怎么生成的,参考官方WP: