## 引言 嵌入式产品一旦量产,固件安全便成为核心问题。攻击者可能通过调试接口(如 SWD/JTAG)直接读取 Flash,或者通过复制芯片和固件来仿制产品。STM32 提供了硬件级别的安全机制,包括读保护(RDP)和唯一 ID(Unique ID),合理组合使用可以有效提升破解难度。本文将从原理到实践,带你构建固件加密与防破解的双重防线。 ## 原理篇:STM32 的安全机制 ### 1. 读保护(RDP) 读保护是 STM32 内嵌 Flash 模块提供的一种访问控制机制,通过设置选项字节(Option Bytes)中的 RDP 级别来限制调试接口和内置程序对 Flash 的访问。 - **Level 0**:无保护,所有访问均允许。 - **Level 1**:禁止调试接口(SWD/JTAG)读取 Flash,但程序仍可正常执行。这是最常用的保护级别。 - **Level 2**:最高保护,禁止所有外部访问,且无法降级,芯片相当于永久锁定。适用于对安全要求极高的场景,但需谨慎使用,因为一旦设置无法恢复。 注意:从 Level 1 降级到 Level 0 会触发 Flash 全片擦除,防止固件被读取。 ### 2. 唯一 ID(Unique ID) 每颗 STM32 芯片在出厂时都有一个 96 位的唯一标识符(UID),存储在特定地址(如 0x1FFFF7E8)。该 ID 无法修改,可用于软件层面的身份验证。通过将固件与特定芯片绑定,即使固件被完整复制,也无法在其他芯片上运行。 ## 配置步骤:启用读保护 ### 方式一:使用 STM32CubeProgrammer 1. 连接开发板,打开 STM32CubeProgrammer。 2. 在 "Option Bytes" 选项卡中,找到 "Read Protection" 设置。 3. 选择 "Level 1",点击 "Apply"。 4. 若需解除保护,选择 "Level 0",软件会提示将擦除 Flash,确认后执行。 ### 方式二:通过代码设置(以 STM32F1 为例) 在程序启动阶段,可以检查当前 RDP 级别,若未保护则主动设置。以下代码使用标准外设库实现: ```c #include "stm32f1xx.h" void EnableReadProtection(void) { FLASH_Unlock(); // 解锁 Flash 寄存器 // 检查当前 RDP 级别 if (FLASH_GetReadOutProtectionStatus() == DISABLE) { // 设置 RDP 为 Level 1 FLASH_ReadOutProtection(ENABLE); } FLASH_Lock(); // 重新锁定 Flash } ``` 注意:设置 RDP 后,调试器将无法读取 Flash,但程序运行不受影响。若需调试,请先解除保护(会擦除 Flash)。 ## 唯一 ID 校验实现 ### 读取唯一 ID STM32 的唯一 ID 由三个 32 位字组成,起始地址因系列而异。例如: - STM32F1/F4:0x1FFFF7E8 - STM32L0/L4:0x1FF80050 以下函数读取 UID 并存入数组: ```c void GetUniqueID(uint32_t *uid) { uid[0] = *(volatile uint32_t *)0x1FFFF7E8; uid[1] = *(volatile uint32_t *)0x1FFFF7EC; uid[2] = *(volatile uint32_t *)0x1FFFF7F0; } ``` ### 校验策略 常见做法是将 UID 与预存的密钥进行哈希或加密运算,生成校验码。这里以简单的 CRC32 为例: ```c #include "stm32f1xx.h" #include // 预存校验码(在开发时计算并写入 Flash) uint32_t stored_crc = 0x12345678; // 示例值 uint32_t CalculateCRC(const uint32_t *data, uint32_t len) { // 使用硬件 CRC 或软件实现,此处简化 uint32_t crc = 0xFFFFFFFF; for (uint32_t i = 0; i < len; i++) { crc ^= data[i]; for (int j = 0; j < 32; j++) { if (crc & 0x80000000) { crc = (crc << 1) ^ 0x04C11DB7; } else { crc <<= 1; } } } return ~crc; } int CheckUID(void) { uint32_t uid[3]; GetUniqueID(uid); uint32_t crc = CalculateCRC(uid, 3); return (crc == stored_crc); } ``` 在 main 函数中调用校验,若失败则停止运行或进入死循环: ```c int main(void) { // 系统初始化... if (!CheckUID()) { while(1); // 非法芯片,停机 } // 正常业务逻辑 } ``` ## 完整示例:结合读保护与 UID 校验 以下是一个综合示例,演示如何初始化系统、启用读保护并执行 UID 校验: ```c #include "stm32f1xx.h" #include // 唯一 ID 地址(根据芯片调整) #define UID_BASE 0x1FFFF7E8 // 预存校验码(开发时生成) #define STORED_CRC 0xA5A5A5A5 void SystemClock_Config(void); void EnableReadProtection(void); uint32_t CalculateCRC(const uint32_t *data, uint32_t len); int CheckUID(void); int main(void) { HAL_Init(); SystemClock_Config(); // 启用读保护(仅首次运行时需要,之后每次启动检查) EnableReadProtection(); // UID 校验 if (!CheckUID()) { // 非法芯片,进入死循环 while(1); } // 用户代码... while(1) { // 主循环 } } void EnableReadProtection(void) { // 使用 HAL 库示例 if (HAL_FLASH_GetReadOutProtection() == OB_RDP_LEVEL_0) { HAL_FLASH_Unlock(); HAL_FLASH_OB_Unlock(); // 设置 RDP Level 1 HAL_FLASH_OB_RDP_LevelConfig(OB_RDP_LEVEL_1); HAL_FLASH_OB_Launch(); HAL_FLASH_OB_Lock(); HAL_FLASH_Lock(); // 注意:设置后系统可能复位,需重新启动 NVIC_SystemReset(); } } uint32_t CalculateCRC(const uint32_t *data, uint32_t len) { // 软件 CRC32 实现(略,可参考上文) // 此处省略具体代码,建议使用硬件 CRC 外设 return 0; } int CheckUID(void) { uint32_t uid[3]; uid[0] = *(volatile uint32_t *)UID_BASE; uid[1] = *(volatile uint32_t *)(UID_BASE + 4); uid[2] = *(volatile uint32_t *)(UID_BASE + 8); uint32_t crc = CalculateCRC(uid, 3); return (crc == STORED_CRC); } ``` ## 注意事项 - **读保护级别选择**:Level 1 适合大多数产品,Level 2 不可逆,务必确认需求。 - **UID 校验的局限性**:攻击者可以 patch 掉校验代码,因此建议将校验逻辑分散在多个位置,并加入反调试技巧。 - **加密算法**:CRC 仅用于示例,实际应使用强哈希(如 SHA-256)或加密算法(如 AES)结合密钥。 - **烧录流程**:量产时,先烧录固件,再设置读保护。若先设置保护,则无法再烧录。 - **调试影响**:设置 Level 1 后,调试器无法连接,需通过解除保护(擦除 Flash)才能重新调试。 - **芯片差异**:不同系列 STM32 的 UID 地址和 RDP 寄存器可能不同,请参考对应参考手册。 ## 总结 通过读保护与唯一 ID 校验,可以构建基础但有效的固件安全防线。读保护阻止了静态读取,而 UID 校验则防止了固件移植。对于高安全需求,建议结合加密存储、安全启动(Secure Boot)等技术。希望本文能帮助你在产品中实现可靠的固件保护。