
一、概括 用的很久之前做的一个电风扇的课设来做实验。+ R# P+ `' ^: {2 i) q ( D: d8 l: o _$ E0 O8 x ![]() 9 r1 D% G% `5 p, j+ r 3 W2 D- ^. t } 用到了外部中断,定时器中断,pwm。MCU是STM32F103ZET6。7 ]# l, J* R9 p" d/ r3 o* S- L* [, S ; y5 t; f! U) W- W z5 g% I ![]() 0 Y. {, j! v" V# G1 p& c/ G. N 在keil的设置中可以看到固件的起始地址,默认情况下为0x8000000。另外,keil生成的是hex文件,hex文件是带有基地址信息的bin文件,如果我们要生成bin文件,需要进行如下设置:& f$ [! a, L. k. v5 z 3 \& \3 X7 D/ ^/ i4 u! y' u( R2 ] ) L' S; }; M0 W4 D0 y ![]() t* R) S) i! o# @ 另外,烧录到mcu中的固件是不带符号表等调试信息的,即hex和bin文件都不带调试信息,因为mcu的内存有限,但keil会单独生成一个带有调试信息的axf文件,我们可以以这个axf文件为基准,对照着恢复bin文件或者hex文件,下面我们来观察一下这三个文件的区别:) Y' e1 R0 E o' N0 x& d 首先是axf文件) b" z/ z3 t! P# e: m6 u) ~- | ![]() ![]() + D; C& M' k% N6 [8 E3 B 几乎和源代码一摸一样,除了一些外设名用地址代替了。5 i7 w" r D+ \" i$ A0 X . L1 c. ?- u; ?8 u- y/ p" t 再看到hex文件: & A$ _& Z2 `: x+ U/ y5 I ![]() ' F7 j5 J: q6 L- B9 H/ ~ 首先需要将架构修改为arm小端序,再修改一下架构版本,stm32f103zet6是cortex-m3内核的,armv7架构。 ![]() 修改完后加载hex文件。 9 I, N; E+ L7 `/ c7 v 3 ] ~, M5 r1 w( c& ?8 G, ^1 K ![]() IDA会自动识别出基地址,能识别出不少函数了,但伪代码是真的难看。 ![]() , M3 P5 b. z: e" k2 O( I& D# O 基本上是看不出个什么东西来。 0 K% G" x) L4 D+ e: f) W 最后是bin文件。 ![]() . H# D, J7 o% s8 x! p- R( z4 V bin文件去除了地址信息的hex文件,需要手动设置基地址。! [3 w- G# I' R0 x( q+ K 9 n' [3 d7 {+ M3 p + q5 P# h" B/ o. U _ 二、手动设置固件基地址9 w+ l) `% X# c/ v- V2 g0 [ # o; ^8 K F, y# g2 Z- o- s; c- M 我们的分析将以bin文件为目标。5 O5 _ @6 G3 L) a7 N/ N+ E% x6 j 将基地址重设为0x8000000,这个地址怎么来的?可以看stm32f103zet6的datasheet。 7 M1 R! h4 w$ I4 i3 W2 k. g1 T ![]() 5 \' o a/ z n$ Y 从0x8000000开始就是flash的空间。 重新设置加载地址之后再将其按照代码解析就变成了hex文件的样子。: _ |( s. `. l! T% N& b % }( h, y( G; t2 ]' W. s$ i ![]() 8 s, O0 Y$ z6 w* g$ }/ o% _& k6 j * d: k% h3 q# u. h1 @* ]: H. L: D 但此时存在很多标红的地址,这是因为这些地址在IDA中并没有设置,因此IDA将其解析为非法地址,标红了,我们现在要做的就是手动添加一些段。+ C, b! o. C* _( K: B8 p$ v. i . u! y" g t5 P( Z- w 三、添加SRAM 段在axf文件中,IDA解析出了如下几个段:6 X/ g0 j( I* N4 F E ![]() 单片机内存被总分为flash(rom)和sram(ram),flash里面的数据掉电可保存,sram中的数据掉电就丢失,sram的执行速度要快于flash,flash容量大于sram上方的最低内存地址,最高地址,都是在flash和sram中。 我们正常下载程序都是下载存储进flash里面,这也是为什么断电可保存的原因。 单片机的程序存储分为code(代码存储区)、RO-data(只读数据存储区)、RW-data(读写数据存储区) 和 ZI-data(零初始化数据区) Flash 存储 code和RO-data Sram 存储 RW-data 和ZI-data3 f2 B% C2 X3 ] - z4 q, h- Y2 J0 _ 在datasheet中一样可以找到sram的起始地址,为0x20000000,结束地址为0x2000ffff,我们自己添加段不必像IDA自动分析的那样详细,一个sram的段即可。9 Q2 y7 [6 M& G' s& X4 w ?+ |/ d 也可以在IDA启动时设置好固件加载地址和sram的开始以及结束地址。 添加了sram段之后,红色的非法地址消失了,如下:9 W6 [5 s' S' J& M( z% c 3 j7 ~) n1 _3 H3 H `9 V ![]() 6 H8 ~4 @ n8 H! A$ v4 |7 X0 U 7 I9 C" d0 G0 c" b ] 四、恢复符号表 下一步,我们需要恢复符号表,由于bin文件不存在调试信息,所以IDA生成的伪代码几乎没眼看,为了方便我们的逆向,我们需要恢复函数名以及导入一些结构体。在这里就需要用到axf文件。 在前面我们看到axf文件几乎和源代码是一样的效果,里面有丰富的调试信息,当然,我们不使用这个课设的axf文件来恢复bin,这样就太刻意了。 9 x: j, B* \7 t9 S 由于我这个课设使用的是库函数来开发的,所以就不能使用stm32cube生成的hal库来恢复。具体该怎么恢复?我们采用bindiff来恢复符号表。 如果有闲工夫或者是对stm32的开发非常上手,就可以自己写一个demo,尽可能多的使用到各种库函数,然后编译出一个axf文件。我这里的话,由于好久没有用stm32了,开发起来有些生疏,所以就不自己手写了,我选择捡现成的项目。我学习stm32的时候买的是xx原子家的开发板,他们提供的例程也是非常丰富的,有五十多个,由易到难,涵盖了stm32开发的方方面面,因此我就直接拿这些例程中的一部分,编译出axf文件。比如下面这个内存分配的例程:( l7 Y; x+ Z2 i- c ) m- T3 m3 ^4 l5 F7 N: i; U. b p, u . M3 g( y$ R& }/ e( l6 ]/ U3 { 又比如这个pwm的例程: ![]() ( i* y- Y1 C) F$ x 可以多选几个例程,能涵盖更多的库函数,将这些axf文件用IDA打开,然后生成idb文件。然后在我们的目标bin文件中,使用bindiff加载idb文件。 " l; b# H; f, V0 @ E, R2 E) u$ ~ ![]() 8 ]: f0 v% M5 q; j0 S. t# K$ ]( o # M% X% k2 g0 m: M1 K 选择一个idb文件,然后会出现这样一个比较界面: ![]() 4 D; X- C$ r4 Y- J4 g. J 选取similarity大的函数导入到bin文件中。! W. T5 j, U7 [5 l( V2 I 0 I* \2 `' x+ l; W$ |$ v& T ![]() " S% c8 f3 n5 R& x 导入之后实际上就能恢复大部分的函数名了。 8 q# t7 a2 ^/ j. Y / N" d# P4 X9 E" \# L1 o3 ] ![]() 3 z }% i, h% Z" ]( k 但实际上,由于我这个课设的开发中使用了一些xx原子封装好的初始化函数(相当于xx原子对原本的库函数做了二次开发),例如uart_init,delay_init这两个函数均是二次封装后的函数,所以恢复出来的函数会比一般情况下多一点。) y+ L( v% L6 {, K2 H5 [6 v 多使用几个idb文件进行恢复后,已经可以看了。 0 s: h6 G9 h, E o* Z. w ![]() ! e+ i \4 ?7 H5 P' v+ ]. l 五、导入SVD文件,恢复外设结构 但这还不够,我们还需要导入SVD文件,啥是SVD文件捏? # J9 U! Z* p0 P9 ^3 z ![]() " o$ q: m I/ d+ h$ J. y 在IDA7.5以后,就自带SVD文件加载插件了,如下图: . z+ n$ m7 P/ A/ x' ~ ![]() 1 Y% \- k$ k4 M) n0 x* g 打开之后如下:( S" p5 N5 H5 _3 r8 m% Z) g3 R% x9 R; } 3 i( \; H# |/ B7 I ![]() ( _# }+ v; G" `: ?0 i* h# `) } 我们可以自行下载相应的SVD文件,或者加载GitHub上的仓库,我这里选择自行下载然后在本地加载。下载链接是这个:cmsis-svd 选中想要加载的svd文件之后,IDA就会自动恢复bin文件中的外设结构,体现在伪代码中就是这样:! Z4 o0 J) {& x) y6 o 6 E* u' \6 L5 U# }0 U ![]() + [( M& z0 Y2 u 这样: ![]() 1 j/ l# T; x7 N& p ) \0 _9 u2 G" y; K 当然,也不是所有的外设都能恢复,和axf文件中的肯定是有差别的,但对我们的逆向已经起到了很大的作用了。6 N$ S B/ ?& V: E/ X8 J 到目前位置我们已经做了如下几步:加载bin文件->设置固件加载基地址->添加sram段->恢复符号表->恢复外设信息。程序还原工作已经接近尾声,还剩下最后一步,恢复结构体。 ( \9 `; ?! j( l' v% X( s* ^' w 六、恢复结构体 打开某个例程的axf文件,可以查看下其中使用的结构体。 ![]() 4 f( `0 x. I% \2 P( M 这些结构体中,有TIM_TimeBaseInitTypeDef,NVIC_InitTypeDef,GPIO_InitTypeDef,USART_InitTypeDef,GPIO_TypeDef等是我们经常使用的,用来进行定时器初始化,中断初始化,IO口初始化等等。/ `4 \2 _- w' a 2 }" O0 L) u+ x" ~4 F- R6 i 我们生成c header file。& O8 c9 N1 b# F [9 D: L 0 i+ f, Q; e C1 [1 K3 x ![]() 然后在bin文件中解析生成的头文件: ![]() 然后在local type中就能看到不少结构体了。 " a+ x6 V$ b8 @ ![]() ! S& `+ v: `) {$ d% j, Z 光导入了结构体还不够,我们还得手动将IDA的伪代码中的变量重新设置类型,怎么确定某个变量的类型是什么呢?需要结合库函数的变量类型来设置。0 O6 b, Y$ v! J, o7 { 以GPIO_Init这个函数为例:3 C- \; Y1 r0 F, e+ Q3 q ![]() 7 \! q' O$ A8 l2 \# H1 C& i8 c& t 我们打开固件库使用手册,找到GPIO_Init的原型。" r- h* V# v& ] G ! l7 P r6 |7 U) }) r2 T/ E0 V 9 V: d$ A& I) m ![]() , m' q0 P9 R* w( X6 A: O2 [+ w 可以看到原型为:8 H: w4 |% u: } $ y+ s2 t, S+ P# ?) m( W- F void GPIO_Init(GPIO_TypeDef* GPIOx, GPIO_InitTypeDef* GPIO_InitStruct) 第二个参数为GPIO_InitTypeDef类型的,因此我们将伪代码中的v10的类型修改为GPIO_InitTypeDef,剩下的结构体我们也一样可以使用这样的方法来恢复,只要有固件库函数,我们就能够恢复对应的结构体,效果如下:. K# t9 w) t9 I3 E+ c3 C; `. k! E . s$ h; S8 V' z8 e- J - f! R4 w9 C% g* u3 x 效果还不错。. c* Z' q# r- a' N! O 七、总结- u ^8 M+ o. h% z0 [9 N# a 此次逆向是基于我的一个小课设,功能不多,而且由于我的课设用到了xx原子的例程二次开发的一些库,而我用来恢复符号表时使用的axf文件也是xx原子的例程编译出来的,所以恢复出来的符号表的比例会偏高。如果能确定使用芯片型号,以及使用的是固件库还是hal库,那么按照此方法大致上能在一定程度上恢复固件,使其可读性大大增加。; ^5 k( I- Z& P; i3 F6 o) p " e. |7 I2 h" T 转载自:Lpwn3 K# X8 [* [. e8 l m* r 7 o: }2 ]' ~( G- d4 E/ {7 k/ Z |
OpenBLT移植到STM32F405开发板
为什么要先开启STM32外设时钟?
【STM32MP157】从ST官方例程中分析RPMsg-TTY/SDB核间通信的使用方法
【经验分享】STM32实例-RTC实时时钟实验④-获取RTC时间函数与中断服务函数
STM32 以太网 MAC Loopback 的实现
刘氓兔的64位入门挑战【1】——MP257芯片下单和硬件准备
刘氓兔的64位入门挑战【0】——MP257选型
STM32功能安全设计包,助力产品功能安全认证
基于STM32启动过程startup_xxxx.s文件经验分享
HRTIM 指南