🖨️
IOT 固件安全 All in One
  • 前言
  • WIKI
    • 固件分类
    • 固件获取
    • 固件解密
    • 固件解剖
    • 固件调试
    • 仿真分析
    • 固件FUZZ
    • 静态分析
    • 同源分析
    • 漏洞挖掘—Web接口
    • 漏洞挖掘—无线协议
    • 漏洞挖掘—APP应用
  • appendix
    • 附录1:信息收集
    • 附录2:工具列表
    • 附录3-1: 指令集分析-MIPS
    • 附录3-2: 指令集分析-PowerPC
    • 附录3-3: 指令集分析-ARM
    • 附录4: 基线
    • 附录5:Azure IOT SDLC Best Practice
    • 附录6: 论文
    • 附录7: 参考资料
Powered by GitBook
On this page
  • 一、固件解包
  • 二、固件打包
  1. WIKI

固件解剖

Previous固件解密Next固件调试

Last updated 2 years ago

一、固件解包

初入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文件必须是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,但是无法解开,比如下面这个固件

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
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,之后根据具体格式解包解压即可,所以固件解包说到底还是数据格式分析。

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])
	...
  • Openwrt Lua

openwrt中的lua脚本和传统的luajit编译后的有点不一样,需要打几个补丁才能正常使用luadec进行反编译,命令如下:

$ 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:

  # 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)

接着执行:

 $ make linux
 $ ldconfig
 $ cd ../luadec
 $ make LUAVER=5.1
 $ sudo cp luadec /usr/local/bin/

利用luadec显示代码结构:

$ 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

1.2 RTOS

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就可以得到基地址,这里显然不适用。

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是函数地址:

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

  • U-boot

  • 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服务会开机运行,可以将其替换:

# mv rootfs/sbin/xxxd sbin/xxxdd
# touch rootfs/sbin/xxxd
# chmod +x rootfs/sbin/xxxd

之后在sbin/xxxd添加

 #!/bin/sh

/usr/sbin/telnetd -F -l /bin/sh -p 1234 &
/sbin/xxxdd &

这样开机启动xxxd时就会先运行telnetd。

  • 交叉编译

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

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

./packet -k %s -f rootfs -b compatible_r6400.txt
		-ok kernel -oall image -or rootfs -i ambitCfg.h
  • firmware-mod-kit

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

# 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 //打包
  • 手动分析

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

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

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

运行binwalk后查看结果,发现没有发现任何可识别的东西,此时可以手动分析或者去搜索一些相关工具。 这里在github上找到,直接根据提示使用命令就可解开固件。

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

需要注意的是,luadec编译与架构相关,用官方luadec无法解析arm环境下的lua文件,但github上也有相应的,这里不再赘述。

首先从应用较广且有套路可循的说起,VxWorks是Wind River System公司推出的一个实时操作系统,广泛应用在通信、军事、航空、航天嵌入式设备领域。因为有标准,所以好识别,以下面这个固件为例:

由于基地址和架构有关,在这里就不详细展开,对于vxworks的分析我们可以借助一款能自动化修复入口和符号的插件--。

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

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

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

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

文件系统
ubi_reader
相关工具
luadec
工具
vxworks
vxhunter
U-boot
GPL
fmk
netgear upnp漏洞的文章
Page cover image