某智能设备固件解密

获取固件

拿到硬件设备的第一个想法就是获取固件。通常来说获取固件有三种方式:从设备官方网址下载,抓包固件升级过程,从设备固件存储器中获取。

此设备的官网提供在线升级的功能,升级过程需要读取硬件的一些基本信息,而此设备恰好不被支持。抄起工具把网站日了下来,却发现网站上只提供其他型号的升级包,而且是加过密的。于是只能从现有的设备入手了。

观察电路板,发现板子上主控芯片为ATMEL SAMA5D2系列的芯片,无明显的UART和JTAG接口,存在SOP8封装的型号为N25Q032A的Flash芯片。所以固件只能从Flash里读取出来了。

从Flash里读固件第一步是把芯片从板子上焊下来,对于SOP8封装的芯片建议用电风枪吹下来,血泪教训告诉我们,用电烙铁非常容易把芯片弄坏。

把Flash芯片焊下来之后就是从芯片里读取固件内容了。这里推荐用编程器来读,淘宝一两百人民币就能买到。我们也尝试过用FlashROM来读,但是有时候会很不稳定,对于有的型号并不能完整的将Flash读出来。

固件查看

用 010editor 查看固件,观察固件发现在固件的0x20000偏移处有F1 F2 F3 F4 CD CD CD CD CD CD CD CD 10 BB 00 00开头的疑似加密之后的数据。猜测该部分为加密部分的头,0xBB10为加密部分的长度。同样的结构在0x70000也有。

即固件0x0开始为设备的启动代码,0x20000开始为一段加密数据,0x70000开始为另一段加密数据。

加密数据前16个byte为加密块的头。头结构为

1
2
3
4
0xf4f3f2f1
0xcdcdcdcd
0xcdcdcdcd
data_length

EncryptedData

通过主控芯片的手册可知,芯片采用的是ARMv7-A的架构。ARM 是 RISC 指令集,在32位模式下一般为ARM指令集,指令长度固定,默认为小端模式。

将固件载入IDA,选择 ARM Little-endian 的处理器,可以看到程序的汇编代码。我们推测在启动代码里存在解密加密数据的算法和密钥。

InterruptTable

ARM代码最开始部分为中断向量表。常见的入口代码如下所示。

1
2
3
4
5
6
7
8
9
10
11
12
13
.globl _start
_start:
b reset
ldr pc, _undefined_instruction
ldr pc, _software_interrupt
ldr pc, _prefetch_abort
ldr pc, _data_abort
ldr pc, _not_used
ldr pc, _irq
ldr pc, _fiq
...
_irq:
.word irq

这样当reset的时候,PC会被设置为base+0,遇到undefined_instruction的时候会将PC设置为base+4。reset指向的函数地址为初始化代码以及程序逻辑真正的入口。

从IDA里可以看到,该固件开始部分汇编代码符合入口的特征。

寻找固件基址

对于这种没有底层操作系统的固件来说,第一步要找到基址,这是二进制文件加载到内存后在内存的地址。如果不调整基址,代码中有些绝对地址寻址的指令,寻找数据的时候可能对应不上。

一种方法是根据代码中的load指令寻找载入立即数的操作,来猜测基址。
在ARM中,大部分的load指令为相对寻址,载入相对于当前PC某个偏移处的内存内容,这个内容往往是一个绝对的在固件运行时的地址(即加上了基址之后的地址)。而在ida里,对于这种情况进行了显示上的处理,会直接显示将这个绝对的地址加载到寄存器里。可以根据load指令加载的地址猜测出一个基址来,如果一些内存能成功的找到xref,即说明基址找的是正确的。一个自动化寻找基址的方案是,用strings找出字符串在文件中的偏移,在ida里找出所有load指令加载的地址,爆破基址,当字符串的偏移加上基址能在load指令加载的地址里找的时,这个基址有很大可能为正确的基址。

另外,对于一个嵌入式设备来讲,一般会将ROM加载到内存中然后执行。查看芯片的手册,手册中的内存设备地址一般为基址。如果外部flash存放的代码是一个bootloader,那么逆向可以获得下一步程序加载的基址。(一般用于固件解密后跳转到真正的代码执行)

对于其他情况,如果固件能通过binwalk识别出u-boot image,在信息里会输出基址,因为在u-boot image的文件头中存有基址信息。

在这里,我们找到固件的基址为0x200000

固件逆向

由于固件有部分数据是加密过的,在运行的时候一定会存在解密操作。用IDA的Find Crypt插件寻找常用密码学算法的常量,发现代码中存在sha256和sha512算法。同时在芯片的手册中发现有硬件AES模块,在此CPU中,AES硬件模块的基址为0xF002C000。在IDA中寻找该立即数即可找到固件中调用硬件AES的代码,根据手册中对应位置内存的功能对代码中函的进行标记。对于这些特殊的地址,在逆向过程中边查手册边将其功能标记出来。

AESCtrolRegister

AESFunctions

我们发现,与AES相关的函数地址以及sha256的地址被存放在了内存中的某个区域,在一段疑似对数据块解密的函数中进行了调用。
此函数先判断了数据块是否以0xf4f3f2f1开头,并检查其长度是否小于0x30000并且是否为0x40的倍数。然后就是获取芯片的序列号并做sha256作为key1,然后把序列号进行byte交换,sha256后作为key2,设置IV为\x00\x10\x00\x00\x00\x00\x00\x00\x00\x08\x00\x00\x20\x00\x00\x00,对前一半的数据用key1解,后一半的数据用key2解。解完之后得到真正的逻辑代码。

按照这个逻辑看来,固件的解密需要获取芯片的序列号,每个芯片的系列号都不相同,所以每个板子中的固件也都不一样。问题来了,我们怎么才能拿到芯片的序列号呢?

解密固件

为了解密固件,我们需要拿到芯片的序列号作为AES的key。网上搜了一会也没搜到有用的信息。由于板子上也没有可以连接电脑输出的接口,即使读到了也很难将其输出出来。难道要从板子上找个灯,将读到的序列号通过控制灯的亮灭一位一位的输出出来吗。。

该设备可以将配置的修改保存下来,那么就一定有写回 flash 的方法。我们可以把序列号读出来之后再写回 flash,这样再读一次flash就能得到序列号了。FLash芯片在写之前需要先erase。因为Flash的write只能使bit从1变成0,而Flash erase会把所有的bit设置为1。

修改固件代码,把读取序列号并写回flash的shellcode填入,焊回flash芯片上电运行一段时间,取下flash,读取出里面的序列号即可解密固件。
由于芯片的序列号是有规律的,我们可以通过暴力序列号的一部分来解密其他设备的固件。

在固件里也发现了一个固定的AES密钥,猜测是用来解密升级固件的,尝试用来解密官网上的其他型号的固件升级包失败。

最后

对于智能设备的固件,厂商很难保证固件不被攻击者拿到。对固件的加密只能增加攻击者获取固件的难度。对于小型的设备最好的方法是把固件保存在主控芯片内的rom中,或者将核心的解密逻辑放在主控芯片内,这样攻击者很难获取到主控芯片内的flash中存储的数据,使固件不能被以较低成本获取。

在逆向的过程中,由于固件的代码没有外部函数的调用,可以用模拟器来模拟执行一些代码段辅助逆向。emu是基于unicorn的一个模拟执行辅助脚本,根据idaemu修改而来,欢迎一起共同维护。

感谢hyperchem大佬带我飞!

参考资料

https://reverseengineering.stackexchange.com/questions/13948/how-to-find-bootloader-load-address
http://stackoverflow.com/questions/21312963/arm-bootloader-interrupt-vector-table-understanding
https://sviehb.wordpress.com/2011/09/09/reverse-engineering-an-obfuscated-firmware-image-e02-analysis/
https://github.com/5alt/emu

分享到 评论