版本号机制:回滚保护的第一道防线
版本号这东西,说白了就是个数字。但在固件安全里,它可是回滚保护的基石。我刚开始做嵌入式安全时,总觉得版本号就是个标识,后来吃过亏才明白——它其实是信任链的起点。
你想想看,攻击者最常用的手段是什么?不是破解你的加密算法,而是把设备降级到有漏洞的旧版本。版本号机制,就是专门防这招的。
版本号在回滚保护中的作用
版本号的核心作用就三个:
- 标识固件新旧:每次发布新固件,版本号必须递增
- 防止降级攻击:系统只允许升级到更高版本号
- 建立信任锚点:安全启动链中,版本号是验证的一环
我在项目中遇到过一种情况:某款IoT设备,固件升级时只检查了签名,没检查版本号。结果攻击者提取了旧版本固件,重新签名后刷入设备,成功绕过了所有安全机制。嗯,这就是典型的「有签名无版本」的坑。
关键原则:版本号必须与签名绑定。如果攻击者能修改版本号而不破坏签名,那版本号机制就形同虚设。
单调计数器原理
单调计数器,名字听着高大上,其实原理很简单——它只能增加,不能减少。就像里程表,只能往前走,不能往回拨。
硬件实现上,常见的有两种:
| 类型 | 实现方式 | 特点 | 典型场景 |
|---|---|---|---|
| OTP(一次性可编程) | 熔丝/反熔丝 | 物理不可逆,安全性高 | 安全芯片、TPM |
| eFuse | 电熔丝阵列 | 可多次烧写,成本适中 | MCU、SoC |
| Flash模拟 | Flash扇区+磨损均衡 | 灵活但需防篡改 | 低成本设备 |
我个人习惯用eFuse。为什么?因为它在安全性和灵活性之间取得了平衡。OTP虽然安全,但烧错了就废了;Flash模拟虽然灵活,但容易被物理攻击篡改。
避坑指南:我曾经在一个项目里用了Flash模拟单调计数器,结果没做磨损均衡,计数器写了几百次就把Flash扇区写坏了。后来改用eFuse,再没出过问题。
单调计数器的工作流程是这样的:
- 设备启动时,读取当前计数器值
- 固件升级时,检查新固件的版本号是否大于当前计数器值
- 如果大于,则更新计数器为新版本号,然后执行升级
- 如果小于或等于,拒绝升级
这里有个细节要注意:计数器更新必须在安全环境中进行。我见过一个设计,计数器更新是在普通固件中完成的,结果攻击者直接跳过了更新步骤,把旧固件刷了进去。
版本号存储位置与安全考量
版本号存哪儿?这个问题看似简单,但选错了位置,整个安全体系都可能崩塌。
常见的存储位置有:
- 固件头部:最简单,但容易被篡改
- 独立存储区:如OTP、eFuse,安全性高
- 安全元件(SE):最安全,但成本高
- Flash特定扇区:需配合访问控制
我建议的做法是:版本号至少存两份——一份在固件头部(用于快速校验),一份在安全存储区(用于防篡改验证)。启动时,比较两者是否一致,不一致就拒绝启动。
重要警告:不要把版本号只存在固件头部!攻击者可以轻松修改头部内容。我曾经逆向过一个设备,它的版本号就在固件偏移0x100处,用十六进制编辑器改一下就能降级。
安全考量方面,有几点必须注意:
- 原子性:版本号更新必须是原子操作,不能写到一半断电
- 防回滚:硬件计数器一旦增加,就不能再减少
- 防旁路:攻击者可能绕过版本检查,直接跳转到旧固件入口
- 防重放:即使有签名,也要防止攻击者重放旧版本的签名固件
这里我画了一张图,帮你理清版本号机制的整体架构:
从这张图可以看出,版本号机制不是孤立的,它需要和签名验证、安全启动、单调计数器协同工作。任何一个环节出问题,整个防线都可能被攻破。
实战建议:设计版本号机制时,我建议你遵循「最小权限」原则——版本号检查代码放在ROM或安全启动固件中,普通固件无权修改。这样即使应用层被攻破,攻击者也改不了版本号。
最后说一句:版本号机制是回滚保护的基础,但不是全部。下一层我们还要聊安全启动链、固件签名、硬件信任根……嗯,这些后面会慢慢展开。先把版本号这块吃透,后面的内容就好理解了。