3、哈希函数基础:从“数字指纹”说起

聊固件校验,绕不开哈希函数。这东西说白了,就是给任意一段数据算出一个固定长度的“摘要”。你想想看,固件动不动几兆甚至几十兆,我们总不能每次都拿整个文件去比对吧?哈希就是那个“数字指纹”。

我记得刚入行那会儿,带我的老工程师跟我说过一句话:“哈希函数是嵌入式安全的基石,你把它搞透了,后面校验、签名、升级,一通百通。”当时没太在意,后来踩了几个坑才明白——这话一点不夸张。

3.1 什么是哈希函数?

哈希函数(Hash Function),也叫散列函数。它接收任意长度的输入,经过计算后输出一个固定长度的值,这个值就叫哈希值(Hash Value)或消息摘要(Message Digest)。

用公式表示就是:

H = Hash(M)

其中:
M —— 任意长度的输入消息
H —— 固定长度的哈希值(比如 256 位)
Hash() —— 哈希函数本身

我习惯把哈希函数想象成一个“碎纸机+印章”的组合。你把一份文件塞进去,出来的是一个固定大小的印章图案。这个图案能唯一代表那份文件,但你没办法从印章反推出文件内容。

核心要点:哈希不是加密。加密是为了隐藏信息,哈希是为了验证完整性。这两个概念千万别搞混了——我在面试新人时发现这是最常见的误区。

3.2 哈希函数的三大特性

一个合格的哈希函数,必须同时满足三个特性。缺一个,它就不适合用在固件校验里。

3.2.1 单向性(One-way)

单向性,也叫抗原像性。意思是:给定哈希值 H,你几乎不可能反推出原始输入 M。

为什么会这样?因为哈希函数在计算过程中做了大量的“信息压缩”和“非线性变换”。输入空间是无限的,输出空间是有限的,必然存在多对一的映射关系。但好的哈希函数让这个逆向过程在计算上不可行。

我曾经做过一个实验:用 SHA256 对一个 1KB 的固件片段算哈希,然后尝试用暴力方式反推。嗯,算到宇宙热寂也出不来结果。

实战经验:在固件升级场景中,单向性保证了即使攻击者拿到了服务器下发的哈希值,也无法反推出固件的二进制内容。这对保护固件知识产权很有用。

3.2.2 抗碰撞性(Collision Resistance)

抗碰撞性分两个层次:

  • 弱抗碰撞性:给定 M1,你找不到另一个 M2(M2 ≠ M1),使得 Hash(M1) = Hash(M2)。
  • 强抗碰撞性:你找不到任意两个不同的 M1 和 M2,使得它们的哈希值相等。

说白了,就是“撞车”的概率要低到可以忽略不计。SHA256 的输出空间是 2^256,这个数字有多大?比宇宙中的原子总数还多。理论上存在碰撞,但实际中你碰不上。

注意:MD5 和 SHA1 已经被证明存在实际的碰撞攻击。我在 2017 年参与过一个项目,客户坚持用 MD5 做固件校验。我花了整整一周时间,用实际碰撞案例说服他们改用 SHA256。从那以后,我个人的项目里再也没用过 MD5。

3.2.3 确定性(Deterministic)

同一个输入,无论你算多少次,得到的哈希值必须完全一样。这个特性看起来简单,但实际工程中容易出问题。

比如,有些哈希库在处理不同字节序的输入时,结果会不一样。我遇到过一例:PC 端用大端序算哈希,嵌入式端用小端序算,两边结果对不上。排查了整整两天才发现是字节序的问题。

3.3 常见哈希算法对比

下面这张表,是我在实际项目中常用的哈希算法对比。建议你收藏一下,选型时直接参考。

算法 输出长度 安全性 速度(相对) 推荐场景
MD5 128 位(16 字节) 已破解,不推荐 最快 仅用于非安全场景(如文件去重)
SHA1 160 位(20 字节) 已破解,不推荐 较快 遗留系统兼容,新项目不要用
SHA256 256 位(32 字节) 安全(截至 2025 年) 中等 固件校验、数字签名、证书验证
SHA384 384 位(48 字节) 安全 较慢 高安全等级场景
SHA512 512 位(64 字节) 安全 最慢 对性能不敏感的高安全场景

我的建议:固件校验场景,直接上 SHA256。它在安全性和性能之间取得了最好的平衡。在 Cortex-M4 上,SHA256 的吞吐量可以达到几十 MB/s,完全够用。

3.4 哈希函数在固件校验中的典型流程

下面这张 SVG 图,展示了哈希函数在固件校验中的核心位置。我画这张图的时候,特意把“哈希计算”放在中间——因为它是连接“固件分发”和“固件验证”的桥梁。

哈希函数在固件校验中的核心流程 原始固件 哈希计算 (SHA256) 哈希值 (32字节) 固件传输(网络/存储) 接收固件 重新计算哈希值 哈希值对比 一致 → 校验通过 不一致 → 固件被篡改

流程其实很简单:

  1. 对原始固件计算哈希值,得到“数字指纹”
  2. 把固件和哈希值一起分发(哈希值可以通过安全通道传输)
  3. 接收端收到固件后,重新计算哈希值
  4. 对比两个哈希值:一致则固件完整,不一致则说明被篡改

3.5 嵌入式环境下的哈希实现注意事项

在实际的嵌入式项目中,哈希函数的实现有几个坑需要避开:

  • 内存管理:哈希计算需要缓冲区。我习惯用静态分配,避免动态内存分配带来的不确定性。
  • 分块计算:固件可能很大,不能一次性加载到内存。用哈希库的“初始化-更新-最终”三步接口,分块计算。
  • 硬件加速:很多 MCU 内置了哈希硬件加速器(比如 STM32 的 HASH 外设)。能用硬件就别用软件,速度和功耗都有优势。
  • 字节序:前面提过,确保发送端和接收端的字节序一致。我一般在协议里固定使用大端序。

代码片段:下面是一个典型的 SHA256 分块计算伪代码,我在多个项目里用过这个模式。

// 伪代码:SHA256 分块计算
SHA256_CTX ctx;
uint8_t hash[32];
uint8_t buffer[1024];
uint32_t offset = 0;
uint32_t remaining = firmware_size;

SHA256_Init(&ctx);

while (remaining > 0) {
    uint32_t chunk = (remaining > 1024) ? 1024 : remaining;
    read_firmware(buffer, offset, chunk);
    SHA256_Update(&ctx, buffer, chunk);
    offset += chunk;
    remaining -= chunk;
}

SHA256_Final(&ctx, hash);

// 此时 hash 数组中就是 32 字节的 SHA256 值
// 与接收到的哈希值逐字节对比即可

嗯,哈希函数的基础就这些。记住一句话:在固件校验的世界里,哈希是你的第一道防线,也是最后一道防线。选对算法、用对方式,后面的事情就顺了。