3. 防回滚核心思想:版本号绑定、一次性编程(OTP)区域、安全存储区
防回滚,说白了就是让设备「只能往前,不能后退」。我做了这么多年嵌入式安全,见过太多因为回滚而翻车的案例。有个客户的产品,就因为攻击者把固件降级到了三年前的版本,结果整个产线都被迫停产整改。嗯,从那以后,我对防回滚的重视程度直接拉满。
这一章,我们来拆解防回滚的三个核心支柱。你理解透了它们,就掌握了固件版本控制的命脉。
3.1 版本号绑定:给固件贴上「身份证」
版本号绑定,是最基础也最容易被忽视的一环。它的逻辑很简单:固件镜像里必须包含一个单调递增的版本号,并且这个版本号要跟硬件绑死。
我习惯的做法是这样的:在固件头部预留 4 字节的版本字段,每次发布新版本时,这个数字必须比上一版大。Bootloader 在加载固件前,会先比较这个版本号。
核心原则:版本号必须存储在不可篡改的区域(如 OTP 或安全存储区),并且比较逻辑必须在可信执行环境(TEE)或 Boot ROM 中完成。
来看一个典型的版本号校验流程:
// 伪代码:Bootloader 中的版本校验
uint32_t current_version = read_otp_version(); // 从 OTP 读取当前版本
uint32_t new_version = parse_firmware_header(); // 从新固件头部解析版本
if (new_version <= current_version) {
// 拒绝加载!版本号没有增加
boot_error("Rollback detected! Version must increase.");
halt();
}
// 通过校验,继续加载
verify_signature();
load_firmware();
这里有个坑,我曾经踩过:版本号不能是字符串。为什么?因为字符串比较太容易出幺蛾子了。"v2.0" 和 "v10.0" 按字典序比较,结果是 "v10.0" 小于 "v2.0"——攻击者直接改个字符串就能绕过。所以,我建议用整数,或者至少用固定长度的 BCD 码。
避坑指南:我曾经见过一个团队用 Git commit 的 SHA 值前 8 位当版本号。结果有一次两个不同分支的 commit 前 8 位撞了,导致合法升级被拒绝。版本号必须单调递增,不能依赖哈希碰撞概率。
3.2 一次性编程(OTP)区域:写一次,管终身
OTP,全称 One-Time Programmable。这玩意儿就像一张只能写一次的纸——写上去就擦不掉了。在防回滚场景里,OTP 用来存储「当前最高版本号」。
你想想看,如果版本号存在普通 Flash 里,攻击者完全可以把它改小,然后刷入旧固件。但 OTP 不一样,每个 bit 只能从 1 变成 0(或者反过来,取决于工艺),而且不可逆。
我常用的 OTP 方案有两种:
| 方案类型 | 实现方式 | 优点 | 缺点 |
|---|---|---|---|
| eFuse | 芯片内部熔丝,通过大电流熔断 | 物理不可逆,安全性极高 | 成本高,占用 die 面积 |
| 一次性寄存器 | 专用 OTP 存储器单元 | 成本低,集成度高 | 可能被物理探针攻击 |
我个人更倾向 eFuse,虽然贵,但胜在物理安全。记得有一次,客户为了省钱选了普通 OTP 寄存器,结果被攻击者用聚焦离子束(FIB)探针直接读出了版本号,然后通过激光切割重置了寄存器。嗯,从那以后,但凡涉及安全等级高的产品,我都建议上 eFuse。
实战技巧:OTP 区域不要只存一个版本号。我习惯把版本号、芯片唯一 ID、以及一个随机数种子一起烧进去。这样即使攻击者读到了版本号,也无法伪造完整的固件签名。
3.3 安全存储区:给关键数据上把锁
安全存储区,是芯片内部一块隔离出来的存储区域。它跟普通 Flash 最大的区别在于:只有特定的安全代码(如 Boot ROM、TEE)才能访问它。
为什么需要它?因为 OTP 虽然安全,但容量太小(通常只有几 KB)。而版本号、密钥、设备证书这些数据,有时候需要频繁更新。安全存储区就充当了「可更新的 OTP」角色。
我设计安全存储区时,会遵循这几个原则:
- 访问控制:只有 Bootloader 和 TEE 能读写,普通固件无权访问。
- 加密存储:数据在写入前先加密,密钥来自芯片唯一 ID。
- 完整性校验:每次读取都做 HMAC 校验,防止篡改。
- 版本回滚保护:存储区内部维护一个单调计数器,每次更新版本号时计数器加一。
来看一个安全存储区的典型布局:
// 安全存储区结构体定义
typedef struct {
uint32_t magic; // 魔数,用于识别有效数据
uint32_t version_counter; // 单调递增计数器
uint8_t firmware_version[4];// 当前固件版本号
uint8_t device_id[16]; // 设备唯一标识
uint8_t reserved[32]; // 预留空间
uint8_t hmac[32]; // 完整性校验值
} secure_storage_t;
避坑指南:我曾经见过一个产品,安全存储区里的版本号是用明文存的。攻击者通过电压故障注入,让 HMAC 校验跳过,然后直接修改了版本号。所以,永远不要信任安全存储区的物理隔离——加密和校验必须双管齐下。
3.4 三者如何协同工作?
版本号绑定、OTP、安全存储区,这三者不是孤立的。它们组成了一条完整的防回滚链条。我画了一张图,帮你理清它们的关系:
这张图展示了完整的流程。你看,版本号绑定是「输入」,OTP 是「锚点」,安全存储区是「缓冲」。三者缺一不可。
我举个例子你就明白了:假设设备当前版本是 5,OTP 里存的是 5。攻击者想刷回版本 3。Bootloader 从 OTP 读到 5,从固件头部读到 3,3 <= 5,直接拒绝。如果攻击者想绕过 OTP,直接改安全存储区——抱歉,HMAC 校验会失败,Bootloader 会认为存储区已损坏,直接进入恢复模式。
总结一下:版本号绑定解决「谁来比较」的问题,OTP 解决「锚点不可篡改」的问题,安全存储区解决「可更新且安全」的问题。这三板斧砍下去,大部分回滚攻击都能防住。