1. 项目背景与核心需求
在嵌入式设备开发中,固件安全一直是个让人头疼的问题。去年我们团队就遇到过客户设备被非法复制的事件——竞争对手直接读取Flash中的固件,稍作修改就变成了他们的"自主产品"。这种赤裸裸的抄袭促使我们深入研究STM32F2系列的硬件保护机制。
STM32F2系列芯片提供了两种级别的Flash保护:
- 读保护(RDP):防止通过调试接口读取Flash内容
- 写保护(WRP):防止意外或恶意修改特定扇区
但硬件保护有个致命缺陷:一旦保护被解除(比如通过整片擦除),固件就完全暴露。这就是为什么我们需要在硬件保护基础上叠加AES软件加密——就像给保险箱里的文件再加个密码本,即使保险箱被撬开,没有密码依然无法获取有效信息。
2. 硬件保护机制实现
2.1 读保护(RDP)配置
读保护通过修改选项字节(Option Bytes)实现。在STM32F2中,这个配置需要特殊操作序列:
c复制#define FLASH_OPT_KEY1 0x08192A3B
#define FLASH_OPT_KEY2 0x4C5D6E7F
void enable_read_protection(void) {
FLASH_OB_Unlock(); // 解锁选项字节编程
FLASH_OB_RDPConfig(OB_RDP_Level_1); // 设置RDP级别1
FLASH_OB_Launch(); // 启动选项字节编程
FLASH_OB_Lock(); // 重新锁定
}
警告:RDP级别一旦从0提升到1,再次降级会触发整片擦除!这意味着所有用户数据都会丢失,必须谨慎操作。
实际项目中我们发现几个关键点:
- 调试器连接状态下无法修改RDP级别
- 修改RDP后必须完全断电重启才能生效
- 级别1保护下,通过SWD/JTAG读取Flash会返回全0或全FF
2.2 写保护(WRP)配置
写保护可以针对特定扇区设置,我们通常保护以下区域:
- 引导程序(Bootloader)区域
- 关键参数存储区
- 固件主程序区
配置代码示例:
c复制void config_write_protection(void) {
FLASH_OB_Unlock();
FLASH_OB_WRPConfig(OB_WRP_Sector_0to3, ENABLE); // 保护扇区0-3
FLASH_OB_WRPConfig(OB_WRP_Sector_4to7, DISABLE);
FLASH_OB_Launch();
FLASH_OB_Lock();
}
实测发现一个有趣现象:即使启用了写保护,通过RAM中运行的代码仍然可以修改被保护区域。这说明写保护主要防范外部编程器的写入,对芯片内部运行的代码限制有限。
3. AES软件加密方案
3.1 加密策略设计
我们采用AES-256-CBC模式,设计要点包括:
- 固件分块加密:每16字节为一个加密块
- 动态IV生成:结合芯片UID计算初始向量
- 密钥分散存储:将主密钥拆分成三部分,分别存放在:
- Flash特定位置(本身被读保护)
- 备份寄存器(BKP)
- 代码中的动态计算值
加密流程伪代码:
code复制for each 16-byte block in firmware:
if first block:
IV = HMAC(UID + SerialNumber)
ciphertext = AES_CBC_encrypt(plaintext, key, IV)
IV = ciphertext // CBC模式IV传递
3.2 STM32硬件加速实现
STM32F2内置了硬件AES加速器,比软件实现快20倍以上:
c复制void AES256_CBC_encrypt(uint8_t* input, uint8_t* output, uint32_t length, uint8_t* key, uint8_t* iv) {
AES_KeyInit(AES_Key_256, key, AES_KEY_MODE_EXPAND);
AES_IVInit(iv);
AES_CBCCmd(ENABLE);
AES_Cmd(ENABLE);
while(length > 0) {
AES_DataIn(*(uint32_t*)input);
input += 4;
while(AES_GetFlagStatus(AES_FLAG_BUSY) == SET);
*(uint32_t*)output = AES_DataOut();
output += 4;
length -= 16;
}
}
实测性能对比:
| 加密方式 | 加密1KB数据耗时 |
|---|---|
| 软件实现 | 12.8ms |
| 硬件加速 | 0.6ms |
3.3 密钥安全管理
我们采用白盒密码技术增强密钥保护:
- 主密钥= K1 ⊕ K2 ⊕ K3
- K1存储在Flash隐藏区域
- K2来自备份寄存器
- K3=HMAC(UID + Flash特定位置数据)
解密时动态重组密钥:
c复制uint8_t reconstruct_key(void) {
uint8_t k1 = read_hidden_flash();
uint8_t k2 = BKP_ReadDR(1);
uint8_t k3 = hmac_compute();
return k1 ^ k2 ^ k3;
}
4. 完整实施方案
4.1 开发阶段流程
- 使用明文固件开发调试
- 发布前执行加密脚本:
bash复制python encrypt_firmware.py \ --input firmware.bin \ --output firmware_enc.bin \ --key $(cat secret.key) \ --iv-seed $(read_uid) - 烧录加密固件+写保护配置
4.2 运行时解密流程
Bootloader中实现解密:
c复制void decrypt_and_jump(void) {
uint8_t* src = FIRMWARE_START;
uint8_t* dst = RAM_BUFFER;
uint8_t key[32] = reconstruct_key();
uint8_t iv[16] = generate_iv();
AES256_CBC_decrypt(src, dst, FIRMWARE_SIZE, key, iv);
if(verify_signature(dst)) {
((void(*)(void))dst)(); // 跳转到解密后的代码
}
}
4.3 防破解增强措施
- 关键函数地址随机化
- 添加反调试代码:
c复制if(CoreDebug->DHCSR & CoreDebug_DHCSR_C_DEBUGEN_Msk) { trigger_self_destruct(); } - Flash中填充伪密钥数据
5. 实战问题排查
5.1 常见问题速查表
| 现象 | 可能原因 | 解决方案 |
|---|---|---|
| 加密固件无法启动 | IV计算错误 | 检查UID读取是否正确 |
| 解密后校验失败 | 密钥重组错误 | 验证三部分密钥源 |
| 硬件AES卡死 | 未对齐访问 | 确保输入/输出地址32位对齐 |
| 写保护失效 | 扇区配置错误 | 重新检查OB_WRP配置值 |
5.2 性能优化技巧
- 使用DMA加速AES数据传输:
c复制DMA_InitStructure.DMA_PeripheralBaseAddr = (uint32_t)&AES->DIN; DMA_InitStructure.DMA_MemoryBaseAddr = (uint32_t)input_buffer; DMA_Init(DMA_Channel0, &DMA_InitStructure); DMA_Cmd(DMA_Channel0, ENABLE); - 预计算轮密钥减少开销
- 将AES操作放在RAM中执行
5.3 安全审计要点
- 定期检查选项字节状态:
c复制if(FLASH_OB_GetRDP() != OB_RDP_Level_1) { alert_security_breach(); } - 监控Flash写操作尝试
- 实现运行时完整性校验
6. 方案效果评估
我们使用行业标准测试工具评估防护效果:
| 测试项目 | 结果 |
|---|---|
| 直接Flash读取 | 全部00(受RDP保护) |
| 芯片解密成本 | >$50,000(专业设备) |
| 固件提取时间 | ≥2周(需物理攻击) |
| 性能损耗 | <3% CPU占用率 |
这套方案在消费级产品中表现优异,但对于超高安全需求场景,建议:
- 添加RSA签名验证
- 使用安全启动(Secure Boot)
- 考虑专用安全芯片
