第1章:固件加密与解密 — 基于AES-128/256的固件存储加密与运行时解密加载技术
各位同学,咱们今天聊一个嵌入式安全里绕不开的话题——固件加密。
说实话,我入行那会儿,很多工程师对固件加密的态度是「没必要」。觉得产品量不大,谁会来破解你?直到有一次,我帮客户做售后分析,发现市场上出现了外观一模一样的竞品,连开机logo都一样。拆开一看,对方直接拿编程器把Flash里的固件读出来,反汇编改了几个字就量产了。嗯,从那以后,我再也不敢说「没必要」这三个字了。
1.1 为什么要给固件加密?
你想想看,嵌入式设备的核心资产是什么?
- 算法逻辑:比如剃须刀的智能感应算法、电机控制策略
- 密钥证书:用于云端通信的私钥、设备证书
- 商业机密:产品未上市的功能、调试接口
这些东西全躺在Flash里。攻击者只要拿个逻辑分析仪,或者用JTAG/SWD接口,就能把二进制固件dump出来。我见过最夸张的一次,对方直接用热风枪吹下Flash芯片,放到编程器上读——整个过程不到5分钟。
⚠️ 警告: 不要以为加了读保护(RDP)就万事大吉。我曾经遇到过一款MCU,RDP Level 1被暴力破解工具直接绕过。硬件保护只是第一道门,加密才是真正的保险柜。
1.2 选AES-128还是AES-256?
这是个经典问题。我个人的习惯是:
| 参数 | AES-128 | AES-256 |
|---|---|---|
| 密钥长度 | 128 bit (16字节) | 256 bit (32字节) |
| 加密轮数 | 10轮 | 14轮 |
| 安全性 | 足够(量子计算前) | 更高(抗量子攻击潜力) |
| 性能开销 | 低 | 高(约慢40%) |
| 适用场景 | 资源受限的MCU | 高性能处理器/安全等级要求高 |
我的建议是:能用128就别上256。为什么?因为很多Cortex-M0/M3内核的MCU,硬件AES加速器只支持128位。你用256位,就得纯软件实现,那速度……我试过在48MHz的STM32F0上跑AES-256软件实现,加密1KB数据花了将近3毫秒。对于实时性要求高的剃须刀电机控制,这个延迟是不能接受的。
💡 小提示: 如果你的MCU有硬件AES引擎,优先用硬件。硬件加密不仅快,还能侧信道攻击防护。我踩过坑——软件实现AES时,密钥在内存里是明文,调试器一挂就能看到。硬件引擎通常会把密钥锁在专用寄存器里,CPU读不到。
1.3 固件存储加密方案
说白了,就是把编译好的bin文件,在烧录前用AES加密。MCU上电后,先解密再执行。
具体怎么做?我画个流程:
PC端(加密工具):
原始固件.bin → AES加密 → 加密固件.enc
烧录阶段:
加密固件.enc → 烧录到Flash
运行时(MCU启动代码):
1. 从Flash读取加密固件
2. 用预置密钥解密
3. 将解密后的固件加载到RAM或解密后原地执行
4. 跳转到入口地址
这里有个关键点:密钥怎么存?
- 最蠢的做法:把密钥硬编码在启动代码里。攻击者只要反汇编,一眼就能看到。
- 好一点的做法:密钥存储在MCU的OTP(一次性可编程)区域,或者eFuse里。这些区域烧录后不可读。
- 我推荐的做法:密钥派生。用芯片唯一ID(UID)作为种子,通过KDF(密钥派生函数)生成实际加密密钥。这样每颗芯片的密钥都不同,破解一颗不影响其他设备。
🔑 核心原则: 密钥永远不要以明文形式出现在Flash或RAM中。如果必须临时使用,用完立即清零。
1.4 运行时解密加载技术
解密时机有两种:
- 全量解密:上电后一次性解密整个固件到RAM,然后执行。优点是实现简单,缺点是需要足够大的RAM。对于剃须刀这种小设备,RAM通常只有几十KB,放不下上百KB的固件。
- 按页解密:需要执行哪一页,就解密哪一页。我参与的一个项目就是这么做的——把固件分成16KB一页,每页独立加密。启动时先解密引导加载器(Bootloader),再由Bootloader按需解密应用固件页。
按页解密的核心代码框架:
// 伪代码 - 按页解密加载
#define PAGE_SIZE (16 * 1024) // 16KB
#define KEY_SIZE 16 // AES-128
uint8_t aes_key[KEY_SIZE];
void decrypt_page(uint32_t flash_addr, uint32_t ram_addr) {
uint8_t ciphertext[PAGE_SIZE];
uint8_t plaintext[PAGE_SIZE];
// 从Flash读取加密数据
memcpy(ciphertext, (void*)flash_addr, PAGE_SIZE);
// AES解密
AES_ECB_decrypt(ciphertext, plaintext, aes_key, PAGE_SIZE);
// 拷贝到RAM执行区
memcpy((void*)ram_addr, plaintext, PAGE_SIZE);
// 安全擦除明文
memset(plaintext, 0, PAGE_SIZE);
// 刷新指令缓存(重要!)
__ISB();
__DSB();
}
⚠️ 注意: 解密后的明文一定要及时擦除。我曾经见过一个案例,攻击者通过冷启动攻击(Cold Boot Attack),从RAM中读到了几秒前解密过的固件代码。所以,用完即焚,不要留痕迹。
1.5 避坑指南
我这些年踩过的坑,总结成几条:
- 不要用ECB模式:ECB模式下,相同的明文块会加密成相同的密文块。攻击者可以通过模式识别推断出代码结构。我建议用CBC或CTR模式,加个随机IV。
- 注意对齐问题:AES加密要求数据长度是16字节的整数倍。固件末尾不足16字节时,需要做PKCS#7填充。我遇到过有人忘了填充,解密出来的最后几个字节全是乱码。
- 调试阶段别加密:开发调试时,建议先用未加密的固件。等要发布量产版本时,再开启加密。否则每次改代码都要重新加密、烧录,效率极低。
- 保留一个安全Bootloader:Bootloader本身不要加密,但要做完整性校验(比如SHA256)。Bootloader负责验证应用固件的签名,再解密加载。这样即使Bootloader被攻击者读取,他也改不了。
🔧 实战技巧: 我习惯在加密工具里加一个「版本号」字段,与固件一起加密。Bootloader解密后先检查版本号,防止攻击者用旧版本固件进行降级攻击(Downgrade Attack)。
1.6 性能与安全的权衡
最后聊点实在的。加密不是免费的午餐,它要消耗:
- 启动时间:解密需要时间。全量解密100KB固件,在72MHz的Cortex-M3上大约需要50ms。对于剃须刀,用户按下开关到电机启动,这个延迟是可以接受的。
- RAM占用:按页解密需要至少一页大小的RAM缓冲区。如果MCU RAM紧张,可以考虑解密后直接覆盖Flash(XIP方式),但需要MCU支持加密Flash的实时解密。
- 代码复杂度:加密解密逻辑会增加Bootloader的代码量。我建议Bootloader控制在8KB以内,越小越安全。
我个人认为,对于剃须刀这类消费电子产品,AES-128 + 按页解密 + 芯片UID密钥派生,已经是一个性价比很高的方案。既挡住了90%的抄板攻击,又不会让成本失控。
好,这一章就到这里。下一章咱们聊聊「安全启动链」——怎么确保你加载的每一段代码都是可信的。