4、固件镜像结构:BootROM、Bootloader、主固件分区、签名头部格式设计
好,咱们今天聊聊固件镜像的结构。说白了,就是你的芯片上电后,代码是怎么一步步被加载和执行的。我做了这么多年嵌入式安全,见过太多因为镜像结构设计不合理导致的启动失败,甚至安全漏洞。所以这一节,咱们把每个分区掰开揉碎了讲清楚。
4.1 整体架构概览
一个典型的安全启动固件镜像,通常包含四个核心部分:BootROM、Bootloader、主固件分区,以及签名头部。它们之间的关系,我画了张图,你一看就明白。
核心原则:每一级在加载下一级之前,都必须验证其完整性和合法性。这就是所谓的「信任链」。
4.2 BootROM:信任的根
BootROM 是芯片出厂时固化在 ROM 里的代码。它不可修改,所以是信任链的起点。我习惯叫它「硬件信任锚点」。
BootROM 的主要职责就两件事:
- 初始化硬件:时钟、内存控制器、基本外设。让芯片能跑起来。
- 验证并加载 Bootloader:从指定的存储介质(比如 SPI Flash、eMMC、NAND)中读取 Bootloader,验证其签名,然后跳转执行。
我的经验:BootROM 的代码空间通常非常有限,一般只有 16KB 到 64KB。所以它只能做最核心的事情。我曾经在一个项目里,因为 BootROM 里集成了太多功能,导致 ROM 空间不够,最后不得不砍掉一些非关键特性。记住,BootROM 越精简越安全。
4.3 Bootloader:承上启下的关键环节
Bootloader 是第二级启动代码。它比 BootROM 灵活,可以放在可擦写的 Flash 里,方便更新。但这也意味着它更容易被攻击,所以签名验证必须严格。
Bootloader 通常分两个阶段:
| 阶段 | 职责 | 典型大小 |
|---|---|---|
| SPL (Secondary Program Loader) | 初始化 DDR 内存、时钟、存储控制器 | 16KB - 64KB |
| 主 Bootloader | 加载主固件、验证签名、提供恢复模式 | 128KB - 512KB |
我个人建议,Bootloader 里一定要保留一个「恢复模式」。万一主固件刷坏了,还能通过这个模式救回来。我遇到过客户把主固件刷成砖,就是因为 Bootloader 没有回退机制。
注意:Bootloader 的签名密钥必须与 BootROM 中存储的根公钥匹配。如果密钥泄露,整个信任链就崩塌了。所以,密钥管理是安全启动的核心中的核心。
4.4 主固件分区:应用程序的安身之所
主固件分区存放的就是你的应用程序代码。它通常包含两个区域:
- 代码区:可执行的二进制代码。
- 数据区:常量、配置文件、资源文件等。
主固件分区的大小取决于你的 Flash 容量。我见过最小的只有 256KB,大的能到 32MB 甚至更大。设计时要注意对齐——很多 Flash 的擦除块大小是 4KB 或 64KB,分区边界最好对齐到擦除块,否则更新时会很麻烦。
另外,我建议在主固件分区里预留一个「备份区」。OTA 升级时,先下载到备份区,验证通过后再切换。这样即使升级过程中断电,也不会变砖。
4.5 签名头部格式设计
签名头部是固件镜像的「身份证」。它告诉 BootROM 和 Bootloader:这个固件是谁签的、用什么算法、签名值是多少。
下面是我常用的签名头部格式,你可以直接参考:
typedef struct {
uint32_t magic; // 魔数,用于识别镜像类型,比如 0x5349474E ("SIGN")
uint32_t version; // 固件版本号,用于防回滚
uint32_t image_size; // 镜像大小(不含签名头部)
uint32_t header_size; // 签名头部大小
uint32_t algo_id; // 签名算法标识,比如 0x01=RSA2048, 0x02=ECDSA256
uint8_t pubkey_hash[32];// 公钥的 SHA-256 哈希,用于验证公钥完整性
uint8_t vendor_id[16]; // 厂商 ID,用于多厂商场景
uint8_t reserved[32]; // 保留字段,用于未来扩展
uint8_t signature[256]; // 签名值,RSA2048 是 256 字节
} firmware_signature_t;
关键字段说明:
- magic:防止 BootROM 把随机数据当成合法镜像。我习惯用 ASCII 字符,方便调试时肉眼识别。
- version:防回滚攻击。Bootloader 会检查新固件的版本号是否大于等于当前版本,否则拒绝升级。
- pubkey_hash:BootROM 里只存了根公钥的哈希,而不是完整的公钥。这样可以节省 ROM 空间,同时防止公钥被篡改。
- signature:对整个镜像(从 magic 到 reserved 末尾)的签名。注意,签名值本身不参与签名计算。
4.6 签名验证流程
验证流程其实不复杂,但每一步都不能出错。我拆解一下:
- 读取签名头部:从 Flash 中读取固定偏移处的签名头部数据。
- 校验魔数:检查 magic 是否匹配。不匹配直接报错。
- 校验版本号:检查 version 是否 >= 当前版本。防止回滚。
- 计算镜像哈希:对签名头部之后的镜像数据计算哈希值。
- 验证签名:用根公钥解密签名值,得到哈希值,与步骤 4 计算的哈希值对比。一致则通过。
- 跳转执行:验证通过后,跳转到镜像入口地址执行。
避坑指南:我曾经在一个项目里,因为签名验证时没有检查镜像大小,导致攻击者通过截断镜像的方式绕过了签名验证。从那以后,我每次都会在签名头部里显式记录镜像大小,并在验证时严格检查。
4.7 多镜像与分区表
有些场景下,一个设备里会有多个固件镜像,比如:
- 主应用程序
- 无线协议栈(比如 BLE、Wi-Fi)
- DSP 固件
- 安全协处理器固件
这时候就需要一个分区表来管理。分区表本身也要签名,防止被篡改。我常用的分区表格式如下:
| 分区名 | 起始地址 | 大小 | 类型 | 签名头部偏移 |
|---|---|---|---|---|
| bootloader | 0x000000 | 256KB | 可执行 | 0x000000 |
| app_main | 0x040000 | 2MB | 可执行 | 0x040000 |
| app_backup | 0x240000 | 2MB | 可执行 | 0x240000 |
| config | 0x440000 | 64KB | 数据 | 0x440000 |
| storage | 0x450000 | 剩余空间 | 文件系统 | 无 |
分区表通常放在 Flash 的固定位置,比如偏移 0x000000 处。BootROM 和 Bootloader 都会读取它。
重要:分区表本身必须签名。否则攻击者可以修改分区表,把恶意固件指向合法分区,或者把合法分区指向无效地址。我见过一个案例,就是因为分区表没签名,攻击者通过修改分区表绕过了签名验证。
4.8 小结
固件镜像结构的设计,直接决定了安全启动的可靠性。BootROM 是信任的根,必须固化且不可修改。Bootloader 是承上启下的关键,要支持恢复模式。主固件分区要预留备份区,防止升级失败变砖。签名头部是固件的身份证,每个字段都有其存在的意义。
嗯,这一节的内容就到这里。记住,设计镜像结构时,多想想「如果攻击者篡改了这个字段,会发生什么?」。带着这个问题去设计,你的系统会更安全。