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 决定各段偏移,任何字段被改动都会导致解析失败或校验不通过。

三类固件镜像结构与元数据对照 U-Boot uImage / FIT 64B Header(魔数/长度/CRC) Kernel 负载(可压缩) DTB / ramdisk 子镜像 元数据:ih_hcrc、ih_dcrc、ih_size FIT 可对每子镜像独立签名 UEFI Firmware Volume FV Header(长度/校验) FFS 文件(PEIM/DXE 驱动) PK / KEK / db / dbx 密钥库 元数据:FV 校验、FFS 校验、签名 Secure Boot 逐级验签 Android boot.img Boot Header(page_size/偏移) Kernel + Ramdisk + DTB vbmeta(哈希+签名) 元数据:kernel_size、签名、AVB 启动时逐分区校验 典型 eMMC 分区布局与安全属性 BootROM 只读/固化 bootloader 签名+防回滚 boot / kernel 哈希+签名 system / rootfs dm-verity userdata 加密/可写 vbmeta / 元数据 签名+版本号 安全需求 → 技术手段映射 机密性 AES-XTS 加密 userdata 完整性 SHA-256 + RSA 签名 可用性 A/B 双分区 + 回滚槽 防回滚 单调计数器 + 版本号

二、分区布局与元数据:安全属性的落点

镜像格式解决“单个文件怎么组织”,分区布局解决“多个镜像怎么摆放”。典型 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 计数器保存“已见最大版本”,镜像元数据中携带版本号,启动时比较。若镜像版本低于计数器值,拒绝启动。注意版本号必须随每次发布递增,且签名覆盖版本号字段,否则攻击者可单独篡改版本号。

工程提示:元数据本身也要被保护。如果只对 kernel 签名,却把 vbmeta 明文放在可写分区,攻击者可以替换 vbmeta 中的 digest 指向恶意镜像。因此元数据分区必须只读、签名,或存放在受硬件保护的区域。

四、一个可对照的检查清单

检查项观察对象合格判据
镜像头校验uImage 头 CRC / FV 头校验篡改任意字节后加载被拒绝
签名验证FIT 签名节点 / vbmeta 签名替换公钥或摘要后验签失败
分区只读bootloader、vbmeta 分区运行时写入返回错误
防回滚版本号与 eFuse 计数器旧版本镜像无法启动
可用性切换A/B 槽与失败计数连续失败后自动切换并启动

把镜像格式、分区布局、元数据三者放在一起看,安全存储的设计思路就清晰了:格式决定“怎么解析”,布局决定“放在哪、谁先加载”,元数据决定“用什么校验、版本是多少”。机密性、完整性、可用性、防回滚这四项需求,最终都要落到具体的分区、具体的字段和具体的校验顺序上,而不是停留在原则层面。