1. 为什么需要SBSFU:固件安全不是加一行读保护
1.1 一个让我印象深刻的翻车现场
之前做过一个表计类产品,硬件上其实是常见的组合:STM32L4主控加一个无线模块,固件里有一些校准参数和业务逻辑。当时的保护措施也很朴素——在STM32CubeProgrammer里把RDP(读保护)调到Level 1,然后就觉得固件安全“差不多得了”。
结果客户那边反馈有设备被“刷砖”。进一步排查后发现,有人通过调试接口把固件完整读了出来,改了里面的业务参数,再用自制工具重新烧回去。RDP并没有真正拦住这次攻击,因为攻击者根本没有去禁掉读保护,而是直接利用固件里一个不校验自身完整性的漏洞,把修改过的镜像当成合法固件运行了。
那次之后我才认真去研究安全启动和安全固件更新怎么做。最后落到实处的方案,就是ST官方推出的STM32Cube扩展包X-CUBE-SBSFU。它解决的核心问题可以浓缩成两句话:设备上电之后只运行经过签名的固件;固件升级时身份可信、内容完整、版本不可回退。
很多从普通裸机开发转过来的朋友,第一次听到“安全启动”会觉得这是车企或者支付终端才需要的东西。实际在IoT设备、传感器节点、表计、医疗手持设备这些场景里,固件被提取、被篡改、被恶意降级的风险远比想象中高。X-CUBE-SBSFU的定位不是“加一把锁”,而是给MCU加一道“安检门”,从复位那一刻起就开始检查。
1.2 SBSFU到底解决哪几个问题
X-CUBE-SBSFU的全称是Secure Boot and Secure Firmware Update,里面包含的不只是一个加密库,而是一整套方案:运行在Flash最前端的Secure Engine(以下简称SE)、安全启动加载器、密码学中间件,以及配套的PC端签名和加密工具。
具体解决的问题可以拆成四块。
第一是安全启动。芯片复位后,CPU不是直接跳去用户App,而是先运行SE_CoreBoot。这个启动代码会先校验自己所在分区的完整性,再校验用户固件是否由受信任的私钥签名。如果签名校验不过,用户App根本不会被加载执行。这相当于从硬件复位到进入业务代码之间,加了一道必须通过的完整性检查。
第二是安全固件更新。SBSFU支持把新固件下载到一个“待安装”分区,下载完成后进行验签和解密,确认无误后才提交为当前运行版本。配合双Bank分区表,可以实现A/B切换,新版本有问题还能自动回滚到上一版,而不是直接变砖。
第三是防回滚。攻击者如果拿不到签名私钥,还有个常见手段是把设备降级到旧版本,因为旧版本可能带着已知漏洞。SBSFU会把每个固件包的版本号记录在受保护的安全存储区,只允许从低版本升到高版本,不允许反过来。
第四是密钥管理。SBSFU会在芯片生命周期里生成或注入一组密钥,包括用于验签的根公钥、用于固件加密的密钥、以及SE内部使用的密钥。这些密钥的分工很明确,即使某一把泄露,也不会马上危及整个设备群。
1.3 哪些芯片、哪些产品值得用SBSFU
X-CUBE-SBSFU并不是所有STM32都能跑。它依赖硬件加密加速和Flash保护机制,官方支持的主要集中在STM32L4/L4+、L5、U5、H7系列,以及部分带TrustZone的Cortex-M33产品。
L5和U5这两条产品线对SBSFU更友好,因为Cortex-M33自带TrustZone,可以做到物理隔离的安全/非安全世界;L4和H7则依赖SBSFU的经典模式,用MPU和Flash保护来做隔离。对入门者来说,我建议如果只是想尽快跑通流程,选一块NUCLEO-L4A6ZG或者NUCLEO-L552ZE开发板都行,前者是标准SBSFU示例,后者是TrustZone版本示例。
产品场景上,SBSFU适合那些“固件本身有商业价值”或者“设备安全事件会造成实际损失”的产品。典型如智能表计、工业传感器、医疗监测设备、充电桩控制板、IoT网关。这些设备往往要现场或远程升级,而且一旦在野外被提取固件、伪造升级包,影响范围是整个产品线。
2. 第一关:把X-CUBE-SBSFU装起来并跑通官方Demo
2.1 获取扩展包的正确姿势
最省事的获取方式是在STM32CubeMX里打开“Manage embedded software packages”,选择你用的MCU系列,然后勾选X-CUBE-SBSFU安装。CubeMX会自动把它作为扩展包集成到工程里。如果不习惯用CubeMX做项目管理,也可以直接去ST官网或GitHub下载扩展包自解压压缩包,里面包含完整的工程源码、文档和脚本。
这里有个新手很容易卡住的依赖问题。安装扩展包时,CubeMX会提示类似这样的报错:
The firmware package (STM32Cube FW_L4 V1.8.7) or one of its dependencies require and will be replaced by ...
后面通常还跟着一堆版本号。这个报错的意思是:X-CUBE-SBSFU这个扩展包所依赖的某个中间件或驱动,和你当前电脑上已经安装的STM32Cube MCU固件包版本不兼容。CubeMX在安装时会强制切换到它要求的版本。这不是工程问题,而是本地包管理器里的版本冲突,解决办法也很简单:在同一个管理界面里,先把对应MCU系列的固件包更新到提示的版本,再重新勾选SBSFU。
2.2 编译和烧录的顺序不能乱
扩展包解压之后,目录结构里并不是只有一个工程,而是多个相互关联的工程,常见的有:
SE_CoreBoot:安全引擎工程,负责启动、验签、解密和跳转,独立编译。SBSFU_Boot:引导加载器工程,负责接收固件更新和启动流程的控制。App:示例用户应用工程,这个才是真正能被SBSFU验签后运行的业务固件。
遇到两个工程不是“谁先编译都行”的关系。烧录顺序上要严格遵守:先烧SE_CoreBoot,再烧SBSFU_Boot(如果使用官方入门脚本,可能会自动处理),最后烧App。因为SE_CoreBoot是整条信任链的根,它必须先存在于Flash最前端才能引导后面所有步骤。
在NUCLEO-L4A6ZG上,官方示例目录是Projects/STM32L4xx-Nucleo/Applications/SBSFU。用IAR、Keil或STM32CubeIDE打开对应工程后,可以按照README里的顺序逐个编译。我第一次跑的时候图省事,只编译了App就下载,结果上电后串口没有任何输出,折腾了半天才发现SE_CoreBoot根本没烧进去。
2.3 第一次上电看到什么才算成功
烧录完成后,把开发板连接到PC的串口,官方示例通常会通过UART打印启动日志。成功的话,日志会依次显示安全引擎版本、固件验签结果、跳转信息,最后进入用户App的启动打印,比如“SBSFU Application running”。
看到类似的日志,说明整条信任链已经被验证通了。这一步的意义很大:你已经证明MCU从复位开始,先经过SE_CoreBoot检查,再加载签名固件,整个流程是通的。后面所有产品化改动,都基于这个最小可信链路展开。
如果串口没有输出,优先检查三件事:板子的BOOT0引脚是否处于正常启动模式,ST-LINK是否能够正常连接目标芯片,以及烧录脚本是否真的把SE_CoreBoot写入到了0x08000000开头的位置。很多“变砖”其实不是真的砖了,而是启动顺序或者烧录地址不对。
3. SBSFU启动机制拆解:一台上电就“过安检”的MCU
3.1 从复位向量到Secure Engine的完整时序
SBSFU的想法很难用一句“固件校验”概括,我建议把它理解成一条信任链。
MCU复位后,CPU从0x08000000取第一条指令。正常情况下这是用户的复位向量,但在SBSFU方案里,这个地址存放的是SE_CoreBoot。它先初始化时钟、MPU和Flash保护策略,随后对自己的代码区域做一个完整性校验。如果自身被破坏,它会停止在当前状态或者进入恢复模式,不会继续往下走。
SE_CoreBoot自检通过后,真正的工作才刚开始:计算待运行固件分区的哈希值,用预置的公钥验证固件签名。验签通过后,再根据配置决定是否需要解密固件内容。SBSFU对固件加密用的通常是对称算法,比如AES-GCM或AES-CCM,加解密密钥由根密钥派生而来。AES的密钥不会以明文形式存放在Flash里,而是存储在被保护的安全数据区,由SE在启动时解密使用。
这套流程最反直觉的地方在于“慢”。MCU上电后不能立刻跑业务代码,要先花几十到几百毫秒做验签和解密。对绝大多数传感器、表计类应用来说完全可接受,但如果你的产品对启动时间极其敏感,比如需要在5毫秒内响应外部事件,就需要仔细评估这部分开销,或者考虑让SE验证之后把控制权快速交接。
3.2 签名、加密和防回滚的关键密钥链
SBSFU的密钥体系是看代码时最容易懵的地方。它不是一个密钥走到底,而是做了分层。
最上层是根密钥对,私钥保存在产线或开发者手里,公钥烧录进芯片的受保护区域。根密钥负责验证用户固件的签名,只要私钥不泄露,攻击者就算拿到了完整固件也无法伪造新版本。第二层是固件加密密钥,用于对固件体本身做机密性保护。第三层是SE内部使用的密钥,用于保护安全数据区和版本号存储区。
不同版本的SBSFU密钥命名会略有差异,但核心思想一致:签名用的公钥可以公开,私钥绝对不进入芯片;加密密钥可以进入芯片,但必须受Flash保护策略约束;版本号存储区要防止被直接改写,所以放在带写保护的页里。
举个例子,攻击者从设备里物理读取Flash,拿到了用户固件,他能得到什么呢?如果固件是加密的,看到的是密文;即使他反向解出明文固件,由于没有根私钥,他修改任何一字节后,SE验签就会失败,设备拒绝启动。这就解释了为什么SBSFU即使被提取固件,“刷第三方固件”这条路也走不通。
3.3 标准版本和TrustZone版本怎么选
SBSFU在同一系列下往往存在两个分支,一个叫“标准版本”,一个叫“TrustZone版本”。选错分支是不少初学者会遇到的问题。
标准版本依赖经典Cortex-M架构的MPU和Flash保护,不需要TrustZone支持。它把安全引擎和用户固件放在同一个物理地址空间里,靠链接脚本划分边界。这种实现简单直接,也便于从老项目迁移,L4和H7系列基本走这个路线。
TrustZone版本则针对Cortex-M33处理器,使用ARM TrustZone技术把系统的安全世界和非安全世界真正隔离开来。安全引擎跑在安全世界,用户应用跑在非安全世界。这两个世界有独立的堆栈、外设和中断配置,即使非安全世界代码被攻破,也碰不到安全世界里的密钥和关键资源。L5、U5系列带有这种能力。
对刚入门的人来说,先从标准版本开始通常更容易理解。如果你最终选型就是L5,那也别怕,直接用官方TrustZone示例工程跑一遍,配置都帮你做好了,重点看安全世界和非安全世界是怎么划分的。
4. 把SBSFU接入自己的工程:分区、配置、签名固件
4.1 Flash分区规划和中断向量偏移
把官方Demo跑通只是第一步,真正开始改自己的板子和业务代码时,第一个绕不开的就是Flash分区。
SBSFU要求Flash里为多个区域分配固定地址:SE_CoreBoot占据最低地址,接着是SBSFU_Boot(有的版本合并在SE区域),然后是用户固件的运行槽位、下载槽位,以及保存安全数据和版本号的区域。以一个典型1MB Flash的STM32L4为例,可以这样规划:
| 区域 | 起始地址 | 大小 | 说明 |
|---|---|---|---|
| SE_CoreBoot | 0x08000000 | 64KB | 安全引擎 |
| SBSFU_Boot | 0x08010000 | 64KB | 引导加载器(按版本可选) |
| Slot 0 | 0x08020000 | 384KB | 当前运行固件 |
| Slot 1 | 0x08080000 | 384KB | 待安装固件 |
| 安全数据区 | 0x080E0000 | 32KB | 版本号、密钥状态 |
| 备份/应用区 | 0x080E8000 | 剩余 | 用户数据和日志 |
具体地址必须和你选用的MCU容量、SBSFU配置头文件保持一致,不能用表里的数字直接套到所有芯片上。这里的核心原则是:SE_CoreBoot必须在0x08000000,用户App必须放在Slot 0或Slot 1,App链接脚本里的FLASH起始地址必须指向对应槽位。
这里就引出最经典的翻车点:用户App的中断向量表偏移。烧进Slot 0的App,复位向量不再是0x08000000,而是Slot 0的起始地址。启动时CPU默认从0x08000000取向量表,所以必须在App初始化代码里设置VTOR寄存器,让异常向量表指向Slot 0。具体做法是在编译链接后,把SCB->VTOR的值改为槽位起始地址,或者在startup汇编文件启动阶段就完成这个设置。我第一次移植到自己的板子时忘了做这件事,现象是SE日志显示验签通过,跳转后却直接HardFault,查了整整一个下午才定位到是向量表没偏移。
4.2 通过STM32CubeMX配置SBSFU参数
如果在CubeMX中勾选了X-CUBE-SBSFU,它会生成一批配置文件,包含SBSFU工作模式、串口调试开关、密钥索引、版本号等关键参数。这里最常见的需求是改固件版本号:每次发布新App,必须递增版本号。如果版本号不变,SBSFU会认为这是同一个固件,可能直接跳过更新或因为防回滚机制拒绝加载。
另外一个需要关注的是“当前有效槽位”配置。SBSFU支持两种升级方式:原地升级和双Bank切换。原地升级把新固件覆盖到当前槽位,升级过程中断电有变砖风险;双Bank切换则先把新固件下载到备用槽位,校验通过后再切换启动地址,更安全,但需要预留和Slot 0等大的Flash空间。
建议在自己的产品里优先选择双Bank模式。虽然多占用一倍的用户固件空间,但换来的是升级过程中设备始终有一个可用的固件,极大降低了现场运维压力。如果Flash容量紧张,也可以用CRC/备份头等手段做断点续传,但复杂度会明显上升,不建议入门阶段折腾。
4.3 制作一包可被SBSFU接受的签名固件
编译生成的App二进制文件,不能直接烧进Flash。它必须经过SBSFU配套的PC端工具处理,加上固件头、签名、加密信息,最终产出一个带.sfb后缀或者类似格式的固件包。
这个过程一般由脚本完成。官方包里的脚本会读取App的bin/hex文件,利用预先生成的根私钥计算签名,同时用固件加密密钥做AES加密,最后拼接出包含版本号、目标槽位、签名信息、固件体长度的新文件。命令行执行的大致形式类似于:
python sbsfu_sign.py --key root_private_key.pem --fw-version 3 \ --slot 0 --in app.bin --out app.sfb我在实际项目里会把这条命令包在一个Makefile或者批处理脚本里,编译完App之后自动执行,避免手工敲错参数。另一个值得养成的习惯是:签名用的私钥文件不要放到代码仓库里,更不要跟着项目一起发给外包或者上传到公开托管平台。私钥泄露意味着攻击者可以直接给恶意固件签名,SBSFU的防护价值也就归零了。
签名固件生成后,烧录顺序同样重要:SE_CoreBoot先烧进去,再把签名后的App下载到对应槽位。如果直接烧原版App而不烧SE,SBSFU是永远不会去执行它的。
5. 实测固件升级流程和典型故障排查
5.1 用Ymodem走一遍完整升级
官方示例的用户App里通常内置了一个简单的升级Demo,支持通过UART接收新固件。最常见的传输协议是Ymodem,因为Tera Term、HyperTerminal、以及很多串口助手都原生支持。
我建议动手做一次完整升级来建立手感。先编译两个不同版本的App,比如一个打印Version 1,一个打印Version 2。把Version 1作为当前固件烧录进去,启动后串口应该显示Version 1。接着在PC端用Tera Term打开对应串口,触发App里的升级命令,然后发送Version 2的.sfb文件。
升级文件传输完成后,SBSFU不会立刻跳转,而是先做校验:验签、比对版本号、确认目标槽位可用,全部通过后才把新固件标记为有效。重启后你会看到串口打印变成了Version 2。整个过程如果任何一个环节失败,SBSFU都会拒绝切换,并保留原固件继续运行。
这种“先验后切”的机制,正是SBSFU和普通IAP最大的区别。普通IAP一般把数据搬进Flash就跳转,电量低、传输中断、数据被篡改都可能导致设备变砖。SBSFU则是在下载阶段就做“安检”,不合格的包根本不会成为候选固件。
5.2 五个高频故障现象与处理
梳理一下入门阶段最常见的五个问题,都是我实测或者被同事问过的,可以直接对着排查。
| 现象 | 可能原因 | 处理办法 |
|---|---|---|
| 串口没有任何输出 | SE_CoreBoot没烧进去或烧错地址 | 用CubeProgrammer确认0x08000000起始区域存在代码,重新烧录 |
| 验签通过但跳转后就HardFault | App中断向量表没偏移到Slot地址 | 在App启动代码设置SCB->VTOR指向Slot起始地址 |
| 新固件下载完重启后还是旧版本 | 版本号没有递增,防回滚拒绝加载 | 重新签名固件,把版本号调到高于当前版本 |
| 升级过程中提示签名校验失败 | 签名私钥和芯片中的根公钥不匹配 | 检查PC端签名工具使用的密钥文件是否和烧录进芯片的公钥成对 |
| CubeMX安装扩展包时报依赖版本错误 | 扩展包要求的中间件版本和本地固件包版本冲突 | 在CubeMX软件包管理器中把MCU固件包升/降到提示版本 |
这些坑很多不是SBSFU本身的问题,而是工程配置和开发习惯的问题。尤其是向量表和密钥匹配这两类,几乎每个人都会踩一次。
5.3 被Flash保护折磨过之后:调试接口的取舍
接入SBSFU之后,Flash保护等级通常会被设置为Level 1甚至Level 2。Level 1下调试器还能通过SWD连接,但读取Flash会受到限制;Level 2则基本永久禁用调试口,主要用于成品。
开发阶段如果反复连接不上调试器,不要慌。先用STM32CubeProgrammer尝试“Connect Under Reset”,也就是在复位信号保持期间建立连接。如果还不行,可以用CubeProgrammer的“Full Chip Erase”来降低保护等级,但要注意这会擦掉整片Flash,也包括你刚烧进去的SE_CoreBoot和密钥,需要重新烧录。
这里我个人的建议是:开发阶段不要急着把保护等级调到最高,先把SBSFU的功能逻辑调通;功能稳定后再烧录到另一片样机上验证正式保护等级。否则每次改App都要经历一次擦除重烧,浪费时间也容易磨损Flash。等量产前,再根据安全需求决定用Level 1还是Level 2。
产品化还有一个容易被忽略的隐患:如果密钥在开发阶段已经被烧进样机,而你在后续测试中反复做全片擦除,密钥会被清掉。建议在开发板上预留一个“重新注入密钥”的脚本,否则后续测试会因为根公钥和签名私钥不匹配而失败。
6. 从入门到落地:一些过来人的建议
6.1 开发密钥量产密钥分离
上手阶段,大家一般都用官方示例自带的测试密钥。测试密钥没问题,千万别把测试密钥直接带到量产固件里。
正规做法是准备两套独立生成的密钥:一套开发密钥,用于公司内部测试机;一套量产密钥,用于产线烧录和后续OTA签名。量产私钥只掌握在受控的发布角色手里,编译服务器、测试环境、现场运维人员都接触不到。签名操作放到一个独立的发布流程里,比如CI/CD里专门跑一个签名任务,产物才允许进入量产固件目录。
这样做的好处很实际:即使开发阶段密钥意外泄露,最多影响测试设备,不会让整个已出货产品线都暴露在伪造固件风险下。如果所有阶段共用一套密钥,一旦泄露,就只能在所有存量设备上做一次远程固件更新回收信任链,代价非常大。
6.2 SBSFU不是保险箱
把SBSFU接上之后,不要以为设备就安全了。它解决的是固件启动和升级这两个环节的可信问题,而不是物理接触攻击的全部场景。
硬件攻击者如果愿意花成本,还可以通过侧信道分析、激光注入、甚至直接剥片读Flash等手段尝试获取明文密钥。SBSFU能做的,是通过安全启动、加密存储和防回滚把攻击门槛抬高,让绝大多数“低成本批量破解”变得不划算。
所以你的产品安全设计应该是一整套组合,而不是只依赖一个扩展包。比如:
- 关闭或限制调试口,生产完成后把RDP等级提升到Level 1或更高;
- 避免在固件里硬编码长寿命密钥,尽量使用每个设备唯一的安全密钥;
- 云端和设备端之间还要有独立于SBSFU的传输层加密;
- 固件内敏感参数即使被读取,也不应该能直接指导攻击者利用漏洞。
SBSFU是整个信任链的地基,但地上建筑还是需要自己一层层搭。
6.3 推荐的学习路径
如果有人问我SBSFU怎么学最不容易劝退,我给的路线通常是这样:
第一步,用官方开发板把Demo完整跑通,看到启动日志和App运行再继续;第二步,改用户App里的打印信息,重新签名、烧录,观察SBSFU版本号变化和启动过程;第三步,在自己选型的板子上做分区规划,把SBSFU移植过去,重点验证Flash地址和向量表偏移;第四步,做一次完整的UART升级,体验下载、验签、切换的闭环;第五步,再回头研究密钥管理、量产注入和云端OTA流程。
这个顺序的核心逻辑是:先把信任链跑起来,再逐步增加复杂度。很多人一上来就深入研究密钥树和代码级防护,结果被各种底层概念淹没。实际上,X-CUBE-SBSFU的设计目标就是让你在不了解全部密码学细节的前提下,也能搭出安全启动框架,后续再慢慢补齐原理。
我第一次跑通SBSFU的时候,卡得最久的就是没搞懂为什么Bootloader总是跳不到用户App,后来才发现是中断向量表偏移没设好。如果你也在入门这个扩展包,不用太过焦虑,先按“官板Demo → 自己的App → 自己的板子 → 完整升级”这条路径走一遍,把串口日志和烧录顺序掌握熟练,后面再向密钥管理和TrustZone深入,会顺畅很多。