第三章 固件映像结构分析:加载地址、入口点与段解析
拿到一个固件文件,很多人第一反应就是直接扔进反汇编器。嗯,我当年也这么干过——结果出来的代码乱七八糟,根本没法看。后来才明白,不搞清楚固件的内部结构,逆向就是盲人摸象。
这一章,我们就来拆解固件的骨架。我会带你从加载地址定位、入口点查找,到 .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 字段。但对于裸机二进制固件,入口点需要我们自己找。
常见的入口点定位方法:
- ARM Cortex-M:复位向量在地址 0x00000004(如果加载地址是 0x0),内容就是入口点地址。
- ARM 非 Cortex-M:通常从 0x00000000 开始是一条跳转指令,比如
B reset_handler。 - MIPS:入口点一般在 0xBFC00000(启动 ROM 区域),第一条指令是
B或J跳转。 - 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 开头很常见。
- 最后,通过交叉引用分析,确认哪些地址是数据、哪些是代码。
3.4 ELF vs 二进制格式:各有各的玩法
嵌入式固件最常见的两种格式就是 ELF 和纯二进制(raw binary)。它们各有优缺点,适合不同的场景。
| 特性 | ELF 格式 | 二进制格式 |
|---|---|---|
| 文件头 | 有完整的 ELF 头、程序头、节头 | 无头,纯数据 |
| 元信息 | 包含入口点、段地址、符号表、重定位信息 | 无任何元信息 |
| 调试支持 | 支持 DWARF 调试信息,方便源码级调试 | 不支持,需要手动分析 |
| 文件大小 | 较大(包含头信息和可能的填充) | 较小,紧凑 |
| 加载方式 | 需要解析 ELF 加载器,按段加载到指定地址 | 直接烧录到 Flash 的起始地址 |
| 逆向难度 | 较低(有符号表和段信息辅助) | 较高(需要手动重建段和符号) |
在实际项目中,我遇到的情况是:开发阶段用 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 的标志。
接下来,我们看看这个固件的段布局。用 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 知识结构图:固件映像分析全景
下面这张图总结了固件映像分析的核心流程和关键节点,你可以把它当作一个快速参考地图。
3.7 本章小结
固件映像结构分析是逆向工程的基石。说白了,就是回答三个问题:
- 它是什么格式? ELF 还是二进制?这决定了你用什么工具和方法。
- 它从哪里开始? 加载地址和入口点,是反汇编的起点。
- 它里面有什么? 代码、数据、BSS 的布局,决定了你如何分析。
我个人的经验是:花 30% 的时间在结构分析上,能省掉后面 70% 的弯路。很多新手一上来就急着看代码,结果地址不对、段分不清,分析出来的东西全是错的。磨刀不误砍柴工,这个道理在固件逆向里特别适用。