4. 固件文件格式分析:从原始二进制到UBoot镜像

做嵌入式逆向,第一关就是跟各种文件格式打交道。你拿到一个.bin文件,或者.hex、.srec、.elf,甚至是个uImage——它们背后藏着完全不同的组织逻辑。我刚开始接触这行时,就吃过格式不对的亏,烧进去的固件直接变砖。

今天咱们就把这五种格式掰开揉碎,看看它们各自长什么样、怎么用、逆向时要注意什么。

4.1 原始二进制(Raw Binary)

原始二进制,说白了就是内存的“快照”。它没有头部、没有校验、没有元数据——就是纯粹的机器码和数据。

核心特征:

  • 文件内容 = 内存映像,从加载地址开始逐字节排列
  • 无地址信息,你必须知道它该加载到哪个地址
  • 无校验和,损坏了你也看不出来
  • 文件大小 = 固件实际大小(不包含任何额外开销)

我在一个IoT设备上遇到过这种情况:厂家给的固件是.bin,但没告诉我加载地址。我试了0x08000000、0x20000000都不对,最后用逻辑分析仪抓SPI Flash的读命令,才确定它映射在0x60000000。嗯,这种坑踩过一次就记住了。

逆向小技巧: 拿到Raw Binary后,先用binwalk扫一遍,看能不能找到已知的magic number。很多裸机固件会在开头放中断向量表——0x00000000处通常是栈指针,0x00000004处是复位向量。这两个值能帮你反推加载地址。

4.2 Intel HEX格式

Intel HEX是文本格式的固件,每行以冒号开头。你想想看,它为什么要用文本存二进制?因为早期烧录器通过串口传输,文本格式不容易出错。

一个典型的HEX行长这样:

:10000000 00 78563412 78563412 78563412 78563412 8A
:04000003 00008000 7C
:00000001 FF

每行的结构:

字段 长度 说明
起始码 1字节 固定为':'
字节计数 1字节 本行数据区的字节数
地址 2字节 数据加载的起始地址(16位)
记录类型 1字节 00=数据, 01=结束, 02=扩展段地址, 04=扩展线性地址
数据 N字节 实际固件内容
校验和 1字节 前面所有字节(含冒号)的和取补码

这里有个关键点:记录类型04(扩展线性地址)用于32位地址空间。比如上面例子中,:04000003 00008000表示后续数据要加载到0x8000:0000这个高位地址。我见过有人解析HEX时忘了处理扩展地址,结果固件加载到错误位置——调试了一整天才发现。

注意: Intel HEX的地址是16位的,要表示32位地址必须用扩展记录。逆向时如果发现固件地址异常,先检查是不是扩展记录没解析对。

4.3 Motorola S-Record格式

S-Record跟Intel HEX类似,也是文本格式,但结构稍有不同。它用'S'开头,后面跟记录类型。

S00A00006D6F746F726F6C61
S1130000 78563412 78563412 78563412 78563412 8A
S9030000 FC

记录类型对照:

S-Record类型 说明 地址长度
S0 头部(通常包含文件名) 2字节
S1 数据记录(16位地址) 2字节
S2 数据记录(24位地址) 3字节
S3 数据记录(32位地址) 4字节
S5 记录计数 2字节
S7 起始记录(32位地址) 4字节
S8 起始记录(24位地址) 3字节
S9 起始记录(16位地址) 2字节

我个人习惯用S-Record做固件传输,因为它的校验机制比Intel HEX更健壮。但逆向时我更喜欢Raw Binary——省去了解析文本的开销。

4.4 ELF文件格式

ELF是Linux和大多数RTOS用的可执行文件格式。它比Raw Binary复杂得多,但信息也更丰富。

ELF的核心结构:

+-------------------+
| ELF Header        |  ← 文件头,描述文件类型、架构、入口点等
+-------------------+
| Program Headers   |  ← 段表,描述如何加载到内存
+-------------------+
| Section Headers   |  ← 节表,描述链接和调试信息
+-------------------+
| Sections          |  ← 实际数据(代码、数据、符号表等)
+-------------------+

逆向ELF时,我最关注这几个字段:

  • e_entry:入口点地址,程序从这里开始执行
  • e_type:ET_REL(可重定位)、ET_EXEC(可执行)、ET_DYN(共享库)
  • e_machine:目标架构,比如ARM、MIPS、RISC-V
  • PT_LOAD段:需要加载到内存的段,包含虚拟地址和文件偏移

实战经验: 我曾经逆向一个路由器固件,厂家把ELF文件strip了,但忘了删.symtab节。我用readelf -S一看,符号表还在!直接导出函数名,省了三天分析时间。所以拿到ELF先别急着转二进制,看看有没有残留的调试信息。

用readelf查看ELF头:

$ readelf -h firmware.elf
ELF Header:
  Magic:   7f 45 4c 46 01 01 01 00 00 00 00 00 00 00 00 00
  Class:                             ELF32
  Data:                              2's complement, little endian
  Entry point address:               0x8000
  Start of program headers:          52 (bytes into file)
  Start of section headers:          123456 (bytes into file)
  Flags:                             0x5000200, Version5 EABI
  Size of this header:               52 (bytes)
  Size of program headers:           32 (bytes)
  Number of program headers:         3
  Size of section headers:           40 (bytes)
  Number of section headers:         12
  Section header string table index: 11

4.5 UBoot镜像格式

UBoot镜像(uImage)是嵌入式Linux启动时常用的格式。它本质上是在Raw Binary外面包了一层头部,用来描述镜像的类型、加载地址、校验和等信息。

UBoot头部结构(64字节):

typedef struct image_header {
    uint32_t    ih_magic;     // 魔数 0x27051956
    uint32_t    ih_hcrc;      // 头部CRC
    uint32_t    ih_time;      // 时间戳
    uint32_t    ih_size;      // 镜像大小(不含头部)
    uint32_t    ih_load;      // 加载地址
    uint32_t    ih_ep;        // 入口点
    uint32_t    ih_dcrc;      // 数据CRC
    uint8_t     ih_os;        // 操作系统类型
    uint8_t     ih_arch;      // CPU架构
    uint8_t     ih_type;      // 镜像类型
    uint8_t     ih_comp;      // 压缩类型
    uint8_t     ih_name[32];  // 镜像名称
} image_header_t;

我拆过不少UBoot镜像,发现一个常见问题:厂家经常把ih_load和ih_ep设成相同值,但有时候入口点会比加载地址偏移几个字节(比如跳过头部)。如果你直接跳到加载地址执行,可能会跑飞。

逆向UBoot镜像的步骤:

  1. 检查前4字节是否为0x27051956(魔数)
  2. 读取ih_load和ih_ep,确认加载地址和入口点
  3. 读取ih_size,跳过64字节头部,提取真正的固件数据
  4. 检查ih_comp,如果是压缩类型(如gzip),需要先解压
  5. 用ih_dcrc验证数据完整性

4.6 五种格式对比总览

格式 存储方式 地址信息 校验 适用场景 逆向难度
Raw Binary 二进制 无 无 裸机、SPI Flash 低(但需知道加载地址)
Intel HEX 文本 有(16/32位) 单字节校验和 烧录器、调试器 中(需解析扩展地址)
S-Record 文本 有(16/24/32位) 单字节校验和 烧录器、传输 中
ELF 二进制 有(段表+节表) 无(但可含CRC) Linux、RTOS调试 高(但信息最丰富)
UBoot镜像 二进制 有(头部内) 双CRC(头部+数据) 嵌入式Linux启动 中(需解析头部)

4.7 格式转换与逆向实战

实际工作中,你经常需要在不同格式间转换。比如厂家给的是ELF,但烧录工具只认Raw Binary。或者你从Flash里dump出来的是Raw Binary,但想用IDA分析——IDA更喜欢ELF。

常用转换工具:

  • objcopy:ELF ↔ Raw Binary、Intel HEX、S-Record
  • hex2bin / bin2hex:Intel HEX ↔ Raw Binary
  • srec_cat:S-Record处理瑞士军刀
  • mkimage:制作/解包UBoot镜像

举个例子,把ELF转成Raw Binary:

$ arm-none-eabi-objcopy -O binary firmware.elf firmware.bin

把Raw Binary包成UBoot镜像:

$ mkimage -A arm -O linux -T kernel -C none -a 0x8000 -e 0x8000 -n "MyFirmware" -d firmware.bin uImage

避坑指南: 我曾经用objcopy把ELF转成Raw Binary,结果烧进去不启动。查了半天发现,ELF里有多个PT_LOAD段,objcopy默认只转第一个段。加上-j .text -j .data指定节,或者用--gap-fill 0xFF填充空洞,才能得到完整的二进制映像。

4.8 知识结构图:固件格式解析流程

固件格式解析流程 固件格式解析决策流程 拿到固件文件 识别文件类型 Raw Binary Intel HEX S-Record ELF UBoot镜像 需确定加载地址 binwalk扫描magic 解析扩展地址记录 验证校验和 识别S1/S2/S3类型 提取地址和数据 解析段表/节表 提取符号表信息 解析64字节头部 检查压缩/校验 提取纯二进制数据 加载到IDA/Ghidra分析

这张图展示了我平时处理固件的决策流程。拿到文件先识别类型,然后根据格式特点做针对性处理,最终目标都是提取出纯二进制数据,丢进反汇编工具里分析。

记住一句话:格式只是壳,数据才是魂。不管外面包了多少层头部、校验、压缩,最终我们要的是那段能在目标CPU上执行的机器码。