ARTICLE DETAIL

资讯详情

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

STM32F103程序加密保护实战:从硬件RDP到软件AES的嵌入式安全方案

STM32F103程序加密保护实战:从硬件RDP到软件AES的嵌入式安全方案 简介本资源是一套面向嵌入式开发初学者与进阶工程师的STM32F103程序加密保护综合实验例程聚焦固件安全防护实践涵盖Bootloader定制、Flash写保护配置、AES软件加解密集成及时间戳密钥生成等核心场景适用于工业控制、物联网终端等对代码防逆向有明确需求的项目开发。压缩包共122个文件含58个头文件.h定义外设驱动与加密接口56个源文件.c实现RTCDS1302、HS0038红外解码、SysTick精准定时及Flash加密加载逻辑另有Keil工程文件.uvprojx/.uvoptx、编译脚本.bat、HEX固件及硬件连接示意图.png总大小395KB结构清晰、模块解耦便于分步调试与功能裁剪。已有177人学习下载提供完整可运行工程框架、关键加密流程注释详尽、外设驱动与安全机制协同实现助读者深入理解STM32底层安全机制并快速迁移至实际产品开发。1. 项目背景与核心需求为什么你的STM32程序需要加密最近在整理一些老项目的资料翻到了一个名为“基于STM32f103单片机程序加密保护实验软件例程源代码.rar”的压缩包。这让我想起了几年前一个朋友公司产品被抄袭的糟心事。他们花了大半年时间开发的一款基于STM32F103的工业控制器刚上市没多久市面上就出现了功能几乎一模一样的“山寨版”价格却只有一半。后来发现问题就出在程序保护上——他们的产品固件被轻易地通过调试接口读取并复制了。这件事在圈子里其实并不少见。很多工程师尤其是刚入行的朋友往往把全部精力都放在了实现功能上认为“代码能跑起来”就是终点。对于STM32这类通用MCU我们默认通过J-Link、ST-Link或者串口ISP就能轻松下载程序却很少反过来想别人是不是也能用同样的方式把我辛辛苦苦写的代码“偷”走这个“加密保护实验”的例程包其核心价值就在于此它不是一个炫技的功能而是一个产品从实验室原型走向商业化市场所必须考虑的“防盗门”。STM32F103作为经典的Cortex-M3内核单片机因其性价比高、生态完善被广泛应用于消费电子、工业控制、物联网设备等各个领域。也正因为它太常见了其程序的安全性往往被忽视。攻击者获取你硬件的手段太多了从市场上买一个你的成品拆下芯片通过简单的飞线连接到编程器上就可能读出完整的Flash内容。更不用说如果产品留有未禁用的调试接口如SWD那几乎就是不设防的状态。所以这个实验例程要解决的绝不是一个“可有可无”的技术点。它针对的是以下几个刚需场景保护知识产权防止核心算法、通信协议、业务逻辑被直接复制避免“为他人做嫁衣”。保障商业利益维护产品的市场定价权和生命周期打击低成本的山寨仿制。满足客户或合规要求一些行业客户或出口产品会对软件的安全性有明确要求。实现功能授权通过程序保护机制可以衍生出按功能收费、试用期控制等商业模式。简单来说给STM32程序加密就是给你的劳动成果和商业价值上一道保险。这个例程包就是教你如何亲手打造这把锁的钥匙和锁芯。2. 理解STM32F103的加密保护机制从硬件特性到软件实现在动手写代码之前我们必须先搞清楚STM32F103为我们提供了哪些“武器”。它的程序加密保护并非一个单一的魔法开关而是一套由硬件特性和软件配置共同构成的防御体系。理解这套体系是避免后续踩坑的关键。2.1 核心硬件防线读保护RDP与写保护WRP这是STM32内置的、最基础的硬件安全功能位于选项字节Option Bytes区域。你可以把它理解为芯片内部的几组物理开关。读保护Read Out Protection, RDP这是最重要的防线。它决定了外部工具如调试器、编程器能否读取芯片Flash内存的内容。Level 0默认状态无保护。任何人都可以随意读取、擦写Flash。Level 1启用读保护。这是最常用的级别。一旦设置任何通过调试接口JTAG/SWD或从RAM启动的代码都无法读取Flash内容。尝试读取会返回全0或全F。但是芯片本身在正常运行时代码是可以读取自身Flash的例如执行代码、查表这为软件加密算法留下了空间。要解除Level 1保护只能通过执行一次全片擦除Mass Erase这会清空所有用户代码和数据。Level 2最高级别保护。启用后调试接口将被永久禁用无法再通过SWD/JTAG连接且无法降级到Level 0或1。这个级别非常彻底但代价是芯片将再也无法被调试或更新程序通常用于量产后的最终产品且需要确保固件100%正确。写保护Write Protection, WRP用于保护指定的Flash扇区不被意外或恶意擦写。你可以选择保护从哪一页到哪一页。这对于保护存储了关键参数如校准数据、序列号、密钥的Flash区域非常有用。即使读保护被破解通过全片擦除写保护区域的内容也会被一并擦除攻击者无法保留你的关键数据。一个关键认知误区很多初学者认为设置了RDP Level 1就高枕无忧了。实际上硬件读保护主要防的是“静态提取”即防止别人直接把芯片内容读出来。一个有经验的攻击者可能会尝试通过电源毛刺、激光注入等物理手段让芯片在运行时“出错”从而绕过保护机制或者直接破解你的软件解密例程。因此硬件保护是基础但并非铜墙铁壁。2.2 软件加密的常见思路在硬件保护的基础上我们还需要增加软件层面的混淆和加密形成纵深防御。例程包里通常会演示以下几种思路Flash中的代码加密存储RAM中解密执行这是最核心的软件加密方法。程序在烧录到Flash时不是原始的机器码而是经过AES、DES或简单异或加密后的“密文”。芯片上电启动后在初始化阶段比如在main函数之前或之初通过一段预先烧录好的、未被加密的“引导程序”Bootloader将加密的代码段解密并拷贝到RAM中执行。因为RAM的内容断电即失攻击者即使通过调试器暂停CPU也只能看到解密后暂存在RAM中的片段而无法从Flash中获得完整的原始程序。校验和与完整性检查在程序中多处插入对自身代码或特定数据的校验和如CRC32计算。如果检测到校验和不匹配则说明程序可能被篡改可以触发复位、进入死循环或启用自毁逻辑。这增加了攻击者修改代码例如跳过License检查的难度。代码混淆与花指令通过插入大量无实际作用但复杂的汇编指令、跳转打乱代码的逻辑流程使反汇编工具生成的代码难以阅读和理解增加逆向工程的成本。利用芯片唯一IDUID每片STM32都有一个96位的唯一芯片标识符。可以将程序的一部分关键逻辑或解密密钥与这个UID进行绑定。这样即使程序被从一个芯片复制到另一个芯片也会因为UID不同而无法正常运行。这是实现“一机一码”的硬件基础。注意软件加密的强度与复杂度、性能开销是成正比的。复杂的加密算法和全程RAM运行会消耗更多的RAM空间和CPU周期。对于STM32F103这种资源有限的芯片需要精心设计平衡安全性与性能。通常只对最核心的算法模块进行加密保护。3. 实验例程深度拆解从工程结构到核心代码拿到“基于STM32f103单片机程序加密保护实验软件例程源代码.rar”并解压后我们看到的通常不是一个可以直接“烧录进去就加密”的魔法文件。它更可能是一个演示工程展示了如何搭建一个具备基础加密能力的框架。下面我们来一步步拆解这个框架。3.1 工程目录与文件结构分析一个典型的加密保护例程工程可能包含以下关键部分Project/ ├── Core/ │ ├── Src/ │ │ ├── main.c │ │ ├── encrypted_app.c (加密后的应用代码或解密后执行的代码) │ │ └── decrypt_boot.c (引导解密程序) │ └── Inc/ (相应头文件) ├── Drivers/ (STM32标准外设库或HAL库) ├── MDK-ARM/ (或IAR、STM32CubeIDE等工程文件) ├── Tools/ (关键) │ ├── encrypt_tool.exe (或.py脚本用于加密bin文件) │ └── key.txt (加密密钥文件) └── README.txt (说明文档)核心文件解读decrypt_boot.c这是整个系统的“信任根”。它必须是一段未经加密、且被设置为从Flash起始地址0x08000000运行的代码。它的职责包括初始化基本时钟和硬件、从Flash指定位置读取加密的应用程序代码、使用密钥进行解密、将解密后的代码搬运到RAM中、最后跳转到RAM中执行。encrypted_app.c或对应的.bin文件这是你真正的应用程序但在烧录前已经通过encrypt_tool工具和key.txt中的密钥进行了加密。它被存放在Flash中decrypt_boot代码之后的位置例如0x08004000。encrypt_tool这是配套的离线加密工具。它的输入是你编译生成的原始.bin或.hex文件输出是加密后的二进制文件。这个工具和密钥key.txt必须妥善保管绝不能泄露它通常实现了一个对称加密算法如AES-128。3.2 核心代码流程剖析让我们深入到decrypt_boot.c的关键函数中看看// decrypt_boot.c 关键片段 #include stm32f10x.h #include aes.h // 假设使用了一个轻量级的AES库 // 定义加密应用程序的存储地址和大小需与链接脚本及加密工具匹配 #define ENCRYPTED_APP_ADDR 0x08004000 #define ENCRYPTED_APP_SIZE 0x10000 // 64KB #define RAM_EXEC_ADDR 0x20000000 // RAM起始地址 // 预置的密钥必须与加密工具使用的密钥一致 static const uint8_t aes_key[16] {0x00, 0x01, 0x02, ...}; void jump_to_ram_app(uint32_t ram_addr) { // 定义一个函数指针指向RAM中的地址 void (*ram_app)(void) (void (*)(void))ram_addr; // 设置主堆栈指针MSP为RAM中应用程序的栈顶 __set_MSP(*(__IO uint32_t*)ram_addr); // 跳转执行 ram_app(); } int main(void) { // 1. 最基本的硬件初始化时钟、必要的外设 SystemInit(); // 2. 初始化加解密模块如AES AES_Init(aes_key); // 3. 从Flash源地址读取加密数据解密后写入RAM目标地址 uint32_t *src (uint32_t*)ENCRYPTED_APP_ADDR; uint32_t *dst (uint32_t*)RAM_EXEC_ADDR; for(int i0; iENCRYPTED_APP_SIZE; i16) { // AES-128以16字节为块 AES_Decrypt(src, dst, aes_key); src 4; // 指针移动16字节4个uint32_t dst 4; } // 4. 可选验证解密后的应用程序完整性如检查前几个字节是否为合法的栈指针和复位向量 if (is_valid_app(RAM_EXEC_ADDR)) { // 5. 跳转到RAM中的应用程序 jump_to_ram_app(RAM_EXEC_ADDR); } else { // 解密或程序损坏进入错误处理如闪烁LED while(1); } // 不会执行到这里 while(1); }而你的真实应用程序encrypted_app.c在编译时需要修改链接脚本.ld或.sct文件将其加载地址Load Address设置为RAM_EXEC_ADDR但其运行地址Execution Address同样也是RAM_EXEC_ADDR。这意味着编译器生成的代码是假定自己在RAM中运行的。一个至关重要的步骤——修改链接脚本 对于Keil MDK你需要修改Scatter File(.sct)。你需要定义两个加载区域LRLR_IROM1起始地址为0x08000000存放decrypt_boot的代码。LR_IROM2起始地址为ENCRYPTED_APP_ADDR(如0x08004000)存放加密后的应用程序二进制码。但其中执行域Execution Region的地址必须设置为RAM_EXEC_ADDR。这告诉链接器“这段代码最终是在RAM里跑的所以所有函数和变量的地址都请按RAM的地址来算。”; 示例 Scatter File 片段 LR_IROM1 0x08000000 0x00004000 { ; 16KB 给 Bootloader ER_IROM1 0x08000000 0x00004000 { *.o (RESET, First) *(InRoot$$Sections) .ANY (RO) ; Bootloader的只读部分放在这里 } RW_IRAM1 0x20000000 0x00005000 { .ANY (RW ZI) ; Bootloader的读写数据放在RAM } } LR_IROM2 0x08004000 0x00010000 { ; 从0x08004000开始存放加密后的App代码加载地址 ER_IROM2 0x20000000 0x00010000 { ; 但执行地址在RAM的0x20000000关键 app.o (RO) ; 你的应用程序目标文件 } }这样编译器为应用程序生成的机器码其内部的函数调用地址、全局变量地址都是基于0x20000000这个RAM地址计算的。当Bootloader将加密的代码解密到RAM的0x20000000后程序就能正确运行了。4. 实战部署全流程与避坑指南理解了原理我们来看如何将这套机制应用到实际项目中。这个过程环环相扣一步出错就可能导致芯片“变砖”或程序跑飞。4.1 步骤一环境准备与工程配置获取并解压例程首先确保你有一个可用的例程包。如果是从网络获取务必在虚拟机或隔离环境中先查毒。安装加解密工具检查Tools/目录下的加密工具。如果是.exe确保能在你的Windows上运行可能需要VC运行库。如果是Python脚本encrypt.py则需要安装Python环境和所需的加密库如pycryptodome。务必测试这个工具是否能正常工作用一个简单的test.bin文件加密后再解密看是否能还原。打开并理解工程用你的IDEKeil、IAR等打开工程。首先不要编译而是逐一看懂decrypt_boot和app两个项目的配置特别是链接脚本和内存分配。备份你的密钥key.txt或代码中的aes_key数组就是命门。立即将其备份到安全的地方并考虑在量产时使用每个芯片的UID派生出一个唯一的密钥增强安全性。4.2 步骤二编译、加密与烧录这是最容易出错的环节请严格按照顺序操作先编译应用程序App在IDE中确保当前激活的是应用程序encrypted_app的目标。编译它生成原始的.axf或.elf文件以及最重要的.bin文件在Keil中需要在User选项卡配置fromelf --bin -o “L.bin” “#L”来生成。使用加密工具处理.bin文件打开命令行切换到工具目录执行命令例如encrypt_tool.exe -e -i ..\MDK-ARM\app.bin -o app_encrypted.bin -k key.txt这会将原始的app.bin加密成app_encrypted.bin。将加密后的文件“注入”工程这里有两种常见方法方法A推荐将app_encrypted.bin转换为C语言数组并包含在decrypt_boot的源文件中。这样你只需要烧录一个包含了Bootloader和加密后App的完整镜像。可以使用bin2c这类工具。方法B单独烧录。先用编程器烧写decrypt_boot的程序到0x08000000。然后使用编程器软件的文件编程功能将app_encrypted.bin文件烧写到指定的Flash地址如0x08004000。务必确认烧录的地址与Bootloader中ENCRYPTED_APP_ADDR的定义完全一致编译并烧录Bootloader如果采用方法A则编译整个包含App数组的Bootloader工程生成一个.bin或.hex一次性烧录到0x08000000即可。4.3 步骤三设置读保护RDP Level 1这是最后也是至关重要的一步。如果不设置读保护攻击者依然可以直接从Flash的0x08004000地址读出加密后的二进制码虽然看不懂但可以直接复制或者尝试分析你的加密算法。如何设置RDP Level 1通过编程器软件如ST-Link Utility, J-Flash连接芯片后在选项字节Option Bytes配置界面将RDP从0xAALevel 0改为0xBB或其他非0xAA的值具体值请查阅参考手册然后点击“Program”。软件会提示你此操作需要全片擦除确认即可。通过代码在Bootloader中设置更优雅的方式是在decrypt_boot的代码里在跳转到App之前检查RDP级别如果未设置则自动设置。这可以通过写特定的值到选项字节的地址来实现。但这样做风险极高一旦代码有bug可能导致芯片锁死。建议初次使用时先用编程器软件手动操作验证整个流程。重大避坑提示地址对齐是魔鬼Bootloader的结束地址、加密App的起始地址、RAM的执行地址都必须严格按照芯片的内存边界如Flash扇区边界通常是1KB或2KB的整数倍和链接脚本的设定来。错一个字节程序必然跑飞。先调试Bootloader再加密在集成加密功能前先让Bootloader能正常跳转执行一个未加密的、存放在Flash后段的App。确保这个基础流程包括链接脚本是通的。RAM空间不足你的应用程序整个都要在RAM里运行必须确保它代码数据的大小不超过芯片可用的RAM总量。STM32F103C8T6只有20KB RAM这非常紧张。务必在map文件中仔细检查。中断向量表重映射如果你的应用程序需要使用中断那么它的中断向量表也必须放在RAM中。Bootloader在跳转前需要将SCB-VTOR向量表偏移寄存器设置为RAM中的向量表地址。这是另一个常见的遗漏点会导致一进中断就HardFault。加密工具与解密代码必须对齐加密工具使用的算法、模式如AES-128-ECB、填充方式必须与decrypt_boot.c中的解密代码完全一致。哪怕一个参数不对解密出来的都是乱码。5. 进阶策略与安全性强化思考基础的加密保护能挡住大部分简单的复制攻击但对于有经验的攻击者还需要更深入的策略。例程包通常只提供基础框架真正的安全性需要你在此基础上进行强化。5.1 结合芯片唯一IDUID实现绑定这是提升安全性的有效手段。思路是加密密钥不是硬编码在程序里的而是通过芯片的UID计算出来的。这样加密后的程序只能在这片特定的芯片上运行。实现方法在decrypt_boot中读取芯片的UIDSTM32F103的UID地址是0x1FFFF7E8。使用一个固定的“根密钥”和UID通过一个不可逆的算法如HMAC-SHA256取前16字节生成每片芯片独有的“派生密钥”。加密工具在加密你的App时也需要输入一个目标芯片的UID或一个代表UID的种子用同样的算法生成密钥来加密。这样每个芯片都需要单独生成一个加密的bin文件。量产流程先烧录通用的Bootloader包含解密和密钥派生算法- 读取芯片UID - 用工具和该UID生成专属加密App - 烧录该专属App。这种方法虽然增加了量产复杂度但做到了“一芯一码”即使一份加密固件泄露也无法在其他芯片上使用。5.2 代码分块加密与动态解密将整个应用程序一次性解密到RAM对RAM要求太高。可以将其分成多个块如按功能模块只有需要执行某个模块时才将其从Flash解密到RAM的一个“缓存区”中执行执行完即可覆盖。这类似于操作系统的分页机制能大大降低对RAM的峰值需求。5.3 反调试与完整性自检反调试在代码中插入检测调试器是否连接的代码。例如检查核心调试寄存器如CoreDebug-DHCSR的某些位。如果检测到调试器可以触发错误路径或清除关键数据。运行时完整性检查不仅仅在启动时检查可以在程序运行的多个关键节点随机对自身代码段或关键数据计算CRC并与预设值比较。这增加了攻击者通过动态调试单步跟踪来定位和绕过检查的难度。5.4 理解安全边界没有绝对的安全必须清醒认识到对于决心破解的专业人员任何基于软件和通用硬件的保护都是可以攻破的区别只是成本和时间。STM32的读保护等级1理论上可以通过聚焦离子束FIB等高端物理手段直接读取Flash存储单元或者利用芯片设计或制造上的漏洞来绕过。我们的目标不是追求“绝对无法破解”而是将破解的成本时间、金钱、技术门槛提高到远高于复制这款产品的收益或者高到让抄袭者觉得不如自己重新开发。对于大多数消费类和一般工业产品本文所述的“硬件读保护软件加密”组合拳已经足够形成有效的商业壁垒。6. 从实验到量产工程化实践要点当你成功在开发板上跑通了这个加密例程接下来就要思考如何将其平滑地集成到你的产品开发与量产流程中。开发阶段流程优化建立两套编译配置在IDE中创建“Debug”和“Release”配置。Debug禁用所有加密和读保护便于调试。应用程序直接编译到Flash地址运行。Release启用加密编译流程。链接脚本指向RAM编译后自动调用外部脚本执行加密并生成最终的生产用二进制文件。这个流程可以通过IDE的“Post-build”命令自动完成。编写自动化脚本将“编译App - 加密 - 与Bootloader合并 - 生成量产文件”这一系列步骤写成一个Python或Shell脚本。确保每次构建的一致性避免人工操作失误。量产烧录策略使用量产编程器如Xeltek、河洛等支持脱机烧录的编程器。将最终的、包含Bootloader和加密App的完整.hex或.bin文件交给烧录厂。在烧录流程中自动设置读保护大多数量产编程器都支持在烧录完成后自动写入选项字节。务必在烧录工单中明确指定“Program Option Bytes: RDP Level 1”。UID绑定产品的量产管理如果采用了UID绑定方案你需要一个生产管理系统MES来跟踪每个芯片的UID和与之对应的加密固件文件。烧录时系统根据扫描的芯片UID自动选择对应的固件进行烧录。这初期投入较大但安全性最高。测试与验证 在设置读保护之前务必进行充分测试因为一旦设置为Level 1想要再次调试就必须全片擦除你的用户程序就没了。测试应包括功能正常测试。功耗测试RAM运行可能比Flash运行功耗略高。异常复位测试看程序能否每次都正确解密并启动。尝试用调试器连接确认无法读取Flash内容读出来应该是0xFF或0x00。最后我个人在多个量产项目中的体会是程序保护是一个“系统工程”它涉及硬件选型是否选用安全性更高的芯片如带有加密引擎的STM32L5、软件架构如何合理划分安全与非安全模块、开发流程和产线管理。这个基于STM32F103的加密实验例程是一个绝佳的起点。它用最经典的芯片揭示了嵌入式软件保护的核心原理和基本方法。吃透它你不仅能保护你的STM32程序其思想也能迁移到其他任何需要软件保护的嵌入式平台上去。本文还有配套的精品资源点击获取
返回列表