3、安全启动链:信任根建立、Bootloader安全设计、固件签名验证、防回滚机制
安全启动链,说白了就是给医疗设备装上一道「从娘胎里就开始的安全锁」。我做了这么多年嵌入式安全,见过太多设备因为启动阶段被攻破,整个系统沦陷。你想想看,如果连最开始的启动代码都不安全,后面再强的加密算法都是白搭。
这一章,我们重点聊聊信任根怎么建立、Bootloader怎么设计才靠谱、固件签名验证的细节,以及那个让人头疼的防回滚机制。
3.1 信任根(Root of Trust)的建立
信任根是整个安全启动链的基石。它必须是一个不可篡改的、硬件级别的信任锚点。我个人习惯把信任根理解为「第一个说真话的人」——如果这个人本身就不靠谱,后面所有人说的话都不能信。
核心原则:信任根必须存储在一次性可编程(OTP)存储器中,比如eFuse或一次性写入的Flash区域。任何软件都无法修改它。
我在项目中遇到过最典型的案例:某款医疗监护仪使用了外部SPI Flash存储Bootloader,结果攻击者直接替换了Flash芯片,绕过了所有安全校验。嗯,这就是信任根没落到硬件里的后果。
建立信任根通常包含以下步骤:
- 硬件绑定:将公钥哈希或根证书哈希烧录到芯片的OTP区域
- 不可逆性:烧录后熔断eFuse,永久锁定该区域
- 最小化原则:信任根只做一件事——验证下一级Bootloader的签名
我的经验:千万别把整个公钥存进去,存哈希就够了。我见过有人把2048位的RSA公钥直接塞进OTP,结果64KB的OTP区域用了三分之一。存SHA-256哈希,只要32字节。
3.2 Bootloader安全设计
Bootloader是安全启动链的第二环,也是攻击者最喜欢下手的地方。为什么?因为它运行在系统最早期,内存、外设都还没初始化完全,防御能力最弱。
我建议Bootloader设计要遵循以下几个铁律:
- 最小化代码量:Bootloader越短越好,功能越少越好。我见过有人把网络协议栈塞进Bootloader,结果漏洞多到数不清。
- 只读执行:Bootloader代码区域必须在运行时设置为只读,防止运行时篡改。
- 硬件隔离:利用MPU或MMU将Bootloader与应用程序空间完全隔离。
这里给一个典型的Bootloader安全启动流程:
// 伪代码:安全Bootloader启动流程
void secure_boot(void) {
// 1. 从OTP读取信任根哈希
uint8_t root_hash[32];
read_otp(OTP_ROOT_HASH_ADDR, root_hash, 32);
// 2. 验证Bootloader自身的完整性
if (!verify_self_integrity()) {
enter_recovery_mode();
return;
}
// 3. 加载并验证下一级固件签名
if (!verify_firmware_signature()) {
enter_recovery_mode();
return;
}
// 4. 检查固件版本号(防回滚)
if (!check_firmware_version()) {
enter_recovery_mode();
return;
}
// 5. 跳转到固件执行
jump_to_firmware();
}
注意:我曾经在某个项目中,Bootloader的verify_self_integrity()函数本身存在缓冲区溢出漏洞。攻击者利用这个漏洞直接跳过了签名验证。所以,Bootloader的代码必须经过严格的形式化验证。
3.3 固件签名验证
固件签名验证,说白了就是给固件发一张「身份证」,系统启动时检查这张身份证是不是真的。我常用的方案是ECDSA签名,比RSA快,密钥也更短。
签名验证的核心流程:
| 步骤 | 操作 | 说明 |
|---|---|---|
| 1 | 计算固件哈希 | 使用SHA-256对整个固件镜像计算哈希值 |
| 2 | 验证签名 | 用OTP中的公钥解密签名,与哈希值比对 |
| 3 | 检查证书链 | 如果使用多级证书,逐级验证直到信任根 |
| 4 | 执行固件 | 验证通过后,将控制权交给固件 |
这里有个细节很多人会忽略——签名验证必须在RAM中进行,不能在Flash中直接操作。为什么?因为Flash读取速度慢,而且容易被DMA攻击。我习惯的做法是:先把固件加载到SRAM,在SRAM中完成验证,验证通过后再复制回Flash执行。
关键点:签名验证的代码本身不能被跳过。我见过一个设计,验证失败后只是打印一条日志,然后继续启动。这等于没验证。正确的做法是:验证失败直接进入恢复模式,不给任何执行固件的机会。
3.4 防回滚机制
防回滚机制,是为了防止攻击者把设备固件降级到有漏洞的旧版本。你想想看,如果攻击者发现某个旧版本有已知漏洞,他只要把固件刷回去,你的所有安全防护就形同虚设了。
我建议的防回滚方案:
- 版本号寄存器:在OTP或安全存储中维护一个单调递增的版本号
- 版本号比较:固件升级时,新固件的版本号必须大于当前版本号
- 硬件辅助:使用eFuse或一次性写入寄存器,每次升级后永久锁定旧版本
// 防回滚检查示例
bool check_firmware_version(uint32_t new_version) {
// 从安全存储读取当前版本号
uint32_t current_version = read_security_register(REG_FW_VERSION);
// 新版本必须大于当前版本
if (new_version <= current_version) {
// 记录安全事件
log_security_event("Rollback attack detected!");
return false;
}
// 更新版本号(不可逆)
write_security_register(REG_FW_VERSION, new_version);
return true;
}
血的教训:我曾经在一个项目中,防回滚机制只检查了主固件的版本号,没检查Bootloader的版本号。结果攻击者把Bootloader降级到旧版本,旧版本的Bootloader没有签名验证功能,整个安全链直接崩溃。所以,所有可升级的组件都要做防回滚。
另外,防回滚机制要和生产流程配合好。我记得有个项目,生产线上需要刷写旧版本固件进行校准测试,结果防回滚机制把生产流程卡住了。解决方案是:在生产模式下关闭防回滚检查,出厂前通过eFuse永久开启防回滚功能。
小技巧:版本号不要用简单的递增整数。我习惯用「主版本号.次版本号.补丁版本号.构建号」的四段式,每段占8位,组合成一个32位整数。这样既能防回滚,又能清晰标识固件版本。
最后总结一下安全启动链的核心思想:信任链必须从硬件开始,每一级只信任上一级,任何一环断裂都要立即停止启动。我在多个医疗设备项目中验证过这套方案,只要实现得当,基本能抵御99%的启动阶段攻击。
下一章我们会聊聊运行时安全监控,包括看门狗、异常检测和内存保护。到时候再细聊。