3、NVCounter(非易失计数器)原理:硬件NVCounter与软件NVCounter的区别,计数器的生命周期管理
好,咱们今天聊聊NVCounter。这东西在ATF移植里,说大不大,说小不小。但你要是没搞明白,回滚保护就是个摆设。
我刚开始接触TrustZone的时候,觉得回滚保护不就是检查版本号嘛。后来踩了坑才发现,版本号存哪儿、怎么存、怎么保证它不被篡改,这才是真正的技术活。
3.1 什么是NVCounter?
NVCounter,全称Non-Volatile Counter,非易失计数器。说白了,就是一个只能增加、不能减少的计数器。它记录的是你固件镜像的版本号。
每次你刷写一个新版本的固件,这个计数器就得加1。下次启动时,ATF会检查当前镜像的版本号是否大于等于这个计数器的值。如果小于,说明有人在尝试回滚到旧版本——直接拒绝启动。
核心原则:计数器值 = 已部署的最高固件版本号。只增不减,永不回头。
你想想看,如果攻击者拿到了一个旧版本的固件,里面有已知漏洞。他想刷回去利用这个漏洞。但NVCounter已经涨上去了,旧版本号比计数器小,系统就不干了。这就是回滚保护的本质。
3.2 硬件NVCounter vs 软件NVCounter
这里有个关键问题:计数器存哪儿?
两种方案:硬件NVCounter和软件NVCounter。我分别说说。
3.2.1 硬件NVCounter
硬件NVCounter,就是芯片内部集成的专用寄存器或存储单元。比如有些SoC里有一块OTP(One-Time Programmable)区域,或者eFuse阵列。
特点很鲜明:
- 物理不可逆:烧进去就回不来了。想清零?除非你把芯片砸了。
- 抗篡改能力强:攻击者没法通过软件手段修改它。
- 次数有限:OTP和eFuse的烧写次数是有限的,通常几十到几百次。
- 成本高:芯片面积、工艺复杂度都上去了。
我在项目中遇到过一款车规级芯片,它的硬件NVCounter只有32位,但烧写寿命只有100次。当时产品要求支持至少50次OTA升级,算上开发阶段的调试,差点就不够用了。嗯,这里要注意,硬件NVCounter的寿命评估一定要提前做。
3.2.2 软件NVCounter
软件NVCounter,就是存在Flash或eMMC里的一个变量。通过文件系统或特定分区来管理。
特点也很明显:
- 理论上无限次:Flash的擦写寿命虽然有限,但比OTP多得多。
- 成本低:不需要额外的硬件支持。
- 容易被篡改:如果攻击者能读写Flash,他就能把计数器改小。
- 需要额外保护:通常要配合签名验证、加密存储、甚至硬件安全引擎来保护。
我曾经调试过一个设备,软件NVCounter存在一个普通分区里。结果攻击者通过串口把分区dump出来,改了计数器值又写回去。系统就以为版本没变,回滚保护形同虚设。后来我们改成了用RPMB分区(Replay Protected Memory Block)来存,才解决了问题。
3.2.3 两者对比
| 对比项 | 硬件NVCounter | 软件NVCounter |
|---|---|---|
| 存储介质 | OTP / eFuse | Flash / eMMC |
| 可逆性 | 不可逆 | 可被篡改 |
| 更新次数 | 有限(几十到几百次) | 几乎无限 |
| 抗攻击能力 | 强 | 弱(需额外保护) |
| 成本 | 高 | 低 |
| 典型应用 | BootROM、安全启动 | OTA升级、多版本管理 |
我个人习惯是:关键的安全启动链用硬件NVCounter,后续的OTA升级用软件NVCounter配合RPMB或安全存储。这样既保证了根信任的不可篡改,又兼顾了升级的灵活性。
3.3 计数器的生命周期管理
计数器不是只管加就完事了。它的生命周期管理涉及几个关键阶段。
3.3.1 初始化阶段
芯片出厂时,NVCounter通常为0。第一次烧录固件时,计数器要更新为1。
这里有个坑:如果芯片的硬件NVCounter在出厂时已经被烧写过(比如用于其他目的),你得先读取它的当前值,然后在此基础上递增。不能直接写死。
注意:初始化时一定要确认计数器的初始状态。我曾经遇到过一批芯片,硬件NVCounter在出厂测试时被烧到了0x5,结果我们的固件版本号从1开始,直接导致启动失败。后来我们加了一个校准步骤,读取硬件计数器的当前值作为基准。
3.3.2 更新阶段
每次固件升级时,流程是这样的:
- 新固件镜像里包含一个版本号(比如3)。
- ATF在启动时读取当前NVCounter的值(比如2)。
- 比较:3 >= 2,通过。允许启动。
- 启动后,新固件将NVCounter更新为3。
这里有个细节:更新计数器的时机。我建议在固件完全验证通过、即将跳转到新固件之前更新。如果更新太早,万一新固件启动失败,计数器已经涨上去了,你就再也回不去了。
我的经验:在ATF的bl2阶段完成所有镜像验证后,再更新NVCounter。这样即使后续bl31或bl33启动失败,计数器也已经更新,下次重启时不会允许回滚到旧版本。这是一种「要么成功,要么死」的策略,但保证了安全性。
3.3.3 回滚场景
假设当前NVCounter是5。攻击者想刷回版本3的固件。
启动时,ATF读取NVCounter=5,新固件版本号=3。3 < 5,拒绝启动。系统进入恢复模式或直接死机。
这就是回滚保护生效了。
但有一种特殊情况:如果攻击者同时修改了固件镜像里的版本号呢?比如他把版本3的固件里的版本号字段改成6。那ATF比较时,6 >= 5,通过。但实际运行的还是旧固件。
所以,固件镜像的版本号必须被签名保护。ATF在验证签名时,会同时验证版本号字段的完整性。如果版本号被篡改,签名验证失败,根本到不了比较那一步。
3.3.4 计数器耗尽
硬件NVCounter的更新次数是有限的。如果产品生命周期内需要大量升级,计数器可能会耗尽。
解决方案有几个:
- 混合方案:硬件NVCounter只用于关键版本(比如安全补丁),普通升级用软件NVCounter。
- 计数器复用:比如用多个bit组合表示版本号,而不是每次升级都烧一个bit。
- 提前规划:在产品设计阶段就评估好升级次数,选择合适位宽的硬件NVCounter。
我记得有个项目,客户要求支持1000次OTA升级。但芯片的硬件NVCounter只有64次寿命。最后我们采用了硬件+软件两级计数器方案:硬件计数器每10次软件升级才递增一次。这样硬件寿命就够用了。
3.4 核心逻辑流程图
下面我用一张SVG图来展示NVCounter在ATF启动过程中的核心逻辑。这张图我画了好几次才满意,希望能帮你理清思路。
3.5 实际移植中的注意事项
最后,我总结几个实际移植中容易踩的坑:
- 计数器位宽:硬件NVCounter的位宽决定了最大版本号。32位够用,但有些芯片只有16位甚至8位。提前确认。
- 原子操作:更新NVCounter时,要保证操作的原子性。如果更新到一半断电,下次启动时计数器状态不确定。硬件NVCounter通常自带原子性,软件NVCounter需要自己处理。
- 回滚保护与调试的矛盾:开发阶段频繁刷机,计数器涨得飞快。我建议在调试版本中关闭回滚保护,或者使用一个专用的调试计数器。量产版本再开启。
- 多镜像管理:如果系统有多个固件镜像(bl1、bl2、bl31、bl33),每个镜像是否需要独立的NVCounter?这取决于你的安全策略。我个人习惯是共享一个计数器,因为版本号是全局递增的。
嗯,关于NVCounter的原理和生命周期管理,就聊到这儿。这东西看着简单,但每个细节都关系到系统的安全性。希望你能在实际移植中少踩坑。