版本号机制:回滚保护的第一道防线

版本号这东西,说白了就是个数字。但在固件安全里,它可是回滚保护的基石。我刚开始做嵌入式安全时,总觉得版本号就是个标识,后来吃过亏才明白——它其实是信任链的起点。

你想想看,攻击者最常用的手段是什么?不是破解你的加密算法,而是把设备降级到有漏洞的旧版本。版本号机制,就是专门防这招的。

版本号在回滚保护中的作用

版本号的核心作用就三个:

  • 标识固件新旧:每次发布新固件,版本号必须递增
  • 防止降级攻击:系统只允许升级到更高版本号
  • 建立信任锚点:安全启动链中,版本号是验证的一环

我在项目中遇到过一种情况:某款IoT设备,固件升级时只检查了签名,没检查版本号。结果攻击者提取了旧版本固件,重新签名后刷入设备,成功绕过了所有安全机制。嗯,这就是典型的「有签名无版本」的坑。

关键原则:版本号必须与签名绑定。如果攻击者能修改版本号而不破坏签名,那版本号机制就形同虚设。

单调计数器原理

单调计数器,名字听着高大上,其实原理很简单——它只能增加,不能减少。就像里程表,只能往前走,不能往回拨。

硬件实现上,常见的有两种:

类型 实现方式 特点 典型场景
OTP(一次性可编程) 熔丝/反熔丝 物理不可逆,安全性高 安全芯片、TPM
eFuse 电熔丝阵列 可多次烧写,成本适中 MCU、SoC
Flash模拟 Flash扇区+磨损均衡 灵活但需防篡改 低成本设备

我个人习惯用eFuse。为什么?因为它在安全性和灵活性之间取得了平衡。OTP虽然安全,但烧错了就废了;Flash模拟虽然灵活,但容易被物理攻击篡改。

避坑指南:我曾经在一个项目里用了Flash模拟单调计数器,结果没做磨损均衡,计数器写了几百次就把Flash扇区写坏了。后来改用eFuse,再没出过问题。

单调计数器的工作流程是这样的:

  1. 设备启动时,读取当前计数器值
  2. 固件升级时,检查新固件的版本号是否大于当前计数器值
  3. 如果大于,则更新计数器为新版本号,然后执行升级
  4. 如果小于或等于,拒绝升级

这里有个细节要注意:计数器更新必须在安全环境中进行。我见过一个设计,计数器更新是在普通固件中完成的,结果攻击者直接跳过了更新步骤,把旧固件刷了进去。

版本号存储位置与安全考量

版本号存哪儿?这个问题看似简单,但选错了位置,整个安全体系都可能崩塌。

常见的存储位置有:

  • 固件头部:最简单,但容易被篡改
  • 独立存储区:如OTP、eFuse,安全性高
  • 安全元件(SE):最安全,但成本高
  • Flash特定扇区:需配合访问控制

我建议的做法是:版本号至少存两份——一份在固件头部(用于快速校验),一份在安全存储区(用于防篡改验证)。启动时,比较两者是否一致,不一致就拒绝启动。

重要警告:不要把版本号只存在固件头部!攻击者可以轻松修改头部内容。我曾经逆向过一个设备,它的版本号就在固件偏移0x100处,用十六进制编辑器改一下就能降级。

安全考量方面,有几点必须注意:

  1. 原子性:版本号更新必须是原子操作,不能写到一半断电
  2. 防回滚:硬件计数器一旦增加,就不能再减少
  3. 防旁路:攻击者可能绕过版本检查,直接跳转到旧固件入口
  4. 防重放:即使有签名,也要防止攻击者重放旧版本的签名固件

这里我画了一张图,帮你理清版本号机制的整体架构:

版本号机制核心架构 固件升级流程 1. 获取新固件(含版本号) 2. 验证签名 + 检查版本号 3. 更新单调计数器 → 写入固件 安全存储区 eFuse / OTP / SE 单调计数器(只增不减) 防篡改、防回滚 写入/验证 常见攻击路径与防御 降级攻击 旁路版本检查 重放旧固件 防御措施 单调计数器 + 签名绑定 + 安全启动链验证 版本号双重存储 + 原子更新 + 防旁路跳转

从这张图可以看出,版本号机制不是孤立的,它需要和签名验证、安全启动、单调计数器协同工作。任何一个环节出问题,整个防线都可能被攻破。

实战建议:设计版本号机制时,我建议你遵循「最小权限」原则——版本号检查代码放在ROM或安全启动固件中,普通固件无权修改。这样即使应用层被攻破,攻击者也改不了版本号。

最后说一句:版本号机制是回滚保护的基础,但不是全部。下一层我们还要聊安全启动链、固件签名、硬件信任根……嗯,这些后面会慢慢展开。先把版本号这块吃透,后面的内容就好理解了。