MCU程序加密保护系列(一):基于闪存读写保护的加密解密技术详解

MCU(微控制器单元)的加密方法可以从硬件、软件和通信协议三个层面来理解。以下是常见的MCU加密手段,按类型分类说明:

针对目前 STM32 系列微控制器在程序加密保护方面手段单一、保护效果有限的问题,本文介绍并分析了四种常用的程序保护方法:

  1. 闪存读写保护

  2. 芯片唯一 ID 验证

  3. 外接加密芯片

  4. 引导程序加密(Bootloader 加密)

实际在 STM32 开发板上的测试表明,合理组合多种保护机制,能显著增强芯片程序与数据的安全性,有效防止闪存程序被读取、拷贝或篡改,为嵌入式系统提供更可靠的安全保障。 

1 闪存读写保护法

STM32 微控制器提供了闪存读写保护的功能,

用来防止对闪存的非法访问。 闪存读写保护功能概述:

读保护(RDP)

  • 通过修改 Option Bytes 中的 RDP 位来启用。

  • 启用后,Flash 只能被内部正常执行的程序读取,无法通过 JTAG/SWD 调试接口或从 RAM 启动的程序读取。

  • 解除读保护会触发整片 Flash 擦除,防止逆向工程与调试工具破解。

  • 以 STM32L1 系列为例,RDP 分为 3 个等级,不同等级对应不同的安全强度。

  • 等级 描述
    0 无保护(默认)。可以通过 JTAG/SWD 或从 RAM 启动的程序任意读取 Flash。
    1 Flash 受保护,防止由调试器(JTAG/SWD)直接读取,也不能通过在 RAM 中加载执行程序来读取。
    解除 Level 1 保护将触发全片擦除。
    2 最严格保护——禁用所有调试功能。
    一旦设为 Level 2,不可复原,需谨慎使用。

    写保护(WRP)

  • 通过在 Option Bytes 中设置 WRP 位来启用。

  • 启用后,对被保护页的任何写或擦除操作都会被硬件阻止,并在状态寄存器中产生错误标志。

  • 可以防止恶意修改中断向量表或关键代码区域。

  • 优缺点对比:

    优点

    由硬件层面直接提供,成本为零,易于配置。

    能有效防止通过调试接口的非法读写

    缺点

    单一的读/写保护难以抵抗高级物理攻击或侧信道分析。

    需要与软件层面的校验(如 CRC、HASH)或更高级的加密方法(如外部加密芯片、Bootloader 加密)组合,才能构建更完善的防护体系。

    实施的方法如下,下面以 STM32G4 系列为例,分别介绍如何通过 ST-Link/CubeProgrammer 工具以及 运行时(HAL)编程 两种方式来启用 Flash 的读写保护(RDP 和 WRP)。

    方法一:通过 STM32CubeProgrammer 界面设置

    打开 CubeProgrammer

    启动 STM32CubeProgrammer,选择 “ST-Link” 连接。

    读取当前 Option Bytes

    在左侧菜单选择 “Option Bytes”

    点击 “Read”,查看当前 RDP(Read Protection)和 WRP(Write Protection) 配置。

    配置读保护(RDP)

    “Read protection” 下拉框中选择:

    Level 0:无保护

    Level 1:Flash 只允许内部执行读取

    Level 2:全部调试接口禁用(不可逆)

    推荐一般项目选 Level 1

    配置写保护(WRP)

    “Write protection” 区域,勾选你要保护的 Flash 扇区(Page)。

    比如要保护第 0~3 扇区,就勾选对应页号。

    写入并重启

    点击 “Apply”,CubeProgrammer 会提示需要复位芯片以生效。

    确认后,设备重启,Option Bytes 即更新完毕。

    方法二:在用户代码中动态配置(HAL 库示例)

    如果希望在程序首次运行时,或通过 Bootloader 进行保护设置,可以在固件内部调用 HAL API 来修改 Option Bytes。

    /**
     * @brief  启用 RDP Level1 并对指定扇区启用写保护
     * @note   必须在系统启动早期执行,且芯片复位后才生效
     */
    void Protect_Flash(void)
    {
        HAL_FLASH_Unlock();
        HAL_FLASH_OB_Unlock();
    
        /* 1) 读保护 → Level1 */
        FLASH_OBProgramInitTypeDef OBInit = {0};
        OBInit.OptionType = OPTIONBYTE_RDP;
        OBInit.RDPLevel   = OB_RDP_LEVEL_1;
        if (HAL_FLASHEx_OBProgram(&OBInit) != HAL_OK) {
            /* 错误处理 */
        }
    
        /* 2) 写保护 → 保护第 0~15 KB(页 0~15) */
        memset(&OBInit, 0, sizeof(OBInit));
        OBInit.OptionType = OPTIONBYTE_WRP;
        OBInit.WRPState   = OB_WRPSTATE_ENABLE;
        OBInit.WRPPage    = OB_WRP_PAGES0TO3   
                          | OB_WRP_PAGES4TO7   
                          | OB_WRP_PAGES8TO11  
                          | OB_WRP_PAGES12TO15;
        if (HAL_FLASHEx_OBProgram(&OBInit) != HAL_OK) {
            /* 错误处理 */
        }
    
        HAL_FLASH_OB_Lock();
        HAL_FLASH_Lock();
    
    //    /* 复位生效 */
    //    NVIC_SystemReset();
    }

     其次你会发现程序也烧写不进去。此时目标也达成。

    我们说了加密,那如何解密呢?问题在于每次切换RDPLevel 1到0的过程中的时候,会将flash的内容全部擦除。因此这是解密的难点之一。

    我们引入思考点一,针对闪存读写保护法这类内容也就是寄存器置位,如果你要切换RDP1到RDP0 的过程伴随着一个问题,STM32 上的 Read-Out Protection(RDP)并不是真正的“加密”,而是一种硬件级的“只读保护”机制——它保证 任何 试图“解开”这层保护的操作(也就是把 RDP 从 Level 1 降回 Level 0),都会触发一次整片 Flash 的 不可逆擦除,从而根本不可能在芯片上恢复出原来的固件!!!

    一旦启用 RDP Level 1,就不可能在器件内“解密”或“读取” Flash 内容;任何试图取消保护的行为都会把所有数据抹掉。

    为什么不能“解密”现有固件???

    STM32 的 RDP 设计上就是「不可逆保护」:降级就擦除。

    Flash 中的代码以明文形式存储,Option Bytes 保护只是阻止「直接」访问,但并不做真实的加密。

    一旦需要「访问明文」,只能通过内部执行(CPU 跳转),而不是通过调试器或外部读口。

    因此问题点来了!!!我们要做的是,在启用了 RDP Level 1 以后,所有基于 JTAG/SWD外部调试器 的直接读 Flash 操作都会被阻止——硬件层面上不可能再通过外部工具把 Flash 整片“拷”出来。不过,RDP Level 1 并不影响 MCU 自身程序对 Flash 的正常读取;换句话说,你只要在芯片内跑一段用户代码,通过 UART/CAN/USB 等总线把 Flash 内容“转发”出去,就能获得完整的二进制镜像。

    首先我们要将程序运行在SRAM中,根据自己的芯片主控观察大小,笔者这边是20KB的如下所示:

    因此我们的环境在keil,如何配置呢,跟着笔者一步一步来吧。

    第一步就是新建立一个模板了 如上所示,我们进行切换。

    其次就是宏定义了,加上下述内容!

    这边就是改配置内容了

    接下来就是这个内容的更改了,配置文件具体如何写如下所示:

    LR_IROM1 0x20000000 0x00005000  {    ; Total 20KB SRAM region
      ER_IROM1 0x20000000 0x00003000  {  ; Code section: 12KB
       *.o (RESET, +First)
       *(InRoot$$Sections)
       .ANY (+RO)
       .ANY (+XO)
      }
      RW_IRAM1 0x20003000 0x00002000  {  ; Data section: 8KB
       .ANY (+RW +ZI)
      }
    }
    

    其次就是这个仿真器命令文件了,如下所示:

    具体内容如下所示:

    /***********************************************************/
    /* Debug_RAM.ini: Initialization File for Debugging from Internal RAM         */
    /******************************************************/
    /* This file is part of the uVision/ARM development tools.                    */
    /* Copyright (c) 2005-2014 Keil Software. All rights reserved.                */
    /* This software may only be used under the terms of a valid, current,        */
    /* end user licence from KEIL for a compatible version of KEIL software       */
    /*development tools. Nothing else gives you the right to use this software  */
    /***************************************************/
    
    FUNC void Setup (void) {
    SP = _RDWORD(0x20000000); // 设置栈指针SP,把0x20000000地址中的内容赋值到SP。
    PC = _RDWORD(0x20000004); // 设置程序指针PC,把0x20000004地址中的内容赋值到PC。
    _WDWORD(0xE000ED08, 0x20000000);  // Setup Vector Table Offset Register
    }
    
    LOAD %L INCREMENTAL                    // 下载axf文件到RAM
    Setup();                           //调用上面定义的setup函数设置运行环境
    
    //g, main   //跳转到main函数,本示例调试时不需要从main函数执行,注释掉了,程序从启动代码开始执行

    最后就是debug处了。

    之后进入调试你会发现程序在SRAM的运行处。观察汇编代码。

    因此,程序成功在SRAM处运行。

    都要重新点击“Debug”按钮下载SRAM程序,再全速运行才能正常查看输出。

    接着就是写拷贝flash的内容到SRAM处通过UART转发出来。

    这是原本的程序地址所示如下了。

    如下是通过SRAM来拷贝Flash转发到uart的内容

    void dump_flash_via_uart(void)
    {
        char hex_str[8];
        for (uint32_t addr = FLASH_START_ADDR; addr < (FLASH_START_ADDR + FLASH_SIZE_BYTES); addr++) {
            uint8_t byte = *(volatile uint8_t*)addr;
            sprintf(hex_str, "%02X ", byte);  // 格式化成十六进制字符串
            HAL_UART_Transmit(&huart1, (uint8_t*)hex_str, strlen(hex_str), HAL_MAX_DELAY);
        }
    
        const char* end_msg = "\r\n[FLASH DUMP DONE]\r\n";
        HAL_UART_Transmit(&huart1, (uint8_t*)end_msg, strlen(end_msg), HAL_MAX_DELAY);
    }
    含义 十六进制值 小端转换(实际值)
    初始栈地址 MSP F8 04 00 20 0x200004F8
    Reset_Handler 05 01 00 08 0x08000105
    NMI_Handler? 5B 13 00 08 0x0800135B
    HardFault_Handler? C9 11 00 08 0x080011C9

    这些值完全一致!

    但是在WDP与DRP设置了之后。。

    📌 根本原因:

    ✅ 你能正常读取 Flash,是因为:

  • STM32 默认 RDP 是 Level 0(无保护)

  • 此时,即便运行在 SRAM,也能直接访问 0x08000000 地址处的 Flash 内容;

  • ❌ 设置 RDP Level 1 后:

  • Cortex-M3 核心(STM32F1)硬件禁止访问 Flash 内容

  • 这是芯片的硬件安全特性,即使代码跑在 SRAM,依然无法读取 Flash 数据,目的就是为了防止“SRAM 下载程序+读取 Flash 代码”这种逆向手段;

  • 因此上述破解方案无效,无法再从芯片内部获取任何 Flash 数据了,数据已经从访问层面彻底封锁。如果想切回回0,flash内容也会全部擦除!

    方法 原理 是否可行 备注
    芯片解封/酸蚀 + 显微探测 打开芯片封装,用电子显微镜直接读 Flash 电荷状态 ✔️理论可行 需要实验室级别设备、极高技术
    功耗分析(侧信道攻击) 用高精度设备分析芯片运行时功耗波动,推测程序数据 理论可行,但难度极高 适用于破解算法或加密密钥,不适合整段程序恢复
    激光/聚焦扫描干扰 用激光打干扰点,欺骗 Flash 控制器读取受保护数据 ✔️高端实验室可能可行 用于芯片逆向取证等高价值目标
    IC 解封 + Flash 直接读出 拆解裸片,对 Flash 数字结构逐位扫描 ✔️有可能,但需要芯片内部结构图 这属于“物理攻击”范畴

    ⚠️ 这些都需要 IC 物理逆向实验室、百万级设备成本,不具备民用可行性。

    手段 原理 应用场景
    X-Ray(X 射线扫描) 非破坏式探测芯片内部结构,识别 layout,理论可判断 Flash 存储 逆向分析芯片、找 ROM Mask
    开盖 + 紫外线(UV)曝光 对芯片裸露的 silicon 层曝光,激发电荷或使 EEPROM 状态改变 用于清除 EEPROM 或恢复旧锁定状态

    补充:

    STM32 跟一些 MCU(比如 AVR、ESP32)那种通过 熔丝(fuse)位 永久写死不同。

    STM32 的 Option Bytes 是可擦除/可重写的 Flash 区域

    只不过 RDP Level 2 一旦写入,就不可降级、无法还原;

    RDP Level 1 还可以通过 全芯片擦除 恢复成 Level 0(但数据没了)。

    作者:7yewh

    物联沃分享整理
    物联沃-IOTWORD物联网 » MCU程序加密保护系列(一):基于闪存读写保护的加密解密技术详解

    发表回复