第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 运行时解密加载技术

解密时机有两种:

  1. 全量解密:上电后一次性解密整个固件到RAM,然后执行。优点是实现简单,缺点是需要足够大的RAM。对于剃须刀这种小设备,RAM通常只有几十KB,放不下上百KB的固件。
  2. 按页解密:需要执行哪一页,就解密哪一页。我参与的一个项目就是这么做的——把固件分成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%的抄板攻击,又不会让成本失控。

好,这一章就到这里。下一章咱们聊聊「安全启动链」——怎么确保你加载的每一段代码都是可信的。