1.概述:
学习stm32加密技术,要先知道stm32系列的单片机上,一个简单的单片机程序是怎样的(C);要知道为什么需要加密,知道有哪些加密技术;接着我们要先知道别人一般是怎么盗取我们的程序,才知道在哪些途径或地方要去加密;
2.网课教程加密教学:
1.生成密钥 (出厂时运行一次):
读取UID,应用MD5算法,得到一个预存值,再把它作为种子去生成随机数,并写入Flash。
2.校验密钥 (每次启动时运行):
重复计算过程,与Flash中的预存值比对,不一致则擦除程序(或进入死循环或安全模式)。
3.开启RDP读写保护
这个教学防范的是:
生成密钥,需要防破解;校验密钥,是为了防止攻击者把你的固件原封不动地复制到另一颗芯片上运行;开启RDP读写保护,防止固件被读取。
即:
首次生成
flash解锁,读取uid,对uid加盐,写入flash,flash上锁
每次运行检查
读取指定位置uid,按秘钥公式计算结果,对比,(设置RDP保护)=> 成功/失败
3.对课程的补充
你的总结非常准确,抓住了课程的核心三步:生成密钥 → 校验密钥 → 开启 RDP 读保护。而且你对每一步的防范目标也理解到位:防止破解、防止固件被复制到别的芯片、防止固件被直接读取。
在此基础上,我再补充几个关键细节和常见遗漏,让你的总结更完整、更实用。
✅ 你的总结已经正确的部分
| 步骤 | 操作 | 防范目标 |
|---|---|---|
| 1. 生成密钥(出厂一次) | 读取 UID → MD5 得到固定值 → 作为种子生成随机数 → 写入 Flash | 让每颗芯片拥有唯一密钥,防止批量克隆 |
| 2. 校验密钥(每次启动) | 重新计算 UID + MD5,与 Flash 中的预存值比对,不一致则死循环 | 防止固件被直接复制到另一颗 UID 不同的芯片上运行 |
| 3. 开启 RDP 读保护 | 设置选项字节为 RDP Level 1 或 Level 2 | 防止通过调试器(ST-Link/J-Link)读出 Flash 中的固件和密钥 |
📌 需要补充完整的几个技术细节
1. 为什么生成密钥时要用"随机数"?
你提到"把 MD5 结果作为种子去生成随机数并写入 Flash",这看似多了一步,其实有实际意义:
-
直接存储 MD5(UID) 是可以的,但攻击者如果知道你的算法,可以自己计算出来(但无济于事,因为 UID 不同)。
-
生成随机数再存储 通常是为了后续使用该随机数作为对称加密的密钥(比如 AES 加密通信数据)。此时 MD5 只是用来验证 UID 是否正确,真正的加密密钥是随机生成的,增加破解难度。
-
简单场景下,完全可以省略随机数,直接存储
MD5(UID + 自定义常数)作为比对值。
2. RDP 的三个级别及注意事项
-
Level 0:无保护(调试接口全开)。
-
Level 1 :读保护开启,调试接口无法读取 Flash;要解除保护会触发 mass erase (全片擦除),固件和密钥都被清空。这是量产产品最常用的级别。
-
Level 2 :调试接口永久关闭,且无法降级到 Level 1 或 0。芯片不再接受任何调试连接。一旦设置,芯片几乎无法再被读取或改写(除非使用特殊的后门,一般不存在)。适合极高安全要求的设备。
⚠️ 重要 :开启 RDP Level 1 或 2 后,你不能再通过调试器烧录新固件 (除非先解除保护并擦除芯片)。所以必须在烧录完整合了校验代码的固件后,最后一步再通过代码设置 RDP 位,或者使用烧录工具在烧录完成后自动设置。
3. 校验失败的处理:不要只死循环
简单的 while(1); 很容易被攻击者通过复位或电源干扰绕过。更可靠的做法:
-
进入硬件陷阱 :例如随机延时后调用
NVIC_SystemReset()不断复位,或者故意触发硬件错误(除零、非法地址访问)。 -
功能降级:不直接死机,而是禁用所有关键功能(比如输出错误数据、停止电机驱动),让设备看起来正常但实际不可用。
-
多次校验:在程序的不同位置、不同时机反复校验(例如在中断服务函数、定时器回调里也加校验)。
4. 防止校验代码本身被跳过或修改
即使有 RDP,攻击者如果通过故障注入(如电压毛刺、激光照射)让 CPU 跳过校验指令,依然可能运行你的程序。因此:
-
将校验代码放在 PCROP(专有代码读保护) 区域,该区域的代码只能执行,不能通过调试器或 CPU 数据读访问读取。这样攻击者无法分析你的校验逻辑。
-
使用校验和:对整个 Flash 或关键代码段计算 CRC,与预先存储的值比对,防止代码被篡改。
5.校验这一步,是为了防止攻击者把你的固件原封不动地复制到另一颗芯片上运行。
下面我用一个场景来解释它的作用,以及它为什么能起到防范作用。
🎯 校验的核心目的:实现"一机一码"绑定
你的方案本质上是让每个芯片的固件和它唯一的UID绑定。
-
出厂时(你的编程器操作):
-
读取芯片A的UID → 经过MD5+随机数种子 → 生成一个独一无二的密钥Key_A。
-
把Key_A写入芯片A的Flash中。
-
-
运行时(芯片A自己执行):
-
再次读取自己的UID → 用完全相同的算法计算出Key_A'。
-
将Key_A'与Flash中预存的Key_A进行比较。
-
如果相等,说明这颗芯片确实是"原装"的,程序继续运行。
-
如果不相等,进入死循环或安全模式,程序不工作。
-
🛡️ 它能防范什么?
它能有效防止"固件克隆"攻击。
假设攻击者:
-
通过某种手段(比如从你的工厂废品中)读出了芯片A的完整Flash内容(包含了你的程序代码 + 预存的Key_A)。
-
然后,他买了一批空白的芯片B、C、D......,想把读出的固件直接烧录进去。
结果会怎样?
-
芯片B启动后,会读取自己的UID_B,计算出Key_B'。
-
但它Flash里存的是Key_A(从芯片A复制过来的)。
-
Key_B' ≠ Key_A,校验失败 → 芯片B拒绝运行。
攻击者得到的固件,只能在芯片A上运行,换任何其他芯片都无效。这就等于让你的程序**"锁死"在原始芯片上**,无法批量克隆。
🧪 那它防不住什么?
你的校验方案也有明显的弱点,主要是下面两种攻击:
-
暴力跳过校验 :攻击者拿到你的固件二进制后,不修改密钥,而是直接用反汇编工具找到校验失败的跳转指令(比如
if (wrong) goto deadloop;),把它改成goto normal_run;,然后重新烧录。这样校验就被"阉割"了。- 如何应对 :不要把校验做成一个明显的
if分支。可以把校验结果作为后续解密的密钥,或者让校验失败后随机死机、破坏关键数据,增加定位难度。
- 如何应对 :不要把校验做成一个明显的
-
算法逆向 :攻击者如果反编译了你的固件,看到你是用
MD5(UID) -> srand() -> rand(),他完全可以自己写个小程序,输入任意UID就能算出对应的Key。这样他就能批量生成合法密钥,然后写入克隆芯片。- 如何应对 :不要使用标准库的
srand/rand,它的序列是公开的。改用你自己设计的、不可逆的混淆算法,或者直接使用硬件加密引擎(如STM32的AES)结合UID生成密钥,让攻击者即使看到代码也无法轻易逆向出从UID到Key的映射关系。
- 如何应对 :不要使用标准库的
✅ 总结:校验这一步到底有没有用?
有用,而且非常必要。 它是实现"一机一码"防复制的核心逻辑。没有这个校验,你的密钥生成得再复杂也是白费------因为程序根本不会去检查密钥是否正确。
但是,仅仅有这个校验还不够。你必须配合:
-
读保护(RDP Level 1或2):防止攻击者轻易读出你的固件二进制,从而无法分析你的校验算法。
-
代码混淆/防篡改:让攻击者难以通过简单修改二进制来跳过校验。
-
强密钥派生算法:让从UID到Key的计算不可逆、不可预测。
你的方向是正确的,只要在这个基础上加固算法和物理防护,就能有效提升盗取成本。
🔒 更完整的加密流程(课程可能未深入讲,但建议了解)
text
┌─────────────────────────────────────────────────────┐
│ 1. 芯片出厂/首次烧录 │
│ - 烧录 Bootloader + 应用程序(不含校验数据) │
│ - 运行一次密钥生成函数: │
│ key = MD5(UID + 自定义常数) │
│ 可选:生成随机数 enc_key │
│ 将 key 和 enc_key 写入 Flash 的特定扇区 │
│ - 开启 RDP Level 1 │
├─────────────────────────────────────────────────────┤
│ 2. 每次上电运行 │
│ - 读取 UID,重新计算 key' │
│ - 与 Flash 中的 key 比对 │
│ - 不一致 → 进入死循环/复位/功能降级 │
│ - 一致 → 正常启动,可选使用 enc_key 进行通信加密 │
│ - 运行时随机位置再次校验(防止被跳过) │
└─────────────────────────────────────────────────────┘
🎯 你的总结最终版(建议)
尚硅谷 STM32 加密课程核心思想:利用芯片唯一 ID(UID)结合 MD5 哈希算法,为每颗芯片生成独一无二的密钥,并在每次启动时校验该密钥。若密钥不匹配(说明固件被复制到其他芯片),则拒绝运行。同时开启 RDP 读保护,防止攻击者通过调试接口直接读取 Flash 中的固件和密钥。此外,还可以配合 PCROP、校验和、运行时多点校验等方法增强安全性。
📖 扩展知识:攻击者可能的盗取途径(帮助你理解为何要这样加密)
| 攻击方式 | 原理 | 对应防御措施 |
|---|---|---|
| 调试接口读取 | 使用 ST-Link 直接连接 SWD/JTAG 读出 Flash | RDP 读保护 |
| 固件复制到同型号空片 | 把读出的固件烧录到另一颗 STM32 上 | UID 校验(新芯片 UID 不同,校验失败) |
| 反汇编 + 跳过校验 | 用 IDA Pro 等工具分析固件,找到校验函数并 patch 掉 | PCROP (隐藏校验代码)、校验和(防止修改) |
| 故障注入 | 在 CPU 执行校验指令时用电压毛刺使其跳过 | 多次校验 、随机延时校验 、硬件安全单元 |
| 物理拆片读取 | 去除芯片封装,用探针直接读取 Flash 单元 | RDP Level 2 + 顶层金属屏蔽(仅部分高端 MCU 支持) |
你的总结已经抓住了精髓,上面补充的细节可以帮助你更深入地理解并实际落地。如果还需要了解如何编写代码实现 UID 读取、MD5 计算或 RDP 设置,我可以提供示例。
4.示例代码
以下代码演示了完整的出厂密钥生成与运行时的校验流程。为防篡改,建议将密钥存储在OTP区域或受保护的Flash扇区。
c
#include <string.h>
#define KEY_STORAGE_ADDR 0x0800FC00 // Flash存储密钥的地址
// 生成16字节MD5密钥(基于UID + 自定义盐)
void Generate_Key(uint8_t *output_key)
{
uint32_t uid[3];
STM32_Get_UID(uid); // 读取96位UID
uint8_t buffer[12 + 8] = {0}; // 12字节UID + 8字节自定义盐
memcpy(buffer, uid, 12);
memcpy(buffer + 12, "YourSalt", 8); // 自定义盐
Compute_MD5(buffer, sizeof(buffer), output_key); // output_key为16字节
}
// 出厂时执行一次:生成密钥并写入Flash
void Factory_Key_Generate(void)
{
uint8_t key[16];
Generate_Key(key);
HAL_FLASH_Unlock();
// 先擦除存储扇区
FLASH_Erase_Sector(KEY_STORAGE_ADDR);
// 写入密钥
for (int i = 0; i < 16; i++) {
HAL_FLASH_Program(FLASH_TYPEPROGRAM_BYTE, KEY_STORAGE_ADDR + i, key[i]);
}
HAL_FLASH_Lock();
}
// 每次启动时执行:校验密钥,不一致则死机
void Check_Key(void)
{
uint8_t calc_key[16];
uint8_t stored_key[16];
Generate_Key(calc_key);
memcpy(stored_key, (uint8_t *)KEY_STORAGE_ADDR, 16);
if (memcmp(calc_key, stored_key, 16) != 0) {
while (1) {
// 校验失败,进入死循环或触发复位
}
}
}
// 主函数示例
int main(void)
{
HAL_Init();
// 判断是否首次运行(例如检测特定Flash标志位)
if (Is_First_Run()) {
Factory_Key_Generate(); // 生成并写入密钥
Set_First_Run_Flag(); // 标记已初始化
}
Check_Key(); // 每次启动校验
// 此处可设置RDP保护(通常最后开启)
// STM32_Set_RDP_Level(OB_RDP_LEVEL_1);
while (1) {
// 正常应用程序
}
}
⚠️ 重要提醒
-
RDP设置时机:建议在固件全部烧录、功能验证无误后,作为量产的最后一步开启RDP。一旦开启,调试接口将被禁用,后续无法通过ST-Link/J-Link读取或烧录,升级固件需通过Bootloader或空中升级(OTA)实现。
-
密钥存储位置:推荐使用OTP(One-Time Programmable)区域存储密钥,该区域只能编程一次且不可擦除,安全性更高。
-
RDP Level 2风险:开启后永久禁用调试接口且不可逆转,量产前务必测试所有功能,否则芯片将变砖无法调试。
以上示例代码可根据具体STM32系列微调。如需更高级的加密(如AES硬件加密),可参考STM32的CRYP模块或安全固件安装(SFI)等机制。
5.AES加密
1.概念和实现
高级加密标准(AES)可以理解为一种能将你的数据锁起来,并且目前公认很难被破解的"高强度锁"。它是一种对称加密算法,意思是加密和解密使用的是同一把"钥匙"(密钥)。
🔒 AES 的核心:密钥长度
AES的安全性很大程度上取决于"钥匙"的长度,主要分为三种:
-
AES-128:使用128位密钥,是目前最常见的选择。它在安全性和性能之间取得了良好的平衡。
-
AES-192:使用192位密钥,安全性比AES-128更高,但速度稍慢,适用于对安全有更高要求的场景。
-
AES-256:使用256位密钥,是AES家族中最安全的版本,也是许多政府和金融机构的加密标准。
选择哪种,本质上是根据你的具体应用场景,在安全性和加解密速度之间做权衡。
⚙️ AES 是如何工作的?
AES的工作过程可以拆解为三个步骤,你可以把它们想象成对数据"加扰"的三个阶段:
-
数据分块:AES先把要加密的数据分成一个个固定大小的小块,每个块是128位(16字节)。然后,它会对每个小块分别进行加密处理。
-
密钥扩展:AES会将原始密钥扩展成一系列不同的子密钥,用于后续的每一轮加密,这个过程叫作"密钥扩展"。
-
多轮加密:对每个数据块进行若干轮(10、12或14轮,取决于密钥长度)的复杂运算。每一轮都包含替换、移位、混合和密钥加等操作,让原始数据和密钥充分"搅"在一起。
🗝️ 选择哪种工作模式?
AES在处理多个数据块时,需要指定一种"工作模式"。这决定了如何将独立的块串联起来。以下是两种最常见模式的对比:
| 特性 | ECB(电子密码本模式) | CBC(密码块链模式) |
|---|---|---|
| 原理 | 每个数据块独立加密,相同明文产生相同密文 | 每个数据块加密前先与前一个密文块进行异或运算 |
| 优点 | 简单、高效,可并行处理 | 安全性高,相同明文产生不同密文 |
| 缺点 | 不安全,会泄露明文模式 | 加密过程无法并行,需要初始化向量 |
| 推荐度 | 强烈不推荐 | 推荐使用 |
此外,还有CTR(计数模式)、GCM(伽罗瓦/计数器模式)等。其中GCM模式很受欢迎,因为它不仅能加密,还能同时做完整性校验。
🚀 在STM32上实现AES
STM32提供了两种实现AES的方式:
1. 硬件加速器(AES外设)
很多STM32系列(如STM32L4, STM32H7等)内部集成了专用的AES硬件加速器。你可以把它理解为一个专门为AES算法设计的硬件模块。
-
极致性能:硬件加速器比纯软件实现快10到20倍。例如,用128位密钥加密一个16字节的数据块,硬件加速器仅需约51个时钟周期。
-
低功耗:硬件完成同样任务消耗的能耗远低于CPU。
-
简化主控:CPU只需将数据和密钥交给AES外设,然后就可以去处理其他任务,无需全程参与复杂的计算。
2. 软件实现
如果你的STM32型号没有AES硬件外设,也可以使用纯软件算法库(如mbedTLS)来实现AES。这种方式更灵活,不依赖特定硬件,但加解密速度会慢很多,并且会占用CPU大量时间。
🛡️ SAES:更安全的AES版本
在一些较新的STM32型号(如STM32U5, STM32H5)中,ST公司还提供了一个更安全的AES版本,称为SAES(Secure AES)。
-
抵抗侧信道攻击:AES硬件虽然快,但运行时可能会通过功耗、电磁辐射等方式"泄露"密钥信息。SAES通过硬件设计上的优化,可以有效抵抗这种攻击。
-
硬件密钥 :SAES可以直接使用芯片内部硬件派生的密钥,例如基于唯一ID派生的DHUK 。这意味着你的密钥永远不会出现在软件中,从根本上防止了密钥被读取的可能。
🔐 安全实践:如何管理好你的密钥?
一个安全的加密系统,密钥的管理比加密算法本身更重要。
-
绝不硬编码 :千万不要在代码中直接将密钥写死为常量。攻击者一旦获得你的固件,就能轻易找到这些密钥。
-
密钥派生:可以利用每颗芯片唯一的96位UID,通过特定算法(如哈希运算)派生出唯一的密钥。这样,每台设备的密钥都不同,可以防止固件被批量克隆。
-
安全存储 :对于SAES这类硬件,可以直接使用硬件派生的密钥,避免密钥暴露在软件中。对于普通AES,应结合使用PCROP(专有代码保护) 等功能,将存储密钥的Flash区域保护起来,禁止任何形式的读取。
-
安全启动(Secure Boot):这是将AES用于固件保护最经典的应用。简单来说,程序启动时会先运行一个不可更改的"引导程序"(Bootloader),它负责将Flash中加密的应用程序代码进行解密,然后再跳转执行。这个过程确保了只有合法的固件才能被解密和运行。
2.通俗来讲
用最生活化的方式来理解 AES 加密。
你可以把 AES 想象成一个 "超级搅拌机" ,把你想保护的信息(比如一段文字、一张图片)放进去,加上一把"钥匙",一按开关,出来就变成一堆谁也看不懂的"乱码"。只有拿着同一把"钥匙"的人,才能把这堆"乱码"重新变回原来的信息。
🥤 第一步:把信息切成小块
AES 这个搅拌机一次只能处理固定大小的原料。它会把你所有的信息,像切香肠一样,切成 16 厘米(16字节) 长的段。一段一段地搅。
🔑 第二步:你需要一把钥匙
这把钥匙就是你的"密码"。AES 提供了三种长度的钥匙:
-
128 位的钥匙:相当于一个 16 位的数字密码(强度足够日常用)
-
192 位的钥匙:相当于 24 位密码(更安全)
-
256 位的钥匙:相当于 32 位密码(银行/军方级别,极难破解)
钥匙越长,搅拌的轮数越多,也就越安全,但速度会稍微慢一点。
🔄 第三步:搅拌过程(这就是加密的核心)
把一段 16 字节的原料和你的钥匙一起放进搅拌机,它会重复做 10 ~ 14 轮(取决于钥匙长度)的混合操作,每一轮都包括:
-
替换:把每个字节按照一张秘密的替换表,换成另一个字节(就像用摩斯密码替换字母)。
-
移位:把整段数据里的字节像排队一样,左右移动位置。
-
混合:把这一行和那一列的数据混合起来,让每个输出字节都依赖于所有输入字节。
-
加钥匙:把这一轮专用的子钥匙混进去。
每一轮之后,数据都变得更加"面目全非"。经过十几轮之后,你根本看不出它和原来的数据有任何关系。
🧩 工作模式:怎么处理多段数据?
因为信息被切成了很多段,每一段都用同样的钥匙单独搅拌,如果不做额外处理,那么两个相同的段搅拌后会得到相同的乱码,这就可能泄露信息。所以 AES 提供了几种"连接方式":
-
ECB 模式(最差) :每一段独立搅拌,完全相同的内容出来完全相同的乱码。不推荐,因为攻击者能看出规律。
-
CBC 模式(常见且安全):先把当前段和上一段的搅拌结果混在一起,再放进搅拌机。这样即使原始内容相同,出来的乱码也不一样。第一段因为没有"上一段",需要额外加一个随机数(叫"初始化向量")。
除了 CBC,还有更先进的 GCM 模式,它不仅能加密,还能在搅拌时顺便贴上一个"防伪标签",用来验证数据没有被篡改。
🛡️ AES 到底有多安全?
目前,没有公开的方法能直接破解 AES。最可行的攻击方式是"暴力尝试",也就是用所有可能的钥匙去试。
-
对于 AES-128:用全世界最快的计算机去试,需要 上亿亿亿年。
-
对于 AES-256:那是给担心量子计算机出现后的人准备的,安全级别更高。
所以只要你的钥匙足够随机(不是 "123456" 这种),并且妥善保管,AES 就是目前最可靠的加密方法之一。
🧪 举个生活中的例子
你要寄一个箱子给朋友,但怕快递员偷看:
-
原始内容:一张写有"明天见面"的纸条。
-
AES 加密 :你把纸条放进一个 AES 搅拌机,用你们约定好的钥匙(比如"苹果123")启动机器。出来的是一堆看起来像"#%¥&*...8f3a"的乱码。
-
运输:快递员看到这堆乱码,完全不知道是什么。
-
解密 :你朋友拿到箱子后,放进同一个 AES 反向搅拌机,输入相同的钥匙"苹果123",机器就把乱码恢复成"明天见面"。
✅ 总结一句话
AES 就是把你的数据切成小块,用一把钥匙反复"搅拌、打乱、混合"很多轮,最后变成一堆没有钥匙就绝对解不开的乱码。它是目前全世界应用最广泛、最可靠的加密算法之一。