第三章 固件映像结构分析:加载地址、入口点与段解析

拿到一个固件文件,很多人第一反应就是直接扔进反汇编器。嗯,我当年也这么干过——结果出来的代码乱七八糟,根本没法看。后来才明白,不搞清楚固件的内部结构,逆向就是盲人摸象。

这一章,我们就来拆解固件的骨架。我会带你从加载地址定位、入口点查找,到 .text/.data/.bss 段的解析,最后对比 ELF 和纯二进制格式的差异。这些都是我日常逆向中最常用的基本功。

3.1 固件加载地址:为什么它这么重要?

加载地址,说白了就是固件在内存里“安家”的起始位置。CPU 上电后,会从一个固定的地址开始取指令执行——这个地址通常由芯片厂商定死,比如 ARM Cortex-M 系列从 0x00000000 启动,而很多 MIPS 路由器从 0xBF000000 开始。

我遇到过最坑的一次:一个 IoT 设备固件,反汇编出来全是跳转到奇怪地址的指令。折腾了半天,才发现是加载地址设错了。你想想看,地址都错了,反汇编器解析出来的代码能对吗?

核心原则:加载地址必须与芯片的复位向量或链接脚本指定的地址一致,否则反汇编结果毫无意义。

怎么确定加载地址?我一般用这几种方法:

  • 查芯片手册:看复位向量地址和内存映射,这是最靠谱的。
  • 看固件头部:很多固件在文件头里直接写了加载地址,比如 U-Boot 的镜像头。
  • 分析中断向量表:ARM Cortex-M 的前 16 个字是向量表,第一个字是栈指针,第二个字是复位入口。如果固件开头看起来像向量表,加载地址大概率就是 0x00000000。
  • 用 binwalk 或固件分析工具:这些工具能自动识别常见架构的入口点。
我的习惯:拿到一个陌生固件,我会先用 binwalk -e 解包,再用 readelf -h(如果是 ELF)或 objdump -d --start-address=0x0 试几个常见加载地址,看哪个能反汇编出合理的指令序列。

3.2 入口点定位:CPU 从哪里开始跑?

入口点就是 CPU 上电后执行的第一条指令的地址。它和加载地址有关,但不一定相同。

对于 ELF 文件,入口点直接写在文件头里,用 readelf -h 就能看到 Entry point address 字段。但对于裸机二进制固件,入口点需要我们自己找。

常见的入口点定位方法:

  1. ARM Cortex-M:复位向量在地址 0x00000004(如果加载地址是 0x0),内容就是入口点地址。
  2. ARM 非 Cortex-M:通常从 0x00000000 开始是一条跳转指令,比如 B reset_handler。
  3. MIPS:入口点一般在 0xBFC00000(启动 ROM 区域),第一条指令是 B 或 J 跳转。
  4. RISC-V:复位向量可配置,常见的是 0x80000000 或 0x00000000。
注意:有些固件会加壳或加密,入口点指向的是解压/解密代码,而不是真正的用户程序入口。我曾经分析过一个路由器固件,入口点是一段自解压代码,真正的入口藏在解压后的内存里。这种情况需要先模拟执行或静态分析解压逻辑。

3.3 段解析:.text、.data、.bss 的奥秘

一个典型的固件映像,内部会分成几个段(section)。理解这些段,是逆向分析的基础。

段名 内容 属性 在固件中的存在形式
.text 可执行代码、常量(字符串、跳转表) 只读、可执行 直接包含在固件文件中
.data 已初始化的全局变量、静态变量 可读写 直接包含在固件文件中,运行时复制到 RAM
.bss 未初始化的全局变量、静态变量 可读写 不占用固件空间,运行时在 RAM 中清零
.rodata 只读数据(字符串常量、const 变量) 只读 通常合并到 .text 段或独立存在

对于 ELF 格式,这些段的信息非常清晰,用 readelf -S 就能看到每个段的地址、大小、偏移量。但对于二进制固件,我们需要手动识别。

我个人习惯的做法是:

  • 先用 binwalk 或 strings 看看固件里有没有明显的字符串,这些字符串通常属于 .rodata 或 .text 段。
  • 然后根据架构的指令编码,找到代码段的起始位置。比如 ARM Thumb 指令以 0x46 或 0x48 开头很常见。
  • 最后,通过交叉引用分析,确认哪些地址是数据、哪些是代码。
避坑指南:我曾经分析过一个固件,它的 .data 段在固件里是压缩存储的,启动时由一段解压代码解压到 RAM。如果不了解这个机制,直接分析固件里的 .data 段内容,会发现全是乱码。所以,一定要先搞清楚固件的启动流程。

3.4 ELF vs 二进制格式:各有各的玩法

嵌入式固件最常见的两种格式就是 ELF 和纯二进制(raw binary)。它们各有优缺点,适合不同的场景。

特性 ELF 格式 二进制格式
文件头 有完整的 ELF 头、程序头、节头 无头,纯数据
元信息 包含入口点、段地址、符号表、重定位信息 无任何元信息
调试支持 支持 DWARF 调试信息,方便源码级调试 不支持,需要手动分析
文件大小 较大(包含头信息和可能的填充) 较小,紧凑
加载方式 需要解析 ELF 加载器,按段加载到指定地址 直接烧录到 Flash 的起始地址
逆向难度 较低(有符号表和段信息辅助) 较高(需要手动重建段和符号)

在实际项目中,我遇到的情况是:开发阶段用 ELF,发布阶段转成二进制。因为二进制格式更紧凑,而且没有符号表,能增加逆向难度——当然,对我们做安全分析的人来说,这反而增加了挑战。

关键区别:ELF 文件包含了“如何加载”的指令,而二进制文件只有“加载什么”。所以分析二进制固件时,你必须自己推断出加载地址和段布局。

3.5 实战:手动解析一个二进制固件

光说不练假把式。我们拿一个典型的 ARM Cortex-M 固件来实操一下。

假设我们有一个名为 firmware.bin 的文件,大小 64KB。第一步,用 hexdump 查看开头:

$ hexdump -C firmware.bin | head -n 16
00000000  00 00 00 00  00 00 00 00  00 00 00 00  00 00 00 00  |................|
00000010  00 00 00 00  00 00 00 00  00 00 00 00  00 00 00 00  |................|
00000020  00 00 00 00  00 00 00 00  00 00 00 00  00 00 00 00  |................|
00000030  00 00 00 00  00 00 00 00  00 00 00 00  00 00 00 00  |................|
00000040  00 00 00 00  00 00 00 00  00 00 00 00  00 00 00 00  |................|
00000050  00 00 00 00  00 00 00 00  00 00 00 00  00 00 00 00  |................|
00000060  00 00 00 00  00 00 00 00  00 00 00 00  00 00 00 00  |................|
00000070  00 00 00 00  00 00 00 00  00 00 00 00  00 00 00 00  |................|
00000080  00 00 00 00  00 00 00 00  00 00 00 00  00 00 00 00  |................|
00000090  00 00 00 00  00 00 00 00  00 00 00 00  00 00 00 00  |................|
000000a0  00 00 00 00  00 00 00 00  00 00 00 00  00 00 00 00  |................|
000000b0  00 00 00 00  00 00 00 00  00 00 00 00  00 00 00 00  |................|
000000c0  00 00 00 00  00 00 00 00  00 00 00 00  00 00 00 00  |................|
000000d0  00 00 00 00  00 00 00 00  00 00 00 00  00 00 00 00  |................|
000000e0  00 00 00 00  00 00 00 00  00 00 00 00  00 00 00 00  |................|
000000f0  00 00 00 00  00 00 00 00  00 00 00 00  00 00 00 00  |................|

嗯,开头全是 0?这不对劲。对于 Cortex-M,向量表的前两个字应该是栈指针和复位向量,不可能是 0。这说明加载地址可能不是 0x00000000。

我们换个地址试试,比如 0x08000000(常见的 Flash 起始地址):

$ arm-none-eabi-objdump -D -b binary -m arm --start-address=0x08000000 firmware.bin | head -n 30

firmware.bin:     file format binary

Disassembly of section .data:

08000000 <.data>:
 8000000:   20010000    .word   0x20010000    ; 栈指针
 8000004:   08000145    .word   0x08000145    ; 复位向量(入口点)
 8000008:   08000149    .word   0x08000149    ; NMI 处理函数
 800000c:   0800014d    .word   0x0800014d    ; HardFault 处理函数
 ...

看到没?加载地址设为 0x08000000 后,反汇编出来的内容就合理了。第一个字 0x20010000 是栈指针(指向 RAM 区域),第二个字 0x08000145 是复位入口点。注意这个地址的最低有效位是 1,表示 Thumb 模式——这是 Cortex-M 的标志。

小技巧:如果你不确定加载地址,可以试试常见的 Flash 地址:0x08000000(STM32)、0x00000000(很多 Cortex-M 默认)、0x9F000000(某些 NXP 芯片)、0x60000000(外部 Flash)。总有一个能对上。

接下来,我们看看这个固件的段布局。用 objdump 的 -h 选项对二进制文件不适用,但我们可以通过分析代码来推断。比如,找到 .data 段的初始化代码:

 8000200:   ldr r0, =_sdata      ; .data 起始地址(RAM)
 8000204:   ldr r1, =_edata      ; .data 结束地址(RAM)
 8000208:   ldr r2, =_sidata     ; .data 在 Flash 中的起始地址
 800020c:   cmp r0, r1
 800020e:   beq 800021c
 8000210:   ldr r3, [r2], #4
 8000214:   str r3, [r0], #4
 8000218:   b 800020c
 800021c:   ...                  ; 接下来是 .bss 清零循环

这段代码就是典型的 .data 段从 Flash 复制到 RAM 的过程。通过分析这些符号地址,我们就能重建出完整的段布局。

3.6 知识结构图:固件映像分析全景

下面这张图总结了固件映像分析的核心流程和关键节点,你可以把它当作一个快速参考地图。

固件映像分析知识结构图 固件映像分析知识结构图 固件映像文件 格式识别 加载地址定位 入口点定位 ELF 格式 二进制格式 芯片手册 向量表分析 复位向量 / 跳转指令 段解析:.text / .data / .bss / .rodata 输出:可反汇编的代码 + 数据布局 漏洞挖掘 签名绕过 核心思路:先定格式 → 再找加载地址 → 定位入口点 → 解析段布局 → 最后才能正确反汇编 每一步都依赖上一步的结果,顺序不能乱。

3.7 本章小结

固件映像结构分析是逆向工程的基石。说白了,就是回答三个问题:

  • 它是什么格式? ELF 还是二进制?这决定了你用什么工具和方法。
  • 它从哪里开始? 加载地址和入口点,是反汇编的起点。
  • 它里面有什么? 代码、数据、BSS 的布局,决定了你如何分析。

我个人的经验是:花 30% 的时间在结构分析上,能省掉后面 70% 的弯路。很多新手一上来就急着看代码,结果地址不对、段分不清,分析出来的东西全是错的。磨刀不误砍柴工,这个道理在固件逆向里特别适用。

最后提醒一句:有些固件会使用自定义的加载格式,或者在标准格式上做手脚(比如修改 ELF 头里的入口点指向解密代码)。遇到这种情况,别慌,回到最基本的原理——CPU 总是从复位向量开始执行,找到那个向量,一切就都清楚了。