如何用 Keil5 和 STM32 实现真正安全的固件更新?
你有没有遇到过这样的问题:产品刚上市,市面上就出现了功能一模一样的“山寨版”?或者远程升级时担心固件被截获、篡改?在物联网设备遍地开花的今天,固件安全早已不是可选项,而是嵌入式开发的底线。
而我们常用的开发工具链——Keil MDK-ARM(俗称 Keil5)搭配 STM32 微控制器,其实本身就具备构建高安全性固件更新系统的能力。只是大多数开发者还停留在“能烧进去就行”的阶段,忽略了这套组合拳背后强大的安全潜力。
今天我们就来拆解一个实战级方案:如何利用 Keil5 的自动化流程 + STM32 硬件安全特性,实现从编译到烧录全链路加密的固件更新机制。这不是理论推演,而是一套可以直接落地、经得起产线考验的技术路径。
为什么普通烧录方式不再够用?
先别急着上加密,咱们得明白风险在哪。
传统的固件发布流程通常是这样的:
- 在 Keil 中点击 Build;
- 输出
.bin或.hex文件; - 把文件交给生产厂或售后人员,用 ST-Link 直接烧录。
看起来没问题?但细想一下:
- 固件文件是明文的,谁拿到都能反汇编;
- 工厂可能私自修改代码加入后门;
- OTA 升级包如果没保护,中间人攻击轻而易举;
- 调试接口开着,黑客连上 JTAG 就能把 Flash 内容读个精光。
这些问题归结为三个核心挑战:
保密性(Confidentiality):防止固件泄露
完整性与真实性(Integrity & Authenticity):确保固件未被篡改且来源可信
防复制(Anti-cloning):阻止非法量产和仿制
要解决这些,光靠软件层面打补丁不行,必须软硬结合,深入芯片底层。
第一步:给固件穿上“隐身衣”——AES 加密实战
最直接的防护手段,就是让别人即使拿到了你的固件文件也看不懂。这就轮到AES(高级加密标准)登场了。
STM32F4/F7/H7/L4+ 等主流型号都内置了硬件 AES 加速器,性能强劲、功耗极低。我们可以用它对输出的.bin文件进行预加密,生成只有目标设备才能解密的密文镜像。
为什么选硬件 AES?
很多人第一反应是“写个软件加密函数不就行了?”但真正在工程中对比就会发现差距:
| 指标 | 软件实现 | 硬件 AES(以 STM32H7 为例) |
|---|---|---|
| 吞吐率 | ~1–5 Mbps | 可达 100+ Mbps |
| CPU 占用 | 高(全程阻塞) | 几乎为零(DMA 支持) |
| 安全性 | 易受时序/功耗分析攻击 | 支持抗侧信道设计 |
| 实时影响 | 明显延迟 | 可忽略 |
所以结论很明确:只要有硬件 AES,就绝不用软件实现。
加密怎么做?别自己造轮子
STM32 的 HAL 库已经封装好了HAL_AES_Encrypt()接口,使用起来非常简单。比如下面这段代码,就能完成一段数据的 CBC 模式加密:
AES_HandleTypeDef haes; uint8_t aes_key[16] = { /* 128位密钥 */ }; uint8_t iv[16] = { /* 初始化向量 */ }; void encrypt_chunk(uint8_t *plain, uint8_t *cipher, uint32_t size) { haes.Instance = AES; haes.Init.KeySize = AES_KEYSIZE_128B; haes.Init.pKey = aes_key; haes.Init.pInitVect = iv; haes.Init.Algorithm = AES_ENCRYPT; HAL_AES_Init(&haes); HAL_AES_Encrypt(&haes, plain, cipher, size / 16); // 块数 HAL_AES_DeInit(&haes); }关键点来了:这个加密过程不该发生在运行时,而应该在出厂前的构建阶段自动完成。否则每次启动都要等几十毫秒解密,用户体验会很差。
那怎么实现“一键编译+自动加密”?答案就在 Keil5 的隐藏功能里。
第二步:让 Keil 自动帮你加密 —— 构建后脚本的秘密武器
Keil5 提供了一个叫Build Events的功能,允许你在编译完成后自动执行命令行操作。这正是实现自动化安全构建的关键突破口。
怎么配置?
打开工程选项 → Utilities → After Build/Rebuild,填入一行命令:
python "$(PROJECTDIR)\Scripts\encrypt_bin.py" "$(OUTPUT_DIRECTORY)\$(TARGET).bin" "$(OUTPUT_DIRECTORY)\$(TARGET)_encrypted.bin"就这么一句话,就能触发 Python 脚本对刚刚生成的.bin文件进行 AES 加密。
脚本长什么样?
这里是一个典型的encrypt_bin.py示例:
import sys from Crypto.Cipher import AES from Crypto.Util.Padding import pad KEY = bytes.fromhex("2B7E151628AED2A6ABF7158809CF4F3C") IV = bytes.fromhex("000102030405060708090A0B0C0D0E0F") def encrypt_file(in_path, out_path): with open(in_path, 'rb') as f: data = f.read() padded = pad(data, 16) cipher = AES.new(KEY, AES.MODE_CBC, IV) ciphertext = cipher.encrypt(padded) with open(out_path, 'wb') as ef: ef.write(ciphertext) if __name__ == "__main__": if len(sys.argv) != 3: print("Usage: python encrypt_bin.py <input.bin> <output.enc)") exit(1) encrypt_file(sys.argv[1], sys.argv[2])⚠️ 注意事项:
- 必须安装pycryptodome:pip install pycryptodome
- 密钥不要硬编码在脚本中!建议通过环境变量或配置文件注入
- 可扩展为同时签名 + 加密,提升完整性和认证能力
这样一来,开发者只需点一次“Build”,就能得到两个文件:
firmware.bin—— 明文(用于调试)firmware_encrypted.bin—— 加密版(用于生产和 OTA)
整个过程无需人工干预,团队协作时也不会遗漏步骤。
第三步:锁死芯片,杜绝物理提取 —— RDP 与 PCROP 的终极防御
就算固件加密了,如果攻击者能直接连上 SWD/JTAG 把 Flash 全部读出来怎么办?
STM32 提供了两道物理防线:RDP(读出保护)和PCROP(专有代码读出保护)。
RDP Level 2:一锁永逸的“芯片级封印”
当你启用 RDP Level 2 后,会发生什么?
- 所有调试接口(SWD/JTAG)永久禁用;
- 无法通过任何方式读取 Flash 内容;
- 唯一能做的只有擦除整个芯片(连带 Bootloader 一起消失);
- 不可逆!一旦启用,再也无法恢复调试功能。
启用方法也很简单:
void enable_rdp_level2(void) { FLASH_OBProgramInitTypeDef OBInit = {0}; HAL_FLASH_Unlock(); HAL_FLASH_OB_Unlock(); OBInit.OptionType = OPTIONBYTE_RDP; OBInit.RDPLevel = OB_RDP_LEVEL_2; HAL_FLASHEx_OBProgram(&OBInit); HAL_FLASH_OB_Launch(); // 复位生效 }📌最佳实践建议:
- 开发阶段保持 RDP Level 1(仅限制部分区域),方便调试;
- 发布版本前统一刷入 Level 2;
- 生产线上由烧录器批量设置选项字节,避免单片操作。
PCROP:精准打击,只保护核心代码
如果你只想保护某些关键模块(比如加密算法、授权逻辑),又不想完全关闭调试,可以用PCROP。
它的特点是:
- 指定某个 Flash 扇区“只能执行,不能读”;
- 即使使用外部工具也无法 dump 该区域内容;
- 支持多个独立保护区段;
- 可配合 IAP 实现动态加载解密固件。
例如,你可以把 Bootloader 中的验签和解密逻辑放在 PCROP 区域,这样哪怕别人拿到了加密固件,也不知道你是怎么验证和解密的。
第四步:建立信任根 —— Secure Boot 的工作流设计
光有加密和写保护还不够。真正的安全启动需要一套完整的信任链机制。
STM32 的安全启动通常分为三个层级:
ROM Code(信任根 RoT)
芯片出厂时固化在系统存储器中的引导程序,负责检查用户 Flash 是否有效,并根据 BOOT 引脚决定启动模式。Secure Bootloader(第二阶段)
存放在 Flash 起始位置的一段受保护代码,主要职责:
- 计算应用固件的哈希值(SHA-256)
- 使用公钥验证其数字签名(ECDSA)
- 验证通过后,调用硬件 AES 解密并跳转执行Application(主程序)
经过认证和解密的应用代码,运行在可信环境中。
这个过程中最关键的是:Bootloader 必须本身是可信的。因此它也应该:
- 被 PCROP 保护;
- 使用固定密钥签名;
- 支持版本校验,防止降级攻击。
一旦这套机制跑通,你就实现了真正的“安全启动闭环”。
实战架构图:从开发到部署的全流程
最终的系统架构应该是这样的:
[开发主机] │ ├── Keil5 编译 │ └── main.c → firmware.axf → firmware.bin │ ↓ (After Build Script) │ └── firmware_encrypted.bin (AES-CBC + ECDSA签名) │ └── [生产/售后] └── 使用专用烧录器写入加密固件 ↓ [目标设备 STM32] ├── 上电 → ROM Code → 跳转至 Bootloader ├── Bootloader: │ 1. 验证 application 签名 │ 2. 使用内置密钥 AES 解密至 SRAM 或 Bank2 │ 3. 跳转执行 ├── Flash 区域: │ - Bootloader: PCROP + WRP 保护 │ - Application: RDP Level 2 锁死 └── 运行态应用:正常执行业务逻辑常见坑点与避坑指南
再好的设计也会踩坑。以下是几个新手最容易犯的错误:
❌ 密钥写死在代码里
uint8_t key[16] = {0x2B, 0x7E, ...}; // 千万别这么干!一旦固件泄露,密钥也就暴露了。正确做法是:
- 使用 OTP(一次性可编程)区域存储密钥;
- 或通过外部 SE(安全元件)提供密钥服务;
- 或使用 KDF 从设备唯一 ID 衍生密钥;
❌ 忘记填充和对齐
AES 是分组加密,每块 16 字节。如果你的固件长度不是 16 的倍数,必须做 padding(推荐 PKCS#7)。否则最后一块会出错。
❌ 启动时间过长
解密整个应用可能耗时数十毫秒。优化建议:
- 使用 DMA + 硬件 AES 并行处理;
- 只解密关键段,其余按需加载;
- 使用双 Bank Flash,交替更新减少停机时间;
❌ 没有回滚保护
攻击者可能发送旧版本固件进行降级攻击。解决方案:
- 在 EEPROM 或备份寄存器中记录当前固件版本号;
- Bootloader 拒绝低于当前版本的更新;
写在最后:安全不是功能,而是思维方式
这篇文章讲了很多技术细节:AES 加密、RDP 锁定、构建脚本、安全启动……但比技术更重要的是安全意识。
很多团队直到产品被盗版才想起来加保护,结果发现架构根本不支持,只能打补丁式修修补补。
正确的做法是在项目初期就规划好安全模型:
- 哪些代码需要保护?
- 是否支持远程升级?
- 生产环节如何管控密钥?
- 出现漏洞是否有回滚机制?
把这些想清楚了,再回头选择合适的技术组合,才能做到既安全又高效。
至于 Keil5 + STM32 这套组合,虽然不如专用 TEE 或 HSM 那样极致,但对于绝大多数工业控制、医疗设备、消费电子来说,已经足够构筑一道坚实防线。
如果你正在做一款需要长期维护、涉及商业机密或用户隐私的产品,不妨现在就开始动手,在下次提交代码前,多问一句:
“我的固件,真的安全吗?”
欢迎在评论区分享你的安全实践经验,我们一起打造更可靠的嵌入式世界。