哈希算法原理:SHA-256、SHA-384在固件完整性校验中的应用

说到固件保护,哈希算法是绕不开的基础设施。你想想看,不管你是做OTA升级,还是做安全启动,最终都要靠哈希来「验明正身」。我个人习惯把哈希比作固件的「指纹」——世界上没有两片完全相同的树叶,也没有两个完全相同的哈希值。

这一章,我们就来聊聊SHA-256和SHA-384这两个最常见的哈希算法,以及它们在实际固件校验中是怎么用的。

哈希算法到底在干什么?

说白了,哈希算法就是一个「单向压缩函数」。你给它任意长度的数据,它给你一个固定长度的摘要。这个过程不可逆——你不能从摘要反推出原始数据。

我在项目中遇到过不少刚入行的同事,总想把哈希当加密用。这是个大坑。哈希不是加密,加密可以解密,哈希不行。哈希是「完整性校验」的工具,不是「保密性」的工具。

哈希算法的三个核心特性:

  • 抗原像性:给定哈希值,无法反推出原始数据
  • 抗第二原像性:给定数据A,无法找到另一个数据B使得hash(A)=hash(B)
  • 抗碰撞性:无法找到任意两个不同的数据A和B使得hash(A)=hash(B)

SHA-256:最通用的选择

SHA-256输出256位(32字节)的摘要。在固件校验场景里,它是我用得最多的算法。为什么?因为它在安全性和性能之间取得了很好的平衡。

我记得有一次做车载ECU的固件升级方案,MCU主频只有120MHz,RAM也紧张。当时有人提议用SHA-512,我算了一笔账——SHA-256在Cortex-M4上跑1MB固件大约需要200ms,而SHA-512要接近500ms。对于启动校验来说,这个差距是致命的。

我的经验:在资源受限的嵌入式设备上,SHA-256通常是首选。它的安全强度在2030年之前都足够用,而且硬件加速支持最广泛。

SHA-384:什么时候需要它?

SHA-384输出384位(48字节)的摘要。它其实是SHA-512的截断版本,只是初始值不同。你可能会问:为什么不用SHA-512?

嗯,这里有个细节。SHA-384的设计初衷是为了应对「长度扩展攻击」。虽然SHA-256也有这个问题,但在实际固件校验场景中,长度扩展攻击的威胁并不大。不过,如果你的固件签名方案使用了哈希结果作为密钥派生的一部分,那SHA-384会更安全。

我在做金融POS终端的固件保护时,客户明确要求使用SHA-384。他们的理由是:合规要求。有些安全标准(比如FIPS 140-2 Level 3)对哈希算法的安全级别有硬性规定,SHA-256在某些场景下被认为「不够强」。

特性 SHA-256 SHA-384
输出长度 256位(32字节) 384位(48字节)
安全强度 128位 192位
相对速度 快 中等
内存占用 低 中等
典型场景 通用固件校验、启动校验 高安全要求、合规场景

固件完整性校验的典型流程

光讲理论没意思,我们来看看实际代码。下面是一个典型的固件哈希校验流程:

// 伪代码:固件完整性校验
uint8_t expected_hash[32];  // 预存的期望哈希值
uint8_t computed_hash[32];  // 计算得到的哈希值

// 步骤1:读取固件数据(从Flash或外部存储)
uint8_t* firmware_data = read_firmware_from_flash();
uint32_t firmware_size = get_firmware_size();

// 步骤2:计算哈希
sha256_context_t ctx;
sha256_init(&ctx);
sha256_update(&ctx, firmware_data, firmware_size);
sha256_final(&ctx, computed_hash);

// 步骤3:比对哈希
if (memcmp(expected_hash, computed_hash, 32) == 0) {
    // 校验通过,固件未被篡改
    boot_continue();
} else {
    // 校验失败,进入恢复模式
    enter_recovery_mode();
}

这段代码看起来简单,但实际项目中坑不少。我曾经踩过一个雷:sha256_update的调用顺序搞错了,导致计算出来的哈希值一直不对。排查了整整半天才发现是上下文初始化的问题。

避坑指南:我曾经在STM32F4上实现SHA-256校验时,忽略了Flash读取的字节对齐问题。硬件哈希加速器要求输入数据按4字节对齐,而我传了一个未对齐的指针,结果硬件直接报错。后来我加了一层内存拷贝,问题才解决。

哈希值的安全存储

哈希值本身也要保护。你想想看,如果攻击者能篡改Flash里存储的期望哈希值,那你的校验就形同虚设了。

我建议的做法是:

  • 将哈希值存储在芯片的OTP(一次性可编程)区域
  • 或者使用安全元件(Secure Element)来存储
  • 如果以上都没有,至少要把哈希值放在固件镜像的签名区,用公钥验证

说白了,哈希校验只是链条中的一环。它保证的是「固件没有被改过」,但前提是「你用来比对的哈希值是正确的」。

SVG:哈希校验流程全景图

下面这张图展示了从固件生成到启动校验的完整流程。我建议你仔细看看「哈希比对」这个环节——它往往是整个安全链条中最脆弱的一环。

固件哈希校验完整流程 原始固件二进制 计算SHA-256哈希 存储期望哈希值 读取固件数据 计算实际哈希值 读取期望哈希值 哈希比对(memcmp) 校验通过 → 正常启动 校验失败 → 恢复模式 注意:期望哈希值的存储位置决定了整个校验的安全性

实际项目中的性能考量

选哈希算法不能只看安全强度,还得看你的硬件平台。我整理了一份常见MCU上的性能数据,供你参考:

MCU平台 主频 SHA-256(1MB固件) SHA-384(1MB固件)
Cortex-M0+ 48MHz ~450ms ~820ms
Cortex-M4 120MHz ~200ms ~380ms
Cortex-M7 400MHz ~60ms ~110ms
带硬件加速的M4 120MHz ~15ms ~28ms

看到没?硬件加速带来的提升是数量级的。如果你的芯片有硬件哈希加速器,一定要用起来。我见过不少项目,明明芯片支持硬件加速,却还在用软件库算哈希——白白浪费了性能。

实用建议:在项目初期就确认芯片是否支持硬件哈希加速。如果支持,优先使用硬件实现。如果不支持,考虑使用mbedTLS或WolfSSL的优化实现,它们针对ARM Cortex-M系列做了汇编级优化。

哈希校验的常见攻击手法

讲完了怎么用,我们聊聊怎么被攻击。了解攻击手法,才能更好地防御。

  • 哈希值篡改:攻击者修改Flash中存储的期望哈希值。防御方案:将哈希值放在OTP或安全元件中。
  • 哈希碰撞攻击:找到两个不同的固件具有相同的哈希值。SHA-256目前没有已知的实际碰撞攻击,但理论上存在风险。
  • 时序攻击:通过测量哈希比对的时间来推断哈希值。防御方案:使用常量时间比较函数,比如memcmp在某些实现中不是常量时间的。
  • 长度扩展攻击:针对SHA-256的特定攻击。防御方案:使用HMAC或SHA-384。

嗯,这里要注意。对于大多数嵌入式设备来说,最现实的威胁是「哈希值篡改」。攻击者不需要破解SHA-256,他只需要改掉你存哈希值的那几个字节就行了。所以,保护哈希值的存储位置,比纠结选SHA-256还是SHA-384更重要。

我个人习惯在项目设计阶段就画好「信任链」——从BootROM到应用固件,每一级的哈希校验都要明确:期望哈希值存在哪?谁有权写?谁有权读?这些问题想清楚了,哈希算法本身的选择反而没那么纠结。