Page cover

固件解剖

一、固件解包

初入IOT安全研究的小伙伴会觉得固件解包很简单,直接binwalk -Me就可以了,但是理想很丰满,现实很骨感,固件测试多了就会发现binwalk很多情况下都解不开。

IOT固件一般分为两类,一类存在文件系统arrow-up-right,大多基于linux/BSD,另一类固件是一个整体,即我们所说的RTOS(Real-time operating system)。

1.1 存在文件系统

binwalk大家应该都很熟悉,使用binwalk能直接得到rootfs文件系统的情况这里不再赘述,笔者认为binwalk的强大之处在于可以解析识别多重格式的header,为解包提供参考。以下介绍几种需要绕点弯的情况,当然固件千差万别,全看设计者设计,不能一一列举。

  • UBI(Unsorted Block Image)

UBI格式的固件算比较常见的,binwalk并不能直接解包,但是github上有现成的工具ubi_readerarrow-up-right,这里有个需要注意的地方:

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上找到相关工具arrow-up-right,直接根据提示使用命令就可解开固件。

这里简单看一下固件解包关键代码,关键在于找到类似'\xA5\xA5\xA5\x5A\xA5\x5A'的header,之后根据具体格式解包解压即可,所以固件解包说到底还是数据格式分析。

  • Openwrt Lua

lua结构解析放在解包这里可能不太恰当,但鉴于Openwrt的使用基数很大,在这里简单提一下。Lua是一门方便嵌入并可扩展的轻量级脚本语言,Openwrt开发中会使用该脚本语言。值得注意的是,有些设备的lua并不是纯文本,存在混淆,需要使用luadecarrow-up-right反编译。

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上也有相应的工具arrow-up-right,这里不再赘述。

1.2 RTOS

首先从应用较广且有套路可循的vxworksarrow-up-right说起,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的分析我们可以借助一款能自动化修复入口和符号的插件--vxhunterarrow-up-right

以Ghidra为例,载入固件后直接选择vxhunter_firmware_init.py插件和vxworks版本,就可以自动修复入口和符号:

  • U-boot

boot类的固件也是我们常会遇见的一类无文件系统固件,比如很多IOT设备会采用U-bootarrow-up-right作引导,因为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。

  • 交叉编译

如果能从正向开发角度来打包当然最方便,也就是交叉编译的事。笔者研究过的一些设备中,主要是路由器固件会部分遵循GPLarrow-up-right,就是开源一部分代码软件(一般本来就是基于开源工具),并提供剩下软件的二进制文件和整个固件的打包工具(方法)。

比如之前研究的某款路由设备就提供了开源下载:

下载该zip包,根据自己的需求编译rootfs,最后利用zip包中自带的工具打包:

  • firmware-mod-kit

firmware-mod-kit(fmkarrow-up-right)可能是最常用的基于binwalk的解打包工具,但是由于很久没用更新,使用场景有限。

fmk的安装使用都比较简单,如下所示:

  • 手动分析

打包的难度在于固件要与原固件一致,并通过各种校验,否则轻则刷机失败,重则设备变砖。笔者之前有一篇关于netgear upnp漏洞的文章arrow-up-right,涉及netgear固件打包过程,有兴趣的小伙伴可以看一看。

固件一般会分成许多section,为了方便解析,每个section会有指示头,头中可能会存放标志、大小和crc校验等信息,这些信息都为解打包提供依据。

比如可以先获取固件大小(十六进制),根据固件大小端拆分字节,一般是4字节,然后在固件头上寻找类似字节(固件头上的指示长度会减去头长度),接着从指示大小的字节往后分析就可以澄清格式,和分析网络协议的过程很像。

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

Last updated