2. 哈希函数:SHA-256原理、MD5的缺陷、哈希在完整性校验中的应用

哈希函数,说白了就是一把“数字指纹”生成器。你给它任意长度的数据,它给你一个固定长度的摘要。这个摘要就像人的指纹——数据变了,指纹一定变;指纹相同,数据大概率没变。

我在嵌入式安全领域摸爬滚打这些年,哈希函数几乎天天见。从固件校验到密码存储,从数字签名到证书验证,到处都有它的影子。今天咱们就把它彻底聊透。

2.1 哈希函数的核心性质

一个合格的密码学哈希函数,必须满足三个硬性要求:

  • 抗原像性:给定哈希值h,你几乎不可能找到任何消息m使得H(m)=h。说白了就是“不可逆”。
  • 抗第二原像性:给定消息m1,你几乎不可能找到另一个m2使得H(m1)=H(m2)。
  • 抗碰撞性:你几乎不可能找到任意两个不同的m1和m2使得H(m1)=H(m2)。

重要概念:抗碰撞性比抗第二原像性更强。如果哈希函数能抵抗碰撞攻击,它自然也能抵抗第二原像攻击。反过来不一定成立。

你想想看,如果哈希函数不满足这些性质,那它还有什么用?签名可以被伪造,固件可以被篡改,密码可以被逆向——整个安全体系就崩塌了。

2.2 SHA-256:当今的工业标准

SHA-256属于SHA-2家族,输出256位(32字节)的摘要。我个人习惯把它当作“哈希界的AES”——稳定、高效、经过充分验证。

2.2.1 算法流程概览

SHA-256的处理过程可以分为五步:

  1. 消息填充:在消息末尾补一个'1'位,然后补'0'位,直到长度模512等于448。最后附加64位的原始消息长度。
  2. 初始化状态:设置8个32位初始哈希值(H0~H7),这些值来自前8个质数的平方根的小数部分。
  3. 消息分块:将填充后的消息分成512位(64字节)的块。
  4. 消息调度:每个块扩展成64个32位字(W0~W63)。
  5. 压缩函数:对每个块执行64轮迭代,更新8个状态变量。

我的经验:在嵌入式平台上实现SHA-256时,我建议优先使用硬件加速器。如果必须用软件实现,注意查表法的内存对齐问题。我曾经在一个Cortex-M4项目上因为未对齐访问导致性能下降30%。

2.2.2 压缩函数详解

压缩函数是SHA-256的核心。它使用6个逻辑函数:

  • Σ0(x) = ROTR^2(x) ⊕ ROTR^13(x) ⊕ ROTR^22(x)
  • Σ1(x) = ROTR^6(x) ⊕ ROTR^11(x) ⊕ ROTR^25(x)
  • σ0(x) = ROTR^7(x) ⊕ ROTR^18(x) ⊕ SHR^3(x)
  • σ1(x) = ROTR^17(x) ⊕ ROTR^19(x) ⊕ SHR^10(x)
  • Ch(x,y,z) = (x ∧ y) ⊕ (¬x ∧ z)
  • Maj(x,y,z) = (x ∧ y) ⊕ (x ∧ z) ⊕ (y ∧ z)

每一轮的计算过程如下:

T1 = h + Σ1(e) + Ch(e,f,g) + Kt + Wt
T2 = Σ0(a) + Maj(a,b,c)
h = g
g = f
f = e
e = d + T1
d = c
c = b
b = a
a = T1 + T2

嗯,这里要注意:64轮迭代完成后,将当前块的状态变量与上一块的状态变量相加,得到新的状态。这就是所谓的“Davies-Meyer”结构。

2.2.3 代码示例:SHA-256核心轮函数

// SHA-256 单轮计算(C语言简化版)
void sha256_round(uint32_t *state, const uint32_t *w, int t) {
    uint32_t a = state[0], b = state[1];
    uint32_t c = state[2], d = state[3];
    uint32_t e = state[4], f = state[5];
    uint32_t g = state[6], h = state[7];
    
    uint32_t S1 = ROTR(e, 6) ^ ROTR(e, 11) ^ ROTR(e, 25);
    uint32_t ch = (e & f) ^ ((~e) & g);
    uint32_t temp1 = h + S1 + ch + K[t] + w[t];
    
    uint32_t S0 = ROTR(a, 2) ^ ROTR(a, 13) ^ ROTR(a, 22);
    uint32_t maj = (a & b) ^ (a & c) ^ (b & c);
    uint32_t temp2 = S0 + maj;
    
    state[7] = g; state[6] = f; state[5] = e;
    state[4] = d + temp1;
    state[3] = c; state[2] = b; state[1] = a;
    state[0] = temp1 + temp2;
}

2.3 MD5的缺陷:一个活生生的教训

MD5曾经是哈希函数的明星。它由Rivest在1992年设计,输出128位摘要。但到了2004年,中国密码学家王小云团队宣布了MD5的碰撞攻击——可以在普通PC上几分钟内找到碰撞。

严重警告:MD5已经被彻底攻破。任何还在使用MD5做安全校验的系统,都应该立即升级。这不是“可能不安全”,而是“肯定不安全”。

MD5的主要缺陷包括:

  • 输出太短:128位,根据生日悖论,碰撞攻击的复杂度只有2^64。这在今天完全不可接受。
  • 结构弱点:MD5的压缩函数对差分攻击的抵抗力不足。王小云团队正是利用了这个弱点。
  • 无抗量子性:Grover算法可以将碰撞搜索复杂度降到2^32,量子计算机一旦成熟,MD5瞬间崩溃。

我记得有一次做安全审计,发现一个客户的车载系统还在用MD5校验固件。我当场演示了碰撞攻击——用两个不同的固件生成了相同的MD5值。客户的脸都绿了。从那以后,他们所有新项目都强制使用SHA-256。

2.4 哈希在完整性校验中的应用

完整性校验是哈希函数最直接的应用。说白了就是:计算数据的哈希值,保存起来;下次再计算一次,对比是否一致。

2.4.1 固件完整性校验

在安全启动中,固件完整性校验是第一步。流程如下:

  1. 固件开发者计算固件的SHA-256哈希值。
  2. 用私钥对哈希值签名,生成签名数据。
  3. 设备启动时,用公钥验证签名,得到原始哈希值。
  4. 设备计算当前固件的SHA-256哈希值。
  5. 对比两个哈希值。一致则固件未被篡改。

关键点:哈希值本身必须被保护。如果攻击者能同时篡改固件和哈希值,那校验就失去了意义。所以哈希值通常通过数字签名或安全存储来保护。

2.4.2 数据完整性校验表

应用场景 哈希函数 校验方式 安全等级
固件签名 SHA-256 数字签名验证 高
文件下载 SHA-256/SHA-512 哈希值对比 中
密码存储 bcrypt/scrypt 加盐哈希 高
消息认证 HMAC-SHA256 密钥哈希 高
数据去重 SHA-1(不推荐) 哈希值索引 低

2.4.3 嵌入式系统中的实践

在嵌入式系统中做完整性校验,有几个坑需要避开:

  • 内存限制:不要一次性加载整个固件到内存。使用流式哈希(如mbedTLS的mbedtls_sha256_update_ret),分块处理。
  • 时序攻击:比较哈希值时使用恒定时间比较函数,防止攻击者通过时序差异推断信息。
  • 回滚攻击:仅仅校验完整性不够,还要校验版本号。否则攻击者可以用旧版本的合法固件替换新版本。

避坑指南:我曾经在一个IoT项目上踩过回滚攻击的坑。设备只校验了固件的签名,没校验版本号。结果攻击者用一年前的旧固件(有已知漏洞)替换了新固件。从那以后,我所有项目都强制在固件头里加入版本号字段,并在校验时检查版本号是否大于等于当前版本。

2.5 哈希函数的知识体系

下面这张图展示了哈希函数的核心知识结构:

哈希函数知识体系 哈希函数知识体系 哈希函数 抗原像性 抗第二原像性 抗碰撞性 SHA-256(安全) MD5(已攻破) SHA-1(不推荐) 固件完整性校验 数字签名 密码存储 核心原则 选择哈希函数时,优先使用SHA-256或更高版本,避免使用MD5和SHA-1

2.6 总结与建议

哈希函数是密码学的基础工具。选对了,事半功倍;选错了,满盘皆输。

我的建议很简单:

  • 新项目一律用SHA-256。别想着“够用就行”,安全没有“够用”这一说。
  • 尽快淘汰MD5和SHA-1。如果现有系统还在用,制定迁移计划,刻不容缓。
  • 注意实现细节。流式处理、恒定时间比较、版本号校验——这些细节决定了你的系统是“看起来安全”还是“真的安全”。