3、安全启动链与回滚:信任根(RoT)在回滚保护中的角色

安全启动链,说白了就是一条「信任传递链」。你想想看,芯片上电后,第一段代码是谁?是固化在 ROM 里的 BootROM。这段代码是硬件写死的,改不了,这就是整个系统的信任起点——信任根(RoT)。

我做过不少 IoT 设备的固件安全评估,发现很多团队把精力都花在应用层加密上,却忽略了启动链的版本绑定。结果呢?攻击者只要拿一个旧版本的 Bootloader 替换掉新版本,就能绕过所有安全校验。这就是回滚攻击的典型手法。

信任根(RoT)为什么是回滚保护的基石

信任根必须是不可篡改的。常见的实现方式有两种:

  • 硬件 RoT:固化在芯片 ROM 中,比如 BootROM。物理上无法修改。
  • 一次性可编程(OTP)RoT:通过 eFuse 或 OTP 寄存器存储公钥哈希或版本号。烧进去就改不了。

我个人更倾向于硬件 RoT + OTP 的组合。为什么?因为单靠 ROM 代码,你没法动态更新信任策略。而 OTP 可以让你在产线阶段烧录设备专属的密钥和版本锚点。

核心原则:信任根必须能「记住」当前允许的最低版本。这个版本信息要存储在 RoT 能直接访问的防篡改区域,比如 OTP 寄存器或安全存储。

安全启动链的版本绑定机制

安全启动链的每一级,都要校验下一级的版本号。我习惯把这个过程叫做「版本锚定」。来看一个典型的四级启动链:

  1. BootROM(RoT):校验 Bootloader 1 的签名和版本号。
  2. Bootloader 1:校验 Bootloader 2 的签名和版本号。
  3. Bootloader 2:校验 OS Kernel 的签名和版本号。
  4. OS Kernel:校验系统服务和应用的签名和版本号。

每一级在跳转之前,都会检查下一级固件的版本号是否 ≥ 自己存储的「最低允许版本」。如果发现版本过低,直接拒绝启动。

避坑指南:我曾经遇到一个项目,Bootloader 1 校验了 Bootloader 2 的签名,但没校验版本号。攻击者把 Bootloader 2 降级到一个有已知漏洞的旧版本,然后利用那个漏洞提权。嗯,从那以后,我要求所有启动链的版本校验必须和签名校验绑定在一起,缺一不可。

Bootloader 的版本校验流程

Bootloader 的版本校验,不是简单比大小。它需要一套严谨的流程。我总结了一个典型的实现步骤:

步骤 操作 说明
1 读取下一级固件的版本号字段 版本号通常放在固件头的固定偏移处
2 从 RoT 存储区读取最低允许版本 这个值在固件升级时由新版本 Bootloader 更新
3 比较版本号 如果当前版本 < 最低允许版本,则拒绝启动
4 校验固件签名 版本校验通过后,再用公钥验证固件完整性
5 更新最低允许版本(可选) 如果当前版本 > 存储的最低版本,则更新 OTP 或安全存储

这里有个关键点:版本号比较必须是单调递增的。你不能允许版本号回退。我见过有些方案用字符串比较版本号,结果 "1.9" 比 "1.10" 大,这种坑一定要避开。

警告:版本号字段必须放在签名保护的固件头内部。如果攻击者能篡改版本号而不破坏签名,那整个校验就形同虚设。我曾经在逆向一个路由器固件时,发现它的版本号在固件头之外,攻击者可以单独修改版本号字段,绕过版本校验。这种设计等于没做。

版本绑定的代码示例

下面是一个简化的 Bootloader 版本校验伪代码。实际项目中会涉及硬件抽象层和密码学库,但核心逻辑就是这样:

// 从 OTP 读取最低允许版本
uint32_t min_version = read_otp(OTP_MIN_VERSION_OFFSET);

// 从下一级固件头读取版本号
uint32_t fw_version = read_fw_header_version(fw_image);

// 版本校验
if (fw_version < min_version) {
    // 版本过低,拒绝启动
    panic("Firmware version too low: %d < %d", fw_version, min_version);
}

// 签名校验
if (!verify_fw_signature(fw_image, public_key)) {
    panic("Firmware signature verification failed");
}

// 如果当前版本更高,更新 OTP 中的最低版本
if (fw_version > min_version) {
    write_otp(OTP_MIN_VERSION_OFFSET, fw_version);
}

// 跳转到下一级固件
jump_to_fw(fw_image);

注意那个 write_otp 操作。OTP 寄存器一旦写入,就不能再改回更低的值。这就是回滚保护的物理基础。你想想看,攻击者就算拿到了旧版本固件,也没法让 OTP 里的版本号变回去。

安全启动链与回滚保护的关系图

下面这张图展示了安全启动链中,每一级如何通过版本绑定来防止回滚:

安全启动链版本绑定与回滚保护流程 信任根 (RoT) BootROM / OTP Bootloader 1 版本校验 + 签名校验 Bootloader 2 版本校验 + 签名校验 OS Kernel 版本校验 + 签名校验 系统服务 / 应用 版本校验 + 签名校验 版本绑定关键点 • 每级存储最低允许版本 • 版本号单调递增 • 版本字段在签名保护内 • OTP 写入不可逆 回滚攻击路径 攻击者用旧版本固件 替换新版本固件 绕过安全补丁

从图中可以看到,每一级启动组件都承担着「校验者」和「被校验者」的双重角色。信任根是唯一不需要被校验的,因为它本身就是不可篡改的硬件。

实际项目中的避坑经验

我参与过几个车规级 MCU 的安全启动方案设计,踩过不少坑。这里分享几个典型的:

  • 版本号溢出问题:有些芯片的 OTP 只有 8 位,版本号最大 255。升级 256 次后怎么办?我建议用 OTP 位域来扩展,或者用多个 OTP 字组合成更大的版本空间。
  • 回滚到出厂版本:有些方案允许「恢复出厂设置」,但恢复出厂固件时,OTP 里的最低版本也要跟着恢复吗?绝对不能。我曾经见过一个产品,恢复出厂设置后 OTP 被清零,攻击者直接降级到有漏洞的出厂版本。
  • 多镜像版本一致性:如果系统有多个固件镜像(比如 A/B 分区),它们的版本号必须协同管理。我习惯在 Bootloader 里维护一个全局版本表,确保所有镜像的版本都满足最低要求。

总结一下:信任根是回滚保护的物理锚点,安全启动链通过逐级版本绑定来防止降级攻击。Bootloader 的版本校验流程必须包含版本比较、签名验证和 OTP 更新三个核心步骤。这三者缺一不可,否则你的回滚保护就是纸糊的。