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。它的核心思想是:从芯片上电复位开始,每一级代码在运行之前,都要验证下一级代码的签名。只有签名通过,才允许执行。这样就能保证整个启动过程没有被篡改。

你想想看,一个典型的智能照明网关,启动过程大概是这样的:

  1. ROM Bootloader:芯片出厂时固化的只读代码,不可修改。它验证下一级 Bootloader 的签名。
  2. First Stage Bootloader (FSBL):验证 Second Stage Bootloader 的签名。
  3. Second Stage Bootloader (SSBL):验证操作系统内核(如 Linux Kernel)的签名。
  4. 操作系统内核:验证文件系统或应用程序的签名。

每一级都像一把锁,只有上一把钥匙打开了,才能拿到下一把钥匙。我在项目中遇到过一种情况:某个厂商只做了第一级 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、蓝牙这些协议怎么防窃听、防重放。到时候见。