3. 防回滚核心原理:版本号机制、单调计数器、一次性可编程(OTP)存储

防回滚,说白了就是让设备「只能往前更新,不能退回去」。你想想看,如果攻击者拿到了一个旧版本固件,里面刚好有个已知漏洞,他强行刷回去,那你的安全防线不就白搭了吗?

我在好几个项目里都遇到过这种问题。有一次客户说他们的设备被黑了,查了半天,发现是攻击者把固件回滚到了三年前的版本——那个版本有个缓冲区溢出漏洞,早就被修复了。从那以后,我对防回滚的重视程度直接拉满。

要实现防回滚,核心就三板斧:版本号机制、单调计数器、一次性可编程(OTP)存储。咱们一个一个拆开讲。

3.1 版本号机制:最基础的防线

版本号机制是最容易理解的。每个固件镜像里都嵌一个版本号,比如 v2.1.0。更新的时候,Bootloader 检查新固件的版本号是不是比当前运行的高。是,就允许更新;不是,就拒绝。

核心逻辑:版本号必须单调递增,不能回退。

但这里有个坑——版本号存在哪?如果存在 Flash 里,攻击者完全可以把它改大。我见过一个案例,攻击者把 Flash 里存的版本号从 v1.0 改成 v9.9.9,然后刷回 v1.0 的固件,Bootloader 一看版本号比当前低?不对,当前是 v9.9.9,新的是 v1.0,拒绝更新。但攻击者把 Flash 里的版本号也改回 v1.0,Bootloader 就放行了。

警告:版本号如果存在可重写的存储介质(如 Flash、EEPROM)中,防回滚形同虚设。攻击者可以同时篡改固件和版本号。

所以,版本号机制必须搭配一个「不可篡改」的存储来记录当前版本。这就引出了第二个核心组件——单调计数器。

3.2 单调计数器:只能增加,不能减少

单调计数器,顾名思义,它的值只能增加,不能减少。哪怕你把它读到内存里改了,写回去的时候硬件也会拒绝——因为新值比旧值小。

我习惯把单调计数器理解为「固件版本的硬件锚点」。每次固件更新成功,计数器就加 1。Bootloader 在启动时检查:当前固件对应的计数器值,必须等于或大于硬件里存的值。如果小于,说明固件被回滚了。

单调计数器的实现方式主要有两种:

实现方式 原理 优点 缺点
软件模拟 在 Flash 中划出一块区域,每次更新前擦除并写入更大的值 实现简单,无需额外硬件 Flash 有擦写寿命限制;攻击者可能通过物理手段篡改
硬件计数器 芯片内部集成的专用计数器,只能递增 物理不可篡改,安全性高 需要芯片支持,成本略高

我个人建议,只要芯片支持硬件单调计数器,就别用软件模拟。为什么?因为我在一个项目里吃过亏——软件模拟的计数器被攻击者用电压故障注入绕过了,Flash 写入被中断,计数器没加上去,但固件已经刷进去了。结果就是攻击者可以无限次回滚。

小提示:硬件单调计数器通常和芯片的 eFuse 或 OTP 区域绑定,每次递增都会永久烧断一根熔丝。这种设计,攻击者想改回去?除非他把芯片拆了重新流片。

3.3 一次性可编程(OTP)存储:一次写入,永久生效

OTP 存储,就是那种「写进去就改不了」的存储。常见的实现有 eFuse、反熔丝、激光编程等。在防回滚场景里,OTP 用来存储「当前固件的安全版本号」或「单调计数器的最终值」。

OTP 的核心特性:

  • 不可逆:写入后无法擦除或修改。
  • 非易失:掉电不丢失。
  • 物理防篡改:攻击者很难通过探针或电子显微镜读取或修改。

我记得有一次做车规级芯片的安全设计,客户要求固件更新次数不能超过 100 次。我们就在 OTP 里划了 100 个 bit,每次更新烧一个。烧到第 100 个,芯片就永久锁定,不再接受任何固件更新。这种设计虽然极端,但对某些高安全场景(比如金融支付终端)是必须的。

3.4 三者如何协同工作?

版本号机制、单调计数器、OTP 存储,这三者不是孤立的。它们需要配合起来,才能形成完整的防回滚防线。

下面这张图展示了它们之间的协作关系:

防回滚核心机制协作流程图 防回滚核心机制协作流程 固件镜像 内含版本号 v2.1.0 Bootloader 版本校验 新版本 > 当前版本? 单调计数器 +1 硬件递增,不可回退 OTP 存储最终版本 一次写入,永久生效 启动新固件 安全版本已锁定 关键点 版本号校验通过后 计数器才递增 OTP 最终固化

流程其实不复杂:

  1. 固件镜像携带版本号进入系统。
  2. Bootloader 读取 OTP 中存储的当前安全版本号,与新固件版本号比较。
  3. 如果新版本号更大,单调计数器递增一次。
  4. 递增后的计数器值写入 OTP,永久锁定。
  5. 新固件启动,版本号与 OTP 中的值一致。

如果攻击者试图刷回旧版本,Bootloader 发现新版本号小于 OTP 中的值,直接拒绝启动。单调计数器没有递增,OTP 内容不变——攻击者无计可施。

注意:OTP 的写入次数是有限的。如果每次更新都烧一个 bit,那么芯片的更新次数就受限于 OTP 的 bit 数。设计时一定要评估产品的生命周期内需要多少次更新,留足余量。

3.5 实际项目中的避坑指南

讲了这么多理论,我分享几个实际项目中踩过的坑:

  • OTP 写入失败怎么办? 我曾经遇到一个情况,OTP 写入过程中突然掉电,导致 bit 只烧了一半。下次上电,计数器值变成了一个不确定的状态。后来我们在设计里加了「写入确认」机制——写入后读回验证,如果失败,系统进入安全模式,不允许启动任何固件。
  • 单调计数器溢出怎么办? 如果计数器只有 8 bit,最多只能更新 255 次。对于消费类产品可能够用,但工业设备动辄运行十年以上,更新次数可能上千。我建议至少用 16 bit 或 32 bit 的计数器,或者设计成「循环使用 OTP 区域」——但这需要非常谨慎,搞不好就开了一个安全漏洞。
  • 版本号格式要统一。 有的团队用 v1.0.0,有的用 1.0.0,还有的用 0x0100。Bootloader 解析版本号时如果格式不统一,很容易出现误判。我习惯在固件头里固定一个 4 字节的版本号字段,主版本占 1 字节,次版本占 1 字节,修订号占 2 字节,简单粗暴。

我的个人习惯:在 OTP 里同时存储「当前版本号」和「最低可接受版本号」。这样即使某个版本有严重 bug,需要紧急回滚到上一个安全版本,只要最低版本号允许,就可以回滚。但回滚后,OTP 中的当前版本号不会减少——这是底线。

好了,防回滚的核心原理就这些。版本号机制是逻辑基础,单调计数器是硬件保障,OTP 存储是最终防线。三者缺一不可,配合好了,攻击者想回滚固件?门都没有。