引言

嵌入式产品一旦量产,固件安全便成为核心问题。攻击者可能通过调试接口(如 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 级别,若未保护则主动设置。以下代码使用标准外设库实现:

#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 并存入数组:

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 为例:

#include "stm32f1xx.h"
#include <string.h>

// 预存校验码(在开发时计算并写入 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 函数中调用校验,若失败则停止运行或进入死循环:

int main(void) {
    // 系统初始化...
    
    if (!CheckUID()) {
        while(1);  // 非法芯片,停机
    }
    
    // 正常业务逻辑
}

完整示例:结合读保护与 UID 校验

以下是一个综合示例,演示如何初始化系统、启用读保护并执行 UID 校验:

#include "stm32f1xx.h"
#include <stdio.h>

// 唯一 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)等技术。希望本文能帮助你在产品中实现可靠的固件保护。