自举程序设计注意事项-现代嵌入式系统
编辑:宝星微科技 | 发布时间:2020-06-11 10:30 | 浏览次数:292
{attr}0{/attr}
许多现代嵌入式系统需要通过现场固件更新来修复错误或改进功能。通常,此类更新功能通过自举程序来 实现。自举程序是一个独立于主应用程序的特殊应用程序,能够更新主应用程序。 本文档介绍了设计稳健的自举程序时必须考虑的一些细节。
自举程序的类型和固件更新选项
单片机自举程序可以多种不同方式工作,具体取决于硬件的复杂度和用户外部通信接口的可用性。 最基本的自举程序可以通过通信接口接收映像,并在通过完整性验证后写入接收的映像。大多数情况下, 自举程序不可更新,并且仅限用于简单的接口,如 UART、SPI 或 I 2C。最终产品的用户通常无法访问这类 接口,因此这类接口比较适合用于已经与主控制器建立通信接口的从 MCU。 高级自举程序将实现用户可访问的接口,例如 USB、SD 卡或以太网。这会增大自举程序的大小,并且要 求自举程序自行更新,以修复协议实现中的缺陷或者适应外部设备的变化。例如,原版本产品最高仅支持 2 GB 容量的 SD 卡,而随着时间的推移,应用经过不断更新后可支持更高容量的 SD 卡,但旧版自举程序 仍将要求使用 2 GB SD 卡(届时相对来说可能很难购买到)。 如果需要高级更新,则要评估主应用程序是否可以接收和存储低级格式的映像,以便确保可供简单自举程 序使用。凭借这种方法,可以实现高级更新接口,同时能够让实际自举程序依然保持简单而紧凑。 当无线系统需要更新时,将无法在自举程序中复制无线协议栈。因此,通过无线方式更新无线系统的惟一 选择是在应用程序运行时接收新映像,然后让简单的自举程序执行更新。映像可以存储在主闪存阵列的可 用空间中,或存储在外部存储器中。当存储在外部存储器中时,映像必须保持加密状态;而当存储在内部 存储器中时,则可以对映像进行解密和验证,从而简化自举程序的设计。
2. 进入方法
与主机控制器搭配使用的简单自举程序可以使用以下任一进入方法:
• 专用引脚(在复位时必须置为有效)
• 重复使用其中一个接口引脚
• 应用程序请求
• 没有有效的应用程序
专用引脚是最稳健的方法,但它需要在主机和从 MCU 上分配额外的引脚,这并不总是可行的。
许多接口包括处于已知空闲状态的引脚,例如,SPI 或 I
2C 中 SCL 和 SDA 线上的片选。这些引脚通常使
用外部上拉电阻保持空闲状态,因此从器件可以在复位时检测到置为有效的状态并保持在自举程序模式。
这两种方法都需要访问从 MCU 的复位引脚。但其优势在于可完全确保稳健性。即使主应用程序已损坏且
主通信接口无法正常工作,主机也始终可以请求执行自举程序。
应用程序请求不需要任何附加引脚,并且通过正常的主机到从器件通信接口即可发出自举程序的运行请
求。此方法需要正常工作的应用程序来处理请求。
即使应用程序区域为空或已损坏,自举程序也依然可以执行。检测算法既可能简单到仅仅查找闪存存储单
元的非擦除状态,也可能复杂到对整个应用程序映像进行 CRC 或哈希计算。必须针对每个应用程序评估错
误检测的速度和稳健性之间的权衡结果。
更高级的自举程序可能会检测到 U 盘或 SD 卡上存在更新文件,并以这种方式启动更新。
3. 应用程序和自举程序之间的通信
在某些情况下,应用程序和自举程序之间需要交换更多信息,例如新映像通知、更新状态或一些附加请 求。 一些 MCU 具有一组专门为此类用途分配的通用寄存器。这些寄存器可保证在上电时复位为 0,但在复位周 期内保持不变。这样即创建了一个稳健的通信通道。 如果没有特殊寄存器,则可以使用系统 RAM。虽然 RAM 在复位周期内的状态通常保持不变,但在上电后 的状态并不确定。为避免发生错误通信,建议使用多个命令副本。如果所有副本包含相同的值,则命令一 定是有效的。副本数量是误报概率和存储器消耗之间权衡后的结果。至少应使用 4 个副本,对于单字节命 令,通常使用 16 个副本就足够了。 如果认为 RAM 不可靠,或者 MCU 的复位过程擦除了 RAM(在一些面向安全的器件上可能出现),则可 以转为使用 EEPROM 或闪存。但缺点是许多现代 MCU 上暂无法提供真正的 EEPROM,并且闪存具有擦 除和写入粒度要求,这将导致浪费大量的闪存空间。
4. 映像完整性验证
在自举程序中,有两个不同的位置需要进行映像完整性验证。 需要验证新映像的完整性。对于任何自举程序设计而言,此步骤可能是强制性的,因为这一环节最容易受 到意外损坏(由不可靠接口导致)和有意损坏(由主动攻击者导致)。如果使用加密,则此步骤通常与映 像验证相结合,因为有效验证可自动证明映像完整性。 自举程序可以独立于初始验证在每个复位周期执行映像验证。这一步骤通常是可选的,因为它会延迟应用 程序的执行。但是,在某些情况下,如果担心器件安全性,则需要进行二次验证。攻击者可能会尝试中断 其他安全的更新过程,致使闪存处于未知状态。如果自举程序稍后尝试在没有验证的情况下执行应用程 序,则它将执行从未打算运行的代码,可能导致泄露机密信息。 防止此类攻击的另一种方法是使用持久标志来指示映像有效性。必须在更新过程之前清零该标志,并在第 一次成功完成映像验证后将其重新置 1。自举程序必须始终在控制权交给应用程序之前检查该标志。这 样,便不需要在每次复位时进行映像验证。 如果自举程序设计允许在闪存编程开始之前接收整个映像,则应在尝试编程之前验证接收到的映像的完整 性。但是,在许多情况下,自举程序必须以小块的形式接收和编程映像。在这种情况下,务必使自举程序 进入过程在没有有效应用程序映像的情况下也能正常工作,因为更新过程中断可能导致器件无法运行。任 何使用二次映像验证(通过完整验证或使用上述标志)的自举程序都会自动受到保护,防止出现此问题。 即使应用程序映像损坏,自举程序也将继续运行。主机控制器必须为此情况做好准备,并检测自举程序是 否正在运行。因此,必须再一次尝试更新。 选择验证算法时需要在验证速度和性能之间进行权衡。通常,最好使用慢速但安全的算法进行新映像验 证,使用快速算法(如 CRC32)进行应用程序完整性验证。
5. 安全性
如果目标 MCU 具有对 CRC 或安全哈希算法的硬件支持,则最好使用这些功能进行映像验证,因为它们可 能比等效的软件解决方案更为快速。如果硬件不支持高级加密标准,则算法更简单的软件实现通常可提供 足够的安全级别,同时保持高速度和低占用空间。通常认为 XTEA、RC4 和 Spritz 等简单算法对于一般用 途而言功能太弱,但这并不是自举程序应用时的关注点。功能太弱的一个重要原因是观察大量重复数据的 能力太弱,甚至能够影响纯文本。但对于自举程序而言,这些都不重要。映像更新是罕见的事件,映像大 小相对较短,因此攻击者无法收集大量的加密数据。 一种不错的安全措施是从不使用主密钥进行数据的实际加密。必须使用主密钥根据映像头中的信息获取映 像加密密钥。这样,不同的映像将具有不同的加密密钥,从而进一步限制成功攻击的机会。 在所有情况下,必须注意了解所用算法的局限性,以及如何正确应用它们。始终建议由安全专家进行审 查。 安全实现的最重要方面之一是密钥存储。必须使用加密密钥对所有器件进行预配置。假设通用 MCU 没有 用于密钥存储的特殊安全功能,则密钥既可能作为自举程序的一部分存储,也可能存储在单独的数据段 中。将密钥作为自举程序的一部分存储可为闪存节省一些空间,因为自举程序很少占用分配的整个段。但 缺点是如果需要更新密钥,则整个自举程序也必须更新。 密钥也可能存储在一个数据段中,该数据段可能已被应用程序用于存储用户数据。这种方法的缺点是,当 需要写入新数据时,应用程序会擦除数据段,此时密钥也会一并被擦除。通常情况下,应用程序会将密钥 进行回写,但如果在此期间发生断电,则密钥将丢失。存储密钥最可靠的方法是为密钥分配一个单独的扇 区。由于闪存写入粒度小于擦除粒度,因此它可以同时具有多个活动密钥,而不会在密钥更改过程中丢失 任何活动密钥。 某些单片机还可以提供一个单独的段来存储密钥,例如熔丝或用户行。 初始密钥配置必须在安全可信的位置完成。如果原始密钥遭到泄露,则所有进一步的密钥更新也将受到影 响。 有时,可能无法在安全的位置编程器件。在这种情况下,必须使用虚拟密钥,并且不应编程应用程序映 像。这样,主机控制器便可以检测到从器件没有有效的应用程序映像,并执行第一次密钥更新,然后使用 真实密钥加密应用程序映像更新。此步骤必须在安全位置执行。
6. 故障检测和恢复
固件更新的常见问题是故障检测和恢复。 如果自举程序正在与主机处理器通信,则解决方案是重试更新。如果主机控制器有办法在没有运行应用程 序的情况下向自举程序发出请求,则不存在发生永久性故障的风险。 在独立自举程序的情况下,如果自举程序使用新映像覆盖现有映像而未备份映像,则无法进行恢复。编写 新映像以替代活动映像的过程是更新过程中最危险的部分。有几种方法可以缓解或最大程度降低这种危 险。 自举程序必须始终执行验证,验证失败时必须重新尝试验证。最终,更新可能成功,也可能因潜在的硬件 问题而失败,而这是软件无法解决的。这意味着如果更新过程因循环上电或复位而中断,则将最终处于一 种不一致的状态,自举程序必须再次执行更新。 完全避免映像切换过程的一种方法是让自举程序从闪存的任一部分运行应用程序。使用这种方法时,自举 程序必须记住闪存的哪个部分包含活动应用程序。该方法的其中一个缺点是必须链接不同的映像才能在不 同的地址正常工作。借助一些架构和工具链,可以创建与位置无关的代码,但更通用的解决方案是构建多 个映像,即在每个可能的位置构建一个映像。然后,更新过程将下载与非活动槽匹配的映像。只要有足够 的存储空间可用,此方法还可以扩展到两个以上的映像。 一些单片机支持双分区闪存,能够使用单条原子级命令切换分区。配备这种硬件功能后,完全可以没有专 用的自举程序。在这种情况下,正在运行的应用程序会将新应用程序写入第二个分区,并在写入的数据通 过验证后执行切换。这种方法的缺点是无法检测固件故障或损坏并加以恢复。 即便使用双分区闪存,也可以通过自举程序在启动时检查映像完整性,并在出现错误时执行恢复操作。在 这种情况下,相同的自举程序代码必须复制两份,每个分区各一份。 双分区闪存可以轻松执行恢复。由于旧映像始终可用且已知功能正常,因此需要一种简单的分区切换。 假设在自举程序准备好将控制权交给应用程序时,更新的映像已经通过验证。这会留下许多潜在的问题, 例如有效的应用程序无法正常运行。这可能是由于无意中使用了不正确的映像,或者新映像与环境之间存 在某种程度的不兼容。 为了检测这类故障,自举程序可以设置永久标志,指示它将要运行新的应用程序映像。应用程序将检查该 标志并运行功能测试,或者至少确保固件更新通道可用。之后,应用程序将清零标志。如果在下一个复位 周期,自举程序检测到标志仍置 1,它将假定新映像未正确运行或测试失败。然后,自举程序必须从备份 映像(如果可用)执行恢复。