2、镜像校验的必要性:安全启动链的信任根、防止恶意固件注入、保证系统完整性
好,咱们接着聊。上一章我们把 BootROM 的基本概念捋了一遍,这一章我打算深入聊聊一个核心问题——为什么非要搞镜像校验?
说白了,BootROM 是整个芯片上电后第一个执行的代码。它要是被篡改了,后面所有的安全措施都是白搭。你想想看,如果连最底层的代码都不干净,那上层跑什么系统、做什么加密,不都是给别人打工吗?
我早年做过一个 IoT 网关的项目,当时为了赶工期,BootROM 的校验逻辑写得比较潦草。结果呢?产品上市三个月,就被安全团队发现了一个漏洞——攻击者可以通过物理手段篡改 SPI Flash 里的镜像,绕过校验直接加载恶意固件。那次教训让我彻底明白了:镜像校验不是可选项,是必选项。
2.1 安全启动链的信任根
先讲信任根。这个概念其实不复杂——信任根就是整个安全体系里,唯一一个不需要被验证的组件。它本身必须是可信的,然后由它去验证下一级,下一级再验证下下级,形成一条完整的信任链。
在嵌入式系统里,BootROM 就是这个信任根。为什么?因为 BootROM 是固化在芯片内部的只读存储器,出厂后无法修改。这就意味着,只要芯片设计没问题,BootROM 的代码就是绝对可信的。
信任链的传递过程:
- BootROM(信任根) → 验证 Bootloader 的签名和完整性
- Bootloader(已验证) → 验证 OS Kernel 的签名和完整性
- OS Kernel(已验证) → 验证文件系统和应用程序的签名和完整性
- 应用程序(已验证) → 正常运行
每一级都只信任上一级,上一级只信任更上一级,最终信任的源头就是 BootROM。
我个人习惯把这种结构叫做「洋葱模型」——从最核心的信任根开始,一层一层向外验证。每一层都只负责验证下一层,不越级,不跳步。这样做的好处是:即使某一层被攻破了,攻击者也很难向上回溯去篡改信任根。
嗯,这里要注意一点:信任根必须是硬件级别的。纯软件的信任根是不靠谱的,因为软件可以被修改。我见过一些方案把信任根放在外部 Flash 里,然后通过某种「魔法」来保证它不被篡改……说实话,这种设计我是不太放心的。
2.2 防止恶意固件注入
接下来聊第二个问题——恶意固件注入。这是嵌入式系统面临的最常见攻击手段之一。
攻击者可以通过多种方式把恶意固件塞进你的设备里:
- 物理攻击:直接拆机,用编程器改写 SPI Flash 或 eMMC 里的镜像
- 网络攻击:通过 OTA 更新通道,伪造合法的固件包进行升级
- 供应链攻击:在生产环节或物流环节,替换掉原始的固件镜像
- 调试接口攻击:通过 JTAG/SWD 等调试接口,直接写入恶意代码
你可能会问:「我加个密码验证不就行了?」
嗯,没那么简单。密码验证只能防住一部分人,而且密码本身也可能被破解或泄露。真正靠谱的做法是——数字签名验证。
我的经验:我曾经接手过一个项目,之前的团队用了一个简单的 CRC32 校验来「保护」固件。结果呢?攻击者只需要把恶意代码塞进去,然后重新计算一下 CRC32 值,就能轻松绕过。CRC32 只能检测随机错误,根本防不住蓄意篡改。
镜像校验的核心机制是这样的:
- 签名阶段(在开发/生产环境):用私钥对镜像的哈希值进行签名,生成签名数据
- 验证阶段(在设备启动时):用公钥解密签名,得到原始的哈希值,然后与镜像的实际哈希值进行比对
- 结果判断:如果一致,说明镜像未被篡改;如果不一致,立即停止启动
这里的关键点在于:私钥必须严格保密,公钥可以公开。公钥通常被固化在 BootROM 里,或者存储在一次性可编程(OTP)存储器中。攻击者即使拿到了公钥,也无法伪造签名,因为没有私钥。
注意:公钥的存储位置非常关键。如果公钥存储在可写的 Flash 里,攻击者可以替换成自己的公钥,然后用自己生成的私钥去签名恶意固件。这就是所谓的「公钥替换攻击」。所以,公钥必须存储在只读的、不可篡改的区域——比如 BootROM 内部或 OTP 区域。
2.3 保证系统完整性
最后一个点——系统完整性。这个其实很好理解:你希望设备运行的是你预期中的代码,而不是被篡改过的代码。
系统完整性包含两个层面:
| 完整性层面 | 说明 | 校验方式 |
|---|---|---|
| 静态完整性 | 镜像在存储介质中的完整性,确保未被篡改 | 数字签名 + 哈希校验 |
| 动态完整性 | 镜像在运行过程中的完整性,确保未被注入恶意代码 | 运行时内存保护、MPU/MMU 配置 |
静态完整性是 BootROM 镜像校验要解决的核心问题。设备上电后,BootROM 会先对下一级镜像进行校验,确保它没有被篡改过。只有校验通过,才会把控制权交给它。
动态完整性则是在系统运行过程中持续进行的保护。比如,通过内存保护单元(MPU)或内存管理单元(MMU)来隔离不同特权级别的代码,防止普通程序篡改内核代码。
我举个例子你就明白了:
假设你有一个智能门锁,里面跑着 Linux 系统。攻击者通过 Wi-Fi 漏洞入侵了系统,拿到了 root 权限。如果没有动态完整性保护,攻击者可以直接修改内核代码,让门锁在收到特定指令时自动开锁。但如果有动态完整性保护,内核代码区域是只读的,攻击者就无法修改它。
系统完整性的核心价值:
- 可信执行:确保设备执行的每一行代码都是你预期的
- 防回滚:防止攻击者把固件回滚到有漏洞的旧版本(需要配合版本号校验)
- 可追溯:任何对固件的修改都会留下痕迹,便于安全审计
说到防回滚,我得多说两句。有些攻击者不会直接注入恶意固件,而是把设备回滚到一个有已知漏洞的旧版本,然后利用那个漏洞进行攻击。所以,镜像校验不仅要检查签名,还要检查版本号——禁止回滚到低于当前版本的固件。