3、固件逆向分析基础:二进制文件格式解析(ELF/BIN/HEX)、固件加载地址分析、字符串提取与敏感信息发现

好,咱们进入第三章。这一章可以说是固件逆向的「基本功」。你想想看,拿到一个陌生的固件,第一件事是什么?不是急着上反汇编器,而是先搞清楚它是什么格式、从哪里加载、里面藏了哪些信息。

我个人习惯把这一步叫做「固件体检」。就像医生看病先量体温、测血压一样,我们得先摸清这个二进制文件的底细。今天我就带你走一遍这个流程。

3.1 二进制文件格式:ELF、BIN、HEX 的区别

嵌入式世界里,最常见的三种格式就是 ELF、BIN 和 HEX。它们各有各的用途,也各有各的坑。

3.1.1 ELF 格式

ELF(Executable and Linkable Format)是 Linux 和很多 RTOS 下的标准格式。它最大的特点是——结构完整。里面有段表、符号表、重定位信息,甚至调试信息。

我在项目中遇到过一件事:有一次客户给了一个 ELF 文件,说「直接烧录就行」。我习惯性地用 readelf 扫了一眼,发现符号表里居然还留着 debug_password_check 这样的函数名。嗯,这家的安全审计怕是没过关。

ELF 的核心结构包括:

  • ELF 头:文件类型、架构、入口点地址
  • 程序头:描述段如何加载到内存
  • 节头:描述各个节(.text、.data、.rodata 等)
  • 符号表:函数名、变量名(如果没 strip 的话)

关键点:拿到 ELF 文件,第一件事就是检查它有没有被 strip。没 strip 的 ELF 简直就是「源码级」泄露。

3.1.2 BIN 格式

BIN 文件就是纯粹的二进制镜像。没有头、没有段表、没有符号。说白了,它就是内存里要放的原始数据。

为什么嵌入式里常用 BIN?因为很多 MCU 的 Bootloader 不认 ELF,它只认「从地址 X 开始放数据」。BIN 文件就是最直接的「内存快照」。

但 BIN 有个大问题——你不知道它该加载到哪个地址。这就是我们后面要讲的「加载地址分析」的由来。

3.1.3 HEX 格式

HEX(Intel HEX)是一种文本格式的二进制表示。每一行都包含地址、数据类型和校验和。它比 BIN 多了地址信息,但仍然是「线性」的。

我曾经调试过一个 IoT 设备,固件是 HEX 格式。我用 objcopy 转成 BIN 后,发现地址 0x8000000 到 0x8001000 之间有一段全是 0xFF。后来一查,原来是 HEX 文件里跳过了未使用的区域。这个坑让我浪费了半天。

我的建议:分析前先把 HEX 转成 BIN,然后用二进制编辑器查看。HEX 的文本格式不适合直接分析。

3.2 固件加载地址分析

好,现在你手里有一个 BIN 文件。你打开它,看到一堆 0x00 0xFF 和乱码。问题来了:这个固件应该加载到内存的哪个地址?

加载地址错了,反汇编出来的代码全是错的。我见过有人把 ARM 代码当成 Thumb 来反汇编,结果出来一堆「莫名其妙」的指令。嗯,这种事我也干过。

3.2.1 从向量表推断加载地址

对于 ARM Cortex-M 系列的固件,有一个非常可靠的方法——看向量表

Cortex-M 的启动流程是:上电后从地址 0x00000000 读取栈指针,从 0x00000004 读取复位向量。但实际固件可能加载到其他地址,比如 0x08000000(STM32 的 Flash 起始地址)。

怎么找?你打开 BIN 文件,看前 4 个字节。如果它看起来像一个合法的栈指针(通常是 SRAM 地址范围),那这很可能就是向量表。

// 举个例子,一个 STM32 固件的前 8 个字节:
// 0x20001000  -> 栈指针(指向 SRAM)
// 0x08000123  -> 复位向量(指向 Flash)
// 那么加载地址就是 0x08000000

经验法则:向量表的前 4 字节是栈指针,后 4 字节是复位向量。复位向量的地址减去 4,就是加载基址。

3.2.2 从交叉引用推断

如果向量表不明显怎么办?我一般会找字符串引用。

比如你在 BIN 里看到字符串 "UART initialized",然后在附近找代码中引用这个字符串的指令。ARM 的 LDR 指令通常会加载一个 PC 相对偏移的地址。通过计算这个偏移,就能反推出代码段的基址。

说白了,这就是一个「已知点定位」的过程。你找到一个确定地址的符号,就能推算出整个固件的加载地址。

3.2.3 常见加载地址参考

芯片/平台 典型加载地址 备注
STM32F1/F4 0x08000000 Flash 起始地址
ESP32 0x400D0000 IRAM 区域
NXP i.MX 0x80000000 DDR 起始地址
Broadcom BCM 0x00000000 内部 ROM

注意:这只是常见值,不代表所有情况。我遇到过某款国产芯片,加载地址是 0x1FFF0000,查了三天手册才找到。

3.3 字符串提取与敏感信息发现

字符串是固件逆向的「金矿」。很多时候,你不需要反汇编,光看字符串就能猜出固件的大致功能。

3.3.1 使用 strings 命令

最基础的工具就是 strings。用法简单:

strings firmware.bin | head -100
strings firmware.bin | grep -i "password\|key\|secret\|admin"

但要注意,strings 默认只提取长度 >= 4 的可打印字符。有时候关键信息只有 3 个字符,比如 "pwd",就会被漏掉。我习惯加参数:

strings -n 3 firmware.bin

3.3.2 敏感信息分类

我个人把敏感信息分成几类:

  • 硬编码凭据:密码、API Key、Token。我曾经在一个路由器固件里找到 AWS 的 Access Key,直接能访问人家的 S3 桶。
  • 调试接口:UART 输出、调试日志。比如 "[DEBUG] entering AT+CMD mode" 这种字符串,说明固件有 AT 命令接口。
  • 文件路径/etc/config/tmp/update.bin。这些路径能告诉你固件的文件系统结构。
  • 版本信息v2.1.3build 20240315。版本号能帮你找到已知漏洞。
  • 错误消息"Authentication failed""Invalid CRC"。这些能帮你定位关键函数。

避坑指南:我曾经在分析一个 IP Camera 固件时,用 strings 找到了 "root:admin123"。我以为是后门密码,结果发现是出厂默认密码,而且代码里写死了。嗯,这种「假后门」也很常见。

3.3.3 进阶:提取 Unicode 和加密字符串

有些固件会用 Unicode(UTF-16)存储字符串,比如某些日系或韩系芯片。这时候 strings 的默认参数就不够用了:

strings -e l firmware.bin   # 提取 UTF-16LE 字符串
strings -e b firmware.bin   # 提取 UTF-16BE 字符串

还有更狡猾的——加密字符串。有些固件会把敏感字符串做 XOR 或 Base64 编码。比如我看到过这样的代码:

// 伪代码:运行时解码
char key[] = {0x55, 0x4B, 0x3C, 0x7A, 0x19};
for (int i = 0; i < 5; i++) {
    key[i] ^= 0xAA;
}
// 解码后得到 "P@ssw0rd"

遇到这种情况,光靠 strings 就不够了。你需要找到解码函数,或者自己写脚本暴力尝试常见的 XOR Key。

3.3.4 实战:从字符串到功能定位

我举个例子。假设你在固件里找到这些字符串:

  • "AT+CGMI"
  • "AT+CSQ"
  • "+CMGS:"
  • "SMS received from"

看到这些,你基本可以断定:这个固件有 GSM/4G 模块,支持 AT 命令,并且有 SMS 收发功能。接下来你就可以在反汇编器里搜索这些字符串的交叉引用,找到对应的处理函数。

我的习惯:先用 strings 提取所有字符串,然后按功能分类。比如网络相关的、存储相关的、加密相关的。这样能快速建立固件的「功能地图」。

3.4 本章小结

这一章我们讲了三个核心技能:

  1. 格式识别:ELF 有结构、BIN 是裸数据、HEX 带地址。拿到固件先看格式,再决定分析策略。
  2. 加载地址分析:向量表法、交叉引用法。地址错了,后面全白干。
  3. 字符串提取:strings 是基本功,但别忘了 Unicode 和加密字符串。敏感信息往往藏在最不起眼的地方。

下一章我们会进入真正的反汇编分析。但请记住,这一章的基础打不牢,后面你会走很多弯路。我自己就吃过这个亏——当年分析一个 Z-Wave 网关固件,加载地址猜错了,反汇编出来的代码全是垃圾,浪费了整整一周。

嗯,先到这里。你去把今天的内容练一练,找个公开的 IoT 固件试试手。下节课见。