简介:本资源是一套面向嵌入式开发初学者与进阶工程师的STM32F103程序加密保护综合实验例程,聚焦固件安全防护实践,涵盖Bootloader定制、Flash写保护配置、AES软件加解密集成及时间戳密钥生成等核心场景,适用于工业控制、物联网终端等对代码防逆向有明确需求的项目开发。压缩包共122个文件,含58个头文件(.h)定义外设驱动与加密接口,56个源文件(.c)实现RTC(DS1302)、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检查)的难度。
- 代码混淆与花指令:通过插入大量无实际作用但复杂的汇编指令、跳转,打乱代码的逻辑流程,使反汇编工具生成的代码难以阅读和理解,增加逆向工程的成本。
- 利用芯片唯一ID(UID):每片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 i=0; i<ENCRYPTED_APP_SIZE; i+=16) { // 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)。你需要定义两个加载区域(LR):
LR_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文件,加密后再解密,看是否能还原。 - 打开并理解工程:用你的IDE(Keil、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.txtapp.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的定义完全一致!
- 方法A(推荐):将
- 编译并烧录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从0xAA(Level 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 结合芯片唯一ID(UID)实现绑定
这是提升安全性的有效手段。思路是:加密密钥不是硬编码在程序里的,而是通过芯片的UID计算出来的。这样,加密后的程序只能在这片特定的芯片上运行。
实现方法:
- 在
decrypt_boot中,读取芯片的UID(STM32F103的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程序,其思想也能迁移到其他任何需要软件保护的嵌入式平台上去。
本文还有配套的精品资源,点击获取