4、固件安全防护:固件签名验证、安全启动链、防回滚机制
好,咱们接着聊固件安全。说实话,智能照明系统里最容易出问题的环节,往往不是硬件本身,而是跑在芯片里的那一段固件。你想想看,一个灯泡或者一个网关,如果固件被人篡改了,那它就不再是你的设备了,可能变成别人手里的“肉鸡”。
我在做智能家居安全评估的时候,遇到过不少厂商,硬件设计得挺扎实,但固件防护几乎是裸奔的。攻击者只要拿到一个固件包,用十六进制编辑器改几个字节,再刷回去,设备就“叛变”了。所以,固件安全防护必须从三个维度来构建:签名验证、安全启动链、防回滚机制。咱们一个一个说。
4.1 固件签名验证:给固件贴上“防伪标签”
固件签名验证,说白了就是给固件文件加一个数字签名。这个签名只有厂商的私钥能生成,而设备里只存公钥。设备在升级固件时,先用公钥验证签名是否合法。如果签名不对,直接拒绝升级。
我个人习惯用 ECDSA(椭圆曲线数字签名算法)来做签名。相比 RSA,ECDSA 的密钥更短,计算速度更快,特别适合资源受限的 IoT 设备。嗯,这里要注意:私钥一定要保存在安全的硬件安全模块(HSM)里,绝对不能放在编译服务器上。我曾经见过一个项目,私钥就明文写在 Makefile 里,结果被离职员工带走了,整个产品线的设备都能被恶意固件接管。
核心要点:
- 使用 ECDSA 或 Ed25519 算法,避免使用已过时的 SHA-1
- 公钥烧录在设备的一次性可编程(OTP)区域,不可更改
- 签名过程必须在安全的离线环境中完成
下面是一个典型的固件签名验证流程的伪代码,你可以参考一下:
// 设备端固件验证流程
bool verify_firmware(const uint8_t* firmware, size_t len,
const uint8_t* signature, const uint8_t* public_key) {
// 1. 计算固件哈希
uint8_t hash[32];
sha256(firmware, len, hash);
// 2. 使用公钥验证签名
if (ecdsa_verify(public_key, hash, signature) != 0) {
// 签名验证失败
return false;
}
// 3. 检查固件版本号(防回滚,后面会讲)
if (!check_version(firmware)) {
return false;
}
return true;
}
避坑指南:
我曾经遇到过一个案例,厂商把公钥硬编码在固件里,而且固件本身没有加密。攻击者直接反编译固件,把公钥替换成自己的公钥,然后用自己的私钥签名恶意固件。结果就是,设备“合法”地运行了恶意代码。所以,公钥必须存储在硬件隔离区,比如安全元件(SE)或 TrustZone 中。
4.2 安全启动链:从第一行代码开始信任
安全启动链,英文叫 Secure Boot Chain。它的核心思想是:从芯片上电复位开始,每一级代码在运行之前,都要验证下一级代码的签名。只有签名通过,才允许执行。这样就能保证整个启动过程没有被篡改。
你想想看,一个典型的智能照明网关,启动过程大概是这样的:
- ROM Bootloader:芯片出厂时固化的只读代码,不可修改。它验证下一级 Bootloader 的签名。
- First Stage Bootloader (FSBL):验证 Second Stage Bootloader 的签名。
- Second Stage Bootloader (SSBL):验证操作系统内核(如 Linux Kernel)的签名。
- 操作系统内核:验证文件系统或应用程序的签名。
每一级都像一把锁,只有上一把钥匙打开了,才能拿到下一把钥匙。我在项目中遇到过一种情况:某个厂商只做了第一级 Bootloader 的签名验证,后面的环节全跳过了。攻击者只要绕过第一级,后面的代码随便改。这就像你家大门装了把好锁,但窗户全开着。
重要提醒:
安全启动链必须覆盖到最终的应用层。如果只验证到内核,而应用程序没有签名验证,攻击者仍然可以通过替换应用文件来执行恶意代码。我建议在文件系统层面也做完整性校验,比如使用 dm-verity(Android 上用的技术)或 IMA(Integrity Measurement Architecture)。
下面是一个简化的安全启动链验证流程表格:
| 启动阶段 | 验证者 | 被验证者 | 验证方式 |
|---|---|---|---|
| ROM Bootloader | 硬件固化 | FSBL | 公钥验证签名 |
| FSBL | ROM Bootloader | SSBL | 公钥验证签名 |
| SSBL | FSBL | OS Kernel | 公钥验证签名 |
| OS Kernel | SSBL | App/文件系统 | 哈希校验或签名 |
4.3 防回滚机制:不让旧漏洞“复活”
防回滚机制,英文叫 Anti-Rollback。它的作用是防止攻击者把固件降级到有已知漏洞的旧版本。你想想看,如果厂商修复了一个严重漏洞,发布了新固件,但攻击者可以强制设备刷回旧版本,那这个修复就白费了。
我见过一个真实的案例:某品牌的智能灯泡,固件升级没有做版本号检查。攻击者拿到了旧版本固件(里面有一个远程代码执行漏洞),通过物理接触或中间人攻击,把灯泡刷回了旧版本,然后利用漏洞控制了整个 Zigbee 网络。嗯,这就是典型的回滚攻击。
防回滚的实现方式通常有两种:
- 版本号比较:在固件头中嵌入一个单调递增的版本号。设备在升级时,检查新固件的版本号是否大于当前版本号。如果小于或等于,拒绝升级。
- 硬件熔丝(eFuse):在芯片内部使用一次性可编程熔丝来记录当前版本。一旦熔丝烧断,就无法恢复。这种方式更安全,但成本也更高。
实践建议:
我个人建议在智能照明系统中,至少使用版本号比较的方式。如果成本允许,可以结合硬件熔丝。另外,版本号最好存储在安全存储区(如 Flash 的受保护区域),防止被篡改。
下面是一个简单的防回滚检查代码示例:
// 固件头结构体
typedef struct {
uint32_t magic; // 魔数,用于识别固件格式
uint32_t version; // 固件版本号,单调递增
uint32_t firmware_size; // 固件大小
uint8_t signature[64]; // ECDSA 签名
uint8_t data[]; // 固件数据
} firmware_header_t;
// 防回滚检查
bool check_version(const firmware_header_t* new_fw) {
uint32_t current_version = read_current_version_from_secure_storage();
if (new_fw->version <= current_version) {
// 版本号过低,拒绝升级
log_error("Rollback attack detected: current=%u, new=%u",
current_version, new_fw->version);
return false;
}
// 更新存储的版本号
write_version_to_secure_storage(new_fw->version);
return true;
}
避坑指南:
我曾经在项目中犯过一个错误:版本号用的是无符号整数,但比较时用了有符号比较。结果版本号从 0xFFFFFFFF 回滚到 0x00000001 时,有符号比较认为 0xFFFFFFFF 是负数,小于 0x00000001,导致回滚检查失效。所以,一定要用无符号整数比较,并且版本号要单调递增,不能循环。
4.4 三者协同:构建完整的固件安全体系
固件签名验证、安全启动链、防回滚机制,这三者不是孤立的,它们必须协同工作。我习惯把它们比作三道防线:
- 第一道防线(固件签名验证):防止未授权的固件被刷入设备。
- 第二道防线(安全启动链):确保设备启动过程中,每一级代码都是可信的。
- 第三道防线(防回滚机制):防止攻击者利用旧版本的漏洞绕过前两道防线。
举个例子:如果攻击者拿到了一个旧版本的合法签名固件(比如版本 1.0,里面有漏洞),但没有防回滚机制,他就可以直接刷入旧版本,然后利用漏洞关闭安全启动链。有了防回滚机制,设备会拒绝版本 1.0,因为当前版本已经是 2.0 了。
所以,这三者缺一不可。我在做安全架构设计时,一定会把这三项列为强制要求,而不是可选项。
最后提醒:
固件安全不是一锤子买卖。随着时间推移,签名算法可能会被破解(比如 SHA-1 已经被证明不安全),密钥可能会泄露。所以,我建议定期更新签名算法,并建立密钥轮换机制。另外,所有安全相关的日志(如签名验证失败、回滚检测触发)都要记录并上报到云端,方便安全团队分析。
好了,固件安全防护这部分就讲到这里。下一章咱们聊聊通信安全,也就是 Zigbee、Wi-Fi、蓝牙这些协议怎么防窃听、防重放。到时候见。