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 知识结构图:固件二进制分析流程

下面这张图总结了固件二进制分析的核心流程,从拿到文件到提取出可分析的部分,每一步都环环相扣。

固件二进制分析核心流程 获取固件文件 file 命令初步识别 binwalk 深度扫描 熵值分析 (binwalk -E) 手动解析头部结构 提取并解包各部分 提示:如果 binwalk 扫描结果为空,优先检查熵值图,判断是否被加密或强压缩

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 是主力,但千万别迷信工具。我个人的经验是,遇到奇怪的情况,就打开十六进制编辑器,肉眼过一遍。有时候,最原始的方法反而最有效。