固件解剖
一、固件解包
初入IOT安全研究的小伙伴会觉得固件解包很简单,直接binwalk -Me就可以了,但是理想很丰满,现实很骨感,固件测试多了就会发现binwalk很多情况下都解不开。
IOT固件一般分为两类,一类存在文件系统,大多基于linux/BSD,另一类固件是一个整体,即我们所说的RTOS(Real-time operating system)。
1.1 存在文件系统
binwalk大家应该都很熟悉,使用binwalk能直接得到rootfs文件系统的情况这里不再赘述,笔者认为binwalk的强大之处在于可以解析识别多重格式的header,为解包提供参考。以下介绍几种需要绕点弯的情况,当然固件千差万别,全看设计者设计,不能一一列举。
UBI(Unsorted Block Image)
UBI格式的固件算比较常见的,binwalk并不能直接解包,但是github上有现成的工具ubi_reader,这里有个需要注意的地方:
UBI_reader解包,UBI文件必须是1024bytes的整数倍,需要增删内容对齐,比如通过分析某路由器,发现其rootfs是UBI格式:
# binwalk ROM/wifi_firmware_c91ea_1.0.50.bin
DECIMAL HEXADECIMAL DESCRIPTION
--------------------------------------------------------------------------------
684 0x2AC UBI erase count header, version: 1, EC: 0x0, VID header offset: 0x800, data offset: 0x1000首先安装ubi_reader:
$ sudo apt-get install liblzo2-dev
$ sudo pip install python-lzo
$ git clone <https://github.com/jrspruitt/ubi_reader>
$ cd ubi_reader
$ sudo python setup.py install或者直接$ sudo pip install ubi_reader 然后将根据地址将UBI结构提取出来,利用ubireader_extract_files [options] path/to/file 即可解包。
PFS
有些固件binwalk可以识别出header,但是无法解开,比如下面这个固件
运行binwalk后查看结果,发现没有发现任何可识别的东西,此时可以手动分析或者去搜索一些相关工具。 这里在github上找到相关工具,直接根据提示使用命令就可解开固件。
这里简单看一下固件解包关键代码,关键在于找到类似'\xA5\xA5\xA5\x5A\xA5\x5A'的header,之后根据具体格式解包解压即可,所以固件解包说到底还是数据格式分析。
Openwrt Lua
lua结构解析放在解包这里可能不太恰当,但鉴于Openwrt的使用基数很大,在这里简单提一下。Lua是一门方便嵌入并可扩展的轻量级脚本语言,Openwrt开发中会使用该脚本语言。值得注意的是,有些设备的lua并不是纯文本,存在混淆,需要使用luadec反编译。
openwrt中的lua脚本和传统的luajit编译后的有点不一样,需要打几个补丁才能正常使用luadec进行反编译,命令如下:
修改 lua-5.1/src/MakeFile:
接着执行:
利用luadec显示代码结构:
利用luadec反编译指定的函数 (函数 0 包含 子函数): $ luadec -f 0 squashfs-root/usr/lib/lua/luci/sgi/uhttpd.lua
需要注意的是,luadec编译与架构相关,用官方luadec无法解析arm环境下的lua文件,但github上也有相应的工具,这里不再赘述。
1.2 RTOS
首先从应用较广且有套路可循的vxworks说起,VxWorks是Wind River System公司推出的一个实时操作系统,广泛应用在通信、军事、航空、航天嵌入式设备领域。因为有标准,所以好识别,以下面这个固件为例:
binwalk已经识别出固件为Vxworks 5.5.1,并且给出了符号表位置。首先需要识别固件的入口点,如果固件被封装成ELF格式,直接利用readelf就可以得到基地址,这里显然不适用。
通过binwalk -A得到固件架构是ARM,直接用IDA pro载入: 分析固件开头跳转判断加载地址为0x1000。对于Vxworks一般判断基址办法有:


分析固件头部的初始化代码,找到vxworks启动的第一个函数usrInit跳 根据BSS区初始化特征找到BSS边界,根据偏移计算固件加载地址 然后根据binwalk指示的位置,修复符号表名。

函数表存放了函数名和函数地址,通过两者定位也可以反过来验证基地址的正确性,比如上图所示的0x00c813f8是函数名:

0x009aa0f4是函数地址:

由于基地址和架构有关,在这里就不详细展开,对于vxworks的分析我们可以借助一款能自动化修复入口和符号的插件--vxhunter。
以Ghidra为例,载入固件后直接选择vxhunter_firmware_init.py插件和vxworks版本,就可以自动修复入口和符号:


U-boot
boot类的固件也是我们常会遇见的一类无文件系统固件,比如很多IOT设备会采用U-boot作引导,因为U-boot开源,我们可以参照源代码分析,对于有些架构的U-boot也可以采用固定套路,比如mips可以根据$gp寄存器等。

Chip firmware
有些IOT固件没有资料,逆向困难,比如下面某款ARM芯片的固件,将其载入IDA pro发现没有识别出任何函数:

这样我们就需要对固件有一个整体的分析了,我们看到固件0x100的位置十分有趣:

4字节排列后都以0x2开头,这里既不是代码,也不是数据,那就很可能是地址了,这里应该是一张表,所以基地址很可能是0x200000。我们rebase之后再查看字符串:

看到许多类似函数名的字符串,找到具体位置后,在固件中二进制搜索0x16852A,即wlc_probresp_attach地址(小端)。

可以看到真的搜索到了,而且也是一个表的结构:

根据基址找到在IDA pro中的位置:

可以看到完成了部分的交叉引用,后续分析比较复杂,这里就不再展开,实际上0x100位置是函数地址表,在该固件中这样表有很多。所以类似地址表,字符串都是我们分析固件基址和函数的重要线索。
二、固件打包
拆东西容易装东西难,这个道理也适用于固件打包。如果设备留有调试接口,一般不用打包操作,毕竟安全研究是以逆向思维为主。 有时缺乏调试手段,我们就要在解开的固件中手动添加。一般将交叉编译好的telnetd,dropbear(sshd),gdb放入固件文件,再替换启动脚本打包。 linux的启动脚本套路众多,尤其在IOT设备中,这里笔者一般采用比较讨巧的方法,比如确定/sbin/xxxd服务会开机运行,可以将其替换:
之后在sbin/xxxd添加
这样开机启动xxxd时就会先运行telnetd。
交叉编译
如果能从正向开发角度来打包当然最方便,也就是交叉编译的事。笔者研究过的一些设备中,主要是路由器固件会部分遵循GPL,就是开源一部分代码软件(一般本来就是基于开源工具),并提供剩下软件的二进制文件和整个固件的打包工具(方法)。
比如之前研究的某款路由设备就提供了开源下载:

下载该zip包,根据自己的需求编译rootfs,最后利用zip包中自带的工具打包:
firmware-mod-kit
firmware-mod-kit(fmk)可能是最常用的基于binwalk的解打包工具,但是由于很久没用更新,使用场景有限。
fmk的安装使用都比较简单,如下所示:
手动分析
打包的难度在于固件要与原固件一致,并通过各种校验,否则轻则刷机失败,重则设备变砖。笔者之前有一篇关于netgear upnp漏洞的文章,涉及netgear固件打包过程,有兴趣的小伙伴可以看一看。
固件一般会分成许多section,为了方便解析,每个section会有指示头,头中可能会存放标志、大小和crc校验等信息,这些信息都为解打包提供依据。
比如可以先获取固件大小(十六进制),根据固件大小端拆分字节,一般是4字节,然后在固件头上寻找类似字节(固件头上的指示长度会减去头长度),接着从指示大小的字节往后分析就可以澄清格式,和分析网络协议的过程很像。

当然大部分头都是有标准的,可以根据标准格式一一对应。值得注意的是,有些厂商会给固件签名,这个就会增加打包的难度。此时我们可以寻找一些遵循GPL的官方打包工具,或者利用openssl生成公私钥对,覆盖设备中的验证公钥,这里当然要有漏洞,不然就掉进鸡生蛋蛋生鸡的循环。当然还有一个比较好且廉价的办法---固件模拟。
Last updated
