MCU程序加密保护(二)ID 验证法 加密与解密

STM32 微控制器内部具有一个 96 位全球唯一的 CPU ID,不可更改。开发者可利用此 ID 实现芯片绑定和程序加密,增强软件安全性。
ID 验证法就是利用这个 UID,对每颗芯片的身份进行识别和绑定,从而防止程序被复制。
实现方式:

  1. 在程序首次运行或出厂时,读取 CPU ID,经过不可逆算法(如 CRC32、SHA256)处理后生成一个校验值;

  2. 将该校验值写入 Flash;

  3. 每次程序启动时重新计算当前芯片 ID 的校验值,并与 Flash 中存储的值进行比较;

  4. 如果匹配,程序正常运行;否则程序终止,防止非法拷贝。

三、这种方法的优点:

优点 说明
✅ 绑定芯片唯一身份 因为 UID 不可修改,确保每个程序绑定一颗芯片
✅ 防止直接拷贝 同样的程序拷贝到另一颗芯片上,会因 UID 不同而校验失败
✅ 可配合写保护 存放校验值的 Flash 区域可以配合 WRP 写保护
✅ 可量产 可以用上位机脚本自动读取 UID,计算校验值并写入 Flash

下面只讲一种方法,方法肯定是不唯一的!

cpp 复制代码
#define UID_BASE_ADDR       0x1FFFF7E8U
#define FLASH_CRC_ADDRESS   0x0800FC00U  // 最后一页(64KB Flash)起始地址
#define UID_WORD_COUNT      3
#define FLASH_PAGE_SIZE     1024U
#define CRC32_POLYNOMIAL    0xEDB88320U

int fputc(int ch, FILE *f)
{
    HAL_UART_Transmit(&huart1, (uint8_t *)&ch, 1, HAL_MAX_DELAY);
    return ch;
}

void send_string(const char* str)
{
    HAL_UART_Transmit(&huart1, (uint8_t*)str, strlen(str), HAL_MAX_DELAY);
}

void Print_UID(void)
{
    const uint32_t* uid = (const uint32_t*)UID_BASE_ADDR;
    char buf[64];
    snprintf(buf, sizeof(buf), "[STM32 UID] %08lX-%08lX-%08lX\r\n", uid[2], uid[1], uid[0]);
    send_string(buf);
}

uint32_t Calculate_UID_CRC32(void)
{
    const uint32_t *uid = (const uint32_t *)UID_BASE_ADDR;
    uint32_t crc = 0xFFFFFFFF;

    for (int i = 0; i < UID_WORD_COUNT; i++) {
        crc ^= uid[i];
        for (int j = 0; j < 32; j++) {
            if (crc & 1)
                crc = (crc >> 1) ^ CRC32_POLYNOMIAL;
            else
                crc >>= 1;
        }
    }
    return crc ^ 0xFFFFFFFF;
}

uint32_t Read_Stored_CRC(void)
{
    return *(const uint32_t *)FLASH_CRC_ADDRESS;
}

void Verify_UID(void)
{
    uint32_t calc_crc = Calculate_UID_CRC32();
    uint32_t stored_crc = Read_Stored_CRC();

    if (calc_crc != stored_crc) {
        char buf[128];
        snprintf(buf, sizeof(buf), "[ERROR] UID CRC Mismatch! Calc: 0x%08lX, Stored: 0x%08lX\r\n", calc_crc, stored_crc);
        send_string(buf);
        Error_Handler();
    } else {
        send_string("[INFO] UID CRC Verified OK\r\n");
    }
}

void Store_UID_CRC_To_Flash(void)
{
    uint32_t crc = Calculate_UID_CRC32();

    HAL_FLASH_Unlock();

    FLASH_EraseInitTypeDef EraseInitStruct = {
        .TypeErase   = FLASH_TYPEERASE_PAGES,
        .PageAddress = FLASH_CRC_ADDRESS,
        .NbPages     = 1
    };

    uint32_t PageError = 0;

    if (HAL_FLASHEx_Erase(&EraseInitStruct, &PageError) != HAL_OK) {
        send_string("[ERROR] Flash Erase Failed\r\n");
        HAL_FLASH_Lock();
        return;
    }

    if (HAL_FLASH_Program(FLASH_TYPEPROGRAM_WORD, FLASH_CRC_ADDRESS, crc) != HAL_OK) {
        send_string("[ERROR] Flash Program Failed\r\n");
        HAL_FLASH_Lock();
        return;
    }

    HAL_FLASH_Lock();
    send_string("[INFO] UID CRC Stored to Flash\r\n");
}

void Auto_Bind_UID_CRC_If_Needed(void)
{
    uint32_t stored_crc = Read_Stored_CRC();
    if (stored_crc == 0xFFFFFFFF)  // 判断是否首次上电
    {
        send_string("[INFO] No UID CRC Stored. Binding current chip...\r\n");
        Store_UID_CRC_To_Flash();  // 写入当前芯片 UID 的 CRC
    }
}

void SystemClock_Config(void);

int main(void)
{
    HAL_Init();
    SystemClock_Config();

    MX_GPIO_Init();
    MX_USART1_UART_Init();

    Auto_Bind_UID_CRC_If_Needed();
    Print_UID();
    Verify_UID();

    while (1) {
	        。。。。。。
		}
}

防止程序在"非原芯片"上运行

uint32_t calc_crc = Calculate_UID_CRC32();

uint32_t stored_crc = Read_Stored_CRC();

对 MCU 内置唯一的 UID 地址 0x1FFFF7E8 做了 CRC 校验,并将第一次计算的结果写入 Flash:

Store_UID_CRC_To_Flash(); // 用于首次存储 CRC

校验通过才能继续运行,否则 Error_Handler() 阻止执行。
这个机制可有效阻止程序被别人拷贝到其他 STM32 芯片后运行。

其次0xEDB88320U这个是干嘛的?

0xEDB88320UCRC32 多项式的反转形式(Reflected Polynomial) ,它是你代码中用于计算 CRC32 校验值的核心常数

什么是 CRC?

CRC(循环冗余校验)是一种常用于数据完整性校验的哈希算法,在嵌入式系统、通信、存储等领域被广泛使用。

它通过一个多项式除法运算对数据生成一个固定长度的校验值,用于检测数据在传输或存储中是否被篡改或损坏。

你用到的 0xEDB88320U 是什么?

这是标准 CRC32 算法中使用的 反射形式多项式

标准 CRC32 多项式(正向)

x32+x26+x23+x22+x16+x12+x11+x10+x8+x7+x5+x4+x2+x+1x^{32} + x^{26} + x^{23} + x^{22} + x^{16} + x^{12} + x^{11} + x^{10} + x^8 + x^7 + x^5 + x^4 + x^2 + x + 1x32+x26+x23+x22+x16+x12+x11+x10+x8+x7+x5+x4+x2+x+1

对应的二进制多项式值是:0x04C11DB7

反转版本(Reflected CRC):就是你代码中使用的

0xEDB88320\text{0xEDB88320}0xEDB88320

它等价于对 0x04C11DB7 的位反转处理。

STM32 的 HAL_CRC 或很多嵌入式代码实现中,都是按位反转(LSB first)方式计算 CRC ,因此使用 0xEDB88320 是标准做法。

这种反转方式效率高,和实际硬件或工业协议(比如 Ethernet、ZIP 文件、PNG 图片、STM32 Bootloader 校验)保持一致。

此外上述代码已经验证,拷贝后在别的扳机程序无法运行!达到效果!

但是!加密完总得自己解密哈哈,相比于闪存加密的方法,这个方法还有很多漏洞的,毕竟是软件加密,如下为漏洞分析!

安全目标 是否达成 原因
防止程序拷贝到其他芯片运行 ✅ ✅ ✅ UID 校验 OK
防止程序被他人反编译修改绕过校验 没有开启 RDP,程序可读出
防止校验值被改写绕过校验 没有开启写保护(WRP)
防止程序被整个二进制拷贝 RDP 未开启

此为量产方式

方案 需要写入 UID CRC 是否支持量产烧录器 是否支持自动化 推荐场景
A(运行时绑定) 不用提前写入,程序首次运行自动绑定 ✅ 支持全自动烧录 最推荐,适合量产
B(编译时绑定) 烧录前需写入 CRC ❌ 烧录前需人工或脚本操作 ✅ 有产测平台时适用 小批量

当然漏洞是有的,但是我们将闪存保护法跟id验证法放在一起,并且将下述方法加在一起,更多方法进行融合,达到真正的加密效果!

目前只是对 UID 本身做 CRC32,攻击者拿到程序和 CRC 算法很容易在自己芯片上重算并替换。
改进: 在计算 CRC 前,把 UID 跟一个"固化私钥"一起混合,再做 CRC。

cpp 复制代码
#define SECRET_KEY_WORD   0xA5A5BEEFUL

uint32_t Calculate_Mixed_CRC32(void)
{
    const uint32_t *uid = (const uint32_t*)UID_BASE_ADDR;
    uint32_t buffer[UID_WORD_COUNT + 1] = {
        uid[0], uid[1], uid[2], SECRET_KEY_WORD
    };
    uint32_t crc = 0xFFFFFFFF;
    for (int i = 0; i < UID_WORD_COUNT + 1; i++) {
        crc ^= buffer[i];
        for (int j = 0; j < 32; j++) {
            if (crc & 1)
                crc = (crc >> 1) ^ CRC32_POLYNOMIAL;
            else
                crc >>= 1;
        }
    }
    return crc ^ 0xFFFFFFFF;
}

结合 Option Bytes 自动锁定

Auto_Bind_UID_CRC_If_Needed() 中一旦绑定完成,即可顺带触发 RDP Level1 和写保护:

cpp 复制代码
void Secure_Lock_Device(void)
{
    FLASH_OBProgramInitTypeDef ob = {0};
    HAL_FLASH_Unlock();
    HAL_FLASH_OB_Unlock();
    HAL_FLASHEx_OBGetConfig(&ob);

    // 只在 Level0 时生效,避免重复擦除
    if (ob.RDPLevel == OB_RDP_LEVEL_0) {
        ob.OptionType = OPTIONBYTE_RDP | OPTIONBYTE_WRP;
        ob.RDPLevel   = OB_RDP_LEVEL_1;
        ob.WRPState   = OB_WRPSTATE_ENABLE;
        ob.WRPPage    = OB_WRP_PAGES60TO63;  // 保护 CRC 存储页
        HAL_FLASHEx_OBProgram(&ob);
    }

    HAL_FLASH_OB_Lock();
    HAL_FLASH_Lock();
}

使用硬件 CRC 外设或查表加速

软件 bitwise CRC32 性能偏低,可考虑:

STM32F1 的 CRC 外设(部分型号支持)

或者用 256 项的查表法 ,把 0xEDB88320 的查表数组预先生成,运算速度提升 5~10 倍。

cpp 复制代码
static const uint32_t crc32_table[256] = { /* 预生成的 256 项 */ };

uint32_t CRC32_Table(const uint8_t *data, uint32_t len) {
    uint32_t crc = 0xFFFFFFFF;
    while (len--) {
        crc = (crc >> 8) ^ crc32_table[(crc ^ *data++) & 0xFF];
    }
    return crc ^ 0xFFFFFFFF;
}
相关推荐
sinat_286945193 小时前
AI应用安全 - Prompt注入攻击
人工智能·安全·prompt
fengfuyao9853 小时前
STM32如何定位HardFault错误,一种实用方法
stm32·单片机·嵌入式硬件
爱学习的颖颖4 小时前
EXTI外部中断的执行逻辑|以对射式红外传感器计次为例
单片机·嵌入式硬件·exti中断
keer_zu5 小时前
STM32L051 RTC闹钟配置详解
stm32·嵌入式硬件
AI精钢5 小时前
H20芯片与中国的科技自立:一场隐形的博弈
人工智能·科技·stm32·单片机·物联网
etcix8 小时前
implement copy file content to clipboard on Windows
windows·stm32·单片机
谱写秋天8 小时前
在STM32F103上进行FreeRTOS移植和配置(STM32CubeIDE)
c语言·stm32·单片机·freertos
数据智能老司机11 小时前
实现逆向工程——理解 x86 机器架构
安全·逆向
数据智能老司机11 小时前
实现逆向工程——逆向工程的影响
安全·逆向
globbo12 小时前
【嵌入式STM32】I2C总结
单片机·嵌入式硬件