4、二进制文件格式分析:ELF、HEX、BIN、UBoot镜像等格式解析

嵌入式固件逆向工程的第一步,往往不是反汇编,而是先回答一个基础问题:“我拿到的这个文件,到底是什么格式?” 文件格式决定了后续所有分析工具的选择、地址映射的建立、以及数据提取的策略。无论是从Flash芯片中dump出来的原始BIN,还是由编译器生成的ELF,或是烧录器使用的HEX,亦或是Bootloader加载的UBoot镜像,每种格式都承载着不同的元数据与结构约定。本章将系统解析这四种最常见格式的底层结构,并深入探讨段(Section)、节(Segment)、符号表(Symbol Table)与重定位信息(Relocation)在逆向分析中的实际作用。理解这些结构,相当于拿到了固件内部地图的图例,能让你在茫茫字节中快速定位代码、数据与关键逻辑。

固件文件格式解析与逆向映射关系 ELF 含段/节/符号/重定位 HEX (Intel) ASCII文本,记录地址+数据 BIN (Raw) 纯二进制,无元数据 UBoot Image 头部+数据+CRC校验 格式解析与归一化 提取:加载地址、代码段、数据段、入口点、校验信息 静态分析 · 反汇编代码段 · 定位字符串与常量 · 识别函数边界 · 恢复符号表 · 分析重定位表 动态/运行时分析 · 加载地址与链接地址 · 重定位修正 · 全局偏移表(GOT) · 延迟绑定解析 · 内存布局重建 数据提取与修复 · 固件解包/打包 · 文件系统提取 · 签名绕过准备 · 补丁注入 · 固件差分对比 核心:理解格式的最终目的是建立“文件偏移 ↔ 虚拟地址 ↔ 物理加载地址”的映射关系

上图展示了从原始文件到逆向分析产物的整体映射逻辑。在实际操作中,我们首先需要识别文件头部的魔数(Magic Number),例如ELF文件的0x7F 0x45 0x4C 0x46,UBoot镜像的0x27 0x05 0x19 0x56,以及HEX文件的冒号:起始符。识别格式后,才能选择合适的解析工具链(如readelf、objdump、binwalk或自定义Python脚本)。下面我们将逐一拆解每种格式的细节。

4.1 ELF文件格式:段、节与符号的协同

核心原理:链接视图与执行视图

ELF(Executable and Linkable Format)是Linux和大多数RTOS(如VxWorks、QNX)下可执行文件、共享库和内核模块的标准格式。其设计精髓在于区分了链接视图(由Section Header Table描述)和执行视图(由Program Header Table描述)。链接视图面向链接器和调试器,包含详细的节(Section)信息;执行视图面向加载器,包含连续的段(Segment)信息。逆向分析时,我们通常先看Section,因为它提供了更细粒度的划分。

视图类型描述者核心内容逆向分析价值
链接视图Section Header Table.text, .data, .bss, .symtab, .strtab, .rel.text定位代码、全局变量、符号名称、重定位入口
执行视图Program Header TableLOAD段、DYNAMIC段、INTERP段确定加载地址、内存权限(R/W/X)、动态链接依赖

在逆向一个嵌入式ELF固件时,.text节存放可执行指令,.rodata节存放只读常量(如字符串、跳转表),.data节存放已初始化全局变量,.bss节存放未初始化变量(不占文件空间,但占内存)。符号表(.symtab)是逆向工程师的“金矿”,它直接提供了函数名与地址的对应关系。如果固件未剥离符号(strip),那么整个逆向难度将大幅降低。

判断方法与操作流程

使用readelf工具可以快速获取ELF的完整结构信息。推荐按以下顺序执行:

# 1. 查看文件头,确认架构、入口点
readelf -h firmware.elf

# 2. 查看程序头(段),了解加载地址和权限
readelf -l firmware.elf

# 3. 查看节头(节),获取所有节的偏移和大小
readelf -S firmware.elf

# 4. 查看符号表,提取函数名和全局变量名
readelf -s firmware.elf

# 5. 查看重定位表,分析动态链接或加载时修正
readelf -r firmware.elf

对于嵌入式环境,如果目标固件是静态链接的(无INTERP段),那么重定位信息通常较少,但若存在动态加载模块(如Linux内核模块.ko),重定位表则至关重要。

案例拆解:从ELF中恢复固件逻辑

假设我们拿到一个VxWorks的应用程序镜像app.elf。通过readelf -S发现其.text节起始地址为0x00100000,大小为0x2A000。进一步查看.symtab,发现未剥离符号,直接定位到关键函数auth_check位于0x00102340。我们直接反汇编该地址:

objdump -d --start-address=0x00102340 --stop-address=0x00102400 app.elf

通过分析反汇编代码,发现其调用了strcmp函数,且比较的字符串位于.rodata段的0x0012A000处。使用strings -t x命令提取该地址附近的字符串,即可获得硬编码密码。整个过程从格式解析到逻辑还原,仅需数分钟。若符号被剥离,则需要通过特征字节序列(如函数序言push rbp; mov rbp, rsp)或交叉引用(Xrefs)来手动定位。

经验提醒:在分析ELF时,务必区分虚拟地址(VMA)和加载地址(LMA)。对于可执行文件,两者通常一致;但对于Linux内核或引导程序,VMA可能高于LMA(例如VMA在0xC0000000以上,LMA在0x00008000)。使用objdump -h可以同时查看两者的差异。

常见误区与复盘

  • 误区一:认为.text节一定从文件偏移0开始。实际上,ELF文件头(通常52或64字节)和程序头表占据文件起始位置,.text节往往有固定对齐(如0x1000)。
  • 误区二:忽略.bss节。该节在文件中不占空间,但在内存中占用空间。如果从Flash dump中提取固件,.bss中的数据是缺失的,需要根据初始化代码逻辑推断其内容。
  • 误区三:混淆“节”与“段”。在ELF中,一个LOAD段可能包含多个节(如.text和.rodata合并为一个可读可执行的LOAD段)。修改文件时,需要同时考虑段的对齐和权限。

4.2 HEX与BIN格式:烧录与裸数据的处理

核心原理:从文本到字节的映射

Intel HEX是一种ASCII文本格式,每行以冒号开头,包含长度、地址、类型、数据和校验和。它主要用于烧录器,因为它显式指定了每条数据记录的加载地址。而BIN文件是纯二进制数据,没有任何地址信息,必须依赖外部上下文(如Flash基地址)才能正确加载。在逆向中,HEX文件更容易处理,因为地址已内嵌;BIN文件则需要我们通过分析代码中的跳转或数据访问模式来猜测基地址。

特性Intel HEXRaw BIN
文件内容ASCII字符,可读二进制字节,不可读
地址信息每行记录包含16位或32位地址无,需外部指定基地址
典型用途MCU Flash烧录、EEPROM编程固件dump、Bootloader镜像
逆向难度低(地址明确)高(需基地址恢复)
常见工具hexdump, srec_cat, 自定义脚本binwalk, checksec, 010 Editor

操作流程:BIN文件的基地址恢复

当我们从Flash芯片中读取固件得到BIN文件后,第一步是确定其加载地址。常用方法如下:

  1. 向量表分析:对于ARM Cortex-M系列,向量表位于Flash起始地址,第一个32位字是初始SP值,第二个是Reset_Handler地址。如果BIN文件开头是0x20001000之类的值,说明基地址可能为0x08000000(STM32)或0x00000000(内部Flash)。
  2. 字符串交叉引用:使用binwalk或strings查找可读字符串,然后在BIN文件中搜索该字符串的引用地址。例如,找到字符串"Login: "位于文件偏移0x1A2F0,而在代码中搜索到其地址0x0801A2F0,则基地址为0x08000000。
  3. 反汇编试探:使用arm-none-eabi-objdump -b binary -m arm指定架构,尝试不同的基地址(如0x00000000、0x08000000、0x80000000),观察反汇编结果是否合理(如跳转指令的目标是否在代码段内)。

案例拆解:从BIN中提取并定位关键代码

假设我们有一个从IOT设备Flash中dump的firmware.bin,大小512KB。使用binwalk firmware.bin扫描,发现偏移0x100处有LZMA压缩数据。进一步分析,发现文件头前4字节为0x2000A000,这符合Cortex-M3的初始SP值。因此推断基地址为0x08000000。使用objdump -D -b binary -m arm -M force-thumb --adjust-vma=0x08000000 firmware.bin进行反汇编。在反汇编代码中搜索对0x08000000区域的访问,定位到固件入口。通过分析入口处的跳转表,找到OTA升级函数,进而提取出加密密钥。

风险提示:对于BIN文件,如果基地址判断错误,反汇编出的代码将毫无意义,且可能误导后续分析。建议在确定基地址前,先使用binwalk检查是否存在已知文件头(如UBoot的0x27051956),或使用strings查找固件版本信息,这些都能辅助确认基地址。

常见误区与复盘

  • 误区一:认为HEX文件可以直接用IDA加载。IDA支持HEX,但需要手动指定处理器类型和RAM/ROM起始地址。更推荐先将HEX转换为BIN(使用objcopy -I ihex -O binary),再加载BIN。
  • 误区二:忽略HEX文件中的扩展段地址记录(类型02)和扩展线性地址记录(类型04)。这些记录用于指定高16位地址,如果解析错误,会导致数据加载到错误位置。
  • 误区三:在分析BIN时,未考虑字节序(Endianness)。ARM默认小端,但PowerPC或某些网络处理器是大端。错误字节序会导致所有16位/32位数据解释错误。

4.3 UBoot镜像格式:头部信息与校验机制

核心原理:自包含的引导头

UBoot(Universal Boot Loader)镜像格式(Legacy U-Boot Image)是嵌入式Linux系统中常见的固件封装格式。它通过在原始数据前添加一个64字节的头部(image_header_t),实现了自描述功能。头部包含魔数、头部CRC、数据CRC、时间戳、加载地址、入口地址、镜像类型(内核、RAMDisk、Firmware)、压缩类型和操作系统类型。逆向分析UBoot镜像时,头部信息直接告诉我们数据应该加载到哪个地址,以及入口点在哪里。

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;

判断方法与操作流程

使用binwalk可以自动识别UBoot镜像并提取头部信息。手动分析时,可以使用hexdump查看前64字节:

hexdump -C -n 64 uImage
# 输出示例:
# 00000000  27 05 19 56 8a 3c 2f 5a  5f 3c 2f 5a 00 00 2a 00  |'..V.

解析流程:

  1. 验证魔数0x27051956。
  2. 读取ih_load(偏移16)和ih_ep(偏移20),确认加载地址和入口。
  3. 读取ih_size(偏移12),确认数据长度。
  4. 读取ih_comp(偏移27),确认压缩算法(0=无,1=gzip,2=bzip2,3=lzma)。
  5. 使用dd跳过64字节头部,提取原始数据,并根据压缩类型解压。

案例拆解:解包并分析一个UBoot内核镜像

我们拿到一个uImage文件,大小约3MB。使用binwalk uImage输出显示:

DECIMAL       HEXADECIMAL     DESCRIPTION
--------------------------------------------------------------------------------
0             0x0             U-Boot uImage header, header size: 64 bytes, header CRC: 0x8A3C2F5A, created: 2023-10-01 12:00:00, image size: 3145728 bytes, Data Address: 0x80008000, Entry Point: 0x80008000, data CRC: 0x5F3C2F5A, OS: Linux, CPU: ARM, image type: OS Kernel Image, compression type: gzip

根据信息,加载地址为0x80008000,压缩类型为gzip。执行以下命令解包:

dd if=uImage of=kernel.gz bs=64 skip=1
gunzip kernel.gz
# 得到解压后的内核镜像 kernel

此时kernel文件是原始的Linux内核镜像(可能是ELF或BIN)。使用file kernel确认格式,若为ELF,则可用readelf继续分析;若为BIN,则需要通过字符串或反汇编确定基地址。通过这种方式,我们成功将UBoot封装层剥离,进入内核分析阶段。

实战要点:UBoot镜像的CRC校验是逆向修改的障碍。如果修改了数据部分,必须重新计算ih_dcrc(数据CRC)和ih_hcrc(头部CRC)。否则,UBoot在引导时会报错“Bad Data CRC”。使用Python的zlib.crc32可以快速计算。

常见误区与复盘

  • 误区一:忽略头部中的ih_arch和ih_os字段。这些字段决定了后续使用的反汇编架构和系统调用约定。
  • 误区二:未检查ih_comp字段,直接对数据部分进行反汇编,导致看到的是压缩后的乱码。
  • 误区三:在修改UBoot镜像后,未更新CRC,导致目标设备无法启动。务必在修改后重新计算并填充两个CRC字段。

4.4 段、节、符号表与重定位信息的综合应用

核心原理:四者如何协同支撑逆向

在逆向工程中,段(Segment)决定了内存布局和权限,节(Section)提供了逻辑划分,符号表(Symbol Table)提供了命名映射,重定位信息(Relocation)则揭示了代码中的地址修正点。四者结合,可以让我们从宏观布局到微观指令全面掌控固件。

信息类型来源逆向分析中的具体用途
段(Segment)Program Header确定哪些区域可执行(X)、可写(W),用于设置调试器内存断点
节(Section)Section Header定位字符串所在.rodata,定位全局变量所在.data,定位未初始化数据所在.bss
符号表.symtab / .dynsym直接获取函数名、全局变量名、源文件名,极大加速逆向
重定位信息.rel.text / .rela.text分析动态链接库调用、全局偏移表(GOT)填充、函数指针初始化

操作流程:利用重定位信息定位关键函数

在分析一个动态链接的嵌入式Linux固件时,重定位表是理解模块间调用的关键。操作步骤如下:

  1. 使用readelf -r查看重定位表,关注类型为R_ARM_JUMP_SLOT或R_AARCH64_JUMP_SLOT的条目。
  2. 这些条目对应.got.plt(全局偏移表)中的位置,记录了外部函数的实际地址。
  3. 在IDA或Ghidra中,查看.got.plt段的交叉引用,可以找到所有调用外部函数(如system、strcpy)的代码位置。
  4. 通过分析这些调用点,可以快速定位危险函数的使用,从而找到漏洞入口。

案例拆解:通过符号表与重定位还原加密算法

假设我们分析一个路由器固件,发现其包含一个动态库libcrypto.so。通过readelf -s libcrypto.so,我们找到了AES_set_encrypt_key和AES_encrypt的符号。进一步查看主程序的重定位表,发现其引用了这两个函数。在IDA中加载主程序,跳转到.got.plt中对应条目,查看交叉引用,定位到固件中调用AES加密的代码块。通过分析该代码块的参数传递,我们提取出了硬编码的AES密钥。整个过程完全依赖于对符号表和重定位信息的正确解读。

风险提示:在分析重定位表时,注意区分“链接时重定位”和“加载时重定位”。对于静态链接的固件,重定位表可能已被清零或移除;对于动态链接的固件,重定位表是运行时解析的,静态分析时看到的是GOT中的初始值(通常指向PLT桩代码)。

常见误区与总结复盘

  • 误区一:认为符号表总是存在。许多生产固件会使用strip命令移除符号表,此时只能依赖FLIRT签名或手动分析。
  • 误区二:忽略节的对齐属性。某些节(如.data)可能要求4KB对齐,如果修改了节的大小,需要重新调整对齐,否则加载器会报错。
  • 误区三:在分析重定位时,未考虑重定位类型。不同架构(ARM、x86、MIPS)的重定位类型含义不同,错误解释会导致地址计算错误。

总结而言,二进制文件格式分析是固件逆向的基石。掌握ELF的段/节/符号/重定位,能够处理HEX的地址显式性,理解BIN的基地址恢复方法,以及解析UBoot的头部封装,这四项技能构成了逆向工程师处理嵌入式固件的核心能力。在实际项目中,建议先使用自动化工具(如binwalk、readelf)快速获取全貌,再针对关键区域进行手动深度分析,这样才能在保证效率的同时,不遗漏关键细节。