因文章篇幅较长,故本文分为上、下两篇。
本文为 上篇。
当你手上有一个固件? 或许是从芯片上提取出来的亦或是从网上下载的更新文件。 接下来你会怎么做呢?
获取到固件,然后提取出其中的有效信息,这可不是一个简单轻而易举的过程。有时,你不得不面对一些专有的文件格式(几乎没有任何参考文档),奇怪的原始数据 quirks,甚至是加密的数据。
接下来,就让我们来了解了解从固件中获取有效信息的一些策略。
充分了解当前正在处理的文件的使用场景(环境),对后续的分析是很有帮助的。你需要搞清楚支持该固件运行的芯片型号,芯片架构,大端模式还是小端模式,固件上运行的系统是 RTOS 还是 Linux ? 或者是纯裸机?等等问题。
Context will help 。。。
So,文件里面到底是 ASCII 字符串还是二进制数据呢? 用 head
,cat
,hexdump
这些命令亦或是你最喜欢的 GUI 文本编辑器去查看这些文件的类型。
如果你的目标设备是运行在裸机上的,那么你可能会看到这些固件文件分散分布在众多的十六进制文本中(也经常前缀有区分码或者偏移地址/绝对地址,同时有可能在每一行后缀有校验和)。在你对这些文件进行进一步分析前,首先需要将他们整理成二进制文件格式。接下来,我们来学习了解一些常见的二进制文件格式。
通常,该文件格式也称为 SREC
。所有 S-record 文件,每行都是大写字母S
开头。关于该二进制格式的详细信息,参考[链接]。
类似 SREC , [ Intel HEX ] 每行都是以一个冒号(:
)开头。
TI-TXT 是德州仪器(Texas Instruments)公司开发的格式,常用于 MSP430 系列单片机。该格式以符号@
作为地址前缀,而数据是以十六进制存储的,整个文件的看起来如下:
所有 Motorola S-record,Intel HEX 和 TI-TXT 文件都可以用[ bincopy python library ]转换成二进制文件。
咋一看,数据是以一种奇怪的方式存储在 NAND 芯片上的。可是,当你想在对芯片上的数据进行预处理时,你会称赞该设计的智慧和实用。
从上图可以看到,Out-of-band(OOB)“空闲”区块被插入到每页数据的末端,或者是每块数据块的末端。这些空白的存储区块被主控用于跟踪坏块,擦除计数等等。所以,当你整块芯片地 dump 原始数据的时候,这些“空白的”区块也会 dump 下来。
这意味着你需要将所有这些“空闲”区块剔除掉才能从芯片的 dump 中获得连续的真实文件。这样,你就可以开始用下述的策略对这些二进制文件进行深入地分析了。
一旦你得到了二进制格式的固件,你就可以尝试从中分析出一些有趣的信息。
再次提醒,多结合固件的使用场景对进一步分析是十分有益的。如果你了解到该固件是用在某个具体型号的 MCU 上,那么你可能会查找对应的 datasheet 然后直接将固件载入 IDA 进行分析。 如果你知道该固件是用在裸机芯片上的,但是你没有该芯片的 datasheet,那么你可能会做一些字节级别的分析工作。如果该固件是用在一个更加复杂的系统,例如像 Linux 这样的操作系统,那么你很可能需要将文件提取出来。
无论如何,下面的这些工具还是很好用的。
strings
是提取文件初始底层数据的强有力工具。它返回的是一列由null
字符结尾的可打印字符串。strings
命令远比大多数人了解到的强悍。一条基本的命令strings file.bin
将返回 ASCII/ISO 4 字符串。稍微复杂一点的是:
默认的字符串最小长度为 4 。-n
选项用于申明返回的字符串的具体长度。上面例子将输出长度不短于 16 的字符串到 stdout
。
strings
的 -e
选项明确了字符编码。-el
指定字符编码为小端模式,16 位宽。如果二进制文件是大端编码的,那么用 -eb
。16-bit 位宽编码常见于嵌入式视窗系统的固件中。
-t
选项用于返回文件中字符串的偏移。-tx
则返回十六进制格式,-to
则返回八进制(octal),-td
则返回十进制(decimal)。这将非常有用当你在 十六进制编辑器中跨越引用时,或者你只是简单地像知道字符串在文件中的位置。
配合 -n
和 -t
选项,strings
命令的输出效果类似于下图:
字符串的偏移,是以十六进制格式显示在每行的行首。
用 hexdump
对二进制文件进行分析,该命令将会向 stdout
输出该文件中每一字节的十六进制形式。所以,该命令其实就是一个 hex(十六进制)
dump 工具。
据我所知(了解),大多数人常用 -C
选项,该选项的结果是:以单字节方式返回文件数据,同时,添加一行显示可打印字符(若为不可打印字符,则代之以 stop 字符)。因此,借助 hexdump
可以很方便地获取二进制文件中的 字符串以及对该文件的总体轮廓。
hexdump
还通过 *
来指代重复行。但是,假如你希望清楚地看到没一行输出的内容,那么可以通过 -v
选项来取消这一项功能。
-n
选项用来限制返回的字节数目。在下述例子中,hexdump
的-C
选项返回 file.bin 文件 0x200 字节的数据。
用 file
命令检测提取出的固件(包括 binwalk
、 dd
提取出的任何文件)往往能收到不错的效果。 file
是通过检测文件头部(即所给定文件的前面几个字节)的 magic bytes 工作的。
无法界定的文件类型会被标记为“data(数据)”。任何可区别的文件都会被标记为 file
认定的文件格式,同时, file
也会给出相关元数据。下面的示例是 file
对一幅 JPEG 图片的解析:
提取出来的固件有时候是包含大量各种类型文件的不规则文件块。亦或是加密的以完全随机的字节序列开始,这可能使得它的 magic bytes 与合法文件格式相符或不符。在这种情况下,我们得到的结果将会是个错误的方向。
PDP-11 UNIX/RT ldp
是什么文件格式? 我真的不清楚,不过可以肯定的是,这不是固件。这个结果反映的只是该文件的前几个字节与 PDP-11 UNIX/RT ldp
文件相符。
在文件夹目录下运行 file *
是一个快速而高效的做法。举例来说,在 binwalk
输出目录下运行该命令可以快速看到我们将要处理的文件类型。例如, binwalk
可能找到(并成功提取出)了一个 JFFS2 文件系统,以及其他一些东西。运行该命令, binwalk
输出目录的内容可能如下:
binwalk
是一款可靠且很受欢迎的针对运行有操作系统的设备的固件分析工具。 关于这方面,网上的讨论不计其数。 不过,更为重要的是,并不要仅仅认为 binwalk
只是彻头彻脑的一个固件分析工具而已;这只不过是它极其有用而又简单的一方面功能。
从一个更高层次来说,在默认情况下 binwalk
循环迭代地搜索整个二进制文件,检索 magic bytes。如果找到,就把它以表格的形式通过 stdout
打印出来。
binwalk
可以分块的形式提取出被分析的二进制文件数据,这样的话,就可以独立单个地研究这些数据块了。 - e
选项声明将数据提取出来,而非输出到 stdout
。提取出来的文件存放于 _filename.extracted
( 或_filename-[int].extracted
,如果前者已存在)。
由于 binwalk
的本质特征,你很有可能会进入错误的方向。分析的文件大小越大,越有可能出错。这是因为,在偶然情况下,一个普通的文件也可能会有 binwalk
能解析的 magic bytes 。在这种情况下,就意味着 binwalk
报告的结果是无效的。
所以,当你在用 binwalk
分析文件时,你应该大概清楚它可能的结果。举例来说,如果你分析的设备运行的是嵌入式 Linux 系统,那么它的 ROM 文件系统不外乎 squashfs,cramfs 或 jffs2 这些。zImage 或 uImage 也是有可能的。另外,也可能是 bootloader 镜像。
下面是运行 binwalk
分析一个大的加密固件的例子。因为是加密文件,这种简单的方式本来是不可能分析出有价值的信息来的,但 binwalk
却返回了一些可能的结果。
很显然,这是错误的。返回这个结果,纯粹是加密文件的某些字节序列与这些文件类型的 magic bytes 碰巧相符而已。另外,这些“文件”的偏移地址随意分布,使得他们看起来更不像是合法的文件类型。
binwalk
正常的、有意义的输出结果大概是这样的:
从结果可以看到,该文件包含一个 uImage(大小、名称与 entry point 基本一致,) 和 JFFS2 文件系统。因为内核通常是经过 gzip 压缩的,所以紧接着内核头之后的 gzip magic bytes 也说得通。内核和文件系统两者都是启动嵌入式 linux 系统必须的。更加巧妙的是,这两者的偏移地址( 0x200000
和 0x800000
)都十分整齐。尽管在实际情况中,这基本不可能,但是,在这里也只是为了强调效果。
有时,碰到的设备是相当高端的产品,而且它的固件文件很大 -- 往往是整个的驱动镜像。这通常是以为该设备具有丰富的资源,处于向桌面平台发展的过程中。试试 fdisk -l
或 fdisk -lu
( -u
选项指定以 segment(段) 标记分区大小,而不是默认的 “cylinders(柱面)”)。
这是一个不含有效磁盘镜像的文件的 fdisk
输出:
而包含有效文件系统的输出则与下面相似:
从这里可以看到,一个 FAT16 镜像位于 3287936629
到 3304577640
之间。镜像大小是 8 GB。
利用这些数据,我们可以用 dd
(稍后会详细讲解)从固件把该文件系统提取出来。一旦将文件系统提取出来了,就可以用合适 mount
命令将它恰当地挂载到系统上。挂载文件系统是另一门额外的高深学问 ,另写一整篇文章来讨论它也不为过。不过,根据 fdisk
判定的文件系统类型,结合网络,搜索到合适的挂载命令/工具,成功挂载文件系统也不是很难的事情。
另外,特别说明一下。有些情况下,在一个非原生系统上你需要加载内核模块(QNX 系统尤其要注意)。
如果得到的文件包含多种文件系统,想要把这些文件一次性提取出来,可以用下面一行命令:
说到 dd
命令,很多人可能害怕它。的确,使用不当的话确实有可能把磁盘废掉。不过,这也仅仅是当你把 of
设置成错误的路径了(译者注: dd
一般的用法是提取某处的文件,输出到另外一个位置。 of
是指 output file,输出路径,如果把 of =/boot
就会将提取的文件输出到 /boot
下,从而覆盖系统原来的启动文件,导致无法正常启动)。使用中绝对要小心。
即便如此, dd
依然是一个极其简单高效的字节级复制工具。尤其是在支持“dollar bracket bracket”语法的 Shell 中。这在将十六进制转化成十进制以及在处理不同文件块大小等基本算术运算中十分有用。
dd
的几个必须知道的关键参数有:
if = [ FILE ]
dd
的输入文件从这里读取。
of = [ FILE ]
dd
的文件输出位置。
bs = [ NUMBER ]
块大小。 dd
所有的数值参数都将以该大小的倍数计算。默认的块大小为 512 。如果你不介意运行的缓慢,烦人的数学运算的话,设置成 bs=1
也是可以。
skip = [ NUMBER ]
读取输入文件前跳过的块。
count = [ NUMBER ]
从输入文件拷贝到输出的总的块数目。
所以,当我们想从 firmware.bin 中提取 0x200
到 0x400
这一块时:
记住, dd
是对按块对文件操作的。因此,在计算具体的块大小,块数目时,需要特别精确。如果你想确保安全,可以设置块大小 bs = 1
。
当事情更进一步时,为了更具可视化,你可能需要一款 GUI hex 文本编辑器来帮助固件分析。到目前为止,我还没有找到一款完全符合我需求的编辑器。我理想中的 hex 文本编辑器应该时这样的:能够可视化二进制文件,辨别出常见文件中的有效字节,可视化信息熵,包括字节码。支持文本标记和高亮,列出字符串等等。不过,不管怎样,适合自己的就是最好的。
日复一日,我倾向于用 HxD 做基本的单调性工作,wxHexEditor 来做文本标记(note-taking)和高亮显示任务。例如:
[注意]传递专业知识、拓宽行业人脉——看雪讲师团队等你加入!
最后于 2019-10-1 22:50
被StrokMitream编辑
,原因: 2019-10-01,更新图床。