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.3、build 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 本章小结
这一章我们讲了三个核心技能:
- 格式识别:ELF 有结构、BIN 是裸数据、HEX 带地址。拿到固件先看格式,再决定分析策略。
- 加载地址分析:向量表法、交叉引用法。地址错了,后面全白干。
- 字符串提取:strings 是基本功,但别忘了 Unicode 和加密字符串。敏感信息往往藏在最不起眼的地方。
下一章我们会进入真正的反汇编分析。但请记住,这一章的基础打不牢,后面你会走很多弯路。我自己就吃过这个亏——当年分析一个 Z-Wave 网关固件,加载地址猜错了,反汇编出来的代码全是垃圾,浪费了整整一周。
嗯,先到这里。你去把今天的内容练一练,找个公开的 IoT 固件试试手。下节课见。