4. 固件镜像结构
固件签名验签的可靠性,首先取决于固件镜像本身是否具备清晰、可解析、可验证的结构。如果镜像只是“一段二进制数据”,签名就无从附着,验签也无法判断哪些字节受保护、哪些字段允许变化。因此,理解固件镜像的典型布局,是设计启动认证链的基础。
本节围绕一个核心问题展开:一个可签名、可验签、可回滚保护的固件镜像,通常由哪些区域组成,它们之间是什么关系?下面从常见格式、头部、元数据、签名区域、对齐填充以及版本回滚字段六个方面逐一说明。
一、常见固件格式与容器选择
不同芯片平台和启动流程使用不同的固件容器格式。常见的有:
- 原始二进制(raw bin):无头部、无元数据,直接烧录到固定偏移。签名时通常需要外部描述文件说明长度和加载地址。
- 带头部镜像(如 U-Boot uImage、FIT):头部记录魔数、加载地址、入口地址、数据长度、校验和,FIT 还支持多镜像节点和哈希/签名节点。
- Intel HEX / SREC:文本格式,包含地址记录和数据记录,适合烧录器,但签名前通常需转换为二进制。
- 厂商私有容器(如 MCUboot image、ESP image、Android boot.img):自带头部、TLV 元数据、签名块和尾部标记,验签逻辑与格式强绑定。
选择容器格式时,关键不是“哪种更流行”,而是启动 ROM 或一级引导能否识别该格式,并能否在验签前定位到签名区域。例如 MCUboot 镜像头部固定包含 magic、load_addr、hdr_size、img_size、flags、version 等字段,验签工具可据此计算受保护范围。
二、头部、元数据与签名区域的关系
典型可验签固件镜像可抽象为五个连续区域:头部(Header)→ 元数据(Metadata/TLV)→ 固件载荷(Payload)→ 签名区域(Signature)→ 对齐填充(Padding)。它们的关系如下图所示。
从图中可以看出,签名区域本身通常不参与哈希计算,否则会形成“签名保护签名”的循环。验签流程一般是:读取头部 → 解析元数据 → 按声明长度截取受保护数据 → 计算哈希 → 用签名区域中的公钥或证书验证哈希。
三、对齐与填充:容易被忽略的验签陷阱
对齐与填充看似只是“凑长度”,但在签名验签中非常关键。常见规则包括:
- 头部对齐:某些启动 ROM 要求镜像起始地址按 4KB 或 64KB 对齐,否则拒绝加载。
- 签名区域对齐:RSA 签名长度等于模长(如 2048 位 = 256 字节),ECDSA P-256 签名为 64 字节,需按固定长度预留。
- 填充值一致性:如果签名时填充 0x00,验签时却按 0xFF 解析,哈希必然不一致。填充字节必须明确纳入受保护范围。
常见误区:把填充区域排除在哈希之外。攻击者可在填充区写入恶意数据而不破坏签名。正确做法是:头部中声明“受保护长度”,该长度覆盖到填充结束,验签时严格按此长度计算哈希。
四、版本与回滚保护字段
固件签名解决了“来源可信”,但无法阻止攻击者把设备刷回存在漏洞的旧版本。因此,镜像中必须包含版本与回滚保护字段:
| 字段 | 典型位置 | 作用 | 验签/启动时的检查 |
|---|---|---|---|
| image_version | 头部或元数据 | 标识固件版本号 | 与安全存储中的当前版本比较 |
| security_counter | 元数据 TLV | 单调递增计数器 | 必须 ≥ OTP 中记录值,否则拒绝 |
| min_version | 元数据 | 允许启动的最低版本 | 低于该值直接判定回滚攻击 |
| anti_rollback | 头部 flags | 启用回滚保护开关 | 置位后强制检查计数器 |
这些字段必须位于签名保护范围内,否则攻击者可以篡改版本号而不被发现。同时,安全计数器通常需要写入 OTP 或受保护的安全存储区,防止被回写。
五、案例:MCUboot 镜像的验签字段布局
以 MCUboot 为例,其镜像头部结构大致如下:
struct image_header {
uint32_t ih_magic; /* 0x96f3b83d */
uint32_t ih_load_addr;
uint16_t ih_hdr_size;
uint16_t ih_protect_tlv_size;
uint32_t ih_img_size;
uint32_t ih_flags;
struct image_version ih_ver;
uint32_t _pad1;
uint8_t ih_tlv_info[]; /* TLV: 哈希、签名、密钥 */
};
验签时,引导程序先校验 magic,再读取 ih_hdr_size 和 ih_img_size 确定受保护范围,然后解析 TLV 区域中的 IMAGE_TLV_HASH 和 IMAGE_TLV_SIGNATURE,最后用公钥验证。版本字段 ih_ver 与安全计数器共同决定是否允许启动。
工程建议:设计自定义固件格式时,至少保留以下字段:magic、header_size、image_size、version、security_counter、hash_alg、sig_alg、signature。所有字段的字节序、对齐方式和填充规则必须在规范中写死,避免不同工具链产生不一致的镜像。
六、复盘:镜像结构设计中的典型问题
- 头部未纳入哈希:攻击者可修改加载地址或长度,导致验签通过但执行错误代码。
- 版本字段可篡改:回滚保护形同虚设,旧漏洞固件可被重新刷入。
- 对齐填充不一致:签名工具与验签工具对填充理解不同,导致合法镜像验签失败。
- 签名区域长度不固定:解析时越界读取,可能引入拒绝服务或信息泄露。
总结来说,固件镜像结构是签名验签的“契约”。头部定义边界,元数据承载策略,载荷是保护对象,签名区域提供信任根,对齐填充保证一致性,版本与回滚字段维持时间维度上的安全。只有这些区域协同设计,启动认证链才能既验得准,又防得住。