3. 威胁建模基础

OTA升级看起来就是点一下“检查更新”,但背后是一套环环相扣的流程。我做了这么多年嵌入式安全,见过太多在某个环节“翻车”的项目。说白了,每个步骤都有坑,踩一个就可能让设备变砖。

这一章,我把整个流程拆成六个核心阶段,一个一个讲清楚。

3.1 升级包生成:源头决定质量

升级包是怎么来的?不是简单地把新固件打个zip包就完事。生成阶段要做三件事:差异计算、元数据封装、签名保护。

核心原则:升级包越小越好,但安全不能缩水。

我个人习惯用bsdiff/bspatch做差分。为什么?因为全量包动不动几十MB,差分包可能只有几MB。我在一个智能家居项目里,差分包从12MB压到800KB,设备下载时间从5分钟降到30秒——用户体验完全不一样。

生成流程大致如下:

  1. 差异计算:对比旧版本v1.0和新版本v2.0的二进制,生成差分补丁。
  2. 元数据封装:写入版本号、目标设备型号、校验哈希、签名算法标识。
  3. 数字签名:用私钥对元数据+差分数据整体签名,防止篡改。
  4. 压缩打包:可选,进一步减小传输体积。

小技巧:元数据里一定要包含“最低可升级版本号”。我曾经遇到设备从v1.0直接跳到v3.0,中间差了两次不兼容的底层驱动变更,结果升级后外设全部失灵。加了这个字段,就能强制用户先升到v2.0再升v3.0。

3.2 升级包分发:CDN与断点续传

包做好了,怎么送到设备上?大部分方案走HTTPS + CDN。但这里有个容易被忽略的问题——设备网络环境千差万别。

我记得一个工业网关项目,设备部署在偏远山区,4G信号时有时无。下载到一半断网,又得从头开始。用户骂娘,我们加班。

解决方案是断点续传。设备端记录已下载的字节偏移量,重新连接后从断点继续。实现上需要:

  • 服务端支持Range请求头
  • 设备端维护下载进度文件
  • 校验每个分片的完整性

避坑指南:我曾经遇到CDN缓存了旧的升级包,设备下载了10次都是旧版本。后来强制在URL里加版本号时间戳,比如/firmware/v2.0.1.bin?t=1695000000,彻底解决缓存问题。

3.3 设备端下载:内存与存储的博弈

下载阶段,设备端要考虑两个资源:RAM和Flash。

RAM小的设备(比如只有256KB),不能一次性把整个升级包读进内存。我一般用流式下载+分块写入:

// 伪代码示例:流式下载与分块写入
while (downloaded_size < total_size) {
    chunk = http_get_range(url, downloaded_size, CHUNK_SIZE);
    if (verify_chunk_hash(chunk) == FAIL) {
        retry_chunk();
        continue;
    }
    flash_write(backup_partition, offset, chunk);
    downloaded_size += chunk.size;
}

这里有个关键点:下载到哪个分区?绝对不能直接覆盖当前运行的分区。我见过一个团队把下载数据直接写到运行分区,下载到一半断电,设备彻底变砖。

最佳实践:下载到备份分区(B分区),或者下载到独立的下载缓存区。等校验通过后再切换。

3.4 升级包校验:信任的基石

下载完了,能直接安装吗?不行。必须校验。校验分三层:

校验层级 校验内容 防止的攻击
完整性校验 哈希值(SHA-256) 传输过程中数据损坏
签名校验 数字签名(RSA/ECDSA) 恶意固件替换、中间人攻击
版本号校验 版本号 > 当前版本 回滚攻击(降级到有漏洞的版本)

签名校验是重中之重。公钥必须硬编码在Bootloader里,不能被应用层篡改。我见过一个方案把公钥放在文件系统里,结果攻击者替换了公钥,直接签了个恶意固件——这等于没锁门。

我曾经在一个车机项目里,发现签名校验通过了,但升级包里的驱动签名没校验。攻击者把合法固件里的驱动换成了恶意驱动,绕过了整体签名。后来我们改成逐文件签名校验,每个组件都有自己的签名。

3.5 备份与回滚:最后的救命稻草

校验通过,开始安装。但安装过程也可能失败——比如写入Flash时掉电、新固件启动崩溃。这时候,备份与回滚就是救命稻草。

我常用的方案是A/B分区切换:

  • A分区:当前运行版本
  • B分区:新版本安装目标
  • Bootloader:根据标志位决定启动哪个分区

升级流程变成:

  1. 下载到B分区
  2. 校验B分区完整性
  3. 设置“尝试启动标志”
  4. 重启,Bootloader启动B分区
  5. 新系统运行正常 → 清除尝试标志,正式切换
  6. 新系统启动失败 → Bootloader检测到尝试标志未清除,自动回滚到A分区

关键点:回滚后要保留失败日志,方便排查问题。我习惯在A分区留一个“回滚计数器”,连续回滚超过3次就停止自动升级,避免设备陷入“升级→失败→回滚→再升级”的死循环。

3.6 激活新版本:临门一脚

最后一步,激活。激活不只是“重启一下”,它包含:

  • 提交确认:新版本运行一段时间(比如24小时)无异常,才正式提交。
  • 清理旧版本:提交后可以删除旧版本分区,释放空间。
  • 上报状态:通知云端升级成功。

为什么要有“提交确认”?因为有些bug需要长时间运行才会暴露。我遇到过一个设备,升级后正常跑了12小时,然后内存泄漏导致崩溃。如果没有提交确认机制,系统已经删了旧版本,回都回不去。

建议:激活后保留旧版本至少48小时。等确认新版本稳定了,再通过云端指令清理旧分区。这叫“软切换”,比硬切换安全得多。

3.7 全流程架构图

下面这张图把六个阶段串起来,方便你理解整体脉络:

OTA升级全流程架构图 OTA升级全流程架构图 ① 升级包生成 差分计算 + 签名 ② 升级包分发 CDN + HTTPS ③ 设备端下载 流式分块写入 ④ 升级包校验 哈希 + 签名 + 版本 ⑤ 备份与回滚 A/B分区 + 尝试标志 ⑥ 激活新版本 提交确认 + 状态上报 关键安全防线 签名校验防止篡改 | 版本号校验防止回滚 | A/B分区防止变砖 | 提交确认防止隐性bug

嗯,整个流程就是这样。每个阶段都有它的技术难点和安全风险。你想想看,如果签名校验没做好,攻击者直接推送恶意固件;如果备份回滚没做好,一次升级失败设备就变砖。这些都不是理论问题,是我在项目里真金白银踩过的坑。