4、安全启动链:BootROM安全设计、Bootloader签名验证、内核完整性校验、文件系统校验
安全启动链,说白了就是一套「信任传递」机制。从芯片上电的第一条指令开始,每一级都要验证下一级的合法性。只要有一环被攻破,整个系统就裸奔了。
我在项目中见过太多「只做了签名验证,但BootROM裸奔」的设计。嗯,这就像给大门装了金锁,但钥匙就挂在门把手上。今天我们就从最底层的BootROM开始,一层层往上拆解。
4.1 BootROM安全设计——信任的根
BootROM是芯片上电后执行的第一段代码,固化在ROM里,不可修改。它负责初始化硬件、加载Bootloader到SRAM,然后跳转执行。
为什么BootROM是信任的根?因为它不可篡改。ROM是只读的,出厂后谁都没法改。这就保证了第一级代码一定是可信的。
我个人习惯把BootROM的安全设计拆成三个维度:
- 物理隔离:BootROM所在的地址空间,在跳转到Bootloader后必须锁定。防止攻击者通过DMA或调试接口回读ROM内容。
- 最小功能集:只做必要的事——初始化时钟、配置内存控制器、加载Bootloader、验证签名。别加什么USB下载、网络协议栈,那是给自己挖坑。
- 防回滚:BootROM里要固化一个最低安全版本号。如果Bootloader的版本号低于这个值,直接拒绝加载。我曾经遇到一个设备,攻击者把Bootloader降级到有漏洞的旧版本,绕过了所有安全校验——就是因为没做防回滚。
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();
}
4.3 内核完整性校验——别让系统被偷梁换柱
Bootloader验证通过后,会加载操作系统内核。这一步同样需要校验。你想想看,如果Bootloader是干净的,但内核被替换了,系统照样不安全。
内核完整性校验有两种常见做法:
- 签名验证:Bootloader用公钥验证内核镜像的签名。这是最安全的做法,但需要内核镜像包含签名信息。
- 哈希校验:Bootloader计算内核镜像的哈希值,与存储在安全区域(如OTP)的参考哈希比对。这种方法不需要公钥操作,但参考哈希的存储要安全。
我建议优先用签名验证。哈希校验有个问题——参考哈希怎么更新?如果内核升级了,参考哈希也要跟着变。而签名验证只需要更新内核镜像本身,公钥不变。
内核镜像的签名结构通常长这样:
+-------------------------------+
| 内核镜像头 |
| - 镜像大小 |
| - 镜像类型 |
| - 版本号 |
| - 签名算法标识 |
+-------------------------------+
| 内核镜像数据 |
| (压缩或未压缩) |
+-------------------------------+
| 签名信息 |
| - 签名值 (RSA/ECDSA) |
| - 证书链 (可选) |
+-------------------------------+
4.4 文件系统校验——最后一公里的安全
内核启动后,会挂载根文件系统。如果文件系统被篡改,攻击者可以植入恶意程序、修改配置文件、替换动态库。嗯,这一步经常被忽视。
文件系统校验主要有三种方式:
- 只读文件系统:使用SquashFS或EROFS等只读文件系统。内核在挂载时验证整个文件系统的哈希或签名。优点是简单,缺点是无法更新。
- dm-verity:Linux内核的块设备完整性校验机制。它把文件系统分成数据块,每个块都有哈希值,组成一棵Merkle树。内核在读取每个块时验证哈希。优点是支持只读校验,且可以更新(需要重新生成哈希树)。
- IMA (Integrity Measurement Architecture):内核的完整性度量架构。它在文件被访问时计算哈希,与预定义的策略比对。优点是灵活,缺点是性能开销大。
我个人最常用dm-verity。它在性能和安全性之间取得了很好的平衡。Android系统从4.4开始就用它来保护/system分区。
dm-verity的工作流程:
1. 构建文件系统时,计算每个数据块的哈希
2. 用这些哈希构建Merkle树,根哈希存储在安全区域
3. 内核挂载文件系统时,指定dm-verity目标
4. 每次读取数据块,dm-verity验证哈希
5. 如果哈希不匹配,返回I/O错误
4.5 安全启动链全景图
把上面四层串起来,就是一条完整的安全启动链。我画了一张图,帮你理清整个流程:
你看,从BootROM到用户空间,每一层都环环相扣。攻击者只要攻破任何一层,就能控制整个系统。所以,每一层的安全设计都不能有短板。
4.6 常见攻击手法与防御
说了这么多设计原则,我们来看看攻击者通常怎么下手。知己知彼嘛。
| 攻击手法 | 攻击目标 | 防御措施 |
|---|---|---|
| BootROM漏洞利用 | BootROM代码缺陷 | 最小功能集、代码审计、形式化验证 |
| 公钥替换 | OTP/Flash中的公钥 | 公钥存储在OTP,写入后熔断 |
| 签名绕过 | 签名验证逻辑 | 验证失败必须进入安全错误处理,不能跳过 |
| 降级攻击 | 版本号字段 | BootROM固化最低版本号 |
| 侧信道攻击 | 签名验证过程中的密钥 | 使用恒定时间比较、加噪、屏蔽 |
我曾经遇到一个案例:攻击者通过电压毛刺干扰BootROM的签名验证逻辑,让验证函数返回成功。嗯,这就是硬件攻击的威力。防御这种攻击,需要在硬件层面加电压检测电路,或者在软件层面做多次验证、随机化验证顺序。
好了,安全启动链的核心内容就这些。记住一句话:信任不能传递,必须逐级验证。从BootROM到文件系统,每一级都要用自己的信任锚去验证下一级。只有这样,才能构建一个真正可信的嵌入式系统。