4、安全启动链:BootROM安全设计、Bootloader签名验证、内核完整性校验、文件系统校验

安全启动链,说白了就是一套「信任传递」机制。从芯片上电的第一条指令开始,每一级都要验证下一级的合法性。只要有一环被攻破,整个系统就裸奔了。

我在项目中见过太多「只做了签名验证,但BootROM裸奔」的设计。嗯,这就像给大门装了金锁,但钥匙就挂在门把手上。今天我们就从最底层的BootROM开始,一层层往上拆解。

4.1 BootROM安全设计——信任的根

BootROM是芯片上电后执行的第一段代码,固化在ROM里,不可修改。它负责初始化硬件、加载Bootloader到SRAM,然后跳转执行。

为什么BootROM是信任的根?因为它不可篡改。ROM是只读的,出厂后谁都没法改。这就保证了第一级代码一定是可信的。

核心原则:BootROM必须足够小、足够简单、足够安全。复杂是安全的天敌。

我个人习惯把BootROM的安全设计拆成三个维度:

  • 物理隔离:BootROM所在的地址空间,在跳转到Bootloader后必须锁定。防止攻击者通过DMA或调试接口回读ROM内容。
  • 最小功能集:只做必要的事——初始化时钟、配置内存控制器、加载Bootloader、验证签名。别加什么USB下载、网络协议栈,那是给自己挖坑。
  • 防回滚:BootROM里要固化一个最低安全版本号。如果Bootloader的版本号低于这个值,直接拒绝加载。我曾经遇到一个设备,攻击者把Bootloader降级到有漏洞的旧版本,绕过了所有安全校验——就是因为没做防回滚。
注意:有些芯片支持从外部SPI Flash启动。这种情况下,BootROM会先加载一小段「二级BootROM」到SRAM。这段代码必须由BootROM验证签名后才能执行。否则,攻击者可以替换外部Flash,注入恶意代码。

4.2 Bootloader签名验证——第一道防线

BootROM加载Bootloader时,会做两件事:校验签名、验证完整性。签名保证来源可信,完整性保证内容没被篡改。

常见的签名方案有两种:

方案 公钥存储位置 验证速度 安全性
RSA-2048 + SHA-256 BootROM内部OTP 慢(约100ms) 高
ECDSA P-256 + SHA-256 BootROM内部OTP 快(约20ms) 高
HMAC + 对称密钥 BootROM内部OTP 极快(约1ms) 中(密钥泄露风险)

我个人推荐ECDSA。RSA虽然成熟,但密钥长度大、计算慢。ECDSA用更短的密钥达到同等安全强度,对嵌入式设备更友好。

签名验证的流程大致如下:

// 伪代码:BootROM验证Bootloader签名
uint8_t hash[32];
SHA256(bootloader_image, bootloader_size, hash);

// 从OTP读取公钥
uint8_t public_key[64];
otp_read(OTP_PUBLIC_KEY_ADDR, public_key, 64);

// 验证签名
if (ECDSA_verify(public_key, hash, signature) != 0) {
    // 签名验证通过,跳转到Bootloader
    jump_to(bootloader_entry);
} else {
    // 签名验证失败,进入错误处理
    error_handler();
}
避坑指南:我曾经见过一个设计,公钥存储在外部Flash里。攻击者只要替换Flash,就能用自己的公钥签名恶意Bootloader。公钥必须存储在芯片内部的OTP或eFuse里,且写入后要熔断写保护。

4.3 内核完整性校验——别让系统被偷梁换柱

Bootloader验证通过后,会加载操作系统内核。这一步同样需要校验。你想想看,如果Bootloader是干净的,但内核被替换了,系统照样不安全。

内核完整性校验有两种常见做法:

  • 签名验证:Bootloader用公钥验证内核镜像的签名。这是最安全的做法,但需要内核镜像包含签名信息。
  • 哈希校验:Bootloader计算内核镜像的哈希值,与存储在安全区域(如OTP)的参考哈希比对。这种方法不需要公钥操作,但参考哈希的存储要安全。

我建议优先用签名验证。哈希校验有个问题——参考哈希怎么更新?如果内核升级了,参考哈希也要跟着变。而签名验证只需要更新内核镜像本身,公钥不变。

内核镜像的签名结构通常长这样:

+-------------------------------+
| 内核镜像头                     |
| - 镜像大小                     |
| - 镜像类型                     |
| - 版本号                       |
| - 签名算法标识                 |
+-------------------------------+
| 内核镜像数据                   |
| (压缩或未压缩)                 |
+-------------------------------+
| 签名信息                       |
| - 签名值 (RSA/ECDSA)          |
| - 证书链 (可选)                |
+-------------------------------+
关键点:签名信息必须覆盖整个内核镜像,包括镜像头。否则攻击者可以修改镜像头里的版本号或大小字段,绕过某些校验逻辑。

4.4 文件系统校验——最后一公里的安全

内核启动后,会挂载根文件系统。如果文件系统被篡改,攻击者可以植入恶意程序、修改配置文件、替换动态库。嗯,这一步经常被忽视。

文件系统校验主要有三种方式:

  1. 只读文件系统:使用SquashFS或EROFS等只读文件系统。内核在挂载时验证整个文件系统的哈希或签名。优点是简单,缺点是无法更新。
  2. dm-verity:Linux内核的块设备完整性校验机制。它把文件系统分成数据块,每个块都有哈希值,组成一棵Merkle树。内核在读取每个块时验证哈希。优点是支持只读校验,且可以更新(需要重新生成哈希树)。
  3. IMA (Integrity Measurement Architecture):内核的完整性度量架构。它在文件被访问时计算哈希,与预定义的策略比对。优点是灵活,缺点是性能开销大。

我个人最常用dm-verity。它在性能和安全性之间取得了很好的平衡。Android系统从4.4开始就用它来保护/system分区。

dm-verity的工作流程:

1. 构建文件系统时,计算每个数据块的哈希
2. 用这些哈希构建Merkle树,根哈希存储在安全区域
3. 内核挂载文件系统时,指定dm-verity目标
4. 每次读取数据块,dm-verity验证哈希
5. 如果哈希不匹配,返回I/O错误
注意:dm-verity只能检测篡改,不能阻止读取。攻击者如果控制了内核,可以绕过dm-verity直接读取块设备。所以,文件系统校验必须配合内核完整性校验一起使用。

4.5 安全启动链全景图

把上面四层串起来,就是一条完整的安全启动链。我画了一张图,帮你理清整个流程:

安全启动链全景图 安全启动链全景图 BootROM(信任根) 固化在ROM,不可篡改 Bootloader签名验证 ECDSA/RSA验证签名 内核完整性校验 签名验证或哈希校验 文件系统校验 dm-verity / SquashFS / IMA 用户空间(可信环境) 每一级验证通过后,才将执行权交给下一级。任何一级失败,系统停止启动。

你看,从BootROM到用户空间,每一层都环环相扣。攻击者只要攻破任何一层,就能控制整个系统。所以,每一层的安全设计都不能有短板。

我的经验:安全启动链的调试非常痛苦。一旦某一步验证失败,系统就变砖了。我建议在开发阶段保留一个「调试模式」,可以通过物理跳线或OTP标志位关闭签名验证。等产品量产时,再熔断这个标志位。这样既能调试,又能保证最终产品的安全性。

4.6 常见攻击手法与防御

说了这么多设计原则,我们来看看攻击者通常怎么下手。知己知彼嘛。

攻击手法 攻击目标 防御措施
BootROM漏洞利用 BootROM代码缺陷 最小功能集、代码审计、形式化验证
公钥替换 OTP/Flash中的公钥 公钥存储在OTP,写入后熔断
签名绕过 签名验证逻辑 验证失败必须进入安全错误处理,不能跳过
降级攻击 版本号字段 BootROM固化最低版本号
侧信道攻击 签名验证过程中的密钥 使用恒定时间比较、加噪、屏蔽

我曾经遇到一个案例:攻击者通过电压毛刺干扰BootROM的签名验证逻辑,让验证函数返回成功。嗯,这就是硬件攻击的威力。防御这种攻击,需要在硬件层面加电压检测电路,或者在软件层面做多次验证、随机化验证顺序。

重要提醒:安全启动链不是万能的。它只能保证系统启动时的完整性,无法防御运行时的攻击。运行时安全需要结合SELinux、seccomp、地址空间随机化等技术。但安全启动链是基础,没有它,其他安全措施都是空中楼阁。

好了,安全启动链的核心内容就这些。记住一句话:信任不能传递,必须逐级验证。从BootROM到文件系统,每一级都要用自己的信任锚去验证下一级。只有这样,才能构建一个真正可信的嵌入式系统。