3. 固件加密与混淆:把代码锁进保险柜
各位做 IoT 的同行,咱们都清楚一个现实:固件一旦被读出来,基本就等于裸奔。我见过太多产品,Flash 焊下来用编程器一读,明文代码、硬编码密钥、甚至云平台证书全在里面躺着。这哪是安全产品?这分明是给攻击者送人头。
所以这一章,咱们就聊聊怎么给固件穿上铠甲——加密与混淆。说白了,就是让攻击者即使拿到二进制,也看不懂、改不了、跑不起来。
3.1 对称加密 vs 非对称加密:选型不是拍脑袋
先解决第一个问题:用什么算法来加密固件?
我个人习惯把场景分成两类:固件分发和固件存储。这两者的需求完全不同。
| 对比维度 | 对称加密(AES) | 非对称加密(RSA/ECC) |
|---|---|---|
| 加密速度 | 快(硬件加速可达 Gbps) | 慢(通常慢 1000 倍以上) |
| 密钥管理 | 分发密钥困难 | 公钥公开,私钥保密 |
| 适用场景 | 固件镜像加密、存储加密 | 签名验证、密钥交换 |
| 典型算法 | AES-128/256, ChaCha20 | RSA-2048, ECDSA, Ed25519 |
| 硬件开销 | 低(多数 MCU 内置 AES 引擎) | 高(需要大数运算协处理器) |
核心原则:固件镜像本身用对称加密,非对称只用来保护对称密钥。这叫「混合加密体系」,业界标准做法。
为什么?你想想看,一个 2MB 的固件用 RSA 加密,光运算时间就够喝杯咖啡了。而 AES-128-CTR 模式在 Cortex-M4 上跑,硬件加速下每秒能处理几十 MB。这差距,不是一星半点。
3.2 固件镜像加解密流程:从产线到芯片
好,算法选定了,那具体怎么操作?我画了一张流程图,把整个链路串起来。
流程其实不复杂,但有几个坑我得提醒你。
我曾经踩过的坑:某次产线烧录,AES 密钥是写死在烧录脚本里的。结果产线电脑被入侵,所有设备的密钥全部泄露。后来我改成每台设备独立密钥,密钥由 HSM 生成,烧录器只传密文。记住:密钥永远不要出现在脚本或配置文件中。
3.3 代码混淆与反逆向:让反汇编变成噩梦
加密解决了「读不到明文」的问题,但有个前提——攻击者得先拿到密文。如果设备已经被物理破解,Flash 被直接读取呢?这时候就需要第二道防线:代码混淆。
混淆不是加密,它不隐藏数据,而是让代码变得难以理解。我常用的手段有这几类:
- 控制流平坦化:把正常的 if-else 循环全部打散,变成一张巨大的状态机跳转表。IDA 的 F5 反编译直接崩溃。
- 虚假控制流:插入大量永远不会执行的分支,但静态分析看不出来。反编译器会生成一堆垃圾代码。
- 字符串加密:所有敏感字符串(日志、API 路径、密钥提示)在编译时加密,运行时按需解密。
- 指令替换:把标准指令替换成等效的复杂指令序列。比如
MOV R0, #0改成EOR R0, R0, R0。
我的经验:混淆不是越强越好。控制流平坦化会让代码体积膨胀 3-5 倍,对 Flash 紧张的 MCU 来说可能吃不消。我一般只在关键函数(密钥派生、签名验证、安全启动)上做混淆,其他部分保持正常。
3.4 实战:一个简单的固件加解密示例
光说不练假把式。下面是一个基于 OpenSSL 的固件加密脚本,我实际项目里改过几版,现在拿出来给你参考。
#!/bin/bash
# 固件加密脚本 - 产线使用
# 用法:./encrypt_firmware.sh firmware.bin
INPUT=$1
AES_KEY="$(openssl rand -hex 32)" # 生成256位AES密钥
IV="$(openssl rand -hex 16)" # 生成128位IV
# 1. 用AES-256-CTR加密固件
openssl enc -aes-256-ctr \
-in "$INPUT" \
-out "${INPUT}.enc" \
-K "$AES_KEY" \
-iv "$IV"
# 2. 用RSA公钥加密AES密钥
openssl rsautl -encrypt \
-inkey rsa_public.pem \
-pubin \
-in <(echo -n "$AES_KEY$IV") \
-out "${INPUT}.key.enc"
# 3. 计算签名
openssl dgst -sha256 -sign rsa_private.pem \
-out "${INPUT}.sig" \
"${INPUT}.enc"
echo "加密完成!"
echo "输出文件:${INPUT}.enc"
echo "密钥文件:${INPUT}.key.enc"
echo "签名文件:${INPUT}.sig"
对应的设备端解密流程(伪代码):
// 设备端解密 - 在安全环境中执行
int decrypt_firmware(uint8_t* enc_fw, uint32_t len,
uint8_t* enc_key, uint8_t* signature) {
// 1. 验证RSA签名
if (!rsa_verify(enc_fw, len, signature, rsa_pub_key)) {
return ERR_SIGNATURE_INVALID; // 签名不匹配,拒绝执行
}
// 2. 用RSA私钥解密AES密钥和IV
uint8_t aes_key[32], iv[16];
rsa_decrypt(enc_key, aes_key, rsa_priv_key);
// 3. AES解密固件
aes_ctr_decrypt(enc_fw, len, aes_key, iv);
// 4. CRC校验
if (crc32(enc_fw, len) != EXPECTED_CRC) {
return ERR_CRC_MISMATCH;
}
return SUCCESS;
}
注意:设备端的 RSA 私钥必须存储在安全元件(SE)或 TrustZone 中。如果私钥被读出来,整个加密体系就崩塌了。我见过有人把私钥放在普通 Flash 里,结果被 JTAG 读出来——那加密还有什么意义?
3.5 反逆向技术:不止是混淆
混淆只是第一步。真正的反逆向,需要从硬件和软件两个层面同时下手。
我常用的组合拳:
- 反调试检测:在关键函数入口检查调试寄存器(DHCSR、DEMCR),如果发现调试器附着,直接跳入死循环或擦除 Flash。
- 时序攻击防护:加密解密操作加入随机延时,防止攻击者通过功耗分析推断密钥。
- 自修改代码:部分关键函数在运行时从加密区解密到 RAM 执行,执行完立即擦除。这样即使 Flash 被读,也拿不到完整代码。
- JTAG/SWD 熔断:量产最后一步,永久禁用调试接口。这是最有效但最容易被忽略的一步。
我曾经犯过的错:有个产品量产时忘了熔断 JTAG,结果被安全研究员用 20 块钱的调试器就把固件读出来了。后来我定了个规矩:产线烧录完成后,必须做 JTAG 熔断验证测试,没熔断的算不良品。
3.6 小结:加密与混淆的平衡
说了这么多,其实就一句话:没有绝对的安全,只有不断增加的攻击成本。
加密让攻击者拿不到明文,混淆让拿到明文的人也看不懂,反调试让看懂的人也跑不起来。三层叠加,攻击者要付出的代价就指数级上升。但代价是双向的——你的开发调试也会变麻烦。所以我的建议是:
- 对消费级 IoT 产品,AES 加密 + 基本混淆就够了
- 对工业/医疗/金融设备,必须上全套:混合加密 + 控制流混淆 + 反调试 + SE
- 永远不要相信「我的产品没人会破解」这种话——你永远不知道攻击者有多闲