4. 固件更新机制安全

固件更新机制是嵌入式设备生命周期中风险最集中的环节之一。它既是修复漏洞的唯一通道,也是攻击者植入持久化后门的理想入口。面试中,这一主题通常不会只问“OTA 分几步”,而是围绕一条完整攻击链展开:能否篡改更新包、能否绕过签名、能否把设备刷回有漏洞的旧版本、能否在回滚失败时让设备变砖。下面按实际工程视角逐层展开。

一、OTA 流程的真实结构与信任边界

一个可落地的 OTA 流程通常包含六个阶段:版本查询、包下载、完整性校验、签名验证、写入暂存区、切换启动。以常见的 A/B 分区方案为例,设备当前运行在 slot A,新固件写入 slot B,引导程序根据元数据决定下次从哪个 slot 启动。关键点在于:信任边界不在下载阶段,而在写入前的验证阶段。下载通道即使被中间人劫持,只要签名验证在写入前完成且验证逻辑不可绕过,攻击者拿到的也只是一个无法通过校验的包。

面试中常被追问的细节是“验证发生在哪里”。如果验证在应用层完成,而写入动作由独立的后台服务或引导程序执行,就可能出现 TOCTOU(检查时间与使用时间不一致)问题:应用层验证通过后,攻击者在写入前替换暂存文件。正确做法是让验证与写入在同一信任域内原子完成,或由引导程序在启动时对目标分区做二次校验。

二、签名校验:从“有没有”到“对不对”

签名校验的常见缺陷不是“没做”,而是“做错了”。典型错误包括:只校验哈希不校验签名、使用对称密钥导致密钥泄露即全线失守、签名覆盖范围不包含元数据(版本号、分区表、回滚计数器)、以及验证失败后仅记录日志却继续启动。

一个合理的非对称签名方案应满足:私钥离线保存,设备端仅存公钥;签名覆盖固件镜像与所有控制元数据;验证失败时进入安全状态而非继续执行。下面是一段示意性的验证逻辑,用于说明覆盖范围的重要性:

// 示意:签名覆盖镜像 + 元数据,避免元数据被单独篡改
struct fw_header {
    uint32_t magic;
    uint32_t version;
    uint32_t rollback_counter;   // 回滚保护计数器
    uint32_t image_len;
    uint8_t  image_hash[32];
    uint8_t  signature[64];      // 对以上字段 + image 的签名
};

bool verify_firmware(const struct fw_header *hdr, const uint8_t *image) {
    uint8_t digest[32];
    // 1. 先校验镜像哈希是否与头部声明一致
    sha256(image, hdr->image_len, digest);
    if (memcmp(digest, hdr->image_hash, 32) != 0) return false;
    // 2. 再验证头部与镜像整体的签名
    return ed25519_verify(hdr->signature, hdr, sizeof(*hdr), image, hdr->image_len);
}

注意这里先校验哈希再验签,顺序并非绝对,但必须保证两者都通过。若只验签不校验哈希,攻击者可在签名不变的情况下替换镜像内容(前提是签名未覆盖镜像,这本身就是设计错误)。实际产品中还应加入密钥轮换机制,例如头部携带 key_id,设备端保存多把公钥,避免单把密钥泄露导致永久失守。

三、回滚保护与降级攻击

降级攻击的本质是:攻击者获取一个旧版本固件,该版本存在已知漏洞,但签名合法。如果设备只校验签名而不校验版本单调性,攻击者就能把设备刷回旧版本,再利用旧漏洞入侵。防御的核心是单调递增的回滚计数器,且该计数器必须存储在攻击者无法回写的位置,通常是 OTP(一次性可编程)区域或受安全启动保护的安全存储。

回滚保护的基本规则是:新固件的 rollback_counter 必须严格大于设备当前记录的值,否则拒绝更新。设备在成功启动新固件后,将计数器更新为当前值。这里有一个工程难点:如果更新后设备无法启动,计数器已经递增,旧固件也无法回退,设备可能变砖。因此实际方案通常采用“尝试启动 + 确认”机制:新固件首次启动后进入待确认状态,若在限定次数内未收到确认信号,引导程序自动回退到旧分区,但计数器不回退。这样既阻止了恶意降级,又保留了故障恢复能力。

面试中常问的一个判断点是:回滚计数器存在哪里?如果存在普通 Flash 中,攻击者可以通过物理访问或漏洞写入将其改小,回滚保护形同虚设。因此必须明确:回滚保护的安全性取决于计数器存储的不可篡改性,而非校验逻辑本身。

四、降级攻击的完整防御链与验证方法

防御降级不能只靠一个计数器,而应形成多层校验。以下是一份可直接参考的检查清单,用于评估或设计 OTA 安全机制:

检查项 合格标准 常见缺陷
签名覆盖范围 镜像 + 版本号 + 回滚计数器 + 分区元数据 仅覆盖镜像,元数据可篡改
公钥存储 存于安全启动保护的只读区或 OTP 存于可写 Flash,可被替换
回滚计数器 OTP 或安全存储,单调递增 普通 Flash,可回写
验证失败行为 拒绝启动,进入恢复模式 记录日志后继续启动
降级尝试 拒绝并记录安全事件 仅拒绝,无审计
故障回退 回退分区但不回退计数器 回退时重置计数器

验证方法上,不能只看代码,要实际构造测试用例。例如:准备一个签名合法但 rollback_counter 等于当前值的固件包,观察设备是否拒绝;再准备一个签名合法但版本号更低的包,观察是否被拒绝;最后模拟写入过程中断电,观察设备能否恢复到可用状态。判断依据是:拒绝更新时设备仍能正常启动旧固件,且安全日志中记录了降级尝试事件。

五、面试中的高频追问与应答思路

当被问到“如果攻击者拿到了签名私钥怎么办”时,合格的回答应包含:密钥轮换、签名吊销列表、以及将回滚计数器与密钥绑定。更进一步的思路是引入硬件信任根,例如将签名验证放在安全 enclave 或 TEE 中执行,即使主系统被攻破,验证逻辑仍不可篡改。

另一个高频问题是“OTA 过程中断电怎么办”。这考察的是更新原子性设计。A/B 分区方案天然具备原子性:写入 B 分区失败不影响 A 分区运行,引导程序根据元数据决定启动分区。若采用单分区方案,则必须保证写入前完整校验、写入时使用双缓冲或日志式写入,否则断电即变砖。

最后需要强调的是,固件更新安全不是单一技术点,而是一条链:下载通道安全、包完整性、签名验证、回滚保护、故障恢复、审计日志,任何一环缺失都可能让整条链失效。面试中能把这几个环节的依赖关系讲清楚,并给出具体的参数、存储位置和验证方法,就已经超过了大多数候选人。