2. 固件镜像结构与安全存储需求
固件并不是一个“大二进制文件”那么简单。设备上电后,BootROM 先加载一级引导,再逐级把控制权交给 U-Boot、内核、根文件系统,最终进入应用。每一级都对应一种镜像格式、一套元数据约定和一组校验规则。理解这些格式与分区布局,是设计安全存储方案的前提——因为“把什么放在哪里、用什么保护、被篡改时谁先发现”,全部由镜像结构和分区表决定。
一、三类典型镜像格式的解剖
U-Boot 镜像(uImage / FIT):传统 uImage 由 64 字节头部加负载组成,头部字段包括魔数 0x27051956、头 CRC(ih_hcrc)、时间戳、负载长度、负载 CRC(ih_dcrc)、OS 类型、CPU 架构、镜像类型、压缩方式和负载入口地址。加载时先校验头 CRC,再校验负载 CRC,任一不符即拒绝启动。较新的 FIT(Flattened Image Tree)用设备树源码描述多个子镜像,支持哈希节点与签名节点,可对内核、DTB、ramdisk 分别做 SHA-256 摘要和 RSA 签名,实现“同一镜像内多组件独立验签”。
UEFI 固件卷(FV/FFS):UEFI 固件以 Firmware Volume 为容器,内部由 FFS 文件组成,每个文件有头部、类型(如 PEIM、DXE 驱动)和校验。关键安全机制是 FV 的 FvHeader 校验、FFS 文件校验,以及可选的 Firmware Volume 签名。UEFI Secure Boot 依赖 PK/KEK/db/dbx 四级密钥库,镜像必须携带有效签名才能被加载,dbx 则用于吊销已知有问题的镜像哈希。
Android boot.img:由 boot header、kernel、ramdisk、second stage 和 DTB 组成。Android 8 以后引入 boot signature 与 AVB(Android Verified Boot)的 vbmeta 结构,vbmeta 中保存各分区(boot、system、vendor)的哈希与签名,启动时逐级校验。boot header 中的 kernel_size、ramdisk_size、page_size 决定各段偏移,任何字段被改动都会导致解析失败或校验不通过。
二、分区布局与元数据:安全属性的落点
镜像格式解决“单个文件怎么组织”,分区布局解决“多个镜像怎么摆放”。典型 eMMC 设备的分区顺序是:BootROM(固化只读)→ bootloader 分区 → boot/kernel 分区 → system/rootfs 分区 → userdata 分区 → vbmeta/元数据分区。这个顺序不是随意的:越靠前的分区越早被加载,可信根必须从最前面开始建立。
元数据是安全存储的“锚点”。它通常包含:镜像版本号(用于防回滚)、哈希摘要(用于完整性)、签名(用于来源认证)、加密算法标识与密钥槽编号(用于机密性)。以 Android AVB 为例,vbmeta 中每个分区条目包含 salt、digest、flags 和 rollback_index。启动时 bootloader 读取 vbmeta,用内置公钥验证其签名,再用其中的 digest 校验对应分区。如果 rollback_index 小于设备熔丝中记录的值,启动被拒绝——这就是防回滚的物理落点。
三、四项安全需求的工程化拆解
机密性:不是所有分区都需要加密。bootloader 和 kernel 通常明文存储,因为启动早期没有密钥派生环境;userdata 和含密钥的配置分区必须加密。工程上常用 AES-XTS 对块设备加密,密钥由硬件密钥槽(如 TrustZone 或 TEE)派生,而不是硬编码在镜像里。判断加密是否生效,可以读取分区头部:加密后的数据不具备可识别的文件系统魔数,且相同明文块加密后密文不同。
完整性:哈希只能发现“被改”,签名才能确认“谁改的”。推荐做法是对每个可加载镜像计算 SHA-256,再用 RSA-2048 或 ECDSA P-256 对摘要签名,公钥固化在 BootROM 或一次性可编程(OTP)区域。校验顺序必须是“先验签、后使用”,且验签失败要进入安全失败路径(如停止启动或进入恢复模式),而不是继续加载。
可用性:安全机制不能把设备变砖。A/B 双分区是标准做法:A 槽启动失败时自动切到 B 槽,同时记录失败计数。元数据分区需要冗余存放(两份 vbmeta,互为备份),并保证写入是原子操作——先写备份、再写主份、最后更新标志位。判断可用性是否达标,可以人为破坏 A 槽镜像,观察设备是否在限定次数内自动切换到 B 槽并正常启动。
防回滚:攻击者可能把已签名的旧版本固件刷回去,利用旧版本漏洞。防回滚依赖单调递增的版本号与不可篡改的存储:设备端用 eFuse 或 TPM NV 计数器保存“已见最大版本”,镜像元数据中携带版本号,启动时比较。若镜像版本低于计数器值,拒绝启动。注意版本号必须随每次发布递增,且签名覆盖版本号字段,否则攻击者可单独篡改版本号。
四、一个可对照的检查清单
| 检查项 | 观察对象 | 合格判据 |
|---|---|---|
| 镜像头校验 | uImage 头 CRC / FV 头校验 | 篡改任意字节后加载被拒绝 |
| 签名验证 | FIT 签名节点 / vbmeta 签名 | 替换公钥或摘要后验签失败 |
| 分区只读 | bootloader、vbmeta 分区 | 运行时写入返回错误 |
| 防回滚 | 版本号与 eFuse 计数器 | 旧版本镜像无法启动 |
| 可用性切换 | A/B 槽与失败计数 | 连续失败后自动切换并启动 |
把镜像格式、分区布局、元数据三者放在一起看,安全存储的设计思路就清晰了:格式决定“怎么解析”,布局决定“放在哪、谁先加载”,元数据决定“用什么校验、版本是多少”。机密性、完整性、可用性、防回滚这四项需求,最终都要落到具体的分区、具体的字段和具体的校验顺序上,而不是停留在原则层面。