一、固件解包
初入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格式:
Copy # 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:
Copy $ 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
即可解包。
有些固件binwalk可以识别出header,但是无法解开,比如下面这个固件
Copy iot@attifyos ~/Documents> binwalk -Me v2912_389.all
Scan Time: 2020-11-04 18:39:13
Target File: /home/iot/Documents/v2912_389.all
MD5 Checksum: 180c60197aae7e272191695e906c941e
Signatures: 396
DECIMAL HEXADECIMAL DESCRIPTION
--------------------------------------------------------------------------------
1546799 0x179A2F gzip compressed data, last modified: 2042-04-26 20:13:56 (bogus date)
1717744 0x1A35F0 LZ4 compressed data
4171513 0x3FA6F9 SHA256 hash constants, little endian
4179098 0x3FC49A Copyright string: "Copyright (c) 1998-2000 by XXXXX Corp."
4214532 0x404F04 Base64 standard index table
4224780 0x40770C HTML document header
4232369 0x4094B1 SHA256 hash constants, little endian
4307839 0x41BB7F SHA256 hash constants, little endian
4314017 0x41D3A1 XML document, version: "1.0"
4702230 0x47C016 Base64 standard index table
4707197 0x47D37D Certificate in DER format (x509 v3), header length: 4, sequence length: 873
4727609 0x482339 Base64 standard index table
4791281 0x491BF1 PFS filesystem, version 1.0, 12886 files
4807401 0x495AE9 Base64 standard index table
...
iot@attifyos ~/Documents> ls _v2912_389.all.extracted/pfs-root/000/
WEBLOGIN.HTM _WEBLOGIN.HTM.extracted/
iot@attifyos ~/Documents> ls _v2912_389.all.extracted/pfs-root/000/_WEBLOGIN.HTM.extracted/
3CB 3CB.zlib E235 E235.zlib
运行binwalk后查看结果,发现没有发现任何可识别的东西,此时可以手动分析或者去搜索一些相关工具。 这里在github上找到相关工具 ,直接根据提示使用命令就可解开固件。
Copy iot@attifyos ~/D/draytools> python draytools.py -F v2910_61252.all
v2910_61252.all.out written, 12816484 [0x00C39064] bytes
FS extracted to [/home/iot/Documents/draytools/fs_out], 429 files extracted
iot@attifyos ~/D/draytools> ls fs_out/
v2000/ v2910.lst
iot@attifyos ~/D/draytools> ls fs_out/v2000/
act_sta.htm CSS/ header.htm INDEX2.HTM ivr_711u/ jg/ l_m.htm menu.htm STATUS.HTM SYSINFO_C.TXT UPNP/ webauth.htm
CGI-BIN/ DOC/ IMAGES/ ivr_711a/ ivr_729/ JS/ LOGIN.HTM rpage.htm STYLE.CSS SYSINFO.TXT VLAN/
这里简单看一下固件解包关键代码,关键在于找到类似'\xA5\xA5\xA5\x5A\xA5\x5A'的header,之后根据具体格式解包解压即可,所以固件解包说到底还是数据格式分析。
Copy def decompress_firmware(data):
flen = len(data)
sigstart = data.find('\\xA5\\xA5\\xA5\\x5A\\xA5\\x5A')
if sigstart <= 0:
sigstart = data.find('\\x5A\\x5A\\xA5\\x5A\\xA5\\x5A')
if sigstart > 0:
if draytools.verbose:
print 'Signature found at [0x%08X]' % sigstart
lzosizestart = sigstart + 6
lzostart = lzosizestart + 4
lzosize = unpack('>L', data[lzosizestart:lzostart])[0]
return data[0x100:sigstart+2] \\
+ pydelzo.decompress('\\xF0' + pack(">L",0x1000000) \\
+ data[lzostart:lzostart+lzosize])
...
lua结构解析放在解包这里可能不太恰当,但鉴于Openwrt的使用基数很大,在这里简单提一下。Lua是一门方便嵌入并可扩展的轻量级脚本语言,Openwrt开发中会使用该脚本语言。值得注意的是,有些设备的lua并不是纯文本,存在混淆,需要使用luadec 反编译。
openwrt中的lua脚本和传统的luajit编译后的有点不一样,需要打几个补丁才能正常使用luadec进行反编译,命令如下:
Copy $ cd ..
$ mkdir luadec
$ cd luadec/
$ git clone <https://github.com/viruscamp/luadec>
$ cd luadec/
$ git submodule update --init lua-5.1
$ cd lua-5.1
$ make linux
$ make clean
$ mkdir patch
$ cd patch/
$ get <https://dev.openwrt.org/export/HEAD/trunk/package/utils/lua/patches/010-lua-5.1.3-lnum-full-260308.patch>
$ wget <https://dev.openwrt.org/export/HEAD/trunk/package/utils/lua/patches/030-archindependent-bytecode.patch>
$ wget <https://dev.openwrt.org/export/HEAD/trunk/package/utils/lua/patches/011-lnum-use-double.patch>
$ wget <https://dev.openwrt.org/export/HEAD/trunk/package/utils/lua/patches/015-lnum-ppc-compat.patch>
$ wget <https://dev.openwrt.org/export/HEAD/trunk/package/utils/lua/patches/020-shared_liblua.patch>
$ wget <https://dev.openwrt.org/export/HEAD/trunk/package/utils/lua/patches/040-use-symbolic-functions.patch>
$ wget <https://dev.openwrt.org/export/HEAD/trunk/package/utils/lua/patches/050-honor-cflags.patch>
$ wget <https://dev.openwrt.org/export/HEAD/trunk/package/utils/lua/patches/100-no_readline.patch>
$ wget <https://dev.openwrt.org/export/HEAD/trunk/package/utils/lua/patches/200-lua-path.patch>
$ wget <https://dev.openwrt.org/export/HEAD/trunk/package/utils/lua/patches/300-opcode_performance.patch>
$ mv patch/ patches
$ for i in ../patches/*.patch; do patch -p1 <$i ; done
$ for i in ./patches/*.patch; do patch -p1 <$i ; done
$ make linux
修改 lua-5.1/src/MakeFile:
Copy # USE_READLINE=1
+PKG_VERSION = 5.1.5
-CFLAGS= -O2 -Wall $(MYCFLAGS)
+CFLAGS= -fPIC -O2 -Wall $(MYCFLAGS)
- $(CC) -o $@ -L. -llua $(MYLDFLAGS) $(LUA_O) $(LIBS)
+ $(CC) -o $@ $(LUA_O) $(MYLDFLAGS) -L. -llua $(LIBS)
- $(CC) -o $@ -L. -llua $(MYLDFLAGS) $(LUAC_O) $(LIBS)
+ $(CC) -o $@ $(LUAC_O) $(MYLDFLAGS) -L. -llua $(LIBS)
接着执行:
Copy $ make linux
$ ldconfig
$ cd ../luadec
$ make LUAVER=5.1
$ sudo cp luadec /usr/local/bin/
利用luadec显示代码结构:
Copy $ luadec -pn squashfs-root/usr/lib/lua/luci/sgi/uhttpd.lua
0
0_0
0_0_0
0_0_1
0_0_2
利用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公司推出的一个实时操作系统,广泛应用在通信、军事、航空、航天嵌入式设备领域。因为有标准,所以好识别,以下面这个固件为例:
Copy iot@attifyos ~/Documents> binwalk image_vx5.bin
DECIMAL HEXADECIMAL DESCRIPTION
--------------------------------------------------------------------------------
335280 0x51DB0 PEM certificate
...
3721556 0x38C954 GIF image data, version "89a", 10 x 210
8518936 0x81FD18 VxWorks operating system version "5.5.1" , compiled: "Mar 5 2015, 15:56:18"
9736988 0x94931C SHA256 hash constants, little endian
...
13374599 0xCC1487 Copyright string: "Copyright 1999-2001 Wind River Systems."
13387388 0xCC467C VxWorks symbol table, big endian, first entry: [type: function, code address: 0xF4A09A00, symbol address: 0xF813C800]
13391405 0xCC562D VxWorks symbol table, little endian, first entry: [type: function, code address: 0xB8BD, symbol address: 0xD000C800]
binwalk已经识别出固件为Vxworks 5.5.1,并且给出了符号表位置。首先需要识别固件的入口点,如果固件被封装成ELF格式,直接利用readelf就可以得到基地址,这里显然不适用。
Copy iot@attifyos ~/Documents> readelf -a image_vx5_arm_little_eniadn.bin
readelf: Error: Not an ELF file - it has the wrong magic bytes at the start
iot@attifyos ~/Documents> binwalk -A image_vx5.bin |more
DECIMAL HEXADECIMAL DESCRIPTION
--------------------------------------------------------------------------------
244 0xF4 ARM instructions, function prologue
408 0x198 ARM instructions, function prologue
440 0x1B8 ARM instructions, function prologue
472 0x1D8 ARM instructions, function prologue
608 0x260 ARM instructions, function prologue
通过binwalk -A
得到固件架构是ARM,直接用IDA pro载入: 分析固件开头跳转判断加载地址为0x1000。对于Vxworks一般判断基址办法有:
分析固件头部的初始化代码,找到vxworks启动的第一个函数usrInit跳 根据BSS区初始化特征找到BSS边界,根据偏移计算固件加载地址 然后根据binwalk指示的位置,修复符号表名。
函数表存放了函数名和函数地址,通过两者定位也可以反过来验证基地址的正确性,比如上图所示的0x00c813f8是函数名:
0x009aa0f4是函数地址:
由于基地址和架构有关,在这里就不详细展开,对于vxworks的分析我们可以借助一款能自动化修复入口和符号的插件--vxhunter 。
以Ghidra为例,载入固件后直接选择vxhunter_firmware_init.py插件和vxworks版本,就可以自动修复入口和符号:
boot类的固件也是我们常会遇见的一类无文件系统固件,比如很多IOT设备会采用U-boot 作引导,因为U-boot开源,我们可以参照源代码分析,对于有些架构的U-boot也可以采用固定套路,比如mips可以根据$gp寄存器等。
有些IOT固件没有资料,逆向困难,比如下面某款ARM芯片的固件,将其载入IDA pro发现没有识别出任何函数:
这样我们就需要对固件有一个整体的分析了,我们看到固件0x100的位置十分有趣:
4字节排列后都以0x2开头,这里既不是代码,也不是数据,那就很可能是地址了,这里应该是一张表,所以基地址很可能是0x200000。我们rebase之后再查看字符串:
看到许多类似函数名的字符串,找到具体位置后,在固件中二进制搜索0x16852A,即wlc_probresp_attach地址(小端)。
可以看到真的搜索到了,而且也是一个表的结构:
根据基址找到在IDA pro中的位置:
可以看到完成了部分的交叉引用,后续分析比较复杂,这里就不再展开,实际上0x100位置是函数地址表,在该固件中这样表有很多。所以类似地址表,字符串都是我们分析固件基址和函数的重要线索。
二、固件打包
拆东西容易装东西难,这个道理也适用于固件打包。如果设备留有调试接口,一般不用打包操作,毕竟安全研究是以逆向思维为主。 有时缺乏调试手段,我们就要在解开的固件中手动添加。一般将交叉编译好的telnetd,dropbear(sshd),gdb放入固件文件,再替换启动脚本打包。 linux的启动脚本套路众多,尤其在IOT设备中,这里笔者一般采用比较讨巧的方法,比如确定/sbin/xxxd服务会开机运行,可以将其替换:
Copy # mv rootfs/sbin/xxxd sbin/xxxdd
# touch rootfs/sbin/xxxd
# chmod +x rootfs/sbin/xxxd
之后在sbin/xxxd添加
Copy #!/bin/sh
/usr/sbin/telnetd -F -l /bin/sh -p 1234 &
/sbin/xxxdd &
这样开机启动xxxd时就会先运行telnetd。
如果能从正向开发角度来打包当然最方便,也就是交叉编译的事。笔者研究过的一些设备中,主要是路由器固件会部分遵循GPL ,就是开源一部分代码软件(一般本来就是基于开源工具),并提供剩下软件的二进制文件和整个固件的打包工具(方法)。
比如之前研究的某款路由设备就提供了开源下载:
下载该zip包,根据自己的需求编译rootfs,最后利用zip包中自带的工具打包:
Copy ./packet -k %s -f rootfs -b compatible_r6400.txt
-ok kernel -oall image -or rootfs -i ambitCfg.h
firmware-mod-kit(fmk )可能是最常用的基于binwalk的解打包工具,但是由于很久没用更新,使用场景有限。
fmk的安装使用都比较简单,如下所示:
Copy # For ubuntu
$ sudo apt-get install git build-essential zlib1g-dev liblzma-dev python-magic bsdmainutils autoconf
# For redhat/centos
$ yum groupinstall "Development Tools"
$ yum install git zlib1g-dev xz-devel python-magic zlib-devel util-linux
# 使用
$ ./extract-firmware.sh firmware.bin //解包
$ cp new-telnetd fmk/rootfs/usr/sbin/telnetd //按需修改
$ ./build-firmware.sh //打包
打包的难度在于固件要与原固件一致,并通过各种校验,否则轻则刷机失败,重则设备变砖。笔者之前有一篇关于netgear upnp漏洞的文章 ,涉及netgear固件打包过程,有兴趣的小伙伴可以看一看。
固件一般会分成许多section,为了方便解析,每个section会有指示头,头中可能会存放标志、大小和crc校验等信息,这些信息都为解打包提供依据。
比如可以先获取固件大小(十六进制),根据固件大小端拆分字节,一般是4字节,然后在固件头上寻找类似字节(固件头上的指示长度会减去头长度),接着从指示大小的字节往后分析就可以澄清格式,和分析网络协议的过程很像。
当然大部分头都是有标准的,可以根据标准格式一一对应。值得注意的是,有些厂商会给固件签名,这个就会增加打包的难度。此时我们可以寻找一些遵循GPL的官方打包工具,或者利用openssl生成公私钥对,覆盖设备中的验证公钥,这里当然要有漏洞,不然就掉进鸡生蛋蛋生鸡的循环。当然还有一个比较好且廉价的办法---固件模拟。