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)。它们的关系如下图所示。

可验签固件镜像的典型区域布局 头部 Header magic / 版本 长度 / 加载地址 元数据 Metadata TLV / 哈希表 版本 / 回滚计数 固件载荷 Payload 可执行代码 / 数据 受哈希保护的核心区 签名区域 RSA / ECDSA 签名 证书链 / 公钥标识 对齐 填充 Padding 哈希计算范围:头部 + 元数据 + 载荷(签名区域本身不参与哈希) 验签时先按头部声明长度截取受保护数据,再对签名区域做公钥运算 版本字段 major.minor.revision 单调递增,不可回退 写入 OTP / 安全计数器 回滚保护字段 security_counter min_version / anti_rollback 低于阈值则拒绝启动 对齐与填充 4/16/256 字节边界 0x00 或 0xFF 填充 填充值必须纳入哈希

从图中可以看出,签名区域本身通常不参与哈希计算,否则会形成“签名保护签名”的循环。验签流程一般是:读取头部 → 解析元数据 → 按声明长度截取受保护数据 → 计算哈希 → 用签名区域中的公钥或证书验证哈希。

三、对齐与填充:容易被忽略的验签陷阱

对齐与填充看似只是“凑长度”,但在签名验签中非常关键。常见规则包括:

  • 头部对齐:某些启动 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。所有字段的字节序、对齐方式和填充规则必须在规范中写死,避免不同工具链产生不一致的镜像。

六、复盘:镜像结构设计中的典型问题

  • 头部未纳入哈希:攻击者可修改加载地址或长度,导致验签通过但执行错误代码。
  • 版本字段可篡改:回滚保护形同虚设,旧漏洞固件可被重新刷入。
  • 对齐填充不一致:签名工具与验签工具对填充理解不同,导致合法镜像验签失败。
  • 签名区域长度不固定:解析时越界读取,可能引入拒绝服务或信息泄露。

总结来说,固件镜像结构是签名验签的“契约”。头部定义边界,元数据承载策略,载荷是保护对象,签名区域提供信任根,对齐填充保证一致性,版本与回滚字段维持时间维度上的安全。只有这些区域协同设计,启动认证链才能既验得准,又防得住。