第二章 嵌入式系统基础
做固件逆向,说白了就是跟硬件打交道。你光会反汇编、会调试,不懂硬件怎么跑起来的,那很多漏洞你根本挖不到。我刚开始入行时也踩过这个坑——拿到一个固件,上来就IDA Pro一顿分析,结果连内存映射都搞不清楚,分析到一半就卡住了。
这一章,咱们把嵌入式系统的基础打牢。我会结合自己这些年做逆向的经验,把处理器架构、内存、外设、启动流程这些核心知识点串起来。
2.1 嵌入式处理器架构
嵌入式世界里,ARM、MIPS、RISC-V 这三家占了绝大多数份额。我个人的习惯是:拿到固件第一件事,先看它是哪种架构。
2.1.1 ARM 架构
ARM 是嵌入式领域的霸主。从手机、路由器到工控设备,到处都是它的身影。ARM 有个特点——它是 RISC(精简指令集)架构,但指令长度不固定。ARM 模式下每条指令 32 位,Thumb 模式下是 16 位,Thumb-2 则是混合的。
为什么会这样?说白了就是为了平衡性能和代码密度。我在逆向一个 IoT 设备时,发现固件里大量使用了 Thumb 指令,因为 Flash 空间有限,能省一点是一点。
ARM 逆向要点:
- PC 寄存器(R15)在 ARM 模式下指向当前指令+8,Thumb 模式下指向当前指令+4
- LR(R14)保存返回地址,这个在分析函数调用时特别重要
- SP(R13)是栈指针,很多缓冲区溢出漏洞都跟它有关
2.1.2 MIPS 架构
MIPS 曾经是网络设备的主流。像老款的路由器、交换机,很多都用 MIPS 处理器。MIPS 的指令格式非常规整,每条指令都是 32 位,没有 Thumb 这种变长模式。
嗯,这里要注意:MIPS 有个坑——延迟槽。分支指令后面的那条指令,无论分支是否跳转,都会被执行。我刚开始逆向 MIPS 固件时,就因为没注意延迟槽,反汇编出来的控制流图全是乱的。
避坑指南:我曾经在分析一个 MIPS 路由器固件时,发现一个函数调用看起来永远无法返回。后来才发现,是延迟槽里的指令把返回地址改了。所以分析 MIPS 时,一定要把延迟槽指令和分支指令当成一个整体来看。
2.1.3 RISC-V 架构
RISC-V 是后起之秀,开源、简洁、可扩展。这几年在 IoT 和 AI 芯片里越来越常见。它的指令集分 RV32I、RV64I 等基础集,还有 M(乘除)、F(浮点)、C(压缩)等扩展。
RISC-V 的设计哲学就是「少即是多」。基础指令只有 40 多条,比 ARM 和 MIPS 都少。但别小看它,麻雀虽小五脏俱全。我最近在分析一个 RISC-V 的蓝牙芯片固件,发现它的异常处理机制设计得非常优雅。
| 特性 | ARM | MIPS | RISC-V |
|---|---|---|---|
| 指令长度 | 32/16 位混合 | 32 位固定 | 32/16 位(带C扩展) |
| 寄存器数量 | 16/31 个 | 32 个 | 32 个 |
| 延迟槽 | 无 | 有 | 无 |
| 典型应用 | 手机、路由器、工控 | 网络设备、机顶盒 | IoT、AI 加速器 |
2.2 内存映射与寄存器
嵌入式系统的内存不像 PC 那么统一。Flash、RAM、外设寄存器,都映射到同一个地址空间里。你想想看,一个 32 位的处理器,地址空间就 4GB,怎么分?
我一般拿到固件后,会先找它的内存映射表。这张表告诉你:哪个地址段是 Flash(存放固件代码),哪个是 RAM(运行时数据),哪个是外设寄存器(控制硬件)。
个人经验:分析固件时,重点关注外设寄存器的地址。很多漏洞就出在对这些寄存器的错误操作上。比如一个 UART 控制寄存器,如果攻击者能篡改它的配置,就可能造成信息泄露。
举个例子,一个典型的路由器 SoC 内存映射可能是这样的:
0x00000000 - 0x00FFFFFF: Boot ROM(只读)
0x01000000 - 0x01FFFFFF: SPI Flash(固件存储)
0x10000000 - 0x10FFFFFF: DDR RAM(运行时内存)
0x20000000 - 0x20000FFF: UART 寄存器
0x20001000 - 0x20001FFF: GPIO 寄存器
0x20002000 - 0x20002FFF: SPI 控制器寄存器
嗯,这里要注意:不同厂商的 SoC,内存映射差异很大。我建议你建立一个自己的数据库,把常见芯片的映射表存起来,逆向时直接查。
2.3 外设与总线
嵌入式系统里,处理器通过总线跟外设通信。最常见的三种总线是 UART、SPI、I2C。做固件逆向时,你经常需要跟这些总线打交道。
2.3.1 UART(串口)
UART 是最简单的通信接口,两根线(TX、RX)就能干活。很多嵌入式设备都保留 UART 接口,用于调试和日志输出。我经常通过 UART 获取设备的启动日志,里面往往藏着敏感信息。
UART 的波特率常见的有 115200、57600、38400。如果不知道波特率,可以用逻辑分析仪抓一下,测量一个 bit 的宽度就能算出来。
实战技巧:我曾经在分析一个智能家居网关时,通过 UART 接口获取了完整的启动日志。日志里不仅显示了内核版本,还泄露了文件系统的挂载路径。这为后续的固件提取提供了关键线索。
2.3.2 SPI(串行外设接口)
SPI 是高速全双工总线,常用于连接 Flash 芯片、传感器、显示屏。它有四根线:SCK(时钟)、MOSI(主出从入)、MISO(主入从出)、CS(片选)。
SPI Flash 是固件最常见的存储介质。逆向时,如果能物理访问 SPI Flash,就可以用编程器把固件 dump 出来。我建议你备一个 SPI Flash 编程器,比如 CH341A,便宜又好用。
2.3.3 I2C(I方C总线)
I2C 是两线制总线(SCL、SDA),支持多主多从。速度比 SPI 慢,但连线少。常用于连接 EEPROM、温度传感器、RTC 时钟等。
I2C 有个特点:每个设备都有唯一的地址(7 位或 10 位)。分析固件时,如果看到对某个 I2C 地址的读写操作,就能推断出挂载了什么外设。
| 总线 | 线数 | 速度 | 典型应用 |
|---|---|---|---|
| UART | 2(TX/RX) | ~1 Mbps | 调试、日志 |
| SPI | 4+ | ~50 Mbps | Flash、显示屏 |
| I2C | 2(SCL/SDA) | ~3.4 Mbps | 传感器、EEPROM |
2.4 启动流程
嵌入式系统的启动流程,说白了就是「三段式」:Bootloader 引导 → Kernel 启动 → Rootfs 挂载。理解这个流程,对固件逆向至关重要。
2.4.1 Bootloader
Bootloader 是上电后第一个执行的程序。它负责初始化硬件、加载内核到内存、跳转到内核入口。常见的 Bootloader 有 U-Boot、RedBoot、Barebox 等。
U-Boot 是最常见的。它支持网络启动、Flash 读写、内存测试等功能。逆向 U-Boot 时,我一般关注它的环境变量——里面可能藏着启动参数、IP 地址、甚至密码。
避坑指南:我曾经在分析一个 U-Boot 时,发现它的环境变量存储在 Flash 的某个固定偏移处。通过修改环境变量,我成功绕过了设备的签名校验。但要注意,有些设备会对环境变量做 CRC 校验,改了之后会启动失败。
2.4.2 Kernel
Kernel 是操作系统的核心。嵌入式 Linux 内核通常是压缩过的(zImage、uImage),需要先解压再执行。内核启动时会初始化驱动、挂载根文件系统、启动 init 进程。
逆向内核时,我建议先看它的启动参数(cmdline)。这些参数告诉内核:根文件系统在哪里、控制台用哪个串口、内存大小是多少。很多信息都能从这里挖出来。
2.4.3 Rootfs(根文件系统)
Rootfs 是文件系统的根。它包含了所有用户空间的程序、库、配置文件。常见的格式有 SquashFS(只读、压缩)、JFFS2(可写、闪存友好)、UBIFS(新一代闪存文件系统)。
我拿到固件后,第一件事就是解包 Rootfs。用 binwalk 或者 firmware-mod-kit 就能搞定。解包后,重点关注 /bin、/sbin、/etc 目录。很多漏洞就藏在那些二进制程序里。
个人习惯:解包 Rootfs 后,我会先看 /etc/inittab 和 /etc/init.d/ 下的启动脚本。这些脚本里经常有硬编码的密码、调试接口、或者不安全的配置。有一次,我就在一个启动脚本里发现了 root 用户的默认密码。
这张图展示了嵌入式系统从加电到运行用户空间程序的完整流程。每个阶段都有逆向分析的价值点:Bootloader 的环境变量、Kernel 的启动参数、Rootfs 的配置文件,都是我们挖掘漏洞的突破口。
好了,嵌入式系统的基础就讲到这里。这些知识是固件逆向的基石,一定要吃透。下一章我们会深入固件提取技术,到时候这些基础就能派上用场了。
公众号:蓝海资料掘金营,微信deep3321