2. 安全启动链基础:信任根、启动流程与签名验证

各位同学,今天我们聊安全启动链。这是整个防回滚攻击的基石。说白了,如果启动链都不安全,后面所有防护都是空中楼阁。我做了这么多年嵌入式安全,见过太多产品因为启动链没锁死,被攻击者轻松降级固件、植入后门。嗯,咱们一步步来拆解。

2.1 信任根(Root of Trust, RoT)

信任根是什么?它是整个安全体系的「第一块砖」。你想想看,一个链条要可靠,第一个环节必须绝对可信。在硬件安全里,这个环节通常固化在芯片内部,无法被外部篡改。

核心原则:信任根必须是不可变的、物理隔离的、且只能由硬件强制执行的。

我个人习惯把信任根分为三类:

  • 硬件信任根(HW RoT):比如芯片内部的 Boot ROM,出厂后只读,无法修改。我在一个 IoT 项目里遇到过,厂商把启动代码放在 SPI Flash 里,结果攻击者直接替换 Flash 芯片就绕过了校验。这就是典型的没有硬件信任根。
  • 软件信任根(SW RoT):通常指经过签名的 Bootloader。它依赖硬件信任根来验证自身完整性。但要注意,软件信任根本身是可更新的,所以更新机制必须同样安全。
  • 密码学信任根(Crypto RoT):比如芯片内置的密钥存储区(eFuse、OTP),用于存储签名验证的公钥哈希。这些密钥一旦写入,就无法被软件读取或修改。

我的经验:设计信任根时,一定要考虑「密钥的祖先」。我曾经见过一个方案,公钥存储在 Flash 里,结果攻击者通过电压故障注入,让 CPU 跳过了公钥校验。后来我们改用 eFuse 存储公钥哈希,才彻底堵住这个漏洞。

2.2 安全启动流程

安全启动,本质上是一个「链式验证」的过程。每一级在加载下一级之前,都要先验证它的签名。如果验证失败,就拒绝执行。这个流程我画了一张图,大家看看。

安全启动链流程图 安全启动链:链式验证流程 Boot ROM (RoT) 验证 BL1 (Bootloader 1) 验证 BL2 (Bootloader 2) 验证 OS Kernel 系统运行 关键点 • 每一级验证下一级签名 • 验证失败则停止启动 • 硬件强制不可跳过

这个流程看起来简单,但实际落地时坑很多。我举几个例子:

  1. Boot ROM 必须不可绕过:有些芯片支持从外部存储器直接启动,如果这个路径没锁死,攻击者可以跳过 Boot ROM。我记得有个车规级芯片就出过这种问题,后来打了补丁。
  2. 每一级都要验证:不能只验证第一级,后面就放行。攻击者会替换中间环节。我曾经审计过一个方案,厂商只验证了 BL1,结果 BL2 被替换后直接加载了恶意 OS。
  3. 失败处理要硬:验证失败时,不能只是打印一条日志然后继续。应该直接死循环或触发硬件复位。我见过一个产品,验证失败后只是报了个警告,系统照样跑,等于没防护。

注意:安全启动不是「可选项」,而是「强制项」。如果芯片支持安全启动,必须启用,并且要锁定配置寄存器,防止攻击者通过 JTAG 或调试接口关闭它。

2.3 签名验证机制

签名验证是安全启动的核心技术。没有它,信任根和启动流程都是空谈。我们常用的签名算法是 RSA 或 ECDSA,配合哈希算法(如 SHA-256)。

基本流程是这样的:

  • 签名阶段(在开发环境完成):对固件镜像计算哈希,然后用私钥对哈希加密,生成签名。签名和固件一起发布。
  • 验证阶段(在设备上完成):Boot ROM 读取固件,计算哈希,然后用公钥解密签名,得到原始哈希。两个哈希比对,一致则通过。

这里我贴一段伪代码,展示验证逻辑:

// 伪代码:安全启动验证
bool verify_image(uint8_t *image, uint32_t size, uint8_t *signature) {
    // 1. 计算固件哈希
    uint8_t hash[32];
    sha256(image, size, hash);
    
    // 2. 从 eFuse 读取公钥
    uint8_t public_key[64];
    read_efuse(PUBLIC_KEY_REGION, public_key);
    
    // 3. 用公钥解密签名,得到原始哈希
    uint8_t decrypted_hash[32];
    ecdsa_verify(public_key, signature, decrypted_hash);
    
    // 4. 比对哈希
    if (memcmp(hash, decrypted_hash, 32) == 0) {
        return true;  // 验证通过
    } else {
        // 验证失败,进入死循环
        while(1);
    }
}

这段代码看起来简单,但有几个细节要注意:

环节 常见错误 正确做法
哈希计算 只计算部分区域,忽略头部 必须覆盖整个固件镜像,包括头部和负载
公钥存储 放在可读写的 Flash 中 必须放在 eFuse 或 OTP,且写死后锁定
签名算法 使用弱算法(如 MD5、SHA-1) 至少 SHA-256 + RSA-2048 或 ECDSA P-256
失败处理 返回错误码,让上层决定 硬件强制停机,不给攻击者任何机会

我的建议:公钥的哈希值(而不是公钥本身)存储在 eFuse 中。验证时,先加载公钥,计算它的哈希,与 eFuse 中的值比对。这样即使攻击者替换了公钥,哈希比对也会失败。这个技巧我在一个金融终端项目里用过,效果很好。

2.4 防回滚与安全启动的关系

你可能会问:安全启动和防回滚有什么关系?关系大了。安全启动保证「只有签名的固件才能运行」,而防回滚保证「只有新版本的固件才能运行」。两者结合,才能防止攻击者用旧版本固件(包含已知漏洞)来降级攻击。

实现方式很简单:在固件镜像中加入版本号字段,并在签名验证时检查版本号是否大于等于当前版本。如果版本号更低,直接拒绝启动。

一句话总结:安全启动是「能不能跑」,防回滚是「哪个版本能跑」。两者缺一不可。