4、固件安全:固件签名与验证、安全启动、固件加密、防回滚机制
好,咱们来聊聊固件安全。说实话,在物联网设备里,固件就是设备的灵魂。你想想看,如果攻击者能篡改你的固件,那设备基本上就归他管了。我在做智能照明网关项目时,就亲眼见过因为固件没做保护,整批设备被植入挖矿脚本的惨案。所以这一章,咱们把固件安全的四个核心防线讲透。
4.1 固件签名与验证
固件签名,说白了就是给固件打个「防伪标签」。我习惯用非对称加密来做这件事。流程很简单:
- 签名阶段:用私钥对固件哈希值加密,生成签名文件
- 验证阶段:设备用公钥解密签名,比对哈希值是否一致
嗯,这里要注意:私钥必须保存在安全的离线环境中。我在项目中遇到过有人把私钥直接放在CI/CD服务器上,结果被泄露了,整个签名体系瞬间失效。
核心要点:签名只能防篡改,不能防泄密。要防泄密,得靠后面的加密。
代码示例(使用Python和cryptography库):
from cryptography.hazmat.primitives import hashes, asymmetric
from cryptography.hazmat.primitives.asymmetric import padding
# 签名
def sign_firmware(firmware_data, private_key):
signature = private_key.sign(
firmware_data,
padding.PSS(
mgf=padding.MGF1(hashes.SHA256()),
salt_length=padding.PSS.MAX_LENGTH
),
hashes.SHA256()
)
return signature
# 验证
def verify_firmware(firmware_data, signature, public_key):
try:
public_key.verify(
signature,
firmware_data,
padding.PSS(
mgf=padding.MGF1(hashes.SHA256()),
salt_length=padding.PSS.MAX_LENGTH
),
hashes.SHA256()
)
return True
except:
return False
4.2 安全启动
安全启动,就是设备上电后,逐级验证每一段代码的完整性。从BootROM开始,到Bootloader,再到操作系统内核,最后到应用程序。每一级都验证下一级的签名。
为什么会这样设计?因为如果BootROM被篡改,整个信任链就断了。我记得在做一个Zigbee网关时,芯片厂商的BootROM有个漏洞,攻击者可以直接跳过签名验证。那段时间我们紧急联系厂商打补丁,折腾了整整两周。
我的建议:选芯片时,优先选那些有硬件安全启动模块的。比如NXP的i.MX系列、STM32MP1系列,它们都有OTP(一次性可编程)存储公钥哈希,从硬件层面保证信任根。
安全启动的典型流程:
- 芯片上电,执行BootROM中的固化代码
- BootROM验证Bootloader的签名
- Bootloader验证操作系统内核的签名
- 内核验证关键驱动和服务的签名
- 系统启动完成
你想想看,如果中间任何一步验证失败,设备就应该进入恢复模式,而不是继续启动。我曾经见过一个产品,验证失败后只是打印了警告日志,然后照常启动——这跟没做安全启动有什么区别?
4.3 固件加密
固件加密,是为了防止攻击者通过读取Flash来逆向你的代码。我一般用AES-256-GCM来做,因为它既能加密,又能提供完整性校验。
加密流程:
- 生成随机密钥(每个设备不同)
- 用设备唯一密钥加密固件
- 将加密后的固件烧录到设备
- 设备启动时,用内部密钥解密
警告:千万不要把加密密钥硬编码在固件里!我曾经在审计一个项目时,发现他们把AES密钥写在了代码注释里,还标注了「不要泄露」——这简直是掩耳盗铃。
正确的做法是利用芯片的硬件安全模块(HSM)或安全元件(SE)来存储密钥。比如:
| 方案 | 密钥存储位置 | 安全性 | 成本 |
|---|---|---|---|
| 软件加密 | Flash/代码中 | 低 | 低 |
| HSM | 芯片内部OTP | 中 | 中 |
| 安全元件 | 独立安全芯片 | 高 | 高 |
对于智能照明这种成本敏感的场景,我建议用芯片自带的HSM。比如ESP32的eFuse就可以存储密钥,虽然容量小了点,但够用。
4.4 防回滚机制
防回滚,就是防止攻击者把固件降级到有漏洞的旧版本。你想想看,如果攻击者发现新版本修复了某个漏洞,他完全可以刷回旧版本继续利用那个漏洞。
我常用的做法是维护一个「版本计数器」。每次升级时,计数器递增,设备只允许升级到更高版本号的固件。
关键点:版本计数器必须存储在不可篡改的区域,比如OTP或安全存储区。如果存在普通Flash里,攻击者可以直接改掉。
实现示例:
// 伪代码示例
#define MAX_VERSION 10 // OTP中存储的最大允许版本
bool check_version(uint8_t new_version) {
uint8_t current_max = read_otp(MAX_VERSION_ADDR);
if (new_version <= current_max) {
// 版本过低或相同,拒绝升级
return false;
}
// 更新OTP中的版本号
write_otp(MAX_VERSION_ADDR, new_version);
return true;
}
嗯,这里有个坑:如果版本计数器写坏了,设备就永远无法升级了。我曾经在项目中遇到过OTP写入失败的情况,结果那批设备只能停留在旧版本。后来我加了个「熔断机制」——如果连续3次写入失败,就允许跳过版本检查,但会记录异常日志。
避坑指南:我曾经把版本号存在Flash里,结果攻击者直接修改了Flash内容,绕过了回滚保护。后来改用OTP,虽然成本高了一点,但安全性提升了一个档次。
总结
固件安全的四个防线,缺一不可:
- 签名验证:保证固件来源可信
- 安全启动:建立完整的信任链
- 固件加密:防止代码被逆向
- 防回滚:堵住降级攻击的漏洞
说实话,这四个点做下来,固件安全基本上就稳了。但别忘了,安全是一个持续的过程。新的攻击手法层出不穷,咱们也得不断更新防护策略。好了,这一章就到这里,下一章咱们聊聊通信协议的安全。