ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

STM32G0安全机制实践:从Flash保护到系统加固的完整指南

STM32G0安全机制实践:从Flash保护到系统加固的完整指南 在嵌入式开发圈子里聊到 STM32G0 系列大多数人的第一反应是“性价比高”“主频不高但够用”“低功耗表现不错”。但真正到了产品量产阶段尤其是做物联网节点、工业传感器、电池供电设备这类对代码保护和数据安全有要求的场景时光跑通功能是不够的。你写的固件、存的校准参数、OTA 升级流程都可能成为别人逆向分析或恶意篡改的目标。ST 官方那份《STM32G0 系列安全手册》我前前后后翻过好几遍也在一两个量产项目里把里面的机制逐个验证过。这篇文章算是我对这份手册的实践读笔记重点讲 G0 这颗芯片上到底有哪些安全机制、每个机制适合防什么人、实际配置时有哪些需要注意的坑。适合正在用 G0 做产品的开发者也适合刚接触 MCU 安全设计、想系统了解底层防护手段的朋友。1. STM32G0 的安全体系到底包含哪些东西先说一个容易被误解的点STM32G0 基于 Cortex-M0 内核不具备 TrustZone 扩展也没有很多高端芯片才有的专用加密引擎。但这不代表它在安全上一片空白。反过来看正因为内核简单、资源有限它的安全设计更加依赖“外围电路 系统级配置”的组合思路和带 TrustZone 的芯片完全不同。G0 的安全相关资源大致可以分成四层存储保护层包括 Flash 读保护RDP、写保护WRP、PCROP专有代码读保护、选项字节区域的保护。这一层解决的是“固件被读走”和“Flash 内容被改写”的问题。系统运行层包括 MPU内存保护单元、异常处理、堆栈检查等。这一层解决的是“运行时非法访问”和“程序跑飞后破坏关键数据”的问题。调试与接口层包括调试端口使能控制、调试接口密码保护、启动模式配置、复用功能锁定等。这一层主要防止别人通过调试器或者启动模式把固件提出来。设备身份层包括 96 位唯一设备 ID、CRC 硬件单元等。这一层用于设备认证和固件完整性校验是构建安全通信和 OTA 的基础。这里有个关键认知MCU 上的安全没有银弹。每一层机制解决的问题有限组合起来才能形成完整的安全边界。比如你只开了 RDP 但没关调试端口攻击者通过 SWD 接口照样能操作芯片开了 RDP 但没做固件完整性校验攻击者可以直接替换整个固件做伪冒设备。2. 存储保护是安全地基RDP、WRP、PCROP 的实际配置2.1 RDP 读保护你要清楚 Level 0/1/2 的真正含义Flash 读保护是 STM32 安全机制里最基础、也最容易配置错的一项。它通过芯片选项字节Option Bytes里的 RDP 位段控制分为三个等级Level 0无读保护调试接口可以完整访问 Flash这是芯片出厂默认状态。Level 1最常用级别。此时外部调试接口无法读取 Flash 内容但芯片内部程序可以正常访问。需要注意的是从 Level 1回退到 Level 0 会触发整片 Flash 擦除这是 ST 防降级攻击的核心设计。Level 2最高级别调试接口被永久禁止回退到低级只能通过整片擦除实际上等于不可逆而且芯片内部的某些调试能力也会被禁用。实际项目里大多数人选 Level 1。但有个细节经常被忽略Level 1 下如果程序里开启了特定的调试通道比如运行时不关闭调试端口攻击者依然可以通过侵入式手段探查 SRAM 内容。所以 Level 1 并不是绝对安全它防的是“插上 ST-Link 直接读 Flash”这种最常见的手段。配置 RDP 有两种方式一是量产时通过 STM32CubeProgrammer 图形界面或命令行写入二是在应用程序代码里通过修改选项字节实现。第二种方式在某些场景下很实用比如在产品首次上电时由 bootloader 自动将 RDP 从 Level 0 升到 Level 1避免额外产线步骤。// 通过 HAL 库操作选项字节将 RDP 设置为 Level 1 HAL_FLASH_Unlock(); HAL_FLASH_OB_Unlock(); FLASH_OBProgramInitTypeDef ob_config {0}; ob_config.OptionType OPTIONBYTE_USER; ob_config.RDPLevel OB_RDP_LEVEL_1; HAL_FLASHEx_OBProgram(ob_config); HAL_FLASH_OB_Launch();提一句操作选项字节前务必备份好当前配置因为一旦不小心把 RDP 设置成 Level 2芯片基本就宣告“调试死刑”只能通过整片擦除恢复而整片擦除后固件和数据全部丢失。我在一个量产项目里亲眼见过同事手滑把 Level 1 配成 Level 2结果整批 20 片样片全部回厂处理。2.2 WRP 写保护防止 Flash 区域被意外篡改WRPWrite Protection比 RDP 容易理解得多。它按页或按扇区设置从保护的角度来说是“禁止写和擦除”。配置了 WRP 的区域即使在程序运行期间也无法通过内部接口写入或擦除。WRP 的适用场景很明确Bootloader 区域防止应用程序异常时把 bootloader 覆盖掉。关键参数区域比如产品序列号、MAC 地址、校准数据这些数据一旦被破坏设备就可能直接变砖。OTA 回退保护区如果设计的是双 Bank 或双镜像升级方案需要确保回退镜像区域不会被异常升级流程覆盖。要注意的是WRP 对“读”没有限制它只限制写和擦除。如果你需要既防读又防写就得同时配置 RDP 和 WRP。这里有个实操经验STM32G0 的 Flash 是按页组织的不同子型号页大小不完全一样配置 WRP 时要先查对应型号的 Flash 编程手册搞清楚页起始地址和页大小。千万别拿着 G030 的页大小去配置 G070虽然都是 G0 系列但不同子系列的 Flash 容量和页结构差异还是有的。2.3 PCROP保护你的专有算法代码PCROPProprietary Code Read-Out Protection是容易被忽略但价值极高的保护机制。它允许你指定某段 Flash 区域为“专有代码区”这段区域在执行时可以被 CPU 读取但任何调试接口、DMA、其他总线主设备都无法读取而且不能通过外部手段回退来读出内容。什么场景需要 PCROP最常见的做法是把加解密算法、签名校验代码放到 PCROP 区域这样即使攻击者拿到了芯片通过调试接口也提取不到这段代码的二进制内容。对于做算法授权的公司PCROP 是成本最低的防抄板手段之一。配置 PCROP 需要通过选项字节中的 PCROP_RDP 位和 PCROP 地址范围寄存器来实现。注意一个关键点PCROP 区域的保护效果依赖于 RDP 等级。如果 RDP 是 Level 0PCROP 区域虽然不能被外部读取但调试状态下芯片内部行为会受影响通常建议 PCROP 和 RDP Level 1 配合使用。另外PCROP 区域的擦除也有讲究。不是说你加了解除保护命令就能随便擦实际操作上 PCROP 地址范围寄存器需要先解除对应保护才能擦除该区域内容否则会触发 Flash 错误。很多人在首次调试 PCROP 配置时把整个 Flash 都设为 PCROP结果发现自己程序里的常量全被保护了运行时各种 HardFault排查了半天才意识到是 PCROP 范围设得太大。2.4 选项字节的持久化与失效场景选项字节是存储保护配置的“控制中枢”它本身存储在专门的选项字节区域需要按特定时序写入并触发重载。G0 系列在写入选项字节后会进行一次系统复位使新配置生效。有几个失效场景值得警惕供电不稳定时写选项字节写入中途电压跌落可能导致选项字节数据半写状态芯片出现不可预知的行为。频繁改写选项字节选项字节区域也有擦写寿命限制频繁在测试流程里切换 RDP/WRP 配置会缩短其寿命。不检查操作结果HAL 函数执行完只检查返回值不够最好把选项字节回读出来确认写入值符合预期。我的量产脚本里每次都这么处理先读回当前选项字节状态再执行写入写完后重新上电再确认一次保护状态。这样虽然多花几秒但能最大限度避免把昂贵的产线板变成“电子垃圾”。3. 运行时保护MPU 和异常响应的正确打开方式3.1 MPU 的“土办法”防串改Cortex-M0 内核的 MPU 功能比 M3/M4 弱不少但依然能提供基础的区域访问控制。G0 上的 MPU 支持 8 个保护区域每个区域可以独立配置起始地址、大小、访问权限。对小型项目来说用 MPU 把关键外设寄存器区域、SRAM 中的关键变量区域设置为“只读”或者“特权访问”能显著提高系统稳定性。实际中我推荐至少配置两个 MPU 区域Flash 中的关键代码区保护 bootloader 所在区域防止应用代码异常时意外改写。SRAM 中的关键数据区保护上下文切换时不希望被随意访问的缓冲区。MPU 配置后要记得在 HardFault 处理函数里做日志记录。因为一旦 MPU 拦截了非法访问内核会产生 MemManage Fault如果处理函数里没有足够信息你根本不知道是哪个指针越界了。3.2 异常处理里的安全细节G0 的异常处理和 M0 内核一致。有个重要细节Cortex-M0 默认情况下所有异常都在线程模式和处理模式下执行异常优先级是固定编号的不存在可编程优先级除了某些特殊实现。这意味着什么意味着你没法像 M3 那样通过调整优先级来隔离安全关键代码的执行上下文。所以G0 的安全设计更依赖“代码层面的约定”关键操作如 Flash 写入、选项字节修改放在临界区保护中执行中断嵌套时避免重入。利用 SysTick 周期性检查关键变量的 CRC 校验值防止 SRAM 内容被异常程序破坏。如果使用了 RTOS注意任务栈溢出检测G0 的 SRAM 通常只有 8KB 到 36KB很容易出现栈溢出导致静默数据损坏。我在一个项目里遇到过很隐蔽的问题一个全局结构体被多个中断和主循环共用没有加临界区保护结果偶发地出现参数跳变。后来加上了 MPU 把结构体所在页设为只读配合中断中只做标志位置位、主循环里统一处理的方式问题才彻底消失。3.3 看门狗防止“跑飞后进入危险状态”严格来说看门狗不属于安全手册的核心内容但在很多安全导向的教程里它是标配。STM32G0 内置两个看门狗窗口看门狗WWDG和独立看门狗IWDG。对于安全设计我建议用 IWDG因为它由独立的 LSI 时钟驱动不依赖主系统时钟。即使主时钟因为外部干扰失效IWDG 依然在运行。窗口看门狗则需要更复杂的窗口范围配置适合对喂狗时间窗口有精确要求的应用。防跑飞的一个有效组合IWDG 关键代码区域 CRC 校验。在中断服务函数里周期性计算关键代码区域比如温度校准算法的 CRC如果结果异常立即进入安全状态不执行喂狗让系统复位。// 定期校验关键数据区 uint32_t crc_val HAL_CRC_Calculate(hcrc, (uint32_t *)key_data, KEY_DATA_SIZE); if (crc_val ! expected_crc) { error_handler(); // 进入安全状态停止喂狗 }4. 防调试与防探测把后门锁死4.1 调试接口保护不只是“关掉”而已在开发阶段调试接口是开发者的好帮手但在产品阶段它就成了攻击者的后门。STM32G0 默认在复位后 Debug 接口是开启的所以产品量产前一定要做处理。处理方式有三种将 RDP 设置为 Level 1 或 Level 2这是最彻底的方式。在使用完调试功能后在程序运行期间调用 DBGMCU 的配置寄存器禁止调试功能。硬件上把 SWD 引脚PA13/PA14在 PCB 上断开或通过三态门隔离只留产线测试点。比较推荐“软件封锁 量产后再硬件断开”的组合。为什么因为纯粹靠软件封锁不能防止攻击者通过复位后短暂窗口期进行操作。芯片复位后在 main 函数执行到封锁代码之前会有几微秒到几十微秒的时间窗口如果攻击者在这一窗口内发调试命令是可以读取部分内存的。硬件隔离彻底杜绝了这个窗口。4.2 启动模式与复用功能锁STM32G0 支持多种启动模式包括从主 Flash 启动、从系统存储器BootROM启动、从 RAM 启动。如果攻击者能够控制 BOOT0 引脚或选项字节中的启动配置理论上可以把芯片置于系统存储器启动模式绕过部分 Flash 保护机制。所以安全设计上建议在产品化阶段通过选项字节锁定启动配置不让外部引脚电平决定启动模式。在应用代码中检测启动引脚状态如果发现处于非预期状态进入安全处理流程比如擦除关键数据。复用功能锁定GPIO Lock是另一个容易被忽视的机制。STM32 允许将 GPIO 的复用功能配置锁定一旦锁定直到下次复位才能修改。对安全关键系统来说把 SWD 引脚、BOOT 相关引脚锁定到特定功能能有效防止攻击者在动态运行时篡改引脚功能。4.3 攻击模拟自己先当一回黑客写完安全配置后建议做一轮“自攻测试”用 ST-Link 尝试连接看 RDP Level 1 下能否访问 Flash。用 STM32CubeProgrammer 的 memory 读取功能尝试读 SRAM。通过脚本反复复位芯片探测复位后窗口期是否有调试访问机会。冷启动和热启动分别测试因为电压爬坡速度会影响安全模块的初始化时序。我自己的经验是这种自攻测试非常值得做。之前就有一次测试发现在 RDP Level 1 下复位后约 300 微秒内外部调试器是可以读取芯片 ID 和外设寄存器状态的这部分信息虽然不直接泄露出固件但能为攻击者提供有用的芯片上下文。后来我在应用启动代码里把 DBGMCU 的调试功能主动关闭窗口期基本消失。5. 安全启动与固件更新从 bootloader 开始守好5.1 信任根安全的 Bootloader 设计安全启动的核心思想是芯片复位后首先执行一段不可被篡改的引导代码由它校验主应用程序的完整性和来源然后才跳转执行。但 G0 没有专门的 ROM 信任根所以这个“信任根”就得放到自己的 bootloader 上。一个典型的 G0 安全启动链是这样的Flash 起始地址处放 bootloader比如 0x08000000 ~ 0x08003FFF16KB。Bootloader 硬件计算主应用区域的 SHA-256 摘要与主应用头部保存的摘要比较。用 RSA/ECDSA 验签如果无需非对称至少也要做 HMAC 校验。校验通过后设置 MSP 和 PC跳转主应用。主应用中再校验自己的运行时数据和关键参数区。为什么非要自己写 bootloader直接让 CPU 从复位向量跳到主应用不行吗可以但那样没有任何完整性校验攻击者完全可以替换整个 Flash 内容伪造一个“合法”固件。有了 bootloader每次启动至少能确认“我执行的代码是预先授权的”。G0 的 Flash 内存组织对 bootloader 设计其实很友好。它的 Flash 支持双 Bank 启动不同型号有差异可以做 A/B 分区升级。但要注意 M0 内核没有硬件浮点单元所以如果你想在 bootloader 里做 RSA 验签用纯软件算法会比较慢建议选择 ECDSA 或者用 CRC32 做轻量校验后配合安全传输通道而不是对每次启动做强验签。5.2 IAP 升级时的安全考虑在线应用编程IAP在 G0 上很常用但升级过程本身也是安全攻击的高发点。我见过的不少项目升级通道就是裸串口没有任何加密和校验这不是安全设计这是开玩笑。安全 IAP 的基本要求传输通道加密至少用 AES-CTR/AES-CCM 对固件内容加密密钥由设备唯一 ID 派生。完整性校验和验签接收完固件包后先算摘要验签通过后才写入 Flash。防回滚固件版本号存在受保护区域版本只能增加不能降低防止攻击者刷入带已知漏洞的老固件。升级失败处理保证任意时刻掉电都不会出现“半固件砖头”最好用双镜像或至少留下一个不变的 recovery bootloader。我在 G0 上实现过用设备唯一 ID 做 AES 加密的 OTA 方案。流程是云端用设备 UID 派生的密钥加密固件分片MCU 收到后先解密到 SRAM 缓冲区G0 的 SRAM 比较小需要分块处理每块解密后立刻计算 CRC全部完成后验签再写入 Flash。整个过程里 Flash 写入是最后一步避免把半加密的垃圾数据写进去。// 伪代码安全 OTA 分片写入 for (int i 0; i total_segments; i) { // 1. 接收密文段到接收缓冲区 // 2. AES-CBC 解密到解密缓冲区 aes_decrypt(receive_buffer, decrypt_buffer, segment_len); // 3. 计算本段 CRC uint32_t crc HAL_CRC_Calculate(hcrc, decrypt_buffer, segment_len); // 4. 写入 Flash先擦后写 flash_write(target_address i * segment_len, decrypt_buffer, segment_len); }这里有个细节每段写入后建议读回验证因为 Flash 编程偶尔会遇到一些“软错误”电压波动等原因导致写入位不完整读回可以在运行期发现并重试。5.3 随机数的获取策略安全启动和加密通信里随机数很重要比如 AES 密钥盐值、密钥分片向量。STM32G0 的硬件层面没有像 STM32L4 那样的真随机数发生器RNG外设。那怎么办M0 的种子随机源只能靠 ADC 噪声、内部温度传感器的低位抖动、或者时钟抖动。工程上比较可行的方法是结合 ADC 采集的内部参考电压或温度传感器噪声的最低几个有效位加上系统上电时序来构造种子。但这不是密码学意义上的“真随机”用于初始化伪随机序列生成器比如使用标准普尔测试通过的低资源 PRNG足够但不要拿它直接做加密密钥。如果项目对随机性要求高而且产品有通信能力可以在安全握手阶段从服务器下发随机数作为熵源的一部分或者搭配外置随机数芯片。选型时优先考虑带 RNG 外设的系列比如 STM32L4、STM32U5 等可能更好但如果你已经定了 G0就得接受现状从系统层面凑熵源。6. 低功耗模式下的安全盲区STM32G0 的低功耗能力是它的卖点但低功耗模式和安全性有时是冲突的。比如 STOP 模式下时钟被关闭但 SRAM 和 Flash 仍然保持供电调试接口在 STOP 模式下也可能仍然可用。如果你把 RDP 设为 Level 1理论上调试器无法读 Flash但 SRAM 里可能残留着敏感数据比如解密密钥解密后的临时明文、会话密钥。攻击者如果能进入 STOP 模式下的调试状态就有机会读取 SRAM 内容。所以安全设计里有一条容易被忽视的原则不在 SRAM 中长期保存密钥材料。用完即清零。对于必须保存在片内的长期密钥存入 Flash 的 PCROP 区域或受写保护区域只允许 CPU 访问。另外STOP 模式下的唤醒源要及时清理防止攻击者通过特定引脚唤醒后进入某些“调试状态”。建议在进入 STOP 模式前将调试接口的时钟和外设全部关闭并设置一个必须经过安全认证才能执行的关键操作流程。7. 常见问题与排查技巧实录做安全配置过程中会遇到很多奇奇怪怪的问题。这里整理一份速查表都是我自己踩过或帮别人排查过的。现象可能原因排查与对策设置 RDP Level 1 后发现连不上调试器这是预期行为RDP Level 1 下调试读 Flash 已被禁止不要慌需要全片擦除才能恢复如果固件已量产则确定要升级方案修改选项字节后芯片不进 main选项字节重载正常但如果你写错了地址或数据引导配置损坏用 STM32CubeProgrammer 连接读当前选项字节必要时用 BootROM 恢复MPU 配置后产生 HardFault禁止区域配置过大或权限冲突检查 MPU 区域优先级和子区域掩码善用 fault trace挂仿真器分析WRP 区域写操作失败WRP 只限制写如果没开保护也要检查地址是否越界查看 FLASH_SR 的错误位回读 WRP 配置确认PCROP 区域代码忽然 HardFault代码试图通过 DMA 或调试方式访问受保护区域确认 DMA 通道不指向 PCROP 区必要时用内存映射方式访问OTA 升级后设备无法启动升级过程未做完整校验或写入顺序不对检查固件头部 magic number 和版本使用双镜像或 recovery 引导低功耗模式后唤醒异常唤醒事件不处理导致系统流程不完整检查 EXTI 配置和唤醒后的时钟恢复流程必要时加看门狗调试器能连上但读 Flash 为空RDP Level 1 下调试会话打开时 Flash 是无法读取的不是“空”用调试器读 ID 确认连接再检查 RDP 等级把这些典型的坑整理出来开发的时候能少走很多弯路。8. 个人实操总结与建议STM32G0 的安全设计说简单不简单说复杂其实也就那些机制。真正要花心思的不是某个单独的保护点位而是整个系统的安全链从启动引导、到存储保护、到运行时行为、到升级流程每个环节都得有对应的控制手段。我个人的习惯是项目一开始就把安全策略写进设计文档别等产品快量产了才想起要加 RDP那时候改 bootloader、改启动流程成本非常高。安全配置要留后路。比如 RDP 设置 Level 1 但保留一个 bootloader 内部的“恢复模式”防止 OTA 失败后产品变砖。不要把安全做得太过火导致生产和运维没法做。Level 2 虽然最安全但如果产品需要返修更新就只能整片擦除产线流程图要提前规划好。密钥和证书等敏感信息一旦灌装到生产设备就得有一套完整的生命周期管理包括密钥轮换、吊销、设备回收后的擦除流程。这在工业场景里尤其重要。如果你刚开始接触 G0 的安全设计建议拿一块最小开发板先把 RDP、WRP、PCROP、MPU 这几个机制逐个试一遍熟悉配置流程和失效表现再决定要用在哪一层的产品上。这些机制虽然官方文档都有描述但不上手试一遍永远不知道自己的配置里面藏着什么样的低级错误。踩过几次坑之后你就会明白安全设计不是“加个锁”那么简单而是一整套和硬件特性紧密结合的系统工程。
返回列表