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镜像的步骤:
- 检查前4字节是否为0x27051956(魔数)
- 读取ih_load和ih_ep,确认加载地址和入口点
- 读取ih_size,跳过64字节头部,提取真正的固件数据
- 检查ih_comp,如果是压缩类型(如gzip),需要先解压
- 用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 知识结构图:固件格式解析流程
这张图展示了我平时处理固件的决策流程。拿到文件先识别类型,然后根据格式特点做针对性处理,最终目标都是提取出纯二进制数据,丢进反汇编工具里分析。
记住一句话:格式只是壳,数据才是魂。不管外面包了多少层头部、校验、压缩,最终我们要的是那段能在目标CPU上执行的机器码。