4. 固件二进制分析:文件类型识别与结构剖析
拿到一个陌生的固件文件,第一件事是什么?别急着反汇编。我个人的习惯是,先把它当成一个黑盒子,用最基础的工具探一探它的底细。说白了,就是搞清楚这到底是个什么东西——是裸机程序、带文件系统的镜像,还是某种打包后的加密数据?
这一章,我们就来聊聊固件二进制分析的几个核心步骤:文件类型识别、结构分析,以及熵值分析。这些是后续所有逆向工作的基础,地基没打牢,后面很容易翻车。
4.1 文件类型识别:从 file 到 binwalk
识别文件类型,最直接的工具就是 file 命令。它通过检查文件头部的魔数(Magic Number)来判断类型。比如,一个 ELF 文件开头是 7f 45 4c 46,一个 U-Boot 镜像可能以 27 05 19 56 开头。
核心思路: 先用 file 快速扫描,再用 binwalk 深度挖掘。两者配合,基本能覆盖 90% 的常见场景。
4.1.1 使用 file 命令
举个例子,我手头有一个从路由器里提取的固件,叫 fw.bin。直接跑一下:
$ file fw.bin
fw.bin: data
嗯,只返回了 data,说明 file 没认出来。这很常见,很多嵌入式固件是裸二进制,没有标准文件头。这时候就需要 binwalk 上场了。
4.1.2 使用 binwalk 进行深度扫描
binwalk 是我最常用的固件分析工具之一。它不仅能识别文件头,还能扫描文件系统中的常见签名(比如 SquashFS、JFFS2、LZMA 压缩流等)。
$ binwalk fw.bin
DECIMAL HEXADECIMAL DESCRIPTION
--------------------------------------------------------------------------------
0 0x0 TRX firmware header, little endian, image size: 4194304, header size: 28, flags: 0x0, version: 1
28 0x1C LZMA compressed data, properties: 0x5D, dictionary size: 65536 bytes, uncompressed size: 4190208 bytes
1056768 0x102000 Squashfs filesystem, little endian, version 4.0, compression:lzma, size: 3137536 bytes, inodes: 1024, blocksize: 131072 bytes, created: Tue Jan 10 10:30:00 2023
看到了吗?binwalk 直接解析出了固件的结构:
- 偏移 0x0:TRX 头部(一种常见的路由器固件格式)
- 偏移 0x1C:LZMA 压缩数据
- 偏移 0x102000:SquashFS 文件系统
这个信息非常关键。它告诉我们,这个固件是由一个引导头、一段压缩的内核,以及一个只读的文件系统组成的。
小技巧: 如果 binwalk 扫描结果不理想,可以试试 -E 参数显示熵值,或者 -A 参数进行 CPU 指令分析。有时候固件被加密了,binwalk 就扫不出东西,但熵值会暴露一切。
4.2 固件结构分析:从整体到局部
知道了文件类型,下一步就是分析它的内部结构。嵌入式固件通常不是单一的文件,而是由多个部分拼接而成。我习惯把它想象成一个三明治:
- 引导加载程序(Bootloader):比如 U-Boot、RedBoot,负责初始化硬件并加载内核。
- 内核(Kernel):通常是压缩过的 Linux 内核镜像(zImage 或 uImage)。
- 文件系统(Filesystem):存放应用程序、配置和库文件,常见的有 SquashFS、JFFS2、YAFFS2 等。
当然,有些简单的 MCU 固件可能只有一个裸机程序,没有文件系统。但不管怎样,结构分析的目标就是把这些部分拆开,搞清楚它们的分界线在哪里。
4.2.1 手动解析固件头部
以刚才的 TRX 头部为例,我们手动看看它的结构。TRX 头部通常只有 28 字节:
struct trx_header {
uint32_t magic; // 魔数,固定为 "HDR0"
uint32_t len; // 整个固件的大小
uint32_t crc32; // CRC32 校验值
uint32_t flags; // 标志位
uint32_t version; // 版本号
uint32_t partitions[3]; // 三个分区的偏移
};
用 hexdump 或者 xxd 看一下:
$ xxd -l 28 fw.bin
00000000: 4844 5230 0040 0000 0000 0000 0000 0000 HDR0.@..........
00000010: 0100 0000 0010 0000 0000 0100 0000 0200 ................
嗯,魔数 4844 5230 就是 "HDR0",确认是 TRX 格式。然后根据结构体定义,我们可以解析出各个分区的偏移量。
注意: 不同厂商的固件头部格式千差万别。有些会加入自定义的校验算法,有些会使用加密。我曾经遇到过一个大厂的固件,头部里塞了一个 RSA 签名,导致 binwalk 完全识别不出来,最后只能靠手动逐字节分析才搞定。
4.2.2 提取固件中的各个部分
知道了偏移量,提取就很简单了。用 dd 命令就能搞定:
# 提取 SquashFS 文件系统
$ dd if=fw.bin of=squashfs.bin bs=1 skip=1056768 count=3137536
提取出来后,可以用 unsquashfs 解压查看里面的内容:
$ unsquashfs squashfs.bin
$ ls squashfs-root/
bin dev etc lib mnt proc sbin tmp usr var
看到熟悉的 Linux 目录结构,心里就有底了。接下来就可以分析里面的二进制文件、配置文件,甚至寻找硬编码的密码或密钥。
4.3 熵值分析:识别加密与压缩
熵值分析,说白了就是衡量数据的“混乱程度”。纯文本的熵值很低,而加密或压缩后的数据,熵值会非常接近 8(最大值)。
为什么要做熵值分析?因为很多固件会加密敏感部分,比如文件系统或配置分区。如果 binwalk 扫不出东西,但熵值图显示某一段的熵值很高且均匀,那基本可以断定是加密数据。
4.3.1 使用 binwalk 的熵值功能
binwalk 自带了熵值分析功能,用 -E 参数就能生成熵值图:
$ binwalk -E fw.bin
它会输出一个文本形式的熵值图,或者用 -J 参数生成 JSON 格式的数据,方便后续处理。
4.3.2 手动计算熵值
有时候需要更精细的控制,我会用 Python 自己写个小脚本:
import math
import sys
def calculate_entropy(data):
if not data:
return 0
entropy = 0
for x in range(256):
p_x = data.count(x) / len(data)
if p_x > 0:
entropy += - p_x * math.log2(p_x)
return entropy
with open(sys.argv[1], 'rb') as f:
data = f.read()
# 按 1KB 的窗口计算熵值
window_size = 1024
for i in range(0, len(data), window_size):
chunk = data[i:i+window_size]
entropy = calculate_entropy(chunk)
print(f"Offset 0x{i:08x}: Entropy = {entropy:.2f}")
这个脚本会按 1KB 的窗口滑动计算熵值。如果某一段的熵值持续在 7.5 以上,那基本就是加密或压缩数据了。
避坑指南: 我曾经分析过一个 IoT 设备的固件,熵值图显示整个固件的熵值都很高,但 binwalk 什么都没扫出来。一开始我以为整个固件都被加密了,后来仔细一看,原来是厂商把整个固件用 LZMA 压缩了一遍,然后在头部加了一个自定义的解压 stub。所以,高熵值不一定代表加密,也可能是强压缩。区分两者的方法,就是看有没有明显的压缩签名(比如 LZMA 的 5D 属性字节)。
4.4 知识结构图:固件二进制分析流程
下面这张图总结了固件二进制分析的核心流程,从拿到文件到提取出可分析的部分,每一步都环环相扣。
4.5 常见固件格式对比
不同的嵌入式平台,固件格式差异很大。下面这个表格总结了我遇到过的几种常见格式,以及它们的特点:
| 格式名称 | 典型魔数 | 常见平台 | 特点 |
|---|---|---|---|
| TRX | 4844 5230 (HDR0) |
Broadcom 路由器 | 结构简单,包含分区偏移表 |
| U-Boot (uImage) | 2705 1956 |
ARM/MIPS 平台 | 包含内核加载地址和入口点 |
| Intel HEX | 3A (':') |
MCU 固件 | 文本格式,按行记录地址和数据 |
| SREC (Motorola S-record) | 53 ('S') |
MCU 固件 | 类似 Intel HEX,但格式不同 |
| ELF | 7F 45 4C 46 |
Linux 可执行文件 | 包含符号表和重定位信息 |
注意: 魔数不是万能的。有些厂商会魔改标准格式,甚至自定义魔数。比如我见过一个厂商把 TRX 的魔数改成了自己的公司名缩写,导致 binwalk 识别不出来。这时候,就需要结合熵值分析和手动解析来确认了。
4.6 本章小结
固件二进制分析,说白了就是三步走:先识别类型,再分析结构,最后用熵值辅助判断。工具方面,file 和 binwalk 是主力,但千万别迷信工具。我个人的经验是,遇到奇怪的情况,就打开十六进制编辑器,肉眼过一遍。有时候,最原始的方法反而最有效。