11、OTA升级中的安全机制:签名验证链、完整性校验与防回滚
说实话,做车载OTA最让我睡不着觉的,不是升级失败,而是升级被攻破。你想啊,一辆在路上跑的车,要是被刷入了恶意固件,那后果真不敢想。我参与过的几个量产项目中,安全团队最常挂在嘴边的就是一句话:「攻击者可能已经拿到了你的升级包,我们要假设最坏情况。」
所以这一章,我们来聊聊OTA升级里那些「看不见的防线」。说白了,就是一套组合拳:签名验证链、完整性校验、防回滚机制,再加上安全启动流程。它们环环相扣,缺一不可。
11.1 签名验证链:从Bootloader到系统分区的层层信任
先问一个问题:你怎么确定你刷进去的系统镜像是官方发布的?
答案就是签名验证链。Android车载系统里,最核心的就是AVB(Android Verified Boot)和dm-verity。我习惯把AVB理解成「信任的接力棒」——从芯片内部的只读存储开始,一级一级往下传。
核心逻辑:每一级在加载下一级之前,都要验证它的签名。验证通过,才把控制权交出去。任何一级验证失败,系统就拒绝启动。
具体来说,流程是这样的:
- Boot ROM(芯片内部固化,不可修改)验证Bootloader的签名。
- Bootloader验证Boot分区的签名(包含kernel和dtb)。
- Kernel启动后,通过dm-verity机制验证system、vendor等分区的完整性。
我在一个项目里遇到过一个问题:客户自己签了Bootloader,但忘了更新公钥。结果Boot ROM验签失败,机器直接变砖。嗯,从那以后我每次做签名都会反复确认公钥哈希是否匹配。
AVB的签名信息是存放在分区末尾的VBMeta结构体里的。你可以用avbtool来查看:
# 查看一个镜像文件的AVB信息
avbtool info_image --image system.img
# 输出示例:
# Footer version: 1.0
# Image size: 2147483648 bytes
# Original image size: 2147483648 bytes
# VBMeta offset: 2147483648
# VBMeta size: 3200 bytes
# --
# Minimum libavb version: 1.0
# Header block: 256 bytes
# Authentication block: 320 bytes
# Auxiliary block: 2624 bytes
# Public key (sha1): 1234abcd...
# Digest:
# sha256: 5678efgh...
这里有个关键点:AVB支持两种验证模式——验证启动和强制启动。验证启动模式下,验签失败会显示警告但允许继续启动;强制启动模式下,验签失败直接拒绝启动。我个人建议量产车一定要用强制启动模式,别给攻击者留任何余地。
11.2 dm-verity:运行时完整性校验
签名验证只解决了「启动时」的安全问题。那系统跑起来之后呢?如果有人通过漏洞拿到了root权限,修改了system分区里的文件,怎么办?
这就是dm-verity登场的时候了。dm-verity是Linux内核的一个设备映射器目标,它会在你读取每个块设备时,实时计算哈希并与预存的哈希树对比。
说白了,它就像给每个数据块配了一个「指纹」。你读数据的时候,它先核对指纹,对不上就报错。
我的经验:dm-verity的哈希树是存储在分区末尾的。如果分区被修改,哈希树也得重新生成。所以OTA升级时,我们不仅要更新系统镜像,还要同步更新哈希树。我曾经见过一个团队只更新了镜像没更新哈希树,结果升级后系统一直报I/O错误,排查了两天才找到原因。
dm-verity的配置通常在BoardConfig.mk里:
# 启用dm-verity
BOARD_AVB_ENABLE := true
BOARD_AVB_ALGORITHM := SHA256_RSA4096
BOARD_AVB_KEY_PATH := external/avb/test/data/testkey_rsa4096.pem
BOARD_AVB_ROLLBACK_INDEX := 0
这里要注意,BOARD_AVB_ROLLBACK_INDEX就是防回滚的关键参数,我们马上会讲到。
11.3 升级包完整性校验:别让坏数据上车
升级包从云端下载到车机,中间要经过CDN、基站、车载T-Box,任何一个环节都可能出问题。数据损坏、中间人攻击、存储介质坏块……都有可能。
所以,升级包完整性校验是OTA的第一道防线。我常用的做法是双重校验:
- 文件级校验:每个升级文件(如system.img、vendor.img)都附带SHA-256哈希值,下载完成后逐文件校验。
- 包级校验:整个升级包使用RSA或ECDSA签名,升级前验签。
代码层面,校验逻辑大概长这样:
// 伪代码:升级包完整性校验
bool verifyOtaPackage(const char* packagePath) {
// 1. 读取升级包签名
Signature sig = readSignature(packagePath);
// 2. 用预置公钥验签
PublicKey pubKey = getEmbeddedPublicKey();
if (!verifySignature(packagePath, sig, pubKey)) {
logError("签名验证失败!");
return false;
}
// 3. 逐文件校验哈希
Manifest manifest = readManifest(packagePath);
for (FileEntry entry : manifest.files) {
uint8_t actualHash[32];
computeSha256(entry.path, actualHash);
if (memcmp(actualHash, entry.expectedHash, 32) != 0) {
logError("文件 %s 哈希不匹配!", entry.path);
return false;
}
}
return true;
}
注意:公钥一定要固化在Bootloader或Recovery分区里,不能从升级包中读取。否则攻击者可以替换公钥,用自己的签名通过验证。我见过一个设计方案,公钥竟然放在升级包的头部……这相当于把锁的钥匙挂在门上。
11.4 防回滚机制:别让旧版本漏洞复活
攻击者有时候不会直接攻破你的新系统,而是想办法让你「降级」到有漏洞的旧版本。比如,新版本修复了一个内核漏洞,攻击者把旧版本刷回去,再利用那个漏洞提权。
防回滚机制就是干这个的。它的原理很简单:每个系统分区都有一个回滚索引(rollback index),新版本的索引必须大于等于当前版本。
在AVB里,回滚索引是存储在VBMeta中的。每次启动时,Bootloader会读取当前分区的回滚索引,并与存储在安全存储中的「最大已见索引」对比。如果新索引小于最大索引,就拒绝启动。
我画了一张图,帮你理解这个流程:
回滚索引的管理在代码里是这样的:
// 伪代码:防回滚检查
bool checkRollbackProtection(uint64_t newRollbackIndex) {
// 从安全存储读取当前最大索引
uint64_t currentMaxIndex = readSecureStorage("rollback_index");
if (newRollbackIndex < currentMaxIndex) {
logError("防回滚触发:新索引 %lu < 当前最大 %lu",
newRollbackIndex, currentMaxIndex);
return false;
}
// 更新安全存储中的最大索引
writeSecureStorage("rollback_index", newRollbackIndex);
return true;
}
避坑指南:我曾经在一个项目里,回滚索引只存在了文件系统里。结果有人通过恢复出厂设置把索引清掉了,成功降级到了旧版本。后来我们改用了RPMB分区来存储索引,这个问题才彻底解决。记住,防回滚的「记忆」必须存储在不可篡改的区域。
11.5 安全启动流程:把信任链串起来
现在我们把前面讲的所有机制串起来,看看一次完整的安全启动流程是什么样的:
- 上电,Boot ROM执行,验证Bootloader签名。
- Bootloader验证Boot分区签名,同时检查回滚索引。
- Kernel启动,挂载system分区时启用dm-verity。
- init进程启动,挂载vendor、product等分区,同样启用dm-verity。
- 系统服务启动,OTA客户端检查是否有待安装的升级包。
- 升级包下载完成后,验签、校验哈希、检查回滚索引。
- 写入新分区,更新回滚索引。
- 重启,进入新系统,重复步骤1-4。
你看,每一步都在验证上一级的可信度。这就是所谓的「信任链」——从硬件开始,一级一级传递信任。
关键点:信任链的根(Root of Trust)必须是硬件级别的,不可修改的。在车载系统里,这个根通常是芯片内部的Boot ROM和一次性可编程(OTP)存储的公钥哈希。
最后,我整理了一个表格,把今天讲的核心机制做个对比:
| 安全机制 | 作用阶段 | 保护目标 | 关键依赖 |
|---|---|---|---|
| AVB签名验证 | 启动时 | 防止篡改的分区被加载 | 公钥固化在Boot ROM |
| dm-verity | 运行时 | 防止运行时修改分区内容 | 哈希树存储在分区末尾 |
| 升级包完整性校验 | 升级前 | 防止损坏或被篡改的包被安装 | RSA/ECDSA签名 + SHA-256 |
| 防回滚机制 | 升级时 + 启动时 | 防止降级到有漏洞的版本 | 安全存储(如RPMB) |
嗯,这一章的内容就到这里。安全机制这东西,平时感觉不到它的存在,但一旦出了问题,代价就是巨大的。我始终相信一句话:安全不是一种功能,而是一种设计理念。希望你在设计OTA系统时,能把安全刻在骨子里。
公众号:蓝海资料掘金营,微信deep3321